diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index 53831dbc..ec12d7f9 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -9,8 +9,8 @@ on: # yamllint disable-line rule:truthy - "main" env: - MIN_COVERED_MSI: 86 - MIN_MSI: 84 + MIN_COVERED_MSI: 85 + MIN_MSI: 82 PHP_EXTENSIONS: "mbstring, tokenizer" jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index fafadac2..e7630d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased -For a full diff see [`1.1.1...main`][1.1.1...main]. +For a full diff see [`1.2.0...main`][1.2.0...main]. + +## [`1.2.0`][1.2.0] + +For a full diff see [`1.1.1...1.2.0`][1.1.0...1.2.0]. + +### Added + +- Added support for `enum` ([#478]), by [@localheinz] ### Deprecated @@ -105,6 +113,7 @@ For a full diff see [`0.4.0...0.5.0`][0.4.0...0.5.0]. [1.0.1]: https://github.com/localheinz/ergebnis/classy/releases/tag/1.0.1 [1.1.0]: https://github.com/localheinz/ergebnis/classy/releases/tag/1.1.0 [1.1.1]: https://github.com/localheinz/ergebnis/classy/releases/tag/1.1.1 +[1.2.0]: https://github.com/localheinz/ergebnis/classy/releases/tag/1.2.0 [0.4.0...0.5.0]: https://github.com/ergebnis/classy/compare/0.4.0...0.5.0 [0.5.0...0.5.1]: https://github.com/ergebnis/classy/compare/0.5.0...0.5.1 @@ -113,7 +122,8 @@ For a full diff see [`0.4.0...0.5.0`][0.4.0...0.5.0]. [1.0.0...1.0.1]: https://github.com/ergebnis/classy/compare/1.0.0...1.0.1 [1.0.1...1.1.0]: https://github.com/ergebnis/classy/compare/1.0.1...1.1.0 [1.1.0...1.1.1]: https://github.com/ergebnis/classy/compare/1.1.0...1.1.1 -[1.1.1...main]: https://github.com/ergebnis/classy/compare/1.1.1...main +[1.1.1...1.2.0]: https://github.com/ergebnis/classy/compare/1.1.1...1.2.0 +[1.2.0...main]: https://github.com/ergebnis/classy/compare/1.2.0...main [#77]: https://github.com/ergebnis/classy/pull/77 [#88]: https://github.com/ergebnis/classy/pull/88 @@ -123,6 +133,7 @@ For a full diff see [`0.4.0...0.5.0`][0.4.0...0.5.0]. [#235]: https://github.com/ergebnis/classy/pull/235 [#343]: https://github.com/ergebnis/classy/pull/343 [#467]: https://github.com/ergebnis/classy/pull/467 +[#478]: https://github.com/ergebnis/classy/pull/478 [@ergebnis]: https://github.com/ergebnis [@localheinz]: https://github.com/localheinz diff --git a/Makefile b/Makefile index 493b4e47..e103b3dc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -MIN_COVERED_MSI:=86 -MIN_MSI:=84 +MIN_COVERED_MSI:=85 +MIN_MSI:=82 .PHONY: it it: coding-standards static-code-analysis tests ## Runs the coding-standards, static-code-analysis, and tests targets diff --git a/composer-require-checker.json b/composer-require-checker.json index a5333c2b..a95736df 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -15,6 +15,7 @@ "string", "true", "void", + "T_ENUM", "T_NAME_QUALIFIED" ] } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a33c40b..0a3d0e17 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -18,6 +18,21 @@ $constructs + + + Qux + + + + + Qux + + + + + Qux + + __toString @@ -27,6 +42,38 @@ + + + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Bar::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Baz::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Foo::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Qux::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Bar::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Baz::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Foo::class + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Qux::class + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Bar::class + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Baz::class + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Foo::class + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Qux::class + Test\Fixture\Classy\Php81\WithinNamespace\Qux::class + + + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Bar + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Baz + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Foo + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Qux + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Bar + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Baz + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Foo + Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Qux + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Bar + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Baz + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Foo + Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Qux + Test\Fixture\Classy\Php81\WithinNamespace\Qux + + \array_values diff --git a/src/Constructs.php b/src/Constructs.php index 26df6f82..68de484d 100644 --- a/src/Constructs.php +++ b/src/Constructs.php @@ -51,6 +51,22 @@ public static function fromSource(string $source): array ]; } + $classyTokens = [ + \T_CLASS, + \T_INTERFACE, + \T_TRAIT, + ]; + + // https://wiki.php.net/rfc/enumerations + if (\PHP_VERSION_ID >= 80100 && \defined('T_ENUM')) { + $classyTokens = [ + \T_CLASS, + \T_ENUM, + \T_INTERFACE, + \T_TRAIT, + ]; + } + for ($index = 0; $index < $count; ++$index) { $token = $sequence[$index]; @@ -80,7 +96,7 @@ public static function fromSource(string $source): array } // skip non-classy tokens - if (!\is_array($token) || !\in_array($token[0], [\T_CLASS, \T_INTERFACE, \T_TRAIT], true)) { + if (!\is_array($token) || !\in_array($token[0], $classyTokens, true)) { continue; } diff --git a/test/Fixture/Classy/Php81/WithMethodsNamedAfterKeywords/source.php b/test/Fixture/Classy/Php81/WithMethodsNamedAfterKeywords/source.php new file mode 100644 index 00000000..fe36d583 --- /dev/null +++ b/test/Fixture/Classy/Php81/WithMethodsNamedAfterKeywords/source.php @@ -0,0 +1,22 @@ +source()); + + $expected = \array_map(static function (Construct $construct): Construct { + return Construct::fromName($construct->name()); + }, $scenario->constructsSortedByName()); + + self::assertEquals($expected, $constructs); + } + + /** + * @requires PHP 8.1 + * + * @dataProvider provideScenarioWithClassyConstructsOnPhp81 + */ + public function testFromSourceReturnsArrayOfClassyConstructsWithoutFileNamesWhenClassyConstructsHaveBeenFoundOnPhp81(Test\Util\Scenario $scenario): void { $constructs = Constructs::fromSource($scenario->source()); @@ -154,9 +170,9 @@ public function testFromDirectoryReturnsEmptyArrayWhenNoClassyConstructsHaveBeen } /** - * @dataProvider provideScenarioWithClassyConstructs + * @dataProvider provideScenarioWithClassyConstructsBeforePhp81 */ - public function testFromDirectoryReturnsArrayOfClassyConstructsSortedByNameWhenClassyConstructsHaveBeenFound(Test\Util\Scenario $scenario): void + public function testFromDirectoryReturnsArrayOfClassyConstructsSortedByNameWhenClassyConstructsHaveBeenFoundBeforePhp81(Test\Util\Scenario $scenario): void { $constructs = Constructs::fromDirectory($scenario->directory()); @@ -166,20 +182,95 @@ public function testFromDirectoryReturnsArrayOfClassyConstructsSortedByNameWhenC /** * @return \Generator */ - public function provideScenarioWithClassyConstructs(): \Generator + public function provideScenarioWithClassyConstructsBeforePhp81(): \Generator + { + $phpVersion = Test\Util\PhpVersion::fromInt(80100); + + $scenariosWithClassyConstructs = \array_filter(self::scenariosWithClassyConstructs(), static function (Test\Util\Scenario $scenario) use ($phpVersion): bool { + return $scenario->phpVersion()->isLessThan($phpVersion); + }); + + foreach ($scenariosWithClassyConstructs as $scenario) { + yield $scenario->description() => [ + $scenario, + ]; + } + } + + /** + * @requires PHP 8.1 + * + * @dataProvider provideScenarioWithClassyConstructsOnPhp81 + */ + public function testFromDirectoryReturnsArrayOfClassyConstructsSortedByNameWhenClassyConstructsHaveBeenFoundOnPhp81(Test\Util\Scenario $scenario): void + { + $constructs = Constructs::fromDirectory($scenario->directory()); + + self::assertEquals($scenario->constructsSortedByName(), $constructs); + } + + /** + * @return \Generator + */ + public function provideScenarioWithClassyConstructsOnPhp81(): \Generator + { + $phpVersion = Test\Util\PhpVersion::fromInt(80100); + + $scenariosWithClassyConstructs = \array_filter(self::scenariosWithClassyConstructs(), static function (Test\Util\Scenario $scenario) use ($phpVersion): bool { + return $scenario->phpVersion()->isLessThanOrEqualTo($phpVersion); + }); + + foreach ($scenariosWithClassyConstructs as $scenario) { + yield $scenario->description() => [ + $scenario, + ]; + } + } + + public function testFromDirectoryTraversesDirectoriesAndReturnsArrayOfClassyConstructsSortedByName(): void + { + $classyConstructs = [ + Construct::fromName(Test\Fixture\Traversal\Foo::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo.php')), + Construct::fromName(Test\Fixture\Traversal\Foo\Bar::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo/Bar.php')), + Construct::fromName(Test\Fixture\Traversal\Foo\Baz::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo/Baz.php')), + ]; + + self::assertEquals($classyConstructs, Constructs::fromDirectory(__DIR__ . '/../Fixture/Traversal')); + } + + public function testFromDirectoryThrowsMultipleDefinitionsFoundIfMultipleDefinitionsOfSameConstructHaveBeenFound(): void + { + $this->expectException(Exception\MultipleDefinitionsFound::class); + + Constructs::fromDirectory(__DIR__ . '/../Fixture/MultipleDefinitions'); + } + + /** + * @return array + */ + private static function scenariosWithClassyConstructs(): array { - $scenariosWithClassyConstructs = [ + return [ Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace', + 'php72-within-namespace', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespace/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespace\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespace\Baz::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespace\Foo::class) ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-within-namespace', + __DIR__ . '/../Fixture/Classy/Php81/WithinNamespace/source.php', + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespace\Bar::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespace\Baz::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespace\Foo::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespace\Qux::class) + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace-and-shell-style-comments', + 'php72-within-namespace-and-shell-style-comments', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespaceAndShellStyleComments/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndShellStyleComments\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndShellStyleComments\Baz::class), @@ -187,7 +278,7 @@ public function provideScenarioWithClassyConstructs(): \Generator ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace-and-single-line-comments', + 'php72-within-namespace-and-single-line-comments', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespaceAndSingleLineComments/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndSingleLineComments\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndSingleLineComments\Baz::class), @@ -195,7 +286,7 @@ public function provideScenarioWithClassyConstructs(): \Generator ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace-and-multi-line-comments', + 'php72-within-namespace-and-multi-line-comments', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespaceAndMultiLineComments/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndMultiLineComments\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceAndMultiLineComments\Baz::class), @@ -203,15 +294,24 @@ public function provideScenarioWithClassyConstructs(): \Generator ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace-with-braces', + 'php72-within-namespace-with-braces', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespaceWithBraces/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceWithBraces\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceWithBraces\Baz::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinNamespaceWithBraces\Foo::class) ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-within-namespace-with-braces', + __DIR__ . '/../Fixture/Classy/Php81/WithinNamespaceWithBraces/source.php', + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Bar::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Baz::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Foo::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinNamespaceWithBraces\Qux::class) + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-multiple-namespaces-with-braces', + 'php72-within-multiple-namespaces-with-braces', __DIR__ . '/../Fixture/Classy/Php72/WithinMultipleNamespaces/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithinMultipleNamespaces\Bar\Bar::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinMultipleNamespaces\Bar\Baz::class), @@ -220,40 +320,83 @@ public function provideScenarioWithClassyConstructs(): \Generator Construct::fromName(Test\Fixture\Classy\Php72\WithinMultipleNamespaces\Foo\Baz::class), Construct::fromName(Test\Fixture\Classy\Php72\WithinMultipleNamespaces\Foo\Foo::class) ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-within-multiple-namespaces-with-braces', + __DIR__ . '/../Fixture/Classy/Php81/WithinMultipleNamespaces/source.php', + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Bar::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Baz::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Foo::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Bar\Qux::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Bar::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Baz::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Foo::class), + Construct::fromName(Test\Fixture\Classy\Php81\WithinMultipleNamespaces\Foo\Qux::class) + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'within-namespace-with-single-segment', + 'php72-within-namespace-with-single-segment', __DIR__ . '/../Fixture/Classy/Php72/WithinNamespaceWithSingleSegment/source.php', Construct::fromName('Ergebnis\\Bar'), Construct::fromName('Ergebnis\\Baz'), Construct::fromName('Ergebnis\\Foo') ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-within-namespace-with-single-segment', + __DIR__ . '/../Fixture/Classy/Php81/WithinNamespaceWithSingleSegment/source.php', + Construct::fromName('Ergebnis\\Bar'), + Construct::fromName('Ergebnis\\Baz'), + Construct::fromName('Ergebnis\\Foo'), + Construct::fromName('Ergebnis\\Qux') + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'with-methods-named-after-keywords', + 'php72-with-methods-named-after-keywords', __DIR__ . '/../Fixture/Classy/Php72/WithMethodsNamedAfterKeywords/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithMethodsNamedAfterKeywords\Foo::class) ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-with-methods-named-after-keywords', + __DIR__ . '/../Fixture/Classy/Php81/WithMethodsNamedAfterKeywords/source.php', + Construct::fromName(Test\Fixture\Classy\Php81\WithMethodsNamedAfterKeywords\Foo::class) + ), /** * @see https://github.com/zendframework/zend-file/pull/41 */ Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'with-methods-named-after-keywords-and-return-type', + 'php72-with-methods-named-after-keywords-and-return-type', __DIR__ . '/../Fixture/Classy/Php72/WithMethodsNamedAfterKeywordsAndReturnType/source.php', Construct::fromName(Test\Fixture\Classy\Php72\WithMethodsNamedAfterKeywordsAndReturnType\Foo::class) ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-with-methods-named-after-keywords-and-return-type', + __DIR__ . '/../Fixture/Classy/Php81/WithMethodsNamedAfterKeywordsAndReturnType/source.php', + Construct::fromName(Test\Fixture\Classy\Php81\WithMethodsNamedAfterKeywordsAndReturnType\Foo::class) + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'without-namespace', + 'php72-without-namespace', __DIR__ . '/../Fixture/Classy/Php72/WithoutNamespace/source.php', Construct::fromName('Bar'), Construct::fromName('Baz'), Construct::fromName('Foo') ), + Test\Util\Scenario::create( + Test\Util\PhpVersion::fromInt(80100), + 'php81-without-namespace', + __DIR__ . '/../Fixture/Classy/Php81/WithoutNamespace/source.php', + Construct::fromName('Bar'), + Construct::fromName('Baz'), + Construct::fromName('Foo'), + Construct::fromName('Qux') + ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'without-namespace-and-multi-line-comments', + 'php72-without-namespace-and-multi-line-comments', __DIR__ . '/../Fixture/Classy/Php72/WithoutNamespaceAndMultiLineComments/source.php', Construct::fromName('Quux'), Construct::fromName('Quuz'), @@ -261,7 +404,7 @@ public function provideScenarioWithClassyConstructs(): \Generator ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'without-namespace-and-shell-line-comments', + 'php72-without-namespace-and-shell-line-comments', __DIR__ . '/../Fixture/Classy/Php72/WithoutNamespaceAndShellStyleComments/source.php', Construct::fromName('Corge'), Construct::fromName('Garply'), @@ -269,37 +412,13 @@ public function provideScenarioWithClassyConstructs(): \Generator ), Test\Util\Scenario::create( Test\Util\PhpVersion::fromInt(70200), - 'without-namespace-and-single-line-comments', + 'php72-without-namespace-and-single-line-comments', __DIR__ . '/../Fixture/Classy/Php72/WithoutNamespaceAndSingleLineComments/source.php', Construct::fromName('Fred'), Construct::fromName('Plugh'), Construct::fromName('Waldo') ), ]; - - foreach ($scenariosWithClassyConstructs as $scenario) { - yield $scenario->description() => [ - $scenario, - ]; - } - } - - public function testFromDirectoryTraversesDirectoriesAndReturnsArrayOfClassyConstructsSortedByName(): void - { - $classyConstructs = [ - Construct::fromName(Test\Fixture\Traversal\Foo::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo.php')), - Construct::fromName(Test\Fixture\Traversal\Foo\Bar::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo/Bar.php')), - Construct::fromName(Test\Fixture\Traversal\Foo\Baz::class)->definedIn(self::realPath(__DIR__ . '/../Fixture/Traversal/Foo/Baz.php')), - ]; - - self::assertEquals($classyConstructs, Constructs::fromDirectory(__DIR__ . '/../Fixture/Traversal')); - } - - public function testFromDirectoryThrowsMultipleDefinitionsFoundIfMultipleDefinitionsOfSameConstructHaveBeenFound(): void - { - $this->expectException(Exception\MultipleDefinitionsFound::class); - - Constructs::fromDirectory(__DIR__ . '/../Fixture/MultipleDefinitions'); } private static function realPath(string $path): string diff --git a/test/Util/PhpVersion.php b/test/Util/PhpVersion.php index 2c8b9271..8919bfd8 100644 --- a/test/Util/PhpVersion.php +++ b/test/Util/PhpVersion.php @@ -37,4 +37,14 @@ public function toInt(): int { return $this->value; } + + public function isLessThan(self $other): bool + { + return $this->value < $other->value; + } + + public function isLessThanOrEqualTo(self $other): bool + { + return $this->value <= $other->value; + } }