-
-
Notifications
You must be signed in to change notification settings - Fork 165
PHP 8.5 feature-support audit: harness and fixtures #597
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
Merged
+551
−0
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
2105493
Add PHP 8.5 audit harness and feature fixtures
claude 2822ca7
Reorganize audit fixtures into own autoload root; encode confirmed gaps
claude fcaeed9
Remove stale audit output artifacts from tree
claude 69c36d3
Move audit fixtures into existing tests/Stubs folder
claude 274a1f7
Merge remote-tracking branch 'origin/master' into claude/php85-framew…
claude a12817d
Merge master; flip audit expectations for fixed #601/#602
claude d12a4d4
Merge remote-tracking branch 'origin/master' into claude/php85-framew…
claude 7235861
Sync with master after #613; pin follow-up gaps #615/#616
claude 2723ad8
Make #615 gap expectation version-aware
claude b20cad4
Merge remote-tracking branch 'origin/master' into claude/php85-framew…
claude 44d1b2c
Sync with master after #614; enum const-expression cases now asserted…
claude fedc6f9
Flatten audit stubs into tests/Stubs
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types = 1); | ||
| /* | ||
| * PHP 8.5 AUDIT HARNESS (temporary, lives only on the audit branch). | ||
| * Weaves PHP 8.1-8.5 feature fixtures from _files/audit through the real | ||
| * WeavingTransformer and validates that both the woven trait and the | ||
| * generated proxy still lint and preserve the feature under test. | ||
| */ | ||
|
|
||
| namespace Go\Instrument\Transformer; | ||
|
|
||
| use Go\Aop\Advisor; | ||
| use Go\Core\AdviceMatcherInterface; | ||
| use Go\Core\AspectContainer; | ||
| use Go\Core\AspectKernel; | ||
| use Go\Core\AspectLoader; | ||
| use Go\Instrument\ClassLoading\CachePathManager; | ||
| use Go\VirtualFileSystem\FileSystem; | ||
| use PHPUnit\Framework\Attributes\DataProvider; | ||
| use PHPUnit\Framework\TestCase; | ||
| use ReflectionClass; | ||
|
|
||
| #[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations] | ||
| class Php85AuditScratchTest extends TestCase | ||
| { | ||
| private const FIXTURE_DIR = __DIR__ . '/../../Stubs'; | ||
|
|
||
| /** Audit fixture stubs living in tests/Stubs alongside the general-purpose stubs. */ | ||
| private const AUDIT_FIXTURES = [ | ||
| 'Collaborator', | ||
| 'ConstAttr', | ||
| 'ExprAttr', | ||
| 'Php80ClassAttrPlain', | ||
| 'Php80GlobalConstAttrArg', | ||
| 'Php81EnumConstExprCases', | ||
| 'Php81NewInInitializers', | ||
| 'Php81NonScalarAttributeArgs', | ||
| 'Php85CloneWith', | ||
| 'Php85ClosuresInConstExpr', | ||
| 'Php85ConstAttributes', | ||
| 'Php85FinalPromotionAsymStatic', | ||
| 'Php85NoDiscard', | ||
| 'Php85PipeOperator', | ||
| 'RichAttr', | ||
| 'Status', | ||
| ]; | ||
|
|
||
| protected static FileSystem $fileSystem; | ||
|
|
||
| protected WeavingTransformer $transformer; | ||
|
|
||
| protected ?AspectKernel $kernel; | ||
|
|
||
| protected ?CachePathManager $cachePathManager; | ||
|
|
||
| public static function setUpBeforeClass(): void | ||
| { | ||
| static::$fileSystem = FileSystem::mount('vfs'); | ||
| if (!is_dir(self::outDir())) { | ||
| mkdir(self::outDir(), 0777, true); | ||
| } | ||
| } | ||
|
|
||
| private static function outDir(): string | ||
| { | ||
| return sys_get_temp_dir() . '/php85-audit-out'; | ||
| } | ||
|
|
||
| public static function tearDownAfterClass(): void | ||
| { | ||
| static::$fileSystem->unmount(); | ||
| } | ||
|
|
||
| public function setUp(): void | ||
| { | ||
| $container = $this->getContainerMock(); | ||
| $loader = $this | ||
| ->getMockBuilder(AspectLoader::class) | ||
| ->setConstructorArgs([$container]) | ||
| ->getMock(); | ||
|
|
||
| $this->kernel = $this->getKernelMock( | ||
| [ | ||
| 'appDir' => dirname(__DIR__), | ||
| 'cacheDir' => 'vfs://', | ||
| 'cacheFileMode' => 0770, | ||
| 'includePaths' => [], | ||
| 'excludePaths' => [] | ||
| ], | ||
| $container | ||
| ); | ||
| $this->cachePathManager = new CachePathManager($this->kernel); | ||
|
|
||
| $this->transformer = new WeavingTransformer( | ||
| $this->kernel, | ||
| $this->getInterceptEverythingMatcher(), | ||
| $this->cachePathManager, | ||
| $loader | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, array{string}> | ||
| */ | ||
| public static function fixtureNames(): array | ||
| { | ||
| $names = []; | ||
| foreach (self::AUDIT_FIXTURES as $name) { | ||
| $names[$name] = [$name]; | ||
| } | ||
|
|
||
| return $names; | ||
| } | ||
|
|
||
| /** | ||
| * Fixtures currently known to produce a broken weave, keyed to their tracking issue. | ||
| * A fix PR that resolves one of these MUST remove the entry (the test then asserts success). | ||
| */ | ||
| private const KNOWN_GAPS = [ | ||
| // #598-#603 are all fixed on master. Remaining follow-ups (#615/#616, fixed by PR #617): | ||
| // #[\Attribute] on a trait only became a compile error in PHP 8.5, | ||
| // so these three are gaps on 8.5+ but weave cleanly on 8.4 | ||
| 'ConstAttr' => 'https://github.com/goaop/framework/issues/615', | ||
| 'ExprAttr' => 'https://github.com/goaop/framework/issues/615', | ||
| 'RichAttr' => 'https://github.com/goaop/framework/issues/615', | ||
| // new-in-initializer default copied onto the proxy hook property | ||
| 'Php81NewInInitializers' => 'https://github.com/goaop/framework/issues/616', | ||
| ]; | ||
|
|
||
| /** Fixtures whose KNOWN_GAPS entry applies only on PHP >= 8.5 (see above). */ | ||
| private const GAP_ONLY_ON_85 = ['ConstAttr' => true, 'ExprAttr' => true, 'RichAttr' => true]; | ||
|
|
||
| #[DataProvider('fixtureNames')] | ||
| public function testWeaveAndLint(string $name): void | ||
| { | ||
| // 8.5-only syntax cannot lint (nor natively reflect) on older runtimes | ||
| if (str_starts_with($name, 'Php85') && PHP_VERSION_ID < 80500) { | ||
| $this->markTestSkipped('Fixture uses PHP 8.5 syntax'); | ||
| } | ||
|
|
||
| $problems = $this->weaveAndCollectProblems($name); | ||
|
|
||
| $isKnownGap = isset(self::KNOWN_GAPS[$name]) | ||
| && (!isset(self::GAP_ONLY_ON_85[$name]) || PHP_VERSION_ID >= 80500); | ||
|
|
||
| if ($isKnownGap) { | ||
| $issue = self::KNOWN_GAPS[$name]; | ||
| $this->assertNotSame( | ||
| [], | ||
| $problems, | ||
| "$name weaves cleanly now — the gap tracked in $issue looks fixed. " . | ||
| 'Remove it from KNOWN_GAPS so this stays asserted.' | ||
| ); | ||
| $this->addToAssertionCount(1); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->assertSame([], $problems, "$name should weave cleanly:\n" . implode("\n---\n", $problems)); | ||
| } | ||
|
|
||
| /** | ||
| * @return list<string> Problems encountered (transform exception or lint failures); empty = clean weave | ||
| */ | ||
| private function weaveAndCollectProblems(string $name): array | ||
| { | ||
| $metadata = $this->loadAuditMetadata($name); | ||
|
|
||
| try { | ||
| $this->transformer->transform($metadata); | ||
| } catch (\Throwable $e) { | ||
| file_put_contents(self::outDir() . "/$name.ERROR.txt", (string) $e); | ||
|
|
||
| return ["TRANSFORM ERROR: {$e->getMessage()}"]; | ||
| } | ||
|
|
||
| $problems = []; | ||
| $woven = $metadata->source; | ||
| file_put_contents(self::outDir() . "/$name-woven.php", $woven); | ||
| $problems = [...$problems, ...$this->lintProblems(self::outDir() . "/$name-woven.php", "$name woven trait")]; | ||
|
|
||
| if (preg_match_all("/AOP_CACHE_DIR . '(.+)';$/m", $woven, $matches)) { | ||
| foreach ($matches[1] as $i => $proxyPath) { | ||
| $proxyContent = (string) file_get_contents('vfs://' . $proxyPath); | ||
| $suffix = $i > 0 ? "-$i" : ''; | ||
| file_put_contents(self::outDir() . "/$name-proxy$suffix.php", $proxyContent); | ||
| $problems = [...$problems, ...$this->lintProblems(self::outDir() . "/$name-proxy$suffix.php", "$name proxy #$i")]; | ||
| } | ||
| } | ||
|
|
||
| return $problems; | ||
| } | ||
|
|
||
| /** | ||
| * @return list<string> | ||
| */ | ||
| private function lintProblems(string $file, string $label): array | ||
| { | ||
| exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $code); | ||
| if ($code !== 0) { | ||
| return ["$label does not lint:\n" . implode("\n", $output)]; | ||
| } | ||
|
|
||
| return []; | ||
| } | ||
|
|
||
| private function getInterceptEverythingMatcher(): AdviceMatcherInterface | ||
| { | ||
| $mock = $this->createMock(AdviceMatcherInterface::class); | ||
| $mock | ||
| ->method('getAdvicesForClass') | ||
| ->willReturnCallback(function (ReflectionClass $refClass) { | ||
| $advices = []; | ||
| foreach ($refClass->getMethods() as $method) { | ||
| if ($method->getDeclaringClass()->name !== $refClass->name) { | ||
| continue; | ||
| } | ||
| $advisorId = "advisor.{$refClass->name}->{$method->name}"; | ||
| $advices[AspectContainer::METHOD_PREFIX][$method->name][$advisorId] = true; | ||
| } | ||
| foreach ($refClass->getProperties() as $property) { | ||
| if ($property->getDeclaringClass()->name !== $refClass->name) { | ||
| continue; | ||
| } | ||
| // Mirror the real AdviceMatcher gates (static/readonly/hooked are not interceptable) | ||
| if ($property->isStatic() || $property->isReadOnly() || $property->hasHooks()) { | ||
| continue; | ||
| } | ||
| $advisorId = "advisor.{$refClass->name}->{$property->name}"; | ||
| $advices[AspectContainer::PROPERTY_PREFIX][$property->name][$advisorId] = true; | ||
| } | ||
| return $advices; | ||
| }); | ||
| $mock->method('getAdvicesForFunctions')->willReturn([]); | ||
|
|
||
| return $mock; | ||
| } | ||
|
|
||
| protected function getKernelMock(array $options, AspectContainer $container): AspectKernel | ||
| { | ||
| $mock = $this->getMockBuilder(AspectKernel::class) | ||
| ->disableOriginalConstructor() | ||
| ->onlyMethods(['configureAop', 'getOptions', 'getContainer', 'hasFeature']) | ||
| ->getMock(); | ||
|
|
||
| $mock->method('getOptions')->willReturn($options); | ||
| $mock->method('getContainer')->willReturn($container); | ||
|
|
||
| return $mock; | ||
| } | ||
|
|
||
| private function loadAuditMetadata(string $name): StreamMetaData | ||
| { | ||
| $fileName = self::FIXTURE_DIR . '/' . $name . '.php'; | ||
| $stream = fopen('php://filter/string.tolower/resource=' . $fileName, 'r'); | ||
| $source = file_get_contents($fileName); | ||
| $metadata = new StreamMetaData($stream, $source); | ||
| fclose($stream); | ||
|
|
||
| return $metadata; | ||
| } | ||
|
|
||
| private function getContainerMock(): AspectContainer | ||
| { | ||
| $container = $this->createMock(AspectContainer::class); | ||
| $container | ||
| ->method('getServicesByInterface') | ||
| ->willReturnMap([ | ||
| [Advisor::class, []] | ||
| ]); | ||
|
|
||
| return $container; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| class Collaborator | ||
| { | ||
| public function __construct(public string $tag = 'default') | ||
| { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| #[\Attribute(\Attribute::TARGET_CONSTANT | \Attribute::TARGET_CLASS_CONSTANT)] | ||
| class ConstAttr | ||
| { | ||
| public function __construct(public string $reason = '') | ||
| { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| #[\Attribute(\Attribute::TARGET_ALL)] | ||
| class ExprAttr | ||
| { | ||
| public function __construct(public mixed $value = null) | ||
| { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| #[ExprAttr] | ||
| class Php80ClassAttrPlain | ||
| { | ||
| public function run(): int | ||
| { | ||
| return 42; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| class Php80GlobalConstAttrArg | ||
| { | ||
| #[ExprAttr(PHP_INT_MAX)] | ||
| public function limited(): int | ||
| { | ||
| return PHP_INT_MAX; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| enum Php81EnumConstExprCases: int | ||
| { | ||
| private const int SHIFT = 2; | ||
|
|
||
| case Negative = -1; | ||
| case Shifted = 1 << 2; | ||
| case FromConst = self::SHIFT + 10; | ||
|
|
||
| public function describe(): string | ||
| { | ||
| return $this->name . '=' . $this->value; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Go\Stubs; | ||
|
|
||
| class Php81NewInInitializers | ||
| { | ||
| public function __construct( | ||
| private Collaborator $service = new Collaborator('ctor-default'), | ||
| ) { | ||
| } | ||
|
|
||
| public function run(Collaborator $helper = new Collaborator('method-default')): string | ||
| { | ||
| static $memo = new \ArrayObject(); | ||
|
|
||
| return $this->service->tag . '/' . $helper->tag; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.