From 028cfadc150fb8d7317bb48e142a854ed42a6c6a Mon Sep 17 00:00:00 2001 From: TomasVotruba Date: Tue, 8 Sep 2026 17:59:37 +0200 Subject: [PATCH 1/5] Add Docblock Check CI to spot invalid @param/@return array types Runs honestype: instruments src and rules, collects docblock type mismatches while the test suite runs, then lists them PHPStan-style and fails the job when any are found. Also fixes one surfaced mismatch: configure() @param is string[], matching its Assert::allString and list values. --- .github/workflows/docblock_check.yaml | 65 +++++++++++++++++++ .../StringClassNameToClassConstantRector.php | 2 +- .../NodeAnalyzer/CallTypesResolver.php | 9 ++- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docblock_check.yaml diff --git a/.github/workflows/docblock_check.yaml b/.github/workflows/docblock_check.yaml new file mode 100644 index 00000000000..ebefcf6b7e5 --- /dev/null +++ b/.github/workflows/docblock_check.yaml @@ -0,0 +1,65 @@ +name: Docblock Check + +on: + pull_request: null + +env: + COMPOSER_ROOT_VERSION: "dev-main" + DOCBLOCK_CHECK_LOG: "/tmp/docblock-check.log" + +jobs: + docblock_check: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v5 + + - + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + ini-values: zend.assertions=1 + + - uses: "ramsey/composer-install@v4" + + - + uses: actions/setup-go@v5 + with: + go-version: "1.26" + + - + name: Install honestype docblock checker + run: | + GOPROXY=direct GOSUMDB=off go install github.com/rectorphp/honestype@main + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - + name: Instrument sources + run: | + honestype instrument src + honestype instrument rules + + # Load the helper for every PHP process, including any parallel test + # workers which do not inherit a -d auto_prepend_file flag. + - + name: Enable runtime helper globally + run: | + INI="$(php -i | grep '^Loaded Configuration File' | awk '{print $NF}')" + echo "auto_prepend_file=$GITHUB_WORKSPACE/src/docblock_check.php" | sudo tee -a "$INI" + + - + name: Run tests to collect docblock type mismatches + run: vendor/bin/phpunit tests rules-tests utils/phpstan/tests || true + + - + name: Restore sources + if: always() + run: | + git checkout -- src rules + rm -f src/docblock_check.php rules/docblock_check.php + + - + name: List docblock type mismatches + run: honestype report "$DOCBLOCK_CHECK_LOG" diff --git a/rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php b/rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php index 9029e7b5b46..bfc2fa1e6a2 100644 --- a/rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php +++ b/rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php @@ -104,7 +104,7 @@ public function refactor(Node $node): ClassConstFetch|null } /** - * @param array $configuration + * @param string[] $configuration */ public function configure(array $configuration): void { diff --git a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php index 857b8fcfeaa..c5b441ec67c 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php +++ b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php @@ -47,6 +47,11 @@ public function resolveStrictTypesFromCalls(array $calls): array return []; } + // must be int + if (! is_int($position)) { + continue; + } + /** @var Arg $arg */ $staticTypesByArgumentPosition[$position][] = $this->resolveStrictArgValueType($arg); } @@ -110,8 +115,8 @@ private function correctSelfType(Type $argValueType): Type } /** - * @param array $staticTypesByArgumentPosition - * @return array + * @param array $staticTypesByArgumentPosition + * @return array */ private function unionToSingleType(array $staticTypesByArgumentPosition, bool $removeMixedArray = false): array { From 5f628eb650efe964f08cde3ec9c64873eb374d0c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 8 Sep 2026 22:21:06 +0200 Subject: [PATCH 2/5] require stmts in array --- src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php index f05f87dc7e8..b632b3b3594 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php +++ b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php @@ -461,6 +461,8 @@ private function nodeScopeResolverProcessNodes( MutatingScope $mutatingScope, callable $nodeCallback ): void { + Assert::allIsInstanceOf($stmts, Stmt::class); + try { $this->nodeScopeResolver->processNodes($stmts, $mutatingScope, $nodeCallback); } catch (ParserErrorsException|ParserException|ShouldNotHappenException|UndefinedVariableException) { From 417bb3d756eab73f5d63c62a8308b2d6d43a1731 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 8 Sep 2026 22:31:04 +0200 Subject: [PATCH 3/5] fix property hook block body scope resolution Claude-Session: https://claude.ai/code/session_01X2RJNwg9ij4Z8b725WTVB6 --- src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php index b632b3b3594..bc4b6a8591f 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php +++ b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php @@ -631,7 +631,7 @@ private function processProperty(Property $property, MutatingScope $mutatingScop /** @var Stmt[] $stmts */ $stmts = $hook->body instanceof Expr ? [new Expression($hook->body)] - : [$hook->body]; + : $hook->body; $this->nodeScopeResolverProcessNodes($stmts, $mutatingScope, $nodeCallback); } } From 53dddb666359e7725196a094bd96b58a64fc9510 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 8 Sep 2026 22:58:32 +0200 Subject: [PATCH 4/5] fix order-dependent test state leaks in tests/ Claude-Session: https://claude.ai/code/session_01X2RJNwg9ij4Z8b725WTVB6 --- tests/Config/RectorConfigTest.php | 5 +++++ tests/Configuration/ConfigurationFactoryTest.php | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/tests/Config/RectorConfigTest.php b/tests/Config/RectorConfigTest.php index 0f054548c7c..04123037f37 100644 --- a/tests/Config/RectorConfigTest.php +++ b/tests/Config/RectorConfigTest.php @@ -27,6 +27,11 @@ protected function setUp(): void // the registered-rule lists so the assertions hold whether this class // runs alone or batched into one warm process by a parallel runner RectorConfig::resetRecreated(); + + // the container is shared across tests, so its per-rule dedupe maps persist and would + // swallow a re-registration; reset them so REGISTERED_RECTOR_RULES fills regardless of order + self::getContainer()->resetRuleConfigurations(); + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, []); SimpleParameterProvider::setParameter(Option::ROOT_STANDALONE_REGISTERED_RULES, []); } diff --git a/tests/Configuration/ConfigurationFactoryTest.php b/tests/Configuration/ConfigurationFactoryTest.php index 0085c8c8ac2..adae31167c1 100644 --- a/tests/Configuration/ConfigurationFactoryTest.php +++ b/tests/Configuration/ConfigurationFactoryTest.php @@ -15,6 +15,13 @@ final class ConfigurationFactoryTest extends AbstractLazyTestCase { + protected function tearDown(): void + { + SimpleParameterProvider::setParameter(Option::IS_RUN_NARROWED, false); + SimpleParameterProvider::setParameter(Option::SOURCE, []); + SimpleParameterProvider::setParameter(Option::PATHS, []); + } + public function test(): void { $configurationFactory = $this->make(ConfigurationFactory::class); From 2299b7858783ad803e933fad0c5ede014e8cd0d5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 8 Sep 2026 23:53:42 +0200 Subject: [PATCH 5/5] static fixes --- rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php | 2 +- .../NodeAnalyzer/ClassMethodParamTypeCompleter.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php index c5b441ec67c..f315ff24203 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php +++ b/rules/TypeDeclaration/NodeAnalyzer/CallTypesResolver.php @@ -35,7 +35,7 @@ public function __construct( /** * @param MethodCall[]|StaticCall[] $calls - * @return array + * @return array */ public function resolveStrictTypesFromCalls(array $calls): array { diff --git a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodParamTypeCompleter.php b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodParamTypeCompleter.php index 104ac4f9695..91aeb065f16 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/ClassMethodParamTypeCompleter.php +++ b/rules/TypeDeclaration/NodeAnalyzer/ClassMethodParamTypeCompleter.php @@ -25,7 +25,7 @@ public function __construct( } /** - * @param array $classParameterTypes + * @param array $classParameterTypes */ public function complete(ClassMethod $classMethod, array $classParameterTypes, int $maxUnionTypes): ?ClassMethod { @@ -73,7 +73,7 @@ public function complete(ClassMethod $classMethod, array $classParameterTypes, i private function shouldSkipArgumentStaticType( ClassMethod $classMethod, Type $argumentStaticType, - int $position, + int|string $position, int $maxUnionTypes ): bool { if ($argumentStaticType instanceof MixedType) {