Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,20 @@ vendor/bin/roave-backward-compatibility-check --help

## Configuration

There are currently no configuration options available.
The file `.roave-backward-compatibility-check.xml` is read from the current working directory (when it exists) and sets configuration for the command.

It's expected to be an XML file that follows our [schema](resources/schema.xsd):

**Example:**

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/roave/backward-compatibility-check/resources/schema.xsd">
<baseline>
<ignored-regex>#\[BC\] CHANGED: The parameter \$a of of TestArtifact\\TheClass\#method\(\)#</ignored-regex>
<ignored-regex>#\[BC\] CHANGED: The parameter \$b of of TestArtifact\\TheClass\#method2\(\)#</ignored-regex>
</baseline>
</roave-bc-check>
```
30 changes: 30 additions & 0 deletions Resources/schema.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:annotation>
<xs:appinfo source="https://github.com/Roave/BackwardCompatibilityCheck"/>

<xs:documentation source="https://github.com/Roave/BackwardCompatibilityCheck">
This schema file defines the structure for the XML configuration file of roave/backward-compatibility-check.
</xs:documentation>
</xs:annotation>

<xs:element name="roave-bc-check" type="bcCheckType" />

<xs:complexType name="bcCheckType">
<xs:sequence>
<xs:element name="baseline" type="baselineType" minOccurs="0" />
</xs:sequence>
</xs:complexType>

<xs:complexType name="baselineType">
<xs:sequence>
<xs:element name="ignored-regex" minOccurs="0" maxOccurs="unbounded" type="ignore-pattern" />
</xs:sequence>
</xs:complexType>

<xs:simpleType name="ignore-pattern">
<xs:restriction base="xs:string">
<xs:pattern value="#.+#" />
</xs:restriction>
</xs:simpleType>
</xs:schema>
1 change: 1 addition & 0 deletions box.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"main": "bin/roave-backward-compatibility-check.php",
"output": "dist/roave-backward-compatibility-check.phar",
"files-bin": [
"Resources/schema.xsd",
"LICENSE",
"vendor/composer/composer/LICENSE"
],
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"description": "Tool to compare two revisions of a public API to check for BC breaks",
"require": {
"php": "~8.2.0 || ~8.3.0",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"azjezz/psl": "^3.0.2",
"composer/composer": "^2.7.6",
"nikic/php-parser": "^4.19.1",
Expand Down
8 changes: 6 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions src/Baseline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility;

use function array_values;
use function preg_match;

/** @psalm-immutable */
final class Baseline
{
/** @psalm-param list<non-empty-string> $ignoredChanges */
private function __construct(private readonly array $ignoredChanges = [])
{
}

public static function empty(): self
{
return new self();
}

/** @psalm-param list<non-empty-string> $ignoredChanges */
public static function fromList(string ...$ignoredChanges): self
{
return new self(array_values($ignoredChanges));
}

public function ignores(Change $change): bool
{
$changeDescription = $change->__toString();

foreach ($this->ignoredChanges as $ignoredChangeRegex) {
if (preg_match($ignoredChangeRegex, $changeDescription) === 1) {
return true;
}
}

return false;
}
}
17 changes: 17 additions & 0 deletions src/Changes.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ public function mergeWith(self $other): self
return $instance;
}

public function applyBaseline(Baseline $baseline): self
{
$instance = new self([]);

$instance->unBufferedChanges = (function () use ($baseline): Generator {
foreach ($this as $change) {
if ($baseline->ignores($change)) {
continue;
}

yield $change;
}
})();

return $instance;
}

/**
* {@inheritDoc}
*
Expand Down
8 changes: 6 additions & 2 deletions src/Command/AssertBackwardsCompatible.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ public function execute(InputInterface $input, OutputInterface $output): int
$stdErr = $output->getErrorOutput();

// @todo fix flaky assumption about the path of the source repo...
$sourceRepo = CheckedOutRepository::fromPath(Env\current_dir());
$currentDirectory = Env\current_dir();

$sourceRepo = CheckedOutRepository::fromPath($currentDirectory);

$fromRevision = $input->getOption('from') !== null
? $this->parseRevisionFromInput($input, $sourceRepo)
Expand All @@ -126,6 +128,8 @@ public function execute(InputInterface $input, OutputInterface $output): int

$toRevision = $this->parseRevision->fromStringForRepository($to, $sourceRepo);

$configuration = (new DetermineConfigurationFromFilesystem())($currentDirectory, $stdErr);

$stdErr->writeln(Str\format(
'Comparing from %s to %s...',
Type\string()->coerce($fromRevision),
Expand All @@ -149,7 +153,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
$toPath->__toString(),
($this->locateDependencies)($toPath->__toString(), $includeDevelopmentDependencies),
),
);
)->applyBaseline($configuration->baseline);

$formatters = [
'console' => new SymfonyConsoleTextFormatter($stdErr),
Expand Down
37 changes: 37 additions & 0 deletions src/Command/DetermineConfigurationFromFilesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Command;

use Psl\Str;
use Psl\Type;
use Roave\BackwardCompatibility\Configuration\Configuration;
use Roave\BackwardCompatibility\Configuration\ParseConfigurationFile;
use Roave\BackwardCompatibility\Configuration\ParseXmlConfigurationFile;
use Symfony\Component\Console\Output\OutputInterface;

/** @internal */
final class DetermineConfigurationFromFilesystem
{
public function __construct(
private readonly ParseConfigurationFile $parser = new ParseXmlConfigurationFile(),
) {
}

public function __invoke(
string $currentDirectory,
OutputInterface $stdErr,
): Configuration {
$configuration = $this->parser->parse($currentDirectory);

if ($configuration->filename !== null) {
$stdErr->writeln(Str\format(
'Using "%s" as configuration file',
Type\string()->coerce($configuration->filename),
));
}

return $configuration;
}
}
27 changes: 27 additions & 0 deletions src/Configuration/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

use Roave\BackwardCompatibility\Baseline;

/** @psalm-immutable */
final class Configuration
{
private function __construct(
public readonly Baseline $baseline,
public readonly string|null $filename,
) {
}

public static function default(): self
{
return new self(Baseline::empty(), null);
}

public static function fromFile(Baseline $baseline, string $filename): self
{
return new self($baseline, $filename);
}
}
33 changes: 33 additions & 0 deletions src/Configuration/InvalidConfigurationStructure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

use LibXMLError;
use RuntimeException;

use function sprintf;
use function trim;

use const PHP_EOL;

/** @internal */
final class InvalidConfigurationStructure extends RuntimeException
{
/** @param list<LibXMLError> $errors */
public static function fromLibxmlErrors(array $errors): self
{
$message = 'The provided configuration is invalid, errors:' . PHP_EOL;

Check warning on line 21 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "Concat": --- Original +++ New @@ @@ /** @param list<LibXMLError> $errors */ public static function fromLibxmlErrors(array $errors) : self { - $message = 'The provided configuration is invalid, errors:' . PHP_EOL; + $message = PHP_EOL . 'The provided configuration is invalid, errors:'; foreach ($errors as $error) { $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); }

Check warning on line 21 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ /** @param list<LibXMLError> $errors */ public static function fromLibxmlErrors(array $errors) : self { - $message = 'The provided configuration is invalid, errors:' . PHP_EOL; + $message = PHP_EOL; foreach ($errors as $error) { $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); }

Check warning on line 21 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ /** @param list<LibXMLError> $errors */ public static function fromLibxmlErrors(array $errors) : self { - $message = 'The provided configuration is invalid, errors:' . PHP_EOL; + $message = 'The provided configuration is invalid, errors:'; foreach ($errors as $error) { $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); }

foreach ($errors as $error) {
$message .= sprintf(
' - [Line %d] %s' . PHP_EOL,

Check warning on line 25 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "Concat": --- Original +++ New @@ @@ { $message = 'The provided configuration is invalid, errors:' . PHP_EOL; foreach ($errors as $error) { - $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); + $message .= sprintf(PHP_EOL . ' - [Line %d] %s', $error->line, trim($error->message)); } return new self($message); } }

Check warning on line 25 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ { $message = 'The provided configuration is invalid, errors:' . PHP_EOL; foreach ($errors as $error) { - $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); + $message .= sprintf(' - [Line %d] %s', $error->line, trim($error->message)); } return new self($message); } }
$error->line,
trim($error->message),

Check warning on line 27 in src/Configuration/InvalidConfigurationStructure.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "UnwrapTrim": --- Original +++ New @@ @@ { $message = 'The provided configuration is invalid, errors:' . PHP_EOL; foreach ($errors as $error) { - $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, trim($error->message)); + $message .= sprintf(' - [Line %d] %s' . PHP_EOL, $error->line, $error->message); } return new self($message); } }
);
}

return new self($message);
}
}
11 changes: 11 additions & 0 deletions src/Configuration/ParseConfigurationFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

interface ParseConfigurationFile
{
/** @throws InvalidConfigurationStructure When an incorrect file was found on the directory. */
public function parse(string $currentDirectory): Configuration;
}
75 changes: 75 additions & 0 deletions src/Configuration/ParseXmlConfigurationFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

use DOMDocument;
use Psl\File;
use Roave\BackwardCompatibility\Baseline;
use SimpleXMLElement;

use function assert;
use function libxml_get_errors;
use function libxml_use_internal_errors;

/** @internal */
final class ParseXmlConfigurationFile implements ParseConfigurationFile
{
private const CONFIGURATION_FILENAME = '.roave-backward-compatibility-check.xml';

private const SCHEMA = __DIR__ . '/../../Resources/schema.xsd';

public function parse(string $currentDirectory): Configuration
{
$filename = $currentDirectory . '/' . self::CONFIGURATION_FILENAME;

try {
$xmlContents = File\read($filename);

$this->validateStructure($xmlContents);
} catch (File\Exception\InvalidArgumentException) {
return Configuration::default();
}

$configuration = new SimpleXMLElement($xmlContents);

return Configuration::fromFile(
$this->parseBaseline($configuration),
$filename,
);
}

private function validateStructure(string $xmlContents): void
{
$previousConfiguration = libxml_use_internal_errors(true);

$xmlDocument = new DOMDocument();
$xmlDocument->loadXML($xmlContents);

$configurationIsValid = $xmlDocument->schemaValidate(self::SCHEMA);

$parsingErrors = libxml_get_errors();
libxml_use_internal_errors($previousConfiguration);

Check warning on line 53 in src/Configuration/ParseXmlConfigurationFile.php

View workflow job for this annotation

GitHub Actions / QA Checks (Infection (PCOV) [8.2, locked], ubuntu-latest, laminas/laminas-continuous-integration-...

Escaped Mutant for Mutator "FunctionCallRemoval": --- Original +++ New @@ @@ $xmlDocument->loadXML($xmlContents); $configurationIsValid = $xmlDocument->schemaValidate(self::SCHEMA); $parsingErrors = libxml_get_errors(); - libxml_use_internal_errors($previousConfiguration); + if ($configurationIsValid) { return; }

if ($configurationIsValid) {
return;
}

throw InvalidConfigurationStructure::fromLibxmlErrors($parsingErrors);
}

private function parseBaseline(SimpleXMLElement $element): Baseline
{
$ignoredItems = [];

foreach ($element->xpath('baseline/ignored-regex') ?? [] as $node) {
$ignoredItem = (string) $node;

assert($ignoredItem !== '');
$ignoredItems[] = $ignoredItem;
}

return Baseline::fromList(...$ignoredItems);
}
}
Loading