From d7fe9a416c7a1a8273216b71250aa56057ae4169 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 13:43:47 +0800 Subject: [PATCH 01/17] Enhance ContractVisitor to respect @typephp-ignore tags and update tests for inline variable validation --- src/Internal/ContractVisitor.php | 16 +++++++++-- tests/TypeChecking/DocblockIgnoreTagsTest.php | 28 +++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Internal/ContractVisitor.php b/src/Internal/ContractVisitor.php index bc9c8f1..f30cd6e 100644 --- a/src/Internal/ContractVisitor.php +++ b/src/Internal/ContractVisitor.php @@ -5,6 +5,7 @@ namespace TypePHP\Internal; use PhpParser\Node; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use TypePHP\Internal\Visitor\FunctionContractInjector; use TypePHP\Internal\Visitor\NodeBuilder; @@ -26,9 +27,9 @@ public function __construct() /** * Traverses and transforms AST nodes during entry. * - * @return array|null + * @return array|int|null */ - public function enterNode(Node $node): array|null + public function enterNode(Node $node): array|int|null { if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod @@ -47,6 +48,15 @@ public function enterNode(Node $node): array|null } if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { + $doc = $node->getDocComment(); + if ($doc !== null) { + $docText = $doc->getText(); + $shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true); + if ($shouldRespectIgnore && (str_contains($docText, '@typephp-ignore') || str_contains($docText, '@typephp-disable'))) { + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + } + } + FunctionContractInjector::inject($node); return null; @@ -204,4 +214,4 @@ private function extractDestructuringVariables(Node\Expr\List_|Node\Expr\Array_ return $vars; } -} +} \ No newline at end of file diff --git a/tests/TypeChecking/DocblockIgnoreTagsTest.php b/tests/TypeChecking/DocblockIgnoreTagsTest.php index 6cbce28..1b5b930 100644 --- a/tests/TypeChecking/DocblockIgnoreTagsTest.php +++ b/tests/TypeChecking/DocblockIgnoreTagsTest.php @@ -28,9 +28,8 @@ function testIgnoredFunction(int $id): int test('skips type-checking on method marked with @typephp-ignore while enforcing normal methods in same class', function () { $fixture = new IgnoredMethod(); - expect(fn () => $fixture->normalMethod(-5)) - ->toThrow(TypeError::class, 'positive-int') - ; + expect(fn() => $fixture->normalMethod(-5)) + ->toThrow(TypeError::class, 'positive-int'); expect($fixture->ignoredMethod(-100))->toBe(-100); }); @@ -38,9 +37,8 @@ function testIgnoredFunction(int $id): int test('skips type-checking on class property marked with @typephp-ignore', function () { $fixture = new IgnoredMethod(); - expect(fn () => $fixture->setNormalProperty(-5)) - ->toThrow(TypeError::class, 'Property') - ; + expect(fn() => $fixture->setNormalProperty(-5)) + ->toThrow(TypeError::class, 'Property'); $fixture->setIgnoredProperty(-5); expect($fixture->ignoredProperty)->toBe(-5); @@ -52,4 +50,22 @@ function testIgnoredFunction(int $id): int $result = $fileFixture->process(-500); expect($result)->toBe(-500); }); + + test('skips inline variable validation inside methods marked with @typephp-ignore', function () { + $fixture = new class() { + /** + * @typephp-ignore + * @param positive-int $id + */ + public function ignoredMethodWithInlineVar(int $id): bool + { + /** @var string */ + $string = 1; // Invalid type assignment, but skipped because of @typephp-ignore above! + + return true; + } + }; + + expect($fixture->ignoredMethodWithInlineVar(-500))->toBeTrue(); + }); }); From 1ece4ec09db862485b622c3440bb41541b65976f Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 13:44:03 +0800 Subject: [PATCH 02/17] Fix formatting and improve clarity in README.md --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ab97366..a52c340 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ PHPStan Level MAX

------- +--- -TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, setup complex build toolchains, or compile C-extensions and simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime. +TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, set up complex build toolchains, or compile C-extensions. Simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime. **[Read the full TypePHP documentation »](https://typephp-php.github.io/typephp/)** @@ -29,15 +29,17 @@ All the documentation lives on the [typephp-php.github.io/typephp website](https * [Getting Started & Installation Guide](https://typephp-php.github.io/typephp/getting-started/installation) * [Quick Start Guide](https://typephp-php.github.io/typephp/getting-started/quick-start) -* [Architecture: How It Works](https://typephp-php.github.io/typephp/architecture/how-it-works) -* [Core Concepts: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts) -* [Core Concepts: Generics & Bounds](https://typephp-php.github.io/typephp/core-concepts/generics-and-bounds) +* [Configuration Guide](https://typephp-php.github.io/typephp/getting-started/configuration) +* [CLI Commands Reference](https://typephp-php.github.io/typephp/getting-started/cli-commands) +* [Enforcement Boundaries: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts) +* [Runtime Generics & Bounds](https://typephp-php.github.io/typephp/generics/generics-and-bounds) * [Supported Types: Arrays & Shapes](https://typephp-php.github.io/typephp/supported-types/arrays-and-shapes) -* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/advanced/troubleshooting) +* [Architecture: How It Works](https://typephp-php.github.io/typephp/advanced/how-it-works) +* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/troubleshooting) ## Inspiration -TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), but bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes. +TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes. ## Sponsors From bb0172e9c03bd485d360ff824755e9a572b37206 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 13:44:14 +0800 Subject: [PATCH 03/17] Enhance CommandRunner to handle unknown commands and validate PHP file extensions; add corresponding tests --- src/Command/CommandRunner.php | 43 +++++++++++++++++++++++++---- src/Command/RunCommand.php | 13 ++++++++- tests/Command/CommandRunnerTest.php | 39 ++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/Command/CommandRunner.php b/src/Command/CommandRunner.php index e7ab1fb..a9091ca 100644 --- a/src/Command/CommandRunner.php +++ b/src/Command/CommandRunner.php @@ -6,6 +6,14 @@ final class CommandRunner { + private const KNOWN_COMMANDS = [ + 'config:init', + 'cache:clear', + 'cache:warm', + 'cache:rebuild', + 'help', + ]; + /** * Parses CLI arguments and routes execution to the corresponding command class. * @@ -15,28 +23,51 @@ final class CommandRunner */ public static function run(array $args, $outputStream = STDOUT, $errorStream = STDERR): int { - $showHelp = \in_array('help', $args, true) || \in_array('typephp:help', $args, true) || \in_array('--help', $args, true) || \in_array('-h', $args, true); + $c = [CliFormatter::class, 'color']; + + $showHelp = \in_array('help', $args, true) + || \in_array('typephp:help', $args, true) + || \in_array('--help', $args, true) + || \in_array('-h', $args, true) + || $args === []; - if ($showHelp || $args === []) { + if ($showHelp) { return (new HelpCommand())->execute($args, $outputStream, $errorStream); } - if (\in_array('config:init', $args, true) || \in_array('init', $args, true)) { + $firstArg = $args[0] ?? ''; + + if ($firstArg === 'config:init' || $firstArg === 'init') { return (new ConfigInitCommand())->execute($args, $outputStream, $errorStream); } - if (\in_array('cache:rebuild', $args, true)) { + if ($firstArg === 'cache:rebuild') { return (new CacheRebuildCommand())->execute($args, $outputStream, $errorStream); } - if (\in_array('cache:clear', $args, true)) { + if ($firstArg === 'cache:clear') { return (new CacheClearCommand())->execute($args, $outputStream, $errorStream); } - if (\in_array('cache:warm', $args, true)) { + if ($firstArg === 'cache:warm') { return (new CacheWarmCommand())->execute($args, $outputStream, $errorStream); } + $hasFileExtension = str_contains(basename($firstArg), '.'); + $isFileTarget = file_exists($firstArg) || $hasFileExtension; + + if (! $isFileTarget) { + fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n"); + fwrite($errorStream, ' ' . $c('✗', 'red') . ' Command ' . $c('"' . $firstArg . '"', 'bold') . " is not defined.\n\n"); + fwrite($errorStream, ' ' . $c('Did you mean one of these?', 'yellow') . "\n"); + foreach (self::KNOWN_COMMANDS as $cmd) { + fwrite($errorStream, ' ' . $c('•', 'cyan') . ' ' . $cmd . "\n"); + } + fwrite($errorStream, "\n"); + + return 1; + } + return (new RunCommand())->execute($args, $outputStream, $errorStream); } } diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index a576643..67bc72c 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -11,6 +11,8 @@ */ final class RunCommand implements CommandInterface { + private const VALID_PHP_EXTENSIONS = ['php', 'phtml', 'php5', 'php7', 'php8', 'phps']; + public function execute(array $args, $outputStream = STDOUT, $errorStream = STDERR): int { $c = [CliFormatter::class, 'color']; @@ -21,6 +23,15 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE foreach ($args as $arg) { if (! str_starts_with($arg, '--') && ! str_starts_with($arg, '-')) { $givenTargetCandidate = $arg; + $ext = strtolower(pathinfo($arg, PATHINFO_EXTENSION)); + + if ($ext !== '' && ! \in_array($ext, self::VALID_PHP_EXTENSIONS, true)) { + fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n"); + fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $arg . '"', 'bold') . " is not a PHP script file. TypePHP can only execute PHP files.\n\n"); + + return 1; + } + if (file_exists($arg)) { $target = $arg; } @@ -31,7 +42,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE if ($givenTargetCandidate !== null && $target === null) { fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n"); - fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n"); + fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target script file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n"); return 1; } diff --git a/tests/Command/CommandRunnerTest.php b/tests/Command/CommandRunnerTest.php index bd40b0c..30ee72f 100644 --- a/tests/Command/CommandRunnerTest.php +++ b/tests/Command/CommandRunnerTest.php @@ -54,16 +54,49 @@ expect($exitCode)->toBe(0); }); - test('returns exit code 1 when target file does not exist', function () { + test('returns exit code 1 and detects unknown command for typo like helps', function () { + $stream = fopen('php://memory', 'r+'); + $exitCode = CommandRunner::run(['helps'], $stream, $stream); + + rewind($stream); + $rawOutput = stream_get_contents($stream); + fclose($stream); + + $output = preg_replace('/\x1b\[[0-9;]*m/', '', $rawOutput); + + expect($exitCode)->toBe(1) + ->and($output)->toContain('Command "helps" is not defined') + ->and($output)->toContain('Did you mean one of these?') + ; + }); + + test('returns exit code 1 and warns when target file has non-PHP extension', function () { + $stream = fopen('php://memory', 'r+'); + $exitCode = CommandRunner::run(['index.js'], $stream, $stream); + + rewind($stream); + $rawOutput = stream_get_contents($stream); + fclose($stream); + + $output = preg_replace('/\x1b\[[0-9;]*m/', '', $rawOutput); + + expect($exitCode)->toBe(1) + ->and($output)->toContain('Target file "index.js" is not a PHP script file') + ; + }); + + test('returns exit code 1 when target script file ending in .php does not exist', function () { $stream = fopen('php://memory', 'r+'); $exitCode = CommandRunner::run(['non_existent_script_123.php'], $stream, $stream); rewind($stream); - $output = stream_get_contents($stream); + $rawOutput = stream_get_contents($stream); fclose($stream); + $output = preg_replace('/\x1b\[[0-9;]*m/', '', $rawOutput); + expect($exitCode)->toBe(1) - ->and($output)->toContain('Error') + ->and($output)->toContain('Target script file "non_existent_script_123.php" does not exist or is not readable') ; }); }); From 5be4d61148577cbb5e97206a108c5b8154164f7d Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 14:00:07 +0800 Subject: [PATCH 04/17] Reorganize test suites for typechecking --- reorganize-tests.sh | 67 +++++++++++++++++++ .../AdvancedTypesAndEnumsTest.php | 0 .../ArrayAndListTypesTest.php | 0 .../KeyOfValueOfTest.php | 0 .../OffsetAccessTest.php | 0 .../UnionAndIntersectionTypesTest.php | 0 .../BlockScopeShadowingTest.php | 0 .../ClosureVariableScopeTest.php | 0 .../ExtendedReturnTypesTest.php | 0 .../IgnoreUnrecognizeDoctypeTest.php | 0 .../ImportedFunctionsTest.php | 0 .../InlineVariableValidationTest.php | 0 .../ListDestructuringTest.php | 0 .../{ => Boundaries}/NamedArgumentsTest.php | 0 .../{ => Boundaries}/ParamContractsTest.php | 0 .../{ => Boundaries}/PropertyHooksTest.php | 0 .../PropertyValidationTest.php | 0 .../{ => Boundaries}/ReturnContractsTest.php | 0 .../VarAnnotationPrebindingTest.php | 0 .../CallableAndClosureContractsTest.php | 0 .../LazyIteratorsAndGeneratorsTest.php | 0 .../BoundaryConfigTest.php | 0 .../DocblockIgnoreTagsTest.php | 0 .../RecursionAndExceptionLeakTest.php | 0 .../RespectIgnoreTagsConfigTest.php | 0 .../AdvancedGenericsAndShapesTest.php | 0 .../CloneGenericInstanceTest.php | 0 .../ConditionalTypesWithGenericsTest.php | 0 .../DefaultTemplateTypesTest.php | 0 .../GenericPropertyHooksTest.php | 0 .../GenericsAndInheritanceTest.php | 0 ...nheritedGenericCloneAndConditionalTest.php | 0 .../AttributeConstructorInheritanceTest.php | 0 .../DeepInheritanceTest.php | 0 .../LiskovAndVendorIsolationTest.php | 0 .../NamespaceResolutionTest.php | 0 .../OopInheritanceTest.php | 0 .../PhpAttributesCoexistenceTest.php | 0 .../{ => Scalars}/ExtendedScalarTypesTest.php | 0 .../{ => Scalars}/FloatLiteralsTest.php | 0 .../{ => Scalars}/IntMaskTest.php | 0 .../NewScalarAndPseudoTypesTest.php | 0 .../UppercaseAndArrayKeyTest.php | 0 43 files changed, 67 insertions(+) create mode 100644 reorganize-tests.sh rename tests/TypeChecking/{ => ArraysAndShapes}/AdvancedTypesAndEnumsTest.php (100%) rename tests/TypeChecking/{ => ArraysAndShapes}/ArrayAndListTypesTest.php (100%) rename tests/TypeChecking/{ => ArraysAndShapes}/KeyOfValueOfTest.php (100%) rename tests/TypeChecking/{ => ArraysAndShapes}/OffsetAccessTest.php (100%) rename tests/TypeChecking/{ => ArraysAndShapes}/UnionAndIntersectionTypesTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/BlockScopeShadowingTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ClosureVariableScopeTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ExtendedReturnTypesTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/IgnoreUnrecognizeDoctypeTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ImportedFunctionsTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/InlineVariableValidationTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ListDestructuringTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/NamedArgumentsTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ParamContractsTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/PropertyHooksTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/PropertyValidationTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/ReturnContractsTest.php (100%) rename tests/TypeChecking/{ => Boundaries}/VarAnnotationPrebindingTest.php (100%) rename tests/TypeChecking/{ => CallablesAndIterators}/CallableAndClosureContractsTest.php (100%) rename tests/TypeChecking/{ => CallablesAndIterators}/LazyIteratorsAndGeneratorsTest.php (100%) rename tests/TypeChecking/{ => Configuration}/BoundaryConfigTest.php (100%) rename tests/TypeChecking/{ => Configuration}/DocblockIgnoreTagsTest.php (100%) rename tests/TypeChecking/{ => Configuration}/RecursionAndExceptionLeakTest.php (100%) rename tests/TypeChecking/{ => Configuration}/RespectIgnoreTagsConfigTest.php (100%) rename tests/TypeChecking/{ => Generics}/AdvancedGenericsAndShapesTest.php (100%) rename tests/TypeChecking/{ => Generics}/CloneGenericInstanceTest.php (100%) rename tests/TypeChecking/{ => Generics}/ConditionalTypesWithGenericsTest.php (100%) rename tests/TypeChecking/{ => Generics}/DefaultTemplateTypesTest.php (100%) rename tests/TypeChecking/{ => Generics}/GenericPropertyHooksTest.php (100%) rename tests/TypeChecking/{ => Generics}/GenericsAndInheritanceTest.php (100%) rename tests/TypeChecking/{ => Generics}/InheritedGenericCloneAndConditionalTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/AttributeConstructorInheritanceTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/DeepInheritanceTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/LiskovAndVendorIsolationTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/NamespaceResolutionTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/OopInheritanceTest.php (100%) rename tests/TypeChecking/{ => InheritanceAndAttributes}/PhpAttributesCoexistenceTest.php (100%) rename tests/TypeChecking/{ => Scalars}/ExtendedScalarTypesTest.php (100%) rename tests/TypeChecking/{ => Scalars}/FloatLiteralsTest.php (100%) rename tests/TypeChecking/{ => Scalars}/IntMaskTest.php (100%) rename tests/TypeChecking/{ => Scalars}/NewScalarAndPseudoTypesTest.php (100%) rename tests/TypeChecking/{ => Scalars}/UppercaseAndArrayKeyTest.php (100%) diff --git a/reorganize-tests.sh b/reorganize-tests.sh new file mode 100644 index 0000000..2f01f16 --- /dev/null +++ b/reorganize-tests.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# Create Directories +mkdir -p tests/TypeChecking/Boundaries +mkdir -p tests/TypeChecking/Scalars +mkdir -p tests/TypeChecking/ArraysAndShapes +mkdir -p tests/TypeChecking/Generics +mkdir -p tests/TypeChecking/CallablesAndIterators +mkdir -p tests/TypeChecking/InheritanceAndAttributes +mkdir -p tests/TypeChecking/Configuration + +# Move Boundaries Tests +git mv tests/TypeChecking/ParamContractsTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/ReturnContractsTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/ExtendedReturnTypesTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/PropertyValidationTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/PropertyHooksTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/InlineVariableValidationTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/BlockScopeShadowingTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/ClosureVariableScopeTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/ListDestructuringTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/VarAnnotationPrebindingTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/ImportedFunctionsTest.php tests/TypeChecking/Boundaries/ +git mv tests/TypeChecking/NamedArgumentsTest.php tests/TypeChecking/Boundaries/ + +# Move Scalars Tests +git mv tests/TypeChecking/ExtendedScalarTypesTest.php tests/TypeChecking/Scalars/ +git mv tests/TypeChecking/FloatLiteralsTest.php tests/TypeChecking/Scalars/ +git mv tests/TypeChecking/NewScalarAndPseudoTypesTest.php tests/TypeChecking/Scalars/ +git mv tests/TypeChecking/UppercaseAndArrayKeyTest.php tests/TypeChecking/Scalars/ +git mv tests/TypeChecking/IntMaskTest.php tests/TypeChecking/Scalars/ + +# Move ArraysAndShapes Tests +git mv tests/TypeChecking/ArrayAndListTypesTest.php tests/TypeChecking/ArraysAndShapes/ +git mv tests/TypeChecking/UnionAndIntersectionTypesTest.php tests/TypeChecking/ArraysAndShapes/ +git mv tests/TypeChecking/KeyOfValueOfTest.php tests/TypeChecking/ArraysAndShapes/ +git mv tests/TypeChecking/OffsetAccessTest.php tests/TypeChecking/ArraysAndShapes/ +git mv tests/TypeChecking/AdvancedTypesAndEnumsTest.php tests/TypeChecking/ArraysAndShapes/ + +# Move Generics Tests +git mv tests/TypeChecking/GenericsAndInheritanceTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/DefaultTemplateTypesTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/CloneGenericInstanceTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/GenericPropertyHooksTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/ConditionalTypesWithGenericsTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/InheritedGenericCloneAndConditionalTest.php tests/TypeChecking/Generics/ +git mv tests/TypeChecking/AdvancedGenericsAndShapesTest.php tests/TypeChecking/Generics/ + +# Move CallablesAndIterators Tests +git mv tests/TypeChecking/CallableAndClosureContractsTest.php tests/TypeChecking/CallablesAndIterators/ +git mv tests/TypeChecking/LazyIteratorsAndGeneratorsTest.php tests/TypeChecking/CallablesAndIterators/ + +# Move InheritanceAndAttributes Tests +git mv tests/TypeChecking/OopInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ +git mv tests/TypeChecking/DeepInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ +git mv tests/TypeChecking/LiskovAndVendorIsolationTest.php tests/TypeChecking/InheritanceAndAttributes/ +git mv tests/TypeChecking/AttributeConstructorInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ +git mv tests/TypeChecking/PhpAttributesCoexistenceTest.php tests/TypeChecking/InheritanceAndAttributes/ +git mv tests/TypeChecking/NamespaceResolutionTest.php tests/TypeChecking/InheritanceAndAttributes/ + +# Move Configuration Tests +git mv tests/TypeChecking/BoundaryConfigTest.php tests/TypeChecking/Configuration/ +git mv tests/TypeChecking/RespectIgnoreTagsConfigTest.php tests/TypeChecking/Configuration/ +git mv tests/TypeChecking/DocblockIgnoreTagsTest.php tests/TypeChecking/Configuration/ +git mv tests/TypeChecking/RecursionAndExceptionLeakTest.php tests/TypeChecking/Configuration/ + +echo "Reorganization Complete!" \ No newline at end of file diff --git a/tests/TypeChecking/AdvancedTypesAndEnumsTest.php b/tests/TypeChecking/ArraysAndShapes/AdvancedTypesAndEnumsTest.php similarity index 100% rename from tests/TypeChecking/AdvancedTypesAndEnumsTest.php rename to tests/TypeChecking/ArraysAndShapes/AdvancedTypesAndEnumsTest.php diff --git a/tests/TypeChecking/ArrayAndListTypesTest.php b/tests/TypeChecking/ArraysAndShapes/ArrayAndListTypesTest.php similarity index 100% rename from tests/TypeChecking/ArrayAndListTypesTest.php rename to tests/TypeChecking/ArraysAndShapes/ArrayAndListTypesTest.php diff --git a/tests/TypeChecking/KeyOfValueOfTest.php b/tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php similarity index 100% rename from tests/TypeChecking/KeyOfValueOfTest.php rename to tests/TypeChecking/ArraysAndShapes/KeyOfValueOfTest.php diff --git a/tests/TypeChecking/OffsetAccessTest.php b/tests/TypeChecking/ArraysAndShapes/OffsetAccessTest.php similarity index 100% rename from tests/TypeChecking/OffsetAccessTest.php rename to tests/TypeChecking/ArraysAndShapes/OffsetAccessTest.php diff --git a/tests/TypeChecking/UnionAndIntersectionTypesTest.php b/tests/TypeChecking/ArraysAndShapes/UnionAndIntersectionTypesTest.php similarity index 100% rename from tests/TypeChecking/UnionAndIntersectionTypesTest.php rename to tests/TypeChecking/ArraysAndShapes/UnionAndIntersectionTypesTest.php diff --git a/tests/TypeChecking/BlockScopeShadowingTest.php b/tests/TypeChecking/Boundaries/BlockScopeShadowingTest.php similarity index 100% rename from tests/TypeChecking/BlockScopeShadowingTest.php rename to tests/TypeChecking/Boundaries/BlockScopeShadowingTest.php diff --git a/tests/TypeChecking/ClosureVariableScopeTest.php b/tests/TypeChecking/Boundaries/ClosureVariableScopeTest.php similarity index 100% rename from tests/TypeChecking/ClosureVariableScopeTest.php rename to tests/TypeChecking/Boundaries/ClosureVariableScopeTest.php diff --git a/tests/TypeChecking/ExtendedReturnTypesTest.php b/tests/TypeChecking/Boundaries/ExtendedReturnTypesTest.php similarity index 100% rename from tests/TypeChecking/ExtendedReturnTypesTest.php rename to tests/TypeChecking/Boundaries/ExtendedReturnTypesTest.php diff --git a/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php b/tests/TypeChecking/Boundaries/IgnoreUnrecognizeDoctypeTest.php similarity index 100% rename from tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php rename to tests/TypeChecking/Boundaries/IgnoreUnrecognizeDoctypeTest.php diff --git a/tests/TypeChecking/ImportedFunctionsTest.php b/tests/TypeChecking/Boundaries/ImportedFunctionsTest.php similarity index 100% rename from tests/TypeChecking/ImportedFunctionsTest.php rename to tests/TypeChecking/Boundaries/ImportedFunctionsTest.php diff --git a/tests/TypeChecking/InlineVariableValidationTest.php b/tests/TypeChecking/Boundaries/InlineVariableValidationTest.php similarity index 100% rename from tests/TypeChecking/InlineVariableValidationTest.php rename to tests/TypeChecking/Boundaries/InlineVariableValidationTest.php diff --git a/tests/TypeChecking/ListDestructuringTest.php b/tests/TypeChecking/Boundaries/ListDestructuringTest.php similarity index 100% rename from tests/TypeChecking/ListDestructuringTest.php rename to tests/TypeChecking/Boundaries/ListDestructuringTest.php diff --git a/tests/TypeChecking/NamedArgumentsTest.php b/tests/TypeChecking/Boundaries/NamedArgumentsTest.php similarity index 100% rename from tests/TypeChecking/NamedArgumentsTest.php rename to tests/TypeChecking/Boundaries/NamedArgumentsTest.php diff --git a/tests/TypeChecking/ParamContractsTest.php b/tests/TypeChecking/Boundaries/ParamContractsTest.php similarity index 100% rename from tests/TypeChecking/ParamContractsTest.php rename to tests/TypeChecking/Boundaries/ParamContractsTest.php diff --git a/tests/TypeChecking/PropertyHooksTest.php b/tests/TypeChecking/Boundaries/PropertyHooksTest.php similarity index 100% rename from tests/TypeChecking/PropertyHooksTest.php rename to tests/TypeChecking/Boundaries/PropertyHooksTest.php diff --git a/tests/TypeChecking/PropertyValidationTest.php b/tests/TypeChecking/Boundaries/PropertyValidationTest.php similarity index 100% rename from tests/TypeChecking/PropertyValidationTest.php rename to tests/TypeChecking/Boundaries/PropertyValidationTest.php diff --git a/tests/TypeChecking/ReturnContractsTest.php b/tests/TypeChecking/Boundaries/ReturnContractsTest.php similarity index 100% rename from tests/TypeChecking/ReturnContractsTest.php rename to tests/TypeChecking/Boundaries/ReturnContractsTest.php diff --git a/tests/TypeChecking/VarAnnotationPrebindingTest.php b/tests/TypeChecking/Boundaries/VarAnnotationPrebindingTest.php similarity index 100% rename from tests/TypeChecking/VarAnnotationPrebindingTest.php rename to tests/TypeChecking/Boundaries/VarAnnotationPrebindingTest.php diff --git a/tests/TypeChecking/CallableAndClosureContractsTest.php b/tests/TypeChecking/CallablesAndIterators/CallableAndClosureContractsTest.php similarity index 100% rename from tests/TypeChecking/CallableAndClosureContractsTest.php rename to tests/TypeChecking/CallablesAndIterators/CallableAndClosureContractsTest.php diff --git a/tests/TypeChecking/LazyIteratorsAndGeneratorsTest.php b/tests/TypeChecking/CallablesAndIterators/LazyIteratorsAndGeneratorsTest.php similarity index 100% rename from tests/TypeChecking/LazyIteratorsAndGeneratorsTest.php rename to tests/TypeChecking/CallablesAndIterators/LazyIteratorsAndGeneratorsTest.php diff --git a/tests/TypeChecking/BoundaryConfigTest.php b/tests/TypeChecking/Configuration/BoundaryConfigTest.php similarity index 100% rename from tests/TypeChecking/BoundaryConfigTest.php rename to tests/TypeChecking/Configuration/BoundaryConfigTest.php diff --git a/tests/TypeChecking/DocblockIgnoreTagsTest.php b/tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php similarity index 100% rename from tests/TypeChecking/DocblockIgnoreTagsTest.php rename to tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php diff --git a/tests/TypeChecking/RecursionAndExceptionLeakTest.php b/tests/TypeChecking/Configuration/RecursionAndExceptionLeakTest.php similarity index 100% rename from tests/TypeChecking/RecursionAndExceptionLeakTest.php rename to tests/TypeChecking/Configuration/RecursionAndExceptionLeakTest.php diff --git a/tests/TypeChecking/RespectIgnoreTagsConfigTest.php b/tests/TypeChecking/Configuration/RespectIgnoreTagsConfigTest.php similarity index 100% rename from tests/TypeChecking/RespectIgnoreTagsConfigTest.php rename to tests/TypeChecking/Configuration/RespectIgnoreTagsConfigTest.php diff --git a/tests/TypeChecking/AdvancedGenericsAndShapesTest.php b/tests/TypeChecking/Generics/AdvancedGenericsAndShapesTest.php similarity index 100% rename from tests/TypeChecking/AdvancedGenericsAndShapesTest.php rename to tests/TypeChecking/Generics/AdvancedGenericsAndShapesTest.php diff --git a/tests/TypeChecking/CloneGenericInstanceTest.php b/tests/TypeChecking/Generics/CloneGenericInstanceTest.php similarity index 100% rename from tests/TypeChecking/CloneGenericInstanceTest.php rename to tests/TypeChecking/Generics/CloneGenericInstanceTest.php diff --git a/tests/TypeChecking/ConditionalTypesWithGenericsTest.php b/tests/TypeChecking/Generics/ConditionalTypesWithGenericsTest.php similarity index 100% rename from tests/TypeChecking/ConditionalTypesWithGenericsTest.php rename to tests/TypeChecking/Generics/ConditionalTypesWithGenericsTest.php diff --git a/tests/TypeChecking/DefaultTemplateTypesTest.php b/tests/TypeChecking/Generics/DefaultTemplateTypesTest.php similarity index 100% rename from tests/TypeChecking/DefaultTemplateTypesTest.php rename to tests/TypeChecking/Generics/DefaultTemplateTypesTest.php diff --git a/tests/TypeChecking/GenericPropertyHooksTest.php b/tests/TypeChecking/Generics/GenericPropertyHooksTest.php similarity index 100% rename from tests/TypeChecking/GenericPropertyHooksTest.php rename to tests/TypeChecking/Generics/GenericPropertyHooksTest.php diff --git a/tests/TypeChecking/GenericsAndInheritanceTest.php b/tests/TypeChecking/Generics/GenericsAndInheritanceTest.php similarity index 100% rename from tests/TypeChecking/GenericsAndInheritanceTest.php rename to tests/TypeChecking/Generics/GenericsAndInheritanceTest.php diff --git a/tests/TypeChecking/InheritedGenericCloneAndConditionalTest.php b/tests/TypeChecking/Generics/InheritedGenericCloneAndConditionalTest.php similarity index 100% rename from tests/TypeChecking/InheritedGenericCloneAndConditionalTest.php rename to tests/TypeChecking/Generics/InheritedGenericCloneAndConditionalTest.php diff --git a/tests/TypeChecking/AttributeConstructorInheritanceTest.php b/tests/TypeChecking/InheritanceAndAttributes/AttributeConstructorInheritanceTest.php similarity index 100% rename from tests/TypeChecking/AttributeConstructorInheritanceTest.php rename to tests/TypeChecking/InheritanceAndAttributes/AttributeConstructorInheritanceTest.php diff --git a/tests/TypeChecking/DeepInheritanceTest.php b/tests/TypeChecking/InheritanceAndAttributes/DeepInheritanceTest.php similarity index 100% rename from tests/TypeChecking/DeepInheritanceTest.php rename to tests/TypeChecking/InheritanceAndAttributes/DeepInheritanceTest.php diff --git a/tests/TypeChecking/LiskovAndVendorIsolationTest.php b/tests/TypeChecking/InheritanceAndAttributes/LiskovAndVendorIsolationTest.php similarity index 100% rename from tests/TypeChecking/LiskovAndVendorIsolationTest.php rename to tests/TypeChecking/InheritanceAndAttributes/LiskovAndVendorIsolationTest.php diff --git a/tests/TypeChecking/NamespaceResolutionTest.php b/tests/TypeChecking/InheritanceAndAttributes/NamespaceResolutionTest.php similarity index 100% rename from tests/TypeChecking/NamespaceResolutionTest.php rename to tests/TypeChecking/InheritanceAndAttributes/NamespaceResolutionTest.php diff --git a/tests/TypeChecking/OopInheritanceTest.php b/tests/TypeChecking/InheritanceAndAttributes/OopInheritanceTest.php similarity index 100% rename from tests/TypeChecking/OopInheritanceTest.php rename to tests/TypeChecking/InheritanceAndAttributes/OopInheritanceTest.php diff --git a/tests/TypeChecking/PhpAttributesCoexistenceTest.php b/tests/TypeChecking/InheritanceAndAttributes/PhpAttributesCoexistenceTest.php similarity index 100% rename from tests/TypeChecking/PhpAttributesCoexistenceTest.php rename to tests/TypeChecking/InheritanceAndAttributes/PhpAttributesCoexistenceTest.php diff --git a/tests/TypeChecking/ExtendedScalarTypesTest.php b/tests/TypeChecking/Scalars/ExtendedScalarTypesTest.php similarity index 100% rename from tests/TypeChecking/ExtendedScalarTypesTest.php rename to tests/TypeChecking/Scalars/ExtendedScalarTypesTest.php diff --git a/tests/TypeChecking/FloatLiteralsTest.php b/tests/TypeChecking/Scalars/FloatLiteralsTest.php similarity index 100% rename from tests/TypeChecking/FloatLiteralsTest.php rename to tests/TypeChecking/Scalars/FloatLiteralsTest.php diff --git a/tests/TypeChecking/IntMaskTest.php b/tests/TypeChecking/Scalars/IntMaskTest.php similarity index 100% rename from tests/TypeChecking/IntMaskTest.php rename to tests/TypeChecking/Scalars/IntMaskTest.php diff --git a/tests/TypeChecking/NewScalarAndPseudoTypesTest.php b/tests/TypeChecking/Scalars/NewScalarAndPseudoTypesTest.php similarity index 100% rename from tests/TypeChecking/NewScalarAndPseudoTypesTest.php rename to tests/TypeChecking/Scalars/NewScalarAndPseudoTypesTest.php diff --git a/tests/TypeChecking/UppercaseAndArrayKeyTest.php b/tests/TypeChecking/Scalars/UppercaseAndArrayKeyTest.php similarity index 100% rename from tests/TypeChecking/UppercaseAndArrayKeyTest.php rename to tests/TypeChecking/Scalars/UppercaseAndArrayKeyTest.php From 6ba90a423e4b5edec82c3949f47f4bc5f360d551 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 14:00:15 +0800 Subject: [PATCH 05/17] Remove reorganize-tests.sh script for test directory restructuring --- reorganize-tests.sh | 67 --------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 reorganize-tests.sh diff --git a/reorganize-tests.sh b/reorganize-tests.sh deleted file mode 100644 index 2f01f16..0000000 --- a/reorganize-tests.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash - -# Create Directories -mkdir -p tests/TypeChecking/Boundaries -mkdir -p tests/TypeChecking/Scalars -mkdir -p tests/TypeChecking/ArraysAndShapes -mkdir -p tests/TypeChecking/Generics -mkdir -p tests/TypeChecking/CallablesAndIterators -mkdir -p tests/TypeChecking/InheritanceAndAttributes -mkdir -p tests/TypeChecking/Configuration - -# Move Boundaries Tests -git mv tests/TypeChecking/ParamContractsTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/ReturnContractsTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/ExtendedReturnTypesTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/PropertyValidationTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/PropertyHooksTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/InlineVariableValidationTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/BlockScopeShadowingTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/ClosureVariableScopeTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/ListDestructuringTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/VarAnnotationPrebindingTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/ImportedFunctionsTest.php tests/TypeChecking/Boundaries/ -git mv tests/TypeChecking/NamedArgumentsTest.php tests/TypeChecking/Boundaries/ - -# Move Scalars Tests -git mv tests/TypeChecking/ExtendedScalarTypesTest.php tests/TypeChecking/Scalars/ -git mv tests/TypeChecking/FloatLiteralsTest.php tests/TypeChecking/Scalars/ -git mv tests/TypeChecking/NewScalarAndPseudoTypesTest.php tests/TypeChecking/Scalars/ -git mv tests/TypeChecking/UppercaseAndArrayKeyTest.php tests/TypeChecking/Scalars/ -git mv tests/TypeChecking/IntMaskTest.php tests/TypeChecking/Scalars/ - -# Move ArraysAndShapes Tests -git mv tests/TypeChecking/ArrayAndListTypesTest.php tests/TypeChecking/ArraysAndShapes/ -git mv tests/TypeChecking/UnionAndIntersectionTypesTest.php tests/TypeChecking/ArraysAndShapes/ -git mv tests/TypeChecking/KeyOfValueOfTest.php tests/TypeChecking/ArraysAndShapes/ -git mv tests/TypeChecking/OffsetAccessTest.php tests/TypeChecking/ArraysAndShapes/ -git mv tests/TypeChecking/AdvancedTypesAndEnumsTest.php tests/TypeChecking/ArraysAndShapes/ - -# Move Generics Tests -git mv tests/TypeChecking/GenericsAndInheritanceTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/DefaultTemplateTypesTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/CloneGenericInstanceTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/GenericPropertyHooksTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/ConditionalTypesWithGenericsTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/InheritedGenericCloneAndConditionalTest.php tests/TypeChecking/Generics/ -git mv tests/TypeChecking/AdvancedGenericsAndShapesTest.php tests/TypeChecking/Generics/ - -# Move CallablesAndIterators Tests -git mv tests/TypeChecking/CallableAndClosureContractsTest.php tests/TypeChecking/CallablesAndIterators/ -git mv tests/TypeChecking/LazyIteratorsAndGeneratorsTest.php tests/TypeChecking/CallablesAndIterators/ - -# Move InheritanceAndAttributes Tests -git mv tests/TypeChecking/OopInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ -git mv tests/TypeChecking/DeepInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ -git mv tests/TypeChecking/LiskovAndVendorIsolationTest.php tests/TypeChecking/InheritanceAndAttributes/ -git mv tests/TypeChecking/AttributeConstructorInheritanceTest.php tests/TypeChecking/InheritanceAndAttributes/ -git mv tests/TypeChecking/PhpAttributesCoexistenceTest.php tests/TypeChecking/InheritanceAndAttributes/ -git mv tests/TypeChecking/NamespaceResolutionTest.php tests/TypeChecking/InheritanceAndAttributes/ - -# Move Configuration Tests -git mv tests/TypeChecking/BoundaryConfigTest.php tests/TypeChecking/Configuration/ -git mv tests/TypeChecking/RespectIgnoreTagsConfigTest.php tests/TypeChecking/Configuration/ -git mv tests/TypeChecking/DocblockIgnoreTagsTest.php tests/TypeChecking/Configuration/ -git mv tests/TypeChecking/RecursionAndExceptionLeakTest.php tests/TypeChecking/Configuration/ - -echo "Reorganization Complete!" \ No newline at end of file From dfc2fa86edb771606d1019987d86bc9eaad6c867 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 15:26:19 +0800 Subject: [PATCH 06/17] Add class constant key tests and fixtures for array shape validation --- src/Internal/ContractVisitor.php | 2 +- src/Resolver/SpecialTypeResolver.php | 746 ++++++++++-------- tests/Fixtures/Types/ConstKeyContainer.php | 19 + .../Types/MissingConstKeyContainer.php | 16 + .../ClassConstKeyShapeTest.php | 36 + .../Configuration/DocblockIgnoreTagsTest.php | 13 +- 6 files changed, 504 insertions(+), 328 deletions(-) create mode 100644 tests/Fixtures/Types/ConstKeyContainer.php create mode 100644 tests/Fixtures/Types/MissingConstKeyContainer.php create mode 100644 tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php diff --git a/src/Internal/ContractVisitor.php b/src/Internal/ContractVisitor.php index f30cd6e..f80cf8b 100644 --- a/src/Internal/ContractVisitor.php +++ b/src/Internal/ContractVisitor.php @@ -214,4 +214,4 @@ private function extractDestructuringVariables(Node\Expr\List_|Node\Expr\Array_ return $vars; } -} \ No newline at end of file +} diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 4a243f1..8176fa9 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -68,23 +68,13 @@ public static function checkThisIdentity(TypeNode $returnTypeNode, mixed $value, } /** - * Recursively resolves special type identifiers (self, static, parent, FQCNs, ConstFetch class names) in a TypeNode AST using Reflection context. + * Recursively resolves special type identifiers in a TypeNode AST using Reflection context. * * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context */ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context, ?object $thisObj = null): TypeNode { - if (\is_string($context)) { - if (str_contains($context, '::')) { - [$className, $methodName] = explode('::', $context, 2); - $ref = new \ReflectionMethod($className, $methodName); - } else { - $ref = new \ReflectionFunction($context); - } - } else { - $ref = $context; - } - + $ref = self::getReflectionContext($context); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : ($ref instanceof \ReflectionClass ? $ref->getName() : null); if ($node instanceof ThisTypeNode) { @@ -92,55 +82,16 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct } if ($node instanceof IdentifierTypeNode) { - $lower = strtolower($node->name); - - if ($lower === '$this' || $lower === 'static') { - return $node; - } - - if ($lower === 'self' && $declaringClass !== null) { - return new IdentifierTypeNode($declaringClass); - } - - if ($lower === 'parent' && $declaringClass !== null) { - $parentClass = get_parent_class($declaringClass); - if ($parentClass !== false) { - return new IdentifierTypeNode($parentClass); - } - } - - $fqcn = self::resolveFqcn($node->name, $ref); - if ($fqcn !== $node->name) { - return new IdentifierTypeNode($fqcn); - } + return self::resolveIdentifier($node, $declaringClass, $ref); } if ($node instanceof ConstTypeNode) { - if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { - $className = $node->constExpr->className; - $lowerClassName = strtolower($className); - - if ($lowerClassName === 'self' && $declaringClass !== null) { - $resolvedClass = $declaringClass; - } elseif ($lowerClassName === 'parent' && $declaringClass !== null) { - $parentClass = get_parent_class($declaringClass); - $resolvedClass = $parentClass !== false ? $parentClass : $className; - } else { - $resolvedClass = self::resolveFqcn($className, $ref); - } - - return new ConstTypeNode(new ConstFetchNode($resolvedClass, $node->constExpr->name)); - } - - return $node; + return self::resolveConstType($node, $declaringClass, $ref); } if ($node instanceof GenericTypeNode) { - $genericType = self::resolve($node->type, $context, $thisObj); - $innerTypes = array_map( - fn ($t) => self::resolve($t, $context, $thisObj), - $node->genericTypes - ); + $genericType = self::resolve($node->type, $ref, $thisObj); + $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -149,164 +100,56 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct ); } - if ($node instanceof ConditionalTypeNode) { - return new ConditionalTypeNode( - self::resolve($node->subjectType, $context, $thisObj), - self::resolve($node->targetType, $context, $thisObj), - self::resolve($node->if, $context, $thisObj), - self::resolve($node->else, $context, $thisObj), - $node->negated - ); - } - - if ($node instanceof ConditionalTypeForParameterNode) { - return new ConditionalTypeForParameterNode( - $node->parameterName, - self::resolve($node->targetType, $context, $thisObj), - self::resolve($node->if, $context, $thisObj), - self::resolve($node->else, $context, $thisObj), - $node->negated - ); - } - if ($node instanceof OffsetAccessTypeNode) { - $baseType = self::resolve($node->type, $context, $thisObj); - $offsetType = self::resolve($node->offset, $context, $thisObj); - - $offsetKey = null; - if ($offsetType instanceof ConstTypeNode) { - $expr = $offsetType->constExpr; - if ($expr instanceof ConstExprStringNode) { - $offsetKey = $expr->value; - } elseif ($expr instanceof ConstExprIntegerNode) { - $offsetKey = (int) $expr->value; - } - } elseif ($offsetType instanceof IdentifierTypeNode) { - $offsetKey = $offsetType->name; - } - - if ($offsetKey !== null) { - if ($baseType instanceof ArrayShapeNode) { - foreach ($baseType->items as $item) { - $itemKey = null; - if ($item->keyName instanceof ConstExprStringNode) { - $itemKey = $item->keyName->value; - } elseif ($item->keyName instanceof IdentifierTypeNode) { - $itemKey = $item->keyName->name; - } elseif ($item->keyName instanceof ConstExprIntegerNode) { - $itemKey = (int) $item->keyName->value; - } - - if ((string) $itemKey === (string) $offsetKey) { - return $item->valueType; - } - } - } - - if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { - $constExpr = $baseType->constExpr; - $fqcn = $constExpr->className; - $constName = $constExpr->name; - - if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { - try { - $refClass = new \ReflectionClass($fqcn); - if ($refClass->hasConstant($constName)) { - $constValue = $refClass->getConstant($constName); - if (\is_array($constValue) && \array_key_exists($offsetKey, $constValue)) { - $val = $constValue[$offsetKey]; - if (\is_string($val)) { - return new ConstTypeNode(new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED)); - } elseif (\is_int($val)) { - return new ConstTypeNode(new ConstExprIntegerNode((string) $val)); - } - } - } - } catch (\ReflectionException $e) { - } - } - } - } - - return new OffsetAccessTypeNode($baseType, $offsetType); + return self::resolveOffsetAccess($node, $ref, $thisObj); } if ($node instanceof ArrayShapeNode) { - $items = array_map(function ($item) use ($context, $thisObj) { - return new ArrayShapeItemNode( - $item->keyName, - $item->optional, - self::resolve($item->valueType, $context, $thisObj) - ); - }, $node->items); - - if ($node->sealed) { - return ArrayShapeNode::createSealed($items, $node->kind); - } else { - $unsealedType = null; - if ($node->unsealedType !== null) { - $unsealedKey = $node->unsealedType->keyType !== null ? self::resolve($node->unsealedType->keyType, $context, $thisObj) : null; - $unsealedValue = self::resolve($node->unsealedType->valueType, $context, $thisObj); - $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); - } - - return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); - } + return self::resolveArrayShape($node, $ref, $thisObj); } if ($node instanceof ObjectShapeNode) { - $items = array_map(function ($item) use ($context, $thisObj) { - return new ObjectShapeItemNode( - $item->keyName, - $item->optional, - self::resolve($item->valueType, $context, $thisObj) - ); - }, $node->items); - - return new ObjectShapeNode($items); + return self::resolveObjectShape($node, $ref, $thisObj); } if ($node instanceof CallableTypeNode) { - $resolvedParameters = array_map(function (CallableTypeParameterNode $param) use ($context, $thisObj) { - return new CallableTypeParameterNode( - self::resolve($param->type, $context, $thisObj), - $param->isReference, - $param->isVariadic, - $param->parameterName, - $param->isOptional - ); - }, $node->parameters); - - $resolvedReturnType = self::resolve($node->returnType, $context, $thisObj); - - return new CallableTypeNode( - $node->identifier, - $resolvedParameters, - $resolvedReturnType, - $node->templateTypes + return self::resolveCallable($node, $ref, $thisObj); + } + + if ($node instanceof ConditionalTypeNode) { + return new ConditionalTypeNode( + self::resolve($node->subjectType, $ref, $thisObj), + self::resolve($node->targetType, $ref, $thisObj), + self::resolve($node->if, $ref, $thisObj), + self::resolve($node->else, $ref, $thisObj), + $node->negated + ); + } + + if ($node instanceof ConditionalTypeForParameterNode) { + return new ConditionalTypeForParameterNode( + $node->parameterName, + self::resolve($node->targetType, $ref, $thisObj), + self::resolve($node->if, $ref, $thisObj), + self::resolve($node->else, $ref, $thisObj), + $node->negated ); } if ($node instanceof NullableTypeNode) { - return new NullableTypeNode(self::resolve($node->type, $context, $thisObj)); + return new NullableTypeNode(self::resolve($node->type, $ref, $thisObj)); } if ($node instanceof ArrayTypeNode) { - return new ArrayTypeNode(self::resolve($node->type, $context, $thisObj)); + return new ArrayTypeNode(self::resolve($node->type, $ref, $thisObj)); } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map( - fn ($t) => self::resolve($t, $context, $thisObj), - $node->types - )); + return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map( - fn ($t) => self::resolve($t, $context, $thisObj), - $node->types - )); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } return $node; @@ -328,34 +171,17 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode } $fqcn = self::resolveFqcnForFile($node->name, $file); - if ($fqcn !== $node->name) { - return new IdentifierTypeNode($fqcn); - } + + return $fqcn !== $node->name ? new IdentifierTypeNode($fqcn) : clone $node; } if ($node instanceof ConstTypeNode) { - if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { - $className = $node->constExpr->className; - $lowerClassName = strtolower($className); - - if ($lowerClassName === 'self' || $lowerClassName === 'parent') { - $resolvedClass = $className; - } else { - $resolvedClass = self::resolveFqcnForFile($className, $file); - } - - return new ConstTypeNode(new ConstFetchNode($resolvedClass, $node->constExpr->name)); - } - - return clone $node; + return self::resolveConstTypeForFile($node, $file); } if ($node instanceof GenericTypeNode) { $genericType = self::resolveForFile($node->type, $file); - $innerTypes = array_map( - fn ($t) => self::resolveForFile($t, $file), - $node->genericTypes - ); + $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -364,6 +190,22 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode ); } + if ($node instanceof OffsetAccessTypeNode) { + return self::resolveOffsetAccessForFile($node, $file); + } + + if ($node instanceof ArrayShapeNode) { + return self::resolveArrayShapeForFile($node, $file); + } + + if ($node instanceof ObjectShapeNode) { + return self::resolveObjectShapeForFile($node, $file); + } + + if ($node instanceof CallableTypeNode) { + return self::resolveCallableForFile($node, $file); + } + if ($node instanceof ConditionalTypeNode) { return new ConditionalTypeNode( self::resolveForFile($node->subjectType, $file), @@ -384,147 +226,407 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode ); } - if ($node instanceof OffsetAccessTypeNode) { - $baseType = self::resolveForFile($node->type, $file); - $offsetType = self::resolveForFile($node->offset, $file); - - $offsetKey = null; - if ($offsetType instanceof ConstTypeNode) { - $expr = $offsetType->constExpr; - if ($expr instanceof ConstExprStringNode) { - $offsetKey = $expr->value; - } elseif ($expr instanceof ConstExprIntegerNode) { - $offsetKey = (int) $expr->value; - } - } elseif ($offsetType instanceof IdentifierTypeNode) { - $offsetKey = $offsetType->name; + if ($node instanceof NullableTypeNode) { + return new NullableTypeNode(self::resolveForFile($node->type, $file)); + } + + if ($node instanceof ArrayTypeNode) { + return new ArrayTypeNode(self::resolveForFile($node->type, $file)); + } + + if ($node instanceof UnionTypeNode) { + return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); + } + + if ($node instanceof IntersectionTypeNode) { + return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); + } + + return clone $node; + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context + * + * @return \ReflectionClass|\ReflectionFunction|\ReflectionMethod + */ + private static function getReflectionContext(\ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context): \ReflectionClass|\ReflectionFunction|\ReflectionMethod + { + if (\is_string($context)) { + if (str_contains($context, '::')) { + [$className, $methodName] = explode('::', $context, 2); + + return new \ReflectionMethod($className, $methodName); } - if ($offsetKey !== null) { - if ($baseType instanceof ArrayShapeNode) { - foreach ($baseType->items as $item) { - $itemKey = null; - if ($item->keyName instanceof ConstExprStringNode) { - $itemKey = $item->keyName->value; - } elseif ($item->keyName instanceof IdentifierTypeNode) { - $itemKey = $item->keyName->name; - } elseif ($item->keyName instanceof ConstExprIntegerNode) { - $itemKey = (int) $item->keyName->value; - } + return new \ReflectionFunction($context); + } - if ((string) $itemKey === (string) $offsetKey) { - return $item->valueType; - } + return $context; + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveIdentifier(IdentifierTypeNode $node, ?string $declaringClass, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref): IdentifierTypeNode + { + $lower = strtolower($node->name); + + if ($lower === '$this' || $lower === 'static') { + return $node; + } + + if ($lower === 'self' && $declaringClass !== null) { + return new IdentifierTypeNode($declaringClass); + } + + if ($lower === 'parent' && $declaringClass !== null) { + $parentClass = get_parent_class($declaringClass); + if ($parentClass !== false) { + return new IdentifierTypeNode($parentClass); + } + } + + $fqcn = self::resolveFqcn($node->name, $ref); + + return $fqcn !== $node->name ? new IdentifierTypeNode($fqcn) : $node; + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveConstType(ConstTypeNode $node, ?string $declaringClass, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref): ConstTypeNode + { + if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { + $className = $node->constExpr->className; + $lowerClassName = strtolower($className); + + if ($lowerClassName === 'self' && $declaringClass !== null) { + $resolvedClass = $declaringClass; + } elseif ($lowerClassName === 'parent' && $declaringClass !== null) { + $parentClass = get_parent_class($declaringClass); + $resolvedClass = $parentClass !== false ? $parentClass : $className; + } else { + $resolvedClass = self::resolveFqcn($className, $ref); + } + + return new ConstTypeNode(new ConstFetchNode($resolvedClass, $node->constExpr->name)); + } + + return $node; + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveOffsetAccess(OffsetAccessTypeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): TypeNode + { + $baseType = self::resolve($node->type, $ref, $thisObj); + $offsetType = self::resolve($node->offset, $ref, $thisObj); + + $offsetKey = self::extractOffsetKey($offsetType); + + if ($offsetKey !== null) { + if ($baseType instanceof ArrayShapeNode) { + foreach ($baseType->items as $item) { + $itemKey = self::extractItemKey($item->keyName); + if ((string) $itemKey === (string) $offsetKey) { + return $item->valueType; } } + } - if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { - $constExpr = $baseType->constExpr; - $fqcn = $constExpr->className; - $constName = $constExpr->name; - - if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { - try { - $refClass = new \ReflectionClass($fqcn); - if ($refClass->hasConstant($constName)) { - $constValue = $refClass->getConstant($constName); - if (\is_array($constValue) && \array_key_exists($offsetKey, $constValue)) { - $val = $constValue[$offsetKey]; - if (\is_string($val)) { - return new ConstTypeNode(new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED)); - } elseif (\is_int($val)) { - return new ConstTypeNode(new ConstExprIntegerNode((string) $val)); - } - } - } - } catch (\ReflectionException $e) { - } - } + if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { + $resolvedNode = self::resolveConstantOffsetValue($baseType->constExpr->className, $baseType->constExpr->name, $offsetKey); + if ($resolvedNode !== null) { + return $resolvedNode; } } + } + + return new OffsetAccessTypeNode($baseType, $offsetType); + } - return new OffsetAccessTypeNode($baseType, $offsetType); + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): ArrayShapeNode + { + $items = array_map(function ($item) use ($ref, $thisObj) { + /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ + $keyName = $item->keyName; + + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { + $className = $keyName->className; + $lowerClassName = strtolower($className); + $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; + + if ($lowerClassName === 'self' && $declaringClass !== null) { + $resolvedClass = $declaringClass; + } elseif ($lowerClassName === 'parent' && $declaringClass !== null) { + $parentClass = get_parent_class($declaringClass); + $resolvedClass = $parentClass !== false ? $parentClass : $className; + } else { + $resolvedClass = self::resolveFqcn($className, $ref); + } + + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $keyName->name); + if ($resolvedKeyNode !== null) { + $keyName = $resolvedKeyNode; + } + } + + return new ArrayShapeItemNode( + $keyName, + $item->optional, + self::resolve($item->valueType, $ref, $thisObj) + ); + }, $node->items); + + if ($node->sealed) { + return ArrayShapeNode::createSealed($items, $node->kind); } - if ($node instanceof ArrayShapeNode) { - $items = array_map(function ($item) use ($file) { - return new ArrayShapeItemNode( - $item->keyName, - $item->optional, - self::resolveForFile($item->valueType, $file) - ); - }, $node->items); - - if ($node->sealed) { - return ArrayShapeNode::createSealed($items, $node->kind); + $unsealedType = null; + if ($node->unsealedType !== null) { + $unsealedKey = $node->unsealedType->keyType !== null ? self::resolve($node->unsealedType->keyType, $ref, $thisObj) : null; + $unsealedValue = self::resolve($node->unsealedType->valueType, $ref, $thisObj); + $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); + } + + return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveObjectShape(ObjectShapeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): ObjectShapeNode + { + $items = array_map(function ($item) use ($ref, $thisObj) { + return new ObjectShapeItemNode( + $item->keyName, + $item->optional, + self::resolve($item->valueType, $ref, $thisObj) + ); + }, $node->items); + + return new ObjectShapeNode($items); + } + + /** + * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref + */ + private static function resolveCallable(CallableTypeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): CallableTypeNode + { + $resolvedParameters = array_map(function (CallableTypeParameterNode $param) use ($ref, $thisObj) { + return new CallableTypeParameterNode( + self::resolve($param->type, $ref, $thisObj), + $param->isReference, + $param->isVariadic, + $param->parameterName, + $param->isOptional + ); + }, $node->parameters); + + $resolvedReturnType = self::resolve($node->returnType, $ref, $thisObj); + + return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes); + } + + private static function resolveConstTypeForFile(ConstTypeNode $node, string $file): ConstTypeNode + { + if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { + $className = $node->constExpr->className; + $lowerClassName = strtolower($className); + + if ($lowerClassName === 'self' || $lowerClassName === 'parent') { + $resolvedClass = $className; } else { - $unsealedType = null; - if ($node->unsealedType !== null) { - $unsealedKey = $node->unsealedType->keyType !== null ? self::resolveForFile($node->unsealedType->keyType, $file) : null; - $unsealedValue = self::resolveForFile($node->unsealedType->valueType, $file); - $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); + $resolvedClass = self::resolveFqcnForFile($className, $file); + } + + return new ConstTypeNode(new ConstFetchNode($resolvedClass, $node->constExpr->name)); + } + + return clone $node; + } + + private static function resolveOffsetAccessForFile(OffsetAccessTypeNode $node, string $file): TypeNode + { + $baseType = self::resolveForFile($node->type, $file); + $offsetType = self::resolveForFile($node->offset, $file); + + $offsetKey = self::extractOffsetKey($offsetType); + + if ($offsetKey !== null) { + if ($baseType instanceof ArrayShapeNode) { + foreach ($baseType->items as $item) { + $itemKey = self::extractItemKey($item->keyName); + if ((string) $itemKey === (string) $offsetKey) { + return $item->valueType; + } } + } - return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); + if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { + $resolvedNode = self::resolveConstantOffsetValue($baseType->constExpr->className, $baseType->constExpr->name, $offsetKey); + if ($resolvedNode !== null) { + return $resolvedNode; + } } } - if ($node instanceof ObjectShapeNode) { - $items = array_map(function ($item) use ($file) { - return new ObjectShapeItemNode( - $item->keyName, - $item->optional, - self::resolveForFile($item->valueType, $file) - ); - }, $node->items); + return new OffsetAccessTypeNode($baseType, $offsetType); + } - return new ObjectShapeNode($items); - } + private static function resolveArrayShapeForFile(ArrayShapeNode $node, string $file): ArrayShapeNode + { + $items = array_map(function ($item) use ($file) { + /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ + $keyName = $item->keyName; - if ($node instanceof CallableTypeNode) { - $resolvedParameters = array_map(function (CallableTypeParameterNode $param) use ($file) { - return new CallableTypeParameterNode( - self::resolveForFile($param->type, $file), - $param->isReference, - $param->isVariadic, - $param->parameterName, - $param->isOptional - ); - }, $node->parameters); - - $resolvedReturnType = self::resolveForFile($node->returnType, $file); - - return new CallableTypeNode( - $node->identifier, - $resolvedParameters, - $resolvedReturnType, - $node->templateTypes + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { + $className = $keyName->className; + $lowerClassName = strtolower($className); + + if ($lowerClassName !== 'self' && $lowerClassName !== 'parent') { + $resolvedClass = self::resolveFqcnForFile($className, $file); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $keyName->name); + if ($resolvedKeyNode !== null) { + $keyName = $resolvedKeyNode; + } + } + } + + return new ArrayShapeItemNode( + $keyName, + $item->optional, + self::resolveForFile($item->valueType, $file) ); + }, $node->items); + + if ($node->sealed) { + return ArrayShapeNode::createSealed($items, $node->kind); } - if ($node instanceof NullableTypeNode) { - return new NullableTypeNode(self::resolveForFile($node->type, $file)); + $unsealedType = null; + if ($node->unsealedType !== null) { + $unsealedKey = $node->unsealedType->keyType !== null ? self::resolveForFile($node->unsealedType->keyType, $file) : null; + $unsealedValue = self::resolveForFile($node->unsealedType->valueType, $file); + $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); } - if ($node instanceof ArrayTypeNode) { - return new ArrayTypeNode(self::resolveForFile($node->type, $file)); + return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); + } + + private static function resolveObjectShapeForFile(ObjectShapeNode $node, string $file): ObjectShapeNode + { + $items = array_map(function ($item) use ($file) { + return new ObjectShapeItemNode( + $item->keyName, + $item->optional, + self::resolveForFile($item->valueType, $file) + ); + }, $node->items); + + return new ObjectShapeNode($items); + } + + private static function resolveCallableForFile(CallableTypeNode $node, string $file): CallableTypeNode + { + $resolvedParameters = array_map(function (CallableTypeParameterNode $param) use ($file) { + return new CallableTypeParameterNode( + self::resolveForFile($param->type, $file), + $param->isReference, + $param->isVariadic, + $param->parameterName, + $param->isOptional + ); + }, $node->parameters); + + $resolvedReturnType = self::resolveForFile($node->returnType, $file); + + return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes); + } + + // --- Shared Utilities --- + + private static function extractOffsetKey(TypeNode $offsetType): string|int|null + { + if ($offsetType instanceof ConstTypeNode) { + $expr = $offsetType->constExpr; + if ($expr instanceof ConstExprStringNode) { + return $expr->value; + } + if ($expr instanceof ConstExprIntegerNode) { + return (int) $expr->value; + } + } + if ($offsetType instanceof IdentifierTypeNode) { + return $offsetType->name; } - if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map( - fn ($t) => self::resolveForFile($t, $file), - $node->types - )); + return null; + } + + private static function extractItemKey(mixed $keyName): string|int|null + { + if ($keyName instanceof ConstExprStringNode) { + return $keyName->value; + } + if ($keyName instanceof IdentifierTypeNode) { + return $keyName->name; + } + if ($keyName instanceof ConstExprIntegerNode) { + return (int) $keyName->value; } - if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map( - fn ($t) => self::resolveForFile($t, $file), - $node->types - )); + return null; + } + + private static function resolveConstantOffsetValue(string $fqcn, string $constName, string|int $offsetKey): ?TypeNode + { + if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { + try { + $refClass = new \ReflectionClass($fqcn); + if ($refClass->hasConstant($constName)) { + $constValue = $refClass->getConstant($constName); + if (\is_array($constValue) && \array_key_exists($offsetKey, $constValue)) { + $val = $constValue[$offsetKey]; + if (\is_string($val)) { + return new ConstTypeNode(new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED)); + } + if (\is_int($val)) { + return new ConstTypeNode(new ConstExprIntegerNode((string) $val)); + } + } + } + } catch (\ReflectionException $e) { + } } - return clone $node; + return null; + } + + private static function resolveConstantKeyValue(string $fqcn, string $constName): ConstExprStringNode|ConstExprIntegerNode|null + { + if (class_exists($fqcn) || interface_exists($fqcn)) { + try { + $refClass = new \ReflectionClass($fqcn); + if ($refClass->hasConstant($constName)) { + $val = $refClass->getConstant($constName); + if (\is_string($val)) { + return new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED); + } + if (\is_int($val)) { + return new ConstExprIntegerNode((string) $val); + } + } + } catch (\ReflectionException $e) { + } + } + + return null; } /** diff --git a/tests/Fixtures/Types/ConstKeyContainer.php b/tests/Fixtures/Types/ConstKeyContainer.php new file mode 100644 index 0000000..3c56895 --- /dev/null +++ b/tests/Fixtures/Types/ConstKeyContainer.php @@ -0,0 +1,19 @@ +process([ + 'user_id' => 42, + 'user_role' => 'admin', + ]))->toBeTrue(); + }); + + test('throws TypeError when array shape item with class constant key violates type contract', function () { + $container = new ConstKeyContainer(); + + expect(fn () => $container->process([ + 'user_id' => -5, + 'user_role' => 'admin', + ]))->toThrow(TypeError::class, "['user_id'] must be of type positive-int"); + }); + + test('throws TypeError when array shape references a non-existent class constant key', function () { + $container = new MissingConstKeyContainer(); + + expect(fn () => $container->process(['user_id' => 42])) + ->toThrow(TypeError::class, "is missing required key 'self::NON_EXISTENT_KEY'") + ; + }); + +}); diff --git a/tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php b/tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php index 1b5b930..d636966 100644 --- a/tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php +++ b/tests/TypeChecking/Configuration/DocblockIgnoreTagsTest.php @@ -28,8 +28,9 @@ function testIgnoredFunction(int $id): int test('skips type-checking on method marked with @typephp-ignore while enforcing normal methods in same class', function () { $fixture = new IgnoredMethod(); - expect(fn() => $fixture->normalMethod(-5)) - ->toThrow(TypeError::class, 'positive-int'); + expect(fn () => $fixture->normalMethod(-5)) + ->toThrow(TypeError::class, 'positive-int') + ; expect($fixture->ignoredMethod(-100))->toBe(-100); }); @@ -37,8 +38,9 @@ function testIgnoredFunction(int $id): int test('skips type-checking on class property marked with @typephp-ignore', function () { $fixture = new IgnoredMethod(); - expect(fn() => $fixture->setNormalProperty(-5)) - ->toThrow(TypeError::class, 'Property'); + expect(fn () => $fixture->setNormalProperty(-5)) + ->toThrow(TypeError::class, 'Property') + ; $fixture->setIgnoredProperty(-5); expect($fixture->ignoredProperty)->toBe(-5); @@ -52,9 +54,10 @@ function testIgnoredFunction(int $id): int }); test('skips inline variable validation inside methods marked with @typephp-ignore', function () { - $fixture = new class() { + $fixture = new class () { /** * @typephp-ignore + * * @param positive-int $id */ public function ignoredMethodWithInlineVar(int $id): bool From 217b4cfdbb9689f5dd2c451420a1dd861785f55c Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 15:38:53 +0800 Subject: [PATCH 07/17] Add ClassStringFactoryContainer and corresponding tests for class-string validation --- .../Types/ClassStringFactoryContainer.php | 18 ++++++++++++ .../Scalars/ClassStringInterfaceTest.php | 28 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/Fixtures/Types/ClassStringFactoryContainer.php create mode 100644 tests/TypeChecking/Scalars/ClassStringInterfaceTest.php diff --git a/tests/Fixtures/Types/ClassStringFactoryContainer.php b/tests/Fixtures/Types/ClassStringFactoryContainer.php new file mode 100644 index 0000000..6bd01c7 --- /dev/null +++ b/tests/Fixtures/Types/ClassStringFactoryContainer.php @@ -0,0 +1,18 @@ + $class + */ + public static function makeCountable(string $class): string + { + return $class; + } +} diff --git a/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php b/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php new file mode 100644 index 0000000..a41aca8 --- /dev/null +++ b/tests/TypeChecking/Scalars/ClassStringInterfaceTest.php @@ -0,0 +1,28 @@ + Subtype Validation', function () { + + test('accepts concrete class string implementing the target interface', function () { + expect(ClassStringFactoryContainer::makeCountable(CountableArrayAccess::class)) + ->toBe(CountableArrayAccess::class) + ; + }); + + test('accepts the interface string itself', function () { + expect(ClassStringFactoryContainer::makeCountable(Countable::class)) + ->toBe(Countable::class) + ; + }); + + test('throws TypeError when class string does not implement the target interface', function () { + expect(fn () => ClassStringFactoryContainer::makeCountable(Car::class)) + ->toThrow(TypeError::class, 'must be a class-string of Countable') + ; + }); +}); From 437d369d928089b01efd39977ffad5260070a0a9 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 16:05:25 +0800 Subject: [PATCH 08/17] Refactor SpecialTypeResolver to extract class and constant names for improved key resolution in reflection contexts to support 8.2 below --- src/Resolver/SpecialTypeResolver.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 8176fa9..6e76c76 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -245,6 +245,8 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode return clone $node; } + // --- Private Helper Extractions for Reflection Context --- + /** * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context * @@ -356,8 +358,17 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ $keyName = $item->keyName; + $className = null; + $constName = null; + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; + $constName = $keyName->name; + } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { + [$className, $constName] = explode('::', $keyName->name, 2); + } + + if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -370,7 +381,7 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } - $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $keyName->name); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { $keyName = $resolvedKeyNode; } @@ -433,6 +444,8 @@ private static function resolveCallable(CallableTypeNode $node, \ReflectionClass return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes); } + // --- Private Helper Extractions for File Context --- + private static function resolveConstTypeForFile(ConstTypeNode $node, string $file): ConstTypeNode { if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { @@ -485,13 +498,22 @@ private static function resolveArrayShapeForFile(ArrayShapeNode $node, string $f /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ $keyName = $item->keyName; + $className = null; + $constName = null; + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; + $constName = $keyName->name; + } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { + [$className, $constName] = explode('::', $keyName->name, 2); + } + + if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); if ($lowerClassName !== 'self' && $lowerClassName !== 'parent') { $resolvedClass = self::resolveFqcnForFile($className, $file); - $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $keyName->name); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { $keyName = $resolvedKeyNode; } From 2a725041fbbdace5cf3fcc46e3701aaf1f3d204e Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 16:35:34 +0800 Subject: [PATCH 09/17] Improve property hook checks and enhance node handling in PropertyHookInjector --- src/Internal/Visitor/PropertyHookInjector.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Internal/Visitor/PropertyHookInjector.php b/src/Internal/Visitor/PropertyHookInjector.php index 65e26d3..8afc951 100644 --- a/src/Internal/Visitor/PropertyHookInjector.php +++ b/src/Internal/Visitor/PropertyHookInjector.php @@ -16,7 +16,7 @@ final class PropertyHookInjector { public static function process(Node\Stmt\Property $node): void { - if ($node->hooks === []) { + if (! isset($node->hooks) || ! \is_array($node->hooks) || $node->hooks === []) { return; } @@ -76,15 +76,15 @@ public function __construct(private string $propertyName) { } - public function enterNode(Node $n): int|null + public function enterNode(Node $node): int|null { - if ($n instanceof Node\Expr\Closure || $n instanceof Node\Expr\ArrowFunction || $n instanceof Node\Stmt\Function_ || $n instanceof Node\Stmt\ClassMethod) { + if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { return NodeTraverser::DONT_TRAVERSE_CHILDREN; } - if ($n instanceof Node\Stmt\Return_ && $n->expr !== null) { - $checkCall = NodeBuilder::createPropertyCheckCall($n->expr, new Node\Expr\Variable('this'), $this->propertyName); - $n->expr = NodeBuilder::createTernaryThrowExpr($checkCall); + if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) { + $checkCall = NodeBuilder::createPropertyCheckCall($node->expr, new Node\Expr\Variable('this'), $this->propertyName); + $node->expr = NodeBuilder::createTernaryThrowExpr($checkCall); } return null; @@ -96,4 +96,4 @@ public function enterNode(Node $n): int|null return $newStmts; } -} +} \ No newline at end of file From df9316bf45bccaf61b4b1d6e180a8f7ddf9c02f3 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 17:13:49 +0800 Subject: [PATCH 10/17] Refactor CI workflow for PHP 8.2 and streamline test execution; add debug logging in SpecialTypeResolver --- .github/workflows/ci.yml | 23 +++++++----------- src/Resolver/SpecialTypeResolver.php | 36 +++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0318c0..56b036e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,8 @@ permissions: jobs: test: - name: PHP ${{ matrix.php }} (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - php: ['8.2', '8.3', '8.4', '8.5'] + name: PHP 8.2 Debug (ubuntu-latest) + runs-on: ubuntu-latest steps: - name: Checkout Code @@ -29,18 +23,17 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + php-version: '8.2' extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo coverage: none - name: Install Dependencies uses: ramsey/composer-install@v3 with: - dependency-versions: ${{ matrix.php == '8.2' && 'lowest' || 'highest' }} + dependency-versions: lowest - - name: Run Static Analysis (PHPStan) - run: ./vendor/bin/phpstan analyse --no-progress - if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3' + - name: Clear TypePHP Cache + run: ./vendor/bin/typephp cache:clear - - name: Run Test Suite (Pest) - run: ./vendor/bin/pest --ci \ No newline at end of file + - name: Run Test Suite (Pest) with Live Debug + run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php --ci \ No newline at end of file diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 6e76c76..0bdbaf1 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -91,7 +91,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct if ($node instanceof GenericTypeNode) { $genericType = self::resolve($node->type, $ref, $thisObj); - $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); + $innerTypes = array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -145,11 +145,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); } return $node; @@ -181,7 +181,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode if ($node instanceof GenericTypeNode) { $genericType = self::resolveForFile($node->type, $file); - $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); + $innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -235,11 +235,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); + return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); + return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); } return clone $node; @@ -361,13 +361,32 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $className = null; $constName = null; + // --- LIVE CI DEBUG LOGGING --- + $debugClass = $keyName !== null ? get_class($keyName) : 'null'; + $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; + fwrite(\STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); + + if ($keyName instanceof IdentifierTypeNode) { + fwrite(\STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); + } + // ----------------------------- + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { + // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); } + // --- MORE DEBUG LOGGING --- + if ($className !== null && $constName !== null) { + fwrite(\STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); + } else { + fwrite(\STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); + } + // -------------------------- + if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -381,9 +400,14 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } + fwrite(\STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { + fwrite(\STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; + } else { + fwrite(\STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } From 3718f6773b01ff1b0638994fc168e67d555e9eab Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 17:18:07 +0800 Subject: [PATCH 11/17] Refactor code style for consistency in SpecialTypeResolver; update cache clear command in CI workflow --- .github/workflows/ci.yml | 2 +- src/Resolver/SpecialTypeResolver.php | 36 +++++----------------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56b036e..e24412e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: dependency-versions: lowest - name: Clear TypePHP Cache - run: ./vendor/bin/typephp cache:clear + run: php bin/typephp cache:clear - name: Run Test Suite (Pest) with Live Debug run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php --ci \ No newline at end of file diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 0bdbaf1..6e76c76 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -91,7 +91,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct if ($node instanceof GenericTypeNode) { $genericType = self::resolve($node->type, $ref, $thisObj); - $innerTypes = array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); + $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -145,11 +145,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } return $node; @@ -181,7 +181,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode if ($node instanceof GenericTypeNode) { $genericType = self::resolveForFile($node->type, $file); - $innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes); + $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -235,11 +235,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); + return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); } return clone $node; @@ -361,32 +361,13 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $className = null; $constName = null; - // --- LIVE CI DEBUG LOGGING --- - $debugClass = $keyName !== null ? get_class($keyName) : 'null'; - $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; - fwrite(\STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); - - if ($keyName instanceof IdentifierTypeNode) { - fwrite(\STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); - } - // ----------------------------- - if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { - // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); } - // --- MORE DEBUG LOGGING --- - if ($className !== null && $constName !== null) { - fwrite(\STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); - } else { - fwrite(\STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); - } - // -------------------------- - if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -400,14 +381,9 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } - fwrite(\STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); - $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { - fwrite(\STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; - } else { - fwrite(\STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } From 2f15e6e00b5a45ddf817f7018cc2699cc20dd8da Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 17:19:34 +0800 Subject: [PATCH 12/17] Fix typos and improve debug logging in SpecialTypeResolver; correct GenericTypeNode instantiation and add detailed logging for array shape resolution --- src/Resolver/SpecialTypeResolver.php | 57 +++++++++++++++++++--------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 6e76c76..2c2c560 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -89,11 +89,9 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct return self::resolveConstType($node, $declaringClass, $ref); } - if ($node instanceof GenericTypeNode) { - $genericType = self::resolve($node->type, $ref, $thisObj); - $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); + if ($node instanceof $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); - return new GenericTypeNode( + return new GnericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, $innerTypes, $node->variances @@ -148,11 +146,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } - if ($node instanceof IntersectionTypeNode) { + if ($node intanceof IntersectionTypeNode) { return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } - return $node; + return $node } /** @@ -183,7 +181,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode $genericType = self::resolveForFile($node->type, $file); $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); - return new GenericTypeNode( + return new GenricTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, $innerTypes, $node->variances @@ -238,8 +236,13 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); } - if ($node instanceof IntersectionTypeNode) { + if ($node instnceof IntersectionTypeNode) { return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); +(fn($t) => self::resolveForFile($, $file), $node->types)); + } + + if ($node instanceof IntersectionTypeNode) { + return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); } return clone $node; @@ -341,15 +344,7 @@ private static function resolveOffsetAccess(OffsetAccessTypeNode $node, \Reflect if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { $resolvedNode = self::resolveConstantOffsetValue($baseType->constExpr->className, $baseType->constExpr->name, $offsetKey); if ($resolvedNode !== null) { - return $resolvedNode; - } - } - } - - return new OffsetAccessTypeNode($baseType, $offsetType); - } - - /** + /** * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref */ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): ArrayShapeNode @@ -361,13 +356,32 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $className = null; $constName = null; + // --- LIVE CI DEBUG LOGGING --- + $debugClass = $keyName !== null ? get_class($keyName) : 'null'; + $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; + fwrite(\STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); + + if ($keyName instanceof IdentifierTypeNode) { + fwrite(\STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); + } + // ----------------------------- + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { + // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); } + // --- MORE DEBUG LOGGING --- + if ($className !== null && $constName !== null) { + fwrite(\STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); + } else { + fwrite(\STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); + } + // -------------------------- + if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -381,9 +395,14 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } + fwrite(\STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { + fwrite(\STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; + } else { + fwrite(\STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } @@ -405,6 +424,10 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); } + return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); + }nsealedTypeNode($unsealedValue, $unsealedKey); + } + return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); } From 941a8895bde92ff0bf945a3a75918b33fd722ae1 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 17:23:07 +0800 Subject: [PATCH 13/17] Fix typos and improve type handling in SpecialTypeResolver; correct GenericTypeNode instantiation and enhance debug logging --- src/Resolver/SpecialTypeResolver.php | 45 +++++++++++++--------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 2c2c560..4323ee1 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -89,9 +89,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct return self::resolveConstType($node, $declaringClass, $ref); } - if ($node instanceof $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); + if ($node instanceof GenericTypeNode) { + $genericType = self::resolve($node->type, $ref, $thisObj); + $innerTypes = array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); - return new GnericTypeNode( + return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, $innerTypes, $node->variances @@ -143,14 +145,14 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); } - if ($node intanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); + if ($node instanceof IntersectionTypeNode) { + return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); } - return $node + return $node; } /** @@ -179,9 +181,9 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode if ($node instanceof GenericTypeNode) { $genericType = self::resolveForFile($node->type, $file); - $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); + $innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes); - return new GenricTypeNode( + return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, $innerTypes, $node->variances @@ -233,12 +235,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); - } - - if ($node instnceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); -(fn($t) => self::resolveForFile($, $file), $node->types)); + return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); } if ($node instanceof IntersectionTypeNode) { @@ -248,8 +245,6 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode return clone $node; } - // --- Private Helper Extractions for Reflection Context --- - /** * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context * @@ -344,7 +339,15 @@ private static function resolveOffsetAccess(OffsetAccessTypeNode $node, \Reflect if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { $resolvedNode = self::resolveConstantOffsetValue($baseType->constExpr->className, $baseType->constExpr->name, $offsetKey); if ($resolvedNode !== null) { - /** + return $resolvedNode; + } + } + } + + return new OffsetAccessTypeNode($baseType, $offsetType); + } + + /** * @param \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref */ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass|\ReflectionFunction|\ReflectionMethod $ref, ?object $thisObj): ArrayShapeNode @@ -356,7 +359,6 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $className = null; $constName = null; - // --- LIVE CI DEBUG LOGGING --- $debugClass = $keyName !== null ? get_class($keyName) : 'null'; $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; fwrite(\STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); @@ -364,7 +366,6 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass if ($keyName instanceof IdentifierTypeNode) { fwrite(\STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); } - // ----------------------------- if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; @@ -374,13 +375,11 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass [$className, $constName] = explode('::', $keyName->name, 2); } - // --- MORE DEBUG LOGGING --- if ($className !== null && $constName !== null) { fwrite(\STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); } else { fwrite(\STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); } - // -------------------------- if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); @@ -424,10 +423,6 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $unsealedType = new ArrayShapeUnsealedTypeNode($unsealedValue, $unsealedKey); } - return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); - }nsealedTypeNode($unsealedValue, $unsealedKey); - } - return ArrayShapeNode::createUnsealed($items, $unsealedType, $node->kind); } From 777b2a6ad917f0765f9c3dbc6b3bfcb3faf6041e Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 17:27:20 +0800 Subject: [PATCH 14/17] Enhance debug logging in SpecialTypeResolver for PHP 8.2 compatibility; add runtime exception for specific AST node values --- src/Resolver/SpecialTypeResolver.php | 31 ++++++++++------------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 4323ee1..d70544a 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -356,31 +356,27 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ $keyName = $item->keyName; - $className = null; - $constName = null; + // --- CRASH AND DUMP AST NODE CLASS ON PHP 8.2 --- + if ($keyName !== null) { + $classStr = get_class($keyName); + $valStr = method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; - $debugClass = $keyName !== null ? get_class($keyName) : 'null'; - $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; - fwrite(\STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); - - if ($keyName instanceof IdentifierTypeNode) { - fwrite(\STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); + if (str_contains($valStr, 'self::')) { + throw new \RuntimeException("[DEBUG PHP 8.2] AST Class: {$classStr} | Value: {$valStr}"); + } } + // ------------------------------------------------- + + $className = null; + $constName = null; if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { - // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); } - if ($className !== null && $constName !== null) { - fwrite(\STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); - } else { - fwrite(\STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); - } - if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -394,14 +390,9 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } - fwrite(\STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); - $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { - fwrite(\STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; - } else { - fwrite(\STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } From fa0117089904c87e2b6d839d0462e7c17daea96c Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 20:50:40 +0800 Subject: [PATCH 15/17] Enhance debug logging in SpecialTypeResolver; add detailed output for key type resolution and constant reflection --- .github/workflows/ci.yml | 2 +- src/Resolver/SpecialTypeResolver.php | 31 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e24412e..9007a81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,4 +36,4 @@ jobs: run: php bin/typephp cache:clear - name: Run Test Suite (Pest) with Live Debug - run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php --ci \ No newline at end of file + run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php \ No newline at end of file diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index d70544a..520cd54 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -356,27 +356,31 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass /** @var ConstExprIntegerNode|ConstExprStringNode|ConstFetchNode|IdentifierTypeNode|null $keyName */ $keyName = $item->keyName; - // --- CRASH AND DUMP AST NODE CLASS ON PHP 8.2 --- - if ($keyName !== null) { - $classStr = get_class($keyName); - $valStr = method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; - - if (str_contains($valStr, 'self::')) { - throw new \RuntimeException("[DEBUG PHP 8.2] AST Class: {$classStr} | Value: {$valStr}"); - } - } - // ------------------------------------------------- - $className = null; $constName = null; + $debugClass = $keyName !== null ? get_class($keyName) : 'null'; + $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; + fwrite(STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); + + if ($keyName instanceof IdentifierTypeNode) { + fwrite(STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); + } + if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { + // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); } + if ($className !== null && $constName !== null) { + fwrite(STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); + } else { + fwrite(STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); + } + if ($className !== null && $constName !== null) { $lowerClassName = strtolower($className); $declaringClass = $ref instanceof \ReflectionMethod ? $ref->getDeclaringClass()->getName() : null; @@ -390,9 +394,14 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } + fwrite(STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); + $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { + fwrite(STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; + } else { + fwrite(STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } From c6c96261ecfb231e476d43cb529cd459771c7157 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 21:25:08 +0800 Subject: [PATCH 16/17] Add support for wrapping class constant array shape keys in quotes for legacy phpdoc-parser compatibility in DocblockNormalizer --- src/Internal/DocblockNormalizer.php | 2 + src/Internal/Visitor/PropertyHookInjector.php | 2 +- src/Resolver/SpecialTypeResolver.php | 38 +++++-------------- tests/Internal/DocblockNormalizerTest.php | 8 ++++ 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/Internal/DocblockNormalizer.php b/src/Internal/DocblockNormalizer.php index d391f2b..f274f0b 100644 --- a/src/Internal/DocblockNormalizer.php +++ b/src/Internal/DocblockNormalizer.php @@ -28,6 +28,8 @@ final class DocblockNormalizer */ public static function normalize(string $doc): string { + $doc = preg_replace('/(\\\\?[a-zA-Z_\x80-\xff][\\\\a-zA-Z0-9_\x80-\xff]*::[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*(\??:)/', '"$1"$2', $doc) ?? $doc; + if (! str_contains($doc, '{')) { return $doc; } diff --git a/src/Internal/Visitor/PropertyHookInjector.php b/src/Internal/Visitor/PropertyHookInjector.php index 8afc951..0b843c6 100644 --- a/src/Internal/Visitor/PropertyHookInjector.php +++ b/src/Internal/Visitor/PropertyHookInjector.php @@ -96,4 +96,4 @@ public function enterNode(Node $node): int|null return $newStmts; } -} \ No newline at end of file +} diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index 520cd54..8da48d2 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -91,7 +91,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct if ($node instanceof GenericTypeNode) { $genericType = self::resolve($node->type, $ref, $thisObj); - $innerTypes = array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); + $innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -145,11 +145,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types)); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types)); } return $node; @@ -181,7 +181,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode if ($node instanceof GenericTypeNode) { $genericType = self::resolveForFile($node->type, $file); - $innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes); + $innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes); return new GenericTypeNode( $genericType instanceof IdentifierTypeNode ? $genericType : $node->type, @@ -235,11 +235,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode } if ($node instanceof UnionTypeNode) { - return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); + return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); } if ($node instanceof IntersectionTypeNode) { - return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types)); + return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types)); } return clone $node; @@ -359,26 +359,13 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $className = null; $constName = null; - $debugClass = $keyName !== null ? get_class($keyName) : 'null'; - $debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown'; - fwrite(STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n"); - - if ($keyName instanceof IdentifierTypeNode) { - fwrite(STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n"); - } - if ($keyName instanceof ConstFetchNode && $keyName->className !== '') { $className = $keyName->className; $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { - // Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers [$className, $constName] = explode('::', $keyName->name, 2); - } - - if ($className !== null && $constName !== null) { - fwrite(STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n"); - } else { - fwrite(STDERR, "[DEBUG] Did NOT split into Class and Const.\n"); + } elseif ($keyName instanceof ConstExprStringNode && str_contains($keyName->value, '::')) { + [$className, $constName] = explode('::', $keyName->value, 2); } if ($className !== null && $constName !== null) { @@ -394,14 +381,9 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass $resolvedClass = self::resolveFqcn($className, $ref); } - fwrite(STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n"); - $resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName); if ($resolvedKeyNode !== null) { - fwrite(STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n"); $keyName = $resolvedKeyNode; - } else { - fwrite(STDERR, "[DEBUG] FAILED to Reflect Constant!\n"); } } @@ -462,8 +444,6 @@ private static function resolveCallable(CallableTypeNode $node, \ReflectionClass return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes); } - // --- Private Helper Extractions for File Context --- - private static function resolveConstTypeForFile(ConstTypeNode $node, string $file): ConstTypeNode { if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') { @@ -524,6 +504,8 @@ private static function resolveArrayShapeForFile(ArrayShapeNode $node, string $f $constName = $keyName->name; } elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) { [$className, $constName] = explode('::', $keyName->name, 2); + } elseif ($keyName instanceof ConstExprStringNode && str_contains($keyName->value, '::')) { + [$className, $constName] = explode('::', $keyName->value, 2); } if ($className !== null && $constName !== null) { diff --git a/tests/Internal/DocblockNormalizerTest.php b/tests/Internal/DocblockNormalizerTest.php index bb2148e..519d4ba 100644 --- a/tests/Internal/DocblockNormalizerTest.php +++ b/tests/Internal/DocblockNormalizerTest.php @@ -86,4 +86,12 @@ expect(DocblockNormalizer::normalize($doc))->toBe($expected); }); + + test('wraps class constant array shape keys in quotes for legacy phpdoc-parser compatibility', function () { + $doc = '/** @param array{self::KEY_ID: int, App\Constants::ROLE: string} $payload */'; + + $expected = '/** @param array{"self::KEY_ID": int, "App\Constants::ROLE": string} $payload */'; + + expect(DocblockNormalizer::normalize($doc))->toBe($expected); + }); }); From 44bb131cf5e9d6aad571e6403a55151d99e1b090 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 21:28:00 +0800 Subject: [PATCH 17/17] Refactor CI workflow to use matrix strategy for PHP versions and OS; update dependency installation logic and streamline test execution --- .github/workflows/ci.yml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9007a81..c0318c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,14 @@ permissions: jobs: test: - name: PHP 8.2 Debug (ubuntu-latest) - runs-on: ubuntu-latest + name: PHP ${{ matrix.php }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + php: ['8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout Code @@ -23,17 +29,18 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: ${{ matrix.php }} extensions: dom, mbstring, zip, libxml, json, tokenizer, fileinfo coverage: none - name: Install Dependencies uses: ramsey/composer-install@v3 with: - dependency-versions: lowest + dependency-versions: ${{ matrix.php == '8.2' && 'lowest' || 'highest' }} - - name: Clear TypePHP Cache - run: php bin/typephp cache:clear + - name: Run Static Analysis (PHPStan) + run: ./vendor/bin/phpstan analyse --no-progress + if: matrix.os == 'ubuntu-latest' && matrix.php == '8.3' - - name: Run Test Suite (Pest) with Live Debug - run: ./vendor/bin/pest tests/TypeChecking/ArraysAndShapes/ClassConstKeyShapeTest.php \ No newline at end of file + - name: Run Test Suite (Pest) + run: ./vendor/bin/pest --ci \ No newline at end of file