diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bda45f1b..92a1a9c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" env: php-extensions: ds,yaml @@ -33,6 +34,7 @@ jobs: - uses: "ramsey/composer-install@v2" with: dependency-versions: ${{ matrix.dependencies }} + composer-options: "--ignore-platform-reqs" # Remove when Psalm supports PHP 8.4 / @see https://github.com/vimeo/psalm/pull/10928 - name: Running unit tests run: php vendor/bin/phpunit --testsuite=unit diff --git a/composer.json b/composer.json index e9a9226b..3113cb2f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "composer-runtime-api": "^2.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, diff --git a/composer.lock b/composer.lock index 5fee4489..474705b0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d8ad2085ad373dd8ded5f55b062aab34", + "content-hash": "410dad193a0cb42759cf51b835de7831", "packages": [ { "name": "psr/simple-cache", @@ -1185,12 +1185,12 @@ "version": "v5.2.13", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", + "url": "https://github.com/jsonrainbow/json-schema.git", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", "shasum": "" }, @@ -5484,13 +5484,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "composer-runtime-api": "^2.0" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php index 59c61519..7daab277 100644 --- a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php @@ -19,6 +19,7 @@ use function array_map; use function str_ends_with; +use function str_starts_with; /** @internal */ final class ReflectionFunctionDefinitionRepository implements FunctionDefinitionRepository @@ -57,7 +58,8 @@ public function for(callable $function): FunctionDefinition $class = $reflection->getClosureScopeClass(); $returnType = $returnTypeResolver->resolveReturnTypeFor($reflection); $nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection); - $isClosure = $name === '{closure}' || str_ends_with($name, '\\{closure}'); + // PHP8.2 use `ReflectionFunction::isAnonymous()` + $isClosure = $name === '{closure}' || str_ends_with($name, '\\{closure}') || str_starts_with($name, '{closure:'); if ($returnType instanceof UnresolvableType) { $returnType = $returnType->forFunctionReturnType($signature); @@ -83,7 +85,8 @@ public function for(callable $function): FunctionDefinition */ private function signature(ReflectionFunction $reflection): string { - if (str_contains($reflection->name, '{closure}')) { + // PHP8.2 use `ReflectionFunction::isAnonymous()` + if ($reflection->name === '{closure}' || str_ends_with($reflection->name, '\\{closure}') || str_starts_with($reflection->name, '{closure:')) { $startLine = $reflection->getStartLine(); $endLine = $reflection->getEndLine(); diff --git a/src/Type/Parser/Factory/Specifications/AliasSpecification.php b/src/Type/Parser/Factory/Specifications/AliasSpecification.php index 236a1586..0e45e572 100644 --- a/src/Type/Parser/Factory/Specifications/AliasSpecification.php +++ b/src/Type/Parser/Factory/Specifications/AliasSpecification.php @@ -88,11 +88,17 @@ private function resolveNamespaced(string $symbol): string } } - if (! $reflection->inNamespace()) { + if ($reflection->inNamespace()) { + $namespace = $reflection->getNamespaceName(); + } elseif ($reflection instanceof ReflectionFunction) { + $namespace = PhpParser::parseNamespace($reflection); + } + + if (! isset($namespace)) { return $symbol; } - $full = $reflection->getNamespaceName() . '\\' . $symbol; + $full = "$namespace\\$symbol"; if (Reflection::classOrInterfaceExists($full)) { return $full; diff --git a/src/Utility/Reflection/NamespaceFinder.php b/src/Utility/Reflection/NamespaceFinder.php new file mode 100644 index 00000000..f7f9d06d --- /dev/null +++ b/src/Utility/Reflection/NamespaceFinder.php @@ -0,0 +1,34 @@ +is(T_NAMESPACE)) { + /* @infection-ignore-all Unneeded because of the nature of namespace-related token */ + if ($pointer === 0) { + return null; + } + + $pointer--; + } + + while (! $tokens[$pointer]->is([T_NAME_QUALIFIED, T_STRING])) { + $pointer++; + } + + return (string)$tokens[$pointer]; + } +} diff --git a/src/Utility/Reflection/PhpParser.php b/src/Utility/Reflection/PhpParser.php index 9f08755d..19de7caa 100644 --- a/src/Utility/Reflection/PhpParser.php +++ b/src/Utility/Reflection/PhpParser.php @@ -24,7 +24,7 @@ final class PhpParser * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection * @return array */ - public static function parseUseStatements(\ReflectionClass|\ReflectionFunction|\ReflectionMethod $reflection): array + public static function parseUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array { $signature = "{$reflection->getFileName()}:{$reflection->getStartLine()}"; @@ -32,15 +32,23 @@ public static function parseUseStatements(\ReflectionClass|\ReflectionFunction|\ return self::$statements[$signature] ??= self::fetchUseStatements($reflection); } + public static function parseNamespace(ReflectionFunction $reflection): ?string + { + $content = self::getFileContent($reflection); + + if ($content === null) { + return null; + } + + return (new NamespaceFinder())->findNamespace($content); + } + /** * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection * @return array */ - private static function fetchUseStatements(\ReflectionClass|\ReflectionFunction|\ReflectionMethod $reflection): array + private static function fetchUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array { - $filename = $reflection->getFileName(); - $startLine = $reflection->getStartLine(); - if ($reflection instanceof ReflectionMethod) { $namespaceName = $reflection->getDeclaringClass()->getNamespaceName(); } elseif ($reflection instanceof ReflectionFunction && $reflection->getClosureScopeClass()) { @@ -49,29 +57,38 @@ private static function fetchUseStatements(\ReflectionClass|\ReflectionFunction| $namespaceName = $reflection->getNamespaceName(); } - // @infection-ignore-all these values will never be `true` - if ($filename === false || $startLine === false) { - return []; - } + $content = self::getFileContent($reflection); - if (! is_file($filename)) { + if ($content === null) { return []; } - $content = self::getFileContent($filename, $startLine); - return (new TokenParser($content))->parseUseStatements($namespaceName); } - private static function getFileContent(string $filename, int $lineNumber): string + /** + * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection + */ private static function getFileContent(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): ?string { + $filename = $reflection->getFileName(); + $startLine = $reflection->getStartLine(); + + // @infection-ignore-all these values will never be `true` + if ($filename === false || $startLine === false) { + return null; + } + + if (! is_file($filename)) { + return null; + } + // @infection-ignore-all no need to test with `-1` $lineCnt = 0; $content = ''; $file = new SplFileObject($filename); while (! $file->eof()) { - if ($lineCnt++ === $lineNumber) { + if ($lineCnt++ === $startLine) { break; } diff --git a/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php b/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php index 95b6a465..306942ec 100644 --- a/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php +++ b/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php @@ -39,7 +39,11 @@ public function test_function_data_can_be_retrieved(): void $function = $this->repository->for($callback); $parameters = $function->parameters; - self::assertSame(__NAMESPACE__ . '\{closure}', $function->name); + if (PHP_VERSION_ID < 8_04_00) { + self::assertSame(__NAMESPACE__ . '\{closure}', $function->name); + } else { + self::assertSame('{closure:' . self::class . '::' . __FUNCTION__ . '():37}', $function->name); + } self::assertInstanceOf(NativeStringType::class, $function->returnType); self::assertTrue($parameters->has('foo')); diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php index 76fad582..19687a64 100644 --- a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php @@ -98,7 +98,7 @@ public static function native_type_is_resolved_properly_data_provider(): iterabl } // PHP8.2 move to data provider - #[RequiresPhp('8.2')] + #[RequiresPhp('>=8.2')] public function test_disjunctive_normal_form_type_is_resolved_properly(): void { $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativeDisjunctiveNormalFormType::class, 'someProperty'))->getType(); @@ -109,7 +109,7 @@ public function test_disjunctive_normal_form_type_is_resolved_properly(): void } // PHP8.2 move to data provider - #[RequiresPhp('8.2')] + #[RequiresPhp('>=8.2')] public function test_native_null_type_is_resolved_properly(): void { $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeNull'))->getType(); @@ -120,7 +120,7 @@ public function test_native_null_type_is_resolved_properly(): void } // PHP8.2 move to data provider - #[RequiresPhp('8.2')] + #[RequiresPhp('>=8.2')] public function test_native_true_type_is_resolved_properly(): void { $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeTrue'))->getType(); @@ -131,7 +131,7 @@ public function test_native_true_type_is_resolved_properly(): void } // PHP8.2 move to data provider - #[RequiresPhp('8.2')] + #[RequiresPhp('>=8.2')] public function test_native_false_type_is_resolved_properly(): void { $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeFalse'))->getType(); diff --git a/tests/Functional/Utility/Reflection/PhpParserTest.php b/tests/Functional/Utility/Reflection/PhpParserTest.php new file mode 100644 index 00000000..a2a3c91c --- /dev/null +++ b/tests/Functional/Utility/Reflection/PhpParserTest.php @@ -0,0 +1,34 @@ +