Skip to content

Commit

Permalink
Merge pull request #737 from lcobucci/support-baseline-configuration
Browse files Browse the repository at this point in the history
Support baseline configuration
  • Loading branch information
Ocramius authored Feb 5, 2025
2 parents eed5a57 + f6bea18 commit 3afc875
Show file tree
Hide file tree
Showing 17 changed files with 581 additions and 5 deletions.
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

0 comments on commit 3afc875

Please sign in to comment.