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 */