Skip to content

Fix phpstan/phpstan#14138: errors for argument array template types no longer reported (argument.type)#5300

Merged
VincentLanglet merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-9u15nnk
Mar 26, 2026
Merged

Fix phpstan/phpstan#14138: errors for argument array template types no longer reported (argument.type)#5300
VincentLanglet merged 4 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-9u15nnk

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Since 2.1.38, PHPStan stopped reporting argument.type errors when constructing a class that extends a generic parent class with promoted property template types. For example, new Foo(["foo" => 1]) where Foo extends AbstractApiData<array{foo: int, bar: int}> should report a missing bar key error but didn't.

Changes

  • Moved the template type resolution loop for $phpDocParameterTypes outside the if ($resolvedPhpDoc !== null) block in src/Reflection/Php/PhpClassReflectionExtension.php (createUserlandMethodReflection method)
  • Added regression test in tests/PHPStan/Rules/Classes/data/bug-14138.php

Root cause

The phpDoc inheritance rework (commit 617a3a8) changed PhpDocInheritanceResolver::resolvePhpDocForMethod() to return ?ResolvedPhpDocBlock (nullable). When a constructor has no doc comment of its own (only @var T on promoted properties), $resolvedPhpDoc is null. The template type resolution for $phpDocParameterTypes was inside the if ($resolvedPhpDoc !== null) block, so promoted property types like T were never resolved against the class's active template type map (e.g. T => array{foo: int, bar: int} from @extends). The parameter type stayed as the raw TemplateType with bound array, which accepted any array argument.

Test

Added a rule test (testBug14138) that verifies passing ["foo" => 1] to new Foo(...) (where Foo extends AbstractApiData<array{foo: int, bar: int}>) correctly reports the argument.type error about the missing bar key.

Fixes phpstan/phpstan#14138

…late types on promoted properties

- Promoted property @var T types were not resolved against the class template type map when the constructor had no phpDoc of its own
- Moved template type resolution of phpDocParameterTypes outside the `if ($resolvedPhpDoc !== null)` block in PhpClassReflectionExtension::createUserlandMethodReflection
- New regression test in tests/PHPStan/Rules/Classes/data/bug-14138.php
- The root cause was that the phpDoc inheritance rework (617a3a8) made resolvePhpDocForMethod return null for constructors without doc comments, causing the template resolution to be skipped
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if there is more code to move out the if ($resolvedPhpDoc !== null) {

The class-level purity check (areAllMethodsPure/areAllMethodsImpure)
reads from phpDocBlockClassReflection->getResolvedPhpDoc(), not from
the method's resolvedPhpDoc. It should run even when resolvedPhpDoc
is null (e.g. constructors with promoted properties and no method doc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I moved the class-level $isPure check (for areAllMethodsPure/areAllMethodsImpure) outside the if ($resolvedPhpDoc !== null) block, same as was done for the $phpDocParameterTypes template resolution.

This was the only other code in the block that doesn't depend on $resolvedPhpDoc — it reads from $phpDocBlockClassReflection->getResolvedPhpDoc() (the class's phpdoc, not the method's). Everything else in the block ($templateTypeMap, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocReturnType, $phpDocThrowType, param tags, param-out tags, deprecation, $isInternal, $isFinal, $asserts, $acceptsNamedArguments, $selfOutType, $phpDocComment) directly reads from $resolvedPhpDoc and correctly belongs inside the null check.

Tests and static analysis both pass.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a non regression test for the isPure condition you moved

…ties

Verifies that @phpstan-all-methods-pure correctly propagates to methods
in classes with promoted constructor properties, even when the method's
$resolvedPhpDoc is null. This is a non-regression test for the isPure
condition moved outside the `if ($resolvedPhpDoc !== null)` block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added a regression test for the isPure condition that was moved outside the if ($resolvedPhpDoc !== null) block:

  • Test data (tests/PHPStan/Rules/Pure/data/bug-14138-pure.php): Two classes with @phpstan-all-methods-pure and promoted constructor properties. One has a pure method (no errors), the other has an impure echo side effect that should be caught.
  • Test case (testBug14138Pure in PureMethodRuleTest.php): Verifies that the echo side effect in PureClassWithSideEffect::doSomething() is correctly reported as impure, confirming that @phpstan-all-methods-pure propagates even when the method's $resolvedPhpDoc is null.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test testBug14138Pure is strill successful without the fix

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

You're out of extra usage · resets 4pm (UTC)

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test testBug14138Pure is still successful without the fix

@phpstan phpstan deleted a comment from phpstan-bot Mar 25, 2026
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test testBug14138Pure is still successful without the fix so it's not a real non regression test

@VincentLanglet VincentLanglet self-assigned this Mar 26, 2026
The previous test passed even without the fix because PureMethodRule
gets purity from NodeScopeResolver (which already had the class-level
check outside the null block). The new test verifies the fix at the
reflection layer: a @phpstan-pure method instantiating a class with
@phpstan-all-methods-pure and promoted constructor properties. Without
the fix, the constructor's isPure() returns "maybe" from reflection,
causing a false "Possibly impure instantiation" error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. The issue was that the previous testBug14138Pure test was testing purity via the PureMethodRule, which uses NodeScopeResolver -- and NodeScopeResolver already had the areAllMethodsPure() check outside the $resolvedPhpDoc !== null block (line 4349). So the test passed with or without the fix in PhpClassReflectionExtension.

The new test instead verifies the reflection layer fix: a @phpstan-pure method calls new PureClassWithPromotedProps(1) where the class has @phpstan-all-methods-pure. Without the fix, the constructor's isPure() returns maybe (because the class-level check was skipped when $resolvedPhpDoc is null), causing hasSideEffects() to return maybe, which triggers a false "Possibly impure instantiation" error. With the fix, the constructor is correctly marked pure and no error is reported.

@VincentLanglet
Copy link
Copy Markdown
Contributor

I confirm it fails without the fix

@VincentLanglet VincentLanglet requested a review from staabm March 26, 2026 08:21
@VincentLanglet VincentLanglet merged commit c381cc0 into phpstan:2.1.x Mar 26, 2026
653 of 656 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-9u15nnk branch March 26, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants