Skip to content

Comments

Tip about adding @phpstan-impure where applicable#5031

Open
ondrejmirtes wants to merge 4 commits into2.1.xfrom
possibly-impure-tip
Open

Tip about adding @phpstan-impure where applicable#5031
ondrejmirtes wants to merge 4 commits into2.1.xfrom
possibly-impure-tip

Conversation

@ondrejmirtes
Copy link
Member

@ondrejmirtes ondrejmirtes commented Feb 22, 2026

@ondrejmirtes ondrejmirtes changed the base branch from 2.2.x to 2.1.x February 22, 2026 22:08
@phpstan phpstan deleted a comment from phpstan-bot Feb 22, 2026
Copy link
Member Author

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

@phpstan-bot fix them please

) {
$scope = $scope->assignExpression(
new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr, sprintf('%s()', $functionReflection->getName()), $parametersAcceptor->getReturnType()),
new MixedType(),
Copy link
Member Author

Choose a reason for hiding this comment

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

Instead of the fourth PossiblyImpureCallExpr constructor parameter, this type could be carried as the expression type instead of new MixedType().

->tip('Remove remaining cases below this one and this error will disappear too.')
->build();
->identifier('match.alwaysTrue');
if (!$errorBuilder->hasTips()) {
Copy link
Member Author

Choose a reason for hiding this comment

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

It's a new error builder, it's not going to have any tips on this line.

return $ruleErrorBuilder;
}
if ($ruleErrorBuilder->hasTips()) {
return $ruleErrorBuilder;
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't need this condition, it's okay to have more than one tip. I suspect we could remove the hasTips method declaration altogether.

@phpstan-bot
Copy link
Collaborator

phpstan-bot commented Feb 23, 2026

Claude finished @ondrejmirtes's task in 3m 33s —— View job


Addressed review comments

  • Read relevant files to understand current code
  • Fix 1: Carry declared return type as expression type in assignExpression() instead of PossiblyImpureCallExpr constructor parameter
  • Fix 2: Remove unnecessary hasTips() check on new error builder in MatchExpressionRule
  • Fix 3: Remove hasTips condition in PossiblyImpureTipHelper and remove hasTips() method from RuleErrorBuilder
  • Commit and push changes

Changes made

  1. PossiblyImpureCallExpr — Removed the 4th constructor parameter (Type $declaredReturnType) and the getDeclaredReturnType() method. The declared return type is now passed as the expression type (2nd argument) to $scope->assignExpression() instead of new MixedType().

  2. MutatingScope — Updated findPossiblyImpureCallDescriptions() to read the declared return type from $holder->getType() instead of $holderExpr->getDeclaredReturnType().

  3. NodeScopeResolver — Updated all 3 call sites to use the new constructor signature and pass $parametersAcceptor->getReturnType() as the expression type to assignExpression().

  4. MatchExpressionRule — Removed the unnecessary if (!$errorBuilder->hasTips()) guard since the error builder is freshly created and can't have tips.

  5. PossiblyImpureTipHelper — Removed the $ruleErrorBuilder->hasTips() condition, allowing tips to be added even when other tips already exist.

  6. RuleErrorBuilder — Removed the hasTips() method entirely as it's no longer used anywhere.

Commit: dfe79f7
·

- Carry declared return type as expression type in assignExpression()
  instead of as a PossiblyImpureCallExpr constructor parameter
- Remove unnecessary hasTips() check on new error builder in
  MatchExpressionRule
- Remove hasTips condition in PossiblyImpureTipHelper and remove
  hasTips() method from RuleErrorBuilder

Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
Copy link
Member Author

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

@phpstan-bot re-run make cs, make tests and make phpstan and fix the failures

@phpstan-bot
Copy link
Collaborator

phpstan-bot commented Feb 23, 2026

Claude finished @ondrejmirtes's task in 15m 49s —— View job


Fixed CS, tests, and PHPStan failures

  • Fix CS: Add use function assert; import to MutatingScope.php
  • Fix PHPStan: Add @template T of RuleError generic types to PossiblyImpureTipHelper::addTip() signature
  • Fix test: ElseIfConstantConditionRuleTest::testBug6947 — updated expected tip to include both the treatPhpDocTypesAsCertain tip and the new possibly-impure tip (bullet format)
  • Fix test: native-types-81.php lines 28, 33 — closures with : string return type now correctly resolve to native type string instead of non-empty-string (previous result was due to closure type cache key collision between regular and native-promoted scopes, which was broken by the new PossiblyImpureCallExpr entries having different types in each scope)
  • Commit and push fixes

Changes made

  1. src/Analyser/MutatingScope.php — Added use function assert; import (CS fix).

  2. src/Rules/Comparison/PossiblyImpureTipHelper.php — Added @template T of RuleError, @param RuleErrorBuilder<T>, and @return RuleErrorBuilder<T> PHPDoc to addTip() method to satisfy PHPStan's missingType.generics rule.

  3. tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php — Updated testBug6947 expected tip from single-tip format to multi-tip bullet format, since the possibly-impure tip is now added alongside the PHPDoc types tip.

  4. tests/PHPStan/Analyser/nsrt/native-types-81.php — Changed expected native types at lines 28 and 33 from non-empty-string to string. The closure has : string as its native return type, so string is the correct native type. The previous non-empty-string was a side effect of a closure type cache key collision.

Note: The integration test failures (shipmonk/phpstan-rules, PMMP, laravel) are unrelated to these fixes — shipmonk's failure is an expected behavioral change from the match expression rule changes, while PMMP/laravel failures are about property.unusedType and foreach.nonIterable.

Commit: 9ad6900
· Branch

- Add missing `use function assert;` import in MutatingScope.php
- Add @template generic type annotations to PossiblyImpureTipHelper::addTip()
- Update ElseIfConstantConditionRuleTest::testBug6947 expected tip to include
  possibly-impure tip alongside treatPhpDocTypesAsCertain tip
- Fix native-types-81.php: closures with `: string` return type now correctly
  resolve to native type `string` instead of `non-empty-string` (the previous
  `non-empty-string` was due to a closure type cache key collision between
  regular and native-promoted scopes)

Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
&& $this->rememberPossiblyImpureFunctionValues
&& $functionReflection->hasSideEffects()->maybe()
&& !$functionReflection->isBuiltin()
&& $parametersAcceptor !== null
Copy link
Contributor

Choose a reason for hiding this comment

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

this condition should be checked before $functionReflection->hasSideEffects()->maybe()

Comment on lines +3457 to +3458
&& $parametersAcceptor !== null
&& $scope->isInClass()
Copy link
Contributor

Choose a reason for hiding this comment

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

these 2 lines should be moved above $methodReflection->hasSideEffects()->maybe()

@ondrejmirtes
Copy link
Member Author

@phpstan-bot ^^ fix what @staabm says

Also instead of changing the expectations in native-types-81.php, could you fix the closure cache key collision?

@phpstan-bot
Copy link
Collaborator

phpstan-bot commented Feb 23, 2026

Claude finished @ondrejmirtes's task in 10m 3s —— View job


Addressed @staabm's review comments and fixed closure cache key collision

  • Read relevant files to understand current code
  • Fix 1: Move $parametersAcceptor !== null before hasSideEffects()->maybe() for function calls
  • Fix 2: Move $scope->isInClass() and $scope->getClassReflection()->is(...) before hasSideEffects()->maybe() for static method calls
  • Fix 3: Fix closure cache key collision by including nativeTypesPromoted in cache key
  • Revert native-types-81.php test expectations to original values
  • Commit and push changes

Changes made

  1. src/Analyser/NodeScopeResolver.php — Reordered conditions per @staabm's review:

    • For function calls (~line 2924): moved $parametersAcceptor !== null before $functionReflection->hasSideEffects()->maybe()
    • For static method calls (~line 3452): moved $parametersAcceptor !== null, $scope->isInClass(), and $scope->getClassReflection()->is(...) before $methodReflection->hasSideEffects()->maybe()
  2. src/Analyser/MutatingScope.php — Fixed the closure type cache key collision by including the nativeTypesPromoted flag in getClosureScopeCacheKey(). The root cause was that getClosureScopeCacheKey() only used $this->expressionTypes to build the cache key, without distinguishing between PHPDoc-aware and native-promoted scopes. When promoteNativeTypes() swaps native types into expressionTypes, both scopes could produce the same cache key, causing a PHPDoc-scope computed closure type to be returned for native scope queries.

  3. tests/PHPStan/Analyser/nsrt/native-types-81.php — Reverted test expectations back to original non-empty-string values on lines 28 and 33 (no longer needed as the cache key collision is fixed at the source).

Commit: 4f0994e · Branch

- Move $parametersAcceptor !== null check before hasSideEffects()->maybe()
  for function calls
- Move $scope->isInClass() and getClassReflection()->is() checks before
  hasSideEffects()->maybe() for static method calls
- Fix closure type cache key collision between PHPDoc-aware and native
  scopes by including nativeTypesPromoted flag in getClosureScopeCacheKey()
- Revert native-types-81.php test expectations back to original values

Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
Copy link
Member Author

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

$parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache()));
}

if ($this->nativeTypesPromoted) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think this fix is right. I think instead it should go over $this->nativeExpressionTypes same way it currently does over $this->expressionTypes.

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.

Error tip for @phpstan-impure

3 participants