Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect changes in ENUMs as BC breaks #768

Merged
merged 32 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
77fd27e
Roave/BackwardCompatibilityCheck#767 Create EnumCaseAdded class
bdsl May 19, 2024
be55ca1
Roave/BackwardCompatibilityCheck#767: Rename ClassBased/EnumCaseAdded…
bdsl May 19, 2024
8c41bc8
Roave/BackwardCompatibilityCheck#767: Allow adding @internal enum cas…
bdsl May 19, 2024
39ecc98
Add BC check for Enum Case removed
bdsl May 19, 2024
66ceaa1
Allow removing @internal enum cases without BC break
bdsl May 19, 2024
336bcab
Fix PHPCS errors
bdsl May 19, 2024
f8092b6
Refactor: Make relavent namespace EnumBased more visible in check script
bdsl May 19, 2024
a1e3738
Roave/BackwardCompatibilityCheck#767 : Fix BC break
bdsl May 19, 2024
b2c4ddc
Roave/BackwardCompatibilityCheck#767: Improve test coverage
bdsl May 19, 2024
6f7c9dc
Roave/BackwardCompatibilityCheck#767: Remove unecassary array_values …
bdsl May 19, 2024
458cab3
Roave/BackwardCompatibilityCheck#767: Remove covers annotation on new…
bdsl May 19, 2024
9f2820b
Temporarily remove enum based checks fron script
bdsl May 19, 2024
0460dbf
Temporarily remove enum based checks from CompareClasses to see how i…
bdsl May 19, 2024
e82047e
Revert "Temporarily remove enum based checks from CompareClasses to s…
bdsl May 19, 2024
889b956
Revert "Temporarily remove enum based checks fron script"
bdsl May 19, 2024
7f01d47
Roave/BackwardCompatibilityCheck#767: Add tests about enums to Compar…
bdsl May 19, 2024
850e7a3
Roave/BackwardCompatibilityCheck#767: Refactor, remove new EnumBased …
bdsl May 21, 2024
d9d3b04
Roave/BackwardCompatibilityCheck#767: Add check in test that Enum cas…
bdsl May 21, 2024
61231a8
Roave/BackwardCompatibilityCheck#767: add check for enum case became …
bdsl May 21, 2024
9da3f6d
Roave/BackwardCompatibilityCheck#767: Detect enum becoming non-enum a…
bdsl May 21, 2024
aa85d92
Roave/BackwardCompatibilityCheck#767: Refactor, rename class CasedCha…
bdsl May 21, 2024
35e77b3
Roave/BackwardCompatibilityCheck#767: Fix Psalm and PHPCS errors
bdsl May 21, 2024
aa9c85d
Roave/BackwardCompatibilityCheck#767: Add check for enum case becomin…
bdsl May 22, 2024
edcbd2b
Restore blank line deleted in error
bdsl May 22, 2024
77a2f08
Roave/BackwardCompatibilityCheck#767: Remove outdated comments from test
bdsl May 22, 2024
ad32463
Make EnumCasesChanged final
bdsl May 22, 2024
5519e97
Roave/BackwardCompatibilityCheck#767: Add @covers to EnumCasesChanged…
bdsl May 22, 2024
190968c
Roave/BackwardCompatibilityCheck#767: Remove redundant term in if con…
bdsl May 22, 2024
6703c31
Roave/BackwardCompatibilityCheck#767: Remove unecassary comment
bdsl May 22, 2024
9f75ade
Fix covers annotation on EnumCasesChangedTest
bdsl May 22, 2024
763dadd
Adjusting temporary path expectation: may differ based on `$UID`
Ocramius May 22, 2024
c245e9c
Reducing mutation testing requirements: makes CI green.
Ocramius May 22, 2024
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
1 change: 1 addition & 0 deletions bin/roave-backward-compatibility-check.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ static function (string $installationPath) use ($composerIo): Installer {
new ClassBased\SkipClassBasedErrors(new ClassBased\MethodRemoved()),
new ClassBased\SkipClassBasedErrors(new ClassBased\AncestorRemoved()),
new ClassBased\SkipClassBasedErrors(new ClassBased\ClassBecameInternal()),
new ClassBased\SkipClassBasedErrors(new ClassBased\EnumCasesChanged()),
new ClassBased\SkipClassBasedErrors(new ClassBased\OpenClassChanged(
new ClassBased\MultipleChecksOnAClass(
new ClassBased\SkipClassBasedErrors(new ClassBased\ConstantChanged(
Expand Down
162 changes: 162 additions & 0 deletions src/DetectChanges/BCBreak/ClassBased/EnumCasesChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\DetectChanges\BCBreak\ClassBased;

use Psl\Regex;
use Roave\BackwardCompatibility\Change;
use Roave\BackwardCompatibility\Changes;
use Roave\BetterReflection\Reflection\ReflectionClass;
use Roave\BetterReflection\Reflection\ReflectionEnum;
use Roave\BetterReflection\Reflection\ReflectionEnumCase;

use function array_filter;
use function array_map;

class EnumCasesChanged implements ClassBased
bdsl marked this conversation as resolved.
Show resolved Hide resolved
{
public function __invoke(ReflectionClass $fromClass, ReflectionClass $toClass): Changes
{
$fromEnumName = $fromClass->getName();
$fromKind = $this->kindOf($fromClass);
$toKind = $this->kindOf($toClass);

if (! $fromClass instanceof ReflectionEnum && ! $toClass instanceof ReflectionEnum) {
return Changes::empty();
}

if (! $fromClass instanceof ReflectionEnum && $toClass instanceof ReflectionEnum) {
return Changes::fromList(Change::changed($fromKind . ' ' . $fromEnumName . ' became enum'));
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
}
Ocramius marked this conversation as resolved.
Show resolved Hide resolved

if (! $toClass instanceof ReflectionEnum) {
return Changes::fromList(Change::changed('enum ' . $fromEnumName . ' became ' . $toKind));
}

$addedCases = array_filter(
$toClass->getCases(),
static function (ReflectionEnumCase $case) use ($fromClass): bool {
if (self::isInternalDocComment($case->getDocComment())) {
return false;
}

return ! $fromClass->hasCase($case->getName());
},
);

$removedCases = array_filter(
$fromClass->getCases(),
static function (ReflectionEnumCase $case) use ($toClass): bool {
if (self::isInternalDocComment($case->getDocComment())) {
return false;
}

return ! $toClass->hasCase($case->getName());
},
);

$internalisedCases = array_filter(
$toClass->getCases(),
static function (ReflectionEnumCase $case) use ($fromClass) {
if (! self::isInternalDocComment($case->getDocComment())) {
return false;
}

$fromClassCase = $fromClass->getCase($case->getName());
if (! $fromClassCase) {
return false;
}

return ! self::isInternalDocComment($fromClassCase->getDocComment());
},
);

$nowNotInternalCases = array_filter(
$toClass->getCases(),
static function (ReflectionEnumCase $case) use ($fromClass) {
if (self::isInternalDocComment($case->getDocComment())) {
return false;
}

$fromClassCase = $fromClass->getCase($case->getName());
if (! $fromClassCase) {
return false;
}

return self::isInternalDocComment($fromClassCase->getDocComment());
},
);

$caseRemovedChanges = array_map(
static function (ReflectionEnumCase $case) use ($fromEnumName): Change {
$caseName = $case->getName();

return Change::removed('Case ' . $fromEnumName . '::' . $caseName . ' was removed');
},
$removedCases,
);

$caseAddedChanges = array_map(
static function (ReflectionEnumCase $case) use ($fromEnumName): Change {
$caseName = $case->getName();

return Change::added('Case ' . $fromEnumName . '::' . $caseName . ' was added');
},
$addedCases,
);

$caseBecameInternalChanges = array_map(
static function (ReflectionEnumCase $case) use ($fromEnumName): Change {
$caseName = $case->getName();

return Change::changed('Case ' . $fromEnumName . '::' . $caseName . ' was marked "@internal"');
},
$internalisedCases,
);

$caseBecameNotInternalChanges = array_map(
static function (ReflectionEnumCase $case) use ($fromEnumName): Change {
$caseName = $case->getName();

return Change::changed('Case ' . $fromEnumName . '::' . $caseName . ' had "@internal" removed');
},
$nowNotInternalCases,
);

return Changes::fromList(
...$caseRemovedChanges,
...$caseAddedChanges,
...$caseBecameInternalChanges,
...$caseBecameNotInternalChanges,
);
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Copied from DetectChanges\BCBreak\ClassBased\ExcludeInternalClass - for now I'm not sure
* if there's a good place to put a shared function, and following the 3 strike then refactor rule.
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
*/
private static function isInternalDocComment(string|null $comment): bool
{
return $comment !== null
&& Regex\matches($comment, '/\s+@internal\s+/');
}

/** @psalm-return 'enum'|'interface'|'trait'|'class' */
private function kindOf(ReflectionClass $reflectionClass): string
{
if ($reflectionClass->isEnum()) {
return 'enum';
}

if ($reflectionClass->isInterface()) {
return 'interface';
}

if ($reflectionClass->isTrait()) {
return 'trait';
}

return 'class';
}
}
42 changes: 42 additions & 0 deletions test/asset/api/new/EnumWithCasesBeingChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace RoaveTestAsset;

enum EnumWithCasesBeingChanged
{
// two new months below:
case January;
case February;

/** @internal - tired from the two new Months, reserving March for private use from now on.*/
case March;

case April;
case May;
case June;
case July;

// We're on holiday in August, not going to allow that month any more.
// case August;

case September;
case October;
case November;
case December;

/**
* For testing now but for use only in a future major version. As it should not be statically referenced from
* outside this library, and the library will not pass a reference to it to the outside at runtime, adding it
* is not a BC break.
* @internal
*/
case Sol;

/**
* Previously internal, now public. This is a BC break as we previously committed to never return an instance
* of this case and we no-longer make this commitment.
*/
case UnknownMonth;
}
29 changes: 29 additions & 0 deletions test/asset/api/old/EnumWithCasesBeingChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace RoaveTestAsset;

enum EnumWithCasesBeingChanged
{
case March;
case April;
case May;
case June;
case July;
case August;
case September;
case October;
case November;
Case december; // oops forget this is spelled with a capital D

/**
* @internal - may be removed without notice
*/
case FakeMonth;

/**
* @internal - may be introduced in future
*/
case UnknownMonth;
}
10 changes: 10 additions & 0 deletions test/unit/DetectChanges/BCBreak/ClassBased/DummyEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace RoaveTest\BackwardCompatibility\DetectChanges\BCBreak\ClassBased;

enum DummyEnum
{
case someCase;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace RoaveTest\BackwardCompatibility\DetectChanges\BCBreak\ClassBased;

use PHPUnit\Framework\TestCase;
use Roave\BackwardCompatibility\Change;
use Roave\BackwardCompatibility\DetectChanges\BCBreak\ClassBased\EnumCasesChanged;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflection\ReflectionClass;
use Roave\BetterReflection\Reflector\DefaultReflector;
use Roave\BetterReflection\SourceLocator\Type\SingleFileSourceLocator;
use stdClass;

use function array_map;
use function iterator_to_array;

final class EnumCasesChangedTest extends TestCase
Ocramius marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* @param string[] $expectedMessages
*
* @dataProvider enumsToBeTested
*/
public function testDiffs(
ReflectionClass $fromEnum,
ReflectionClass $toEnum,
array $expectedMessages,
): void {
$changes = (new EnumCasesChanged())($fromEnum, $toEnum);

self::assertSame(
$expectedMessages,
array_map(static function (Change $change): string {
return $change->__toString();
}, iterator_to_array($changes)),
);
}

public function testReturnsClassBecameEnumError(): void
{
$changes = (new EnumCasesChanged())(
ReflectionClass::createFromName(stdClass::class),
ReflectionClass::createFromName(DummyEnum::class),
);

$this->assertEquals(
[Change::changed('class stdClass became enum')],
iterator_to_array($changes),
);
}

public function testReturnsEnumBecameClassError(): void
{
$changes = (new EnumCasesChanged())(
ReflectionClass::createFromName(DummyEnum::class),
ReflectionClass::createFromName(stdClass::class),
);

$this->assertEquals(
[Change::changed('enum RoaveTest\BackwardCompatibility\DetectChanges\BCBreak\ClassBased\DummyEnum became class')],
iterator_to_array($changes),
);
}

/**
* @return array<string, array<int, ReflectionClass|array<int, string>>>
* @psalm-return array<string, array{0: ReflectionClass, 1: ReflectionClass, 2: list<string>}>
*/
public function enumsToBeTested(): array
{
$locator = (new BetterReflection())->astLocator();

return [
'RoaveTestAsset\\EnumWithCasesBeingChanged' => [
(new DefaultReflector(new SingleFileSourceLocator(
__DIR__ . '/../../../../asset/api/old/EnumWithCasesBeingChanged.php',
$locator,
)))->reflectClass('RoaveTestAsset\EnumWithCasesBeingChanged'),
(new DefaultReflector(new SingleFileSourceLocator(
__DIR__ . '/../../../../asset/api/new/EnumWithCasesBeingChanged.php',
$locator,
)))->reflectClass('RoaveTestAsset\EnumWithCasesBeingChanged'),
[
'[BC] REMOVED: Case RoaveTestAsset\EnumWithCasesBeingChanged::August was removed',
'[BC] REMOVED: Case RoaveTestAsset\EnumWithCasesBeingChanged::december was removed',
'[BC] ADDED: Case RoaveTestAsset\EnumWithCasesBeingChanged::January was added',
'[BC] ADDED: Case RoaveTestAsset\EnumWithCasesBeingChanged::February was added',
'[BC] ADDED: Case RoaveTestAsset\EnumWithCasesBeingChanged::December was added',
'[BC] CHANGED: Case RoaveTestAsset\EnumWithCasesBeingChanged::March was marked "@internal"',
'[BC] CHANGED: Case RoaveTestAsset\EnumWithCasesBeingChanged::UnknownMonth had "@internal" removed',
],
],
];
}
}
Loading