From aafba824e463fafdeeb12dd2a7bef4f42ce89b87 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 10:03:41 +0000 Subject: [PATCH] test: cover native-fallback reflection of internal #[\Deprecated] functions PHP 8.6 attaches #[\Deprecated] to a number of previously-undeprecated internal functions, strcoll() among them. Internal functions have no source file, so the AST parser can never reflect them and the native reflection is always used instead, which reports the attribute correctly without any change in this library. Pins that behaviour with parity tests next to the existing #[\Deprecated] coverage: - internal functions are never resolved to a parsed reflection, while parsed functions are always user-defined and remain drop-in \ReflectionFunction replacements; - utf8_encode(), deprecated since PHP 8.2, proves the mechanism is not specific to PHP 8.6, with strlen() as a negative control; - strcoll() reports the expected #[\Deprecated] payload on PHP 8.6 and reports no deprecation before it, pinning the transition from both sides. Refs #215 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Sy8BcM8ivUEpu8uVm7wADn --- tests/DeprecatedAndFunctionLikeGapsTest.php | 123 ++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/DeprecatedAndFunctionLikeGapsTest.php b/tests/DeprecatedAndFunctionLikeGapsTest.php index beb0f56..48e8746 100644 --- a/tests/DeprecatedAndFunctionLikeGapsTest.php +++ b/tests/DeprecatedAndFunctionLikeGapsTest.php @@ -53,6 +53,23 @@ class DeprecatedAndFunctionLikeGapsTest extends TestCase 'php84ImportedDeprecatedFunction', ]; + /** + * Internal function that receives the #[\Deprecated] attribute in PHP 8.6 only + * + * @see https://github.com/goaop/parser-reflection/issues/215 + */ + private const PHP86_DEPRECATED_INTERNAL_FUNCTION = 'strcoll'; + + /** + * Internal function that already carries the #[\Deprecated] attribute long before PHP 8.6 + */ + private const LEGACY_DEPRECATED_INTERNAL_FUNCTION = 'utf8_encode'; + + /** + * Internal function that is not deprecated at all, used as a negative control + */ + private const PLAIN_INTERNAL_FUNCTION = 'strlen'; + private string $stubFileName; protected function setUp(): void @@ -216,6 +233,112 @@ public function testCreateFromMethodNameParityWithNativeReflection(): void $this->assertSame($nativeMethod->isDeprecated(), $parsedMethod->isDeprecated()); } + /** + * Internal functions have no source file, so the AST parser can never reflect them and the + * native reflection is always used for them instead. + */ + public function testInternalFunctionsAreReflectedByTheNativeFallback(): void + { + $parsedNamespace = $this->getStubFileNamespace(); + $internalFunctions = [ + self::PHP86_DEPRECATED_INTERNAL_FUNCTION, + self::LEGACY_DEPRECATED_INTERNAL_FUNCTION, + self::PLAIN_INTERNAL_FUNCTION, + ]; + + foreach ($internalFunctions as $functionName) { + $this->assertTrue(function_exists($functionName), "Function {$functionName}() should be available"); + + $nativeFunction = new \ReflectionFunction($functionName); + $this->assertTrue($nativeFunction->isInternal(), "Function {$functionName}() should be internal"); + $this->assertFalse( + $nativeFunction->getFileName(), + "Function {$functionName}() has no source file, therefore it can not be parsed" + ); + $this->assertFalse( + $parsedNamespace->hasFunction($functionName), + "Function {$functionName}() should never be resolved to a parsed reflection" + ); + } + + // Parsed functions are always user-defined, so internal ones can only be served by the native + // reflection, and the parsed one is a drop-in replacement for the native \ReflectionFunction + $parsedFunction = $parsedNamespace->getFunction('php84PlainFunction'); + $this->assertInstanceOf(\ReflectionFunction::class, $parsedFunction); + $this->assertFalse($parsedFunction->isInternal()); + $this->assertTrue($parsedFunction->isUserDefined()); + } + + /** + * The #[\Deprecated] status of internal functions is reflected on every supported runtime, + * the mechanism is not specific to the functions deprecated in PHP 8.6. + */ + public function testDeprecatedInternalFunctionIsReflectedOnEveryRuntime(): void + { + $functionName = self::LEGACY_DEPRECATED_INTERNAL_FUNCTION; + $nativeFunction = new \ReflectionFunction($functionName); + + $this->assertTrue($nativeFunction->isDeprecated(), "Function {$functionName}() is deprecated since PHP 8.2"); + $this->assertSame('8.2', $this->getDeprecatedAttributeArguments($nativeFunction)['since']); + $this->assertStringContainsString('deprecated', $nativeFunction->__toString()); + + $plainFunction = new \ReflectionFunction(self::PLAIN_INTERNAL_FUNCTION); + $this->assertFalse($plainFunction->isDeprecated()); + $this->assertSame([], $plainFunction->getAttributes(\Deprecated::class)); + $this->assertStringNotContainsString('deprecated', $plainFunction->__toString()); + } + + public function testInternalFunctionDeprecatedInPhp86IsReflectedWithItsAttributePayload(): void + { + if (PHP_VERSION_ID < 80600) { + $this->markTestSkipped('Internal function strcoll() is deprecated since PHP 8.6 only'); + } + + $nativeFunction = new \ReflectionFunction(self::PHP86_DEPRECATED_INTERNAL_FUNCTION); + + $this->assertTrue($nativeFunction->isDeprecated()); + $arguments = $this->getDeprecatedAttributeArguments($nativeFunction); + $this->assertSame('8.6', $arguments['since']); + $this->assertSame('use Collator::compare() instead', $arguments['message']); + $this->assertStringContainsString('deprecated', $nativeFunction->__toString()); + + $deprecated = $nativeFunction->getAttributes(\Deprecated::class)[0]->newInstance(); + $this->assertInstanceOf(\Deprecated::class, $deprecated); + $this->assertSame('8.6', $deprecated->since); + } + + public function testInternalFunctionDeprecatedInPhp86IsNotDeprecatedBefore(): void + { + if (PHP_VERSION_ID >= 80600) { + $this->markTestSkipped('Internal function strcoll() is deprecated since PHP 8.6'); + } + + $nativeFunction = new \ReflectionFunction(self::PHP86_DEPRECATED_INTERNAL_FUNCTION); + + $this->assertFalse($nativeFunction->isDeprecated()); + $this->assertSame([], $nativeFunction->getAttributes(\Deprecated::class)); + $this->assertStringNotContainsString('deprecated', $nativeFunction->__toString()); + } + + /** + * Returns the arguments of the single #[\Deprecated] attribute of the given function + * + * @return array + */ + private function getDeprecatedAttributeArguments(\ReflectionFunction $function): array + { + $functionName = $function->getName(); + $attributes = $function->getAttributes(\Deprecated::class); + $this->assertCount(1, $attributes, "Function {$functionName}() should have one #[\\Deprecated] attribute"); + $this->assertSame(\Deprecated::class, $attributes[0]->getName()); + + /** @var array $arguments */ + $arguments = $attributes[0]->getArguments(); + $this->assertArrayHasKey('since', $arguments, "Function {$functionName}() should report the deprecation version"); + + return $arguments; + } + /** * Returns a locator that resolves the stub class without asking composer for it */