From 7a3bb18e63b06484f0cf5f0d2de3bd77199e5027 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 30 Jul 2026 21:10:03 +0200 Subject: [PATCH] Run the impossible-check and nullsafe rules on post-processing virtual nodes Ported from the resolve-type-rewrite-2 branch. The impossible-check rules listened on the raw call nodes and the nullsafe rules on the raw nullsafe nodes - both firing while the expression was still being processed, asking the scope to specify types or read receiver types before the node itself was done. NodeScopeResolver now emits FunctionCallExpressionNode / MethodCallExpressionNode / StaticMethodCallExpressionNode after the call is processed and stored, and the nullsafe handlers emit NullsafeMethodCallExpressionNode / NullsafePropertyFetchExpressionNode carrying the receiver's entry-scope type, so the rules run on the fully processed expression. The rule-facing nodes extend NodeAbstract, following f93536600f. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7 --- .../ExprHandler/NullsafeMethodCallHandler.php | 7 +++ .../NullsafePropertyFetchHandler.php | 7 +++ src/Analyser/NodeScopeResolver.php | 13 ++++ src/Node/FunctionCallExpressionNode.php | 45 ++++++++++++++ src/Node/MethodCallExpressionNode.php | 45 ++++++++++++++ src/Node/NullsafeMethodCallExpressionNode.php | 59 ++++++++++++++++++ .../NullsafePropertyFetchExpressionNode.php | 60 +++++++++++++++++++ src/Node/StaticMethodCallExpressionNode.php | 45 ++++++++++++++ .../ImpossibleCheckTypeFunctionCallRule.php | 42 ++++++------- .../ImpossibleCheckTypeMethodCallRule.php | 46 +++++++------- ...mpossibleCheckTypeStaticMethodCallRule.php | 46 +++++++------- src/Rules/Methods/NullsafeMethodCallRule.php | 14 +++-- .../Properties/NullsafePropertyFetchRule.php | 16 ++--- 13 files changed, 368 insertions(+), 77 deletions(-) create mode 100644 src/Node/FunctionCallExpressionNode.php create mode 100644 src/Node/MethodCallExpressionNode.php create mode 100644 src/Node/NullsafeMethodCallExpressionNode.php create mode 100644 src/Node/NullsafePropertyFetchExpressionNode.php create mode 100644 src/Node/StaticMethodCallExpressionNode.php diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index d7dc61b42e4..30ddfc2c4c2 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -23,6 +23,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\NullsafeMethodCallExpressionNode; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -87,6 +88,8 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; + $calledOnType = $scope->getScopeType($expr->var); + $calledOnNativeType = $scope->getScopeNativeType($expr->var); $scopeBeforeNullsafe = $scope; $varType = $scope->getType($expr->var); @@ -118,6 +121,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->mergeWith($scopeBeforeNullsafe); } + // the nullsafe operation is processed; emit a virtual node carrying the + // receiver's entry-scope type so its rule does not re-ask the scope + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new NullsafeMethodCallExpressionNode($expr, $calledOnType, $calledOnNativeType), $beforeScope, $storage, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 691c19d3910..a81a392a55d 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -23,6 +23,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\NullsafePropertyFetchExpressionNode; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -87,6 +88,8 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; + $calledOnType = $scope->getScopeType($expr->var); + $calledOnNativeType = $scope->getScopeNativeType($expr->var); $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); @@ -97,6 +100,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); + // the nullsafe operation is processed; emit a virtual node carrying the + // receiver's entry-scope type so its rule does not re-ask the scope + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new NullsafePropertyFetchExpressionNode($expr, $calledOnType, $calledOnNativeType), $beforeScope, $storage, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index efa23eb0517..8ddb0dc51c1 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -83,6 +83,7 @@ use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\FunctionCallableNode; +use PHPStan\Node\FunctionCallExpressionNode; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Node\InClassMethodNode; @@ -95,6 +96,7 @@ use PHPStan\Node\InTraitNode; use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\MethodCallableNode; +use PHPStan\Node\MethodCallExpressionNode; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Node\NoopExpressionNode; use PHPStan\Node\PropertyAssignNode; @@ -102,6 +104,7 @@ use PHPStan\Node\PropertyHookStatementNode; use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; +use PHPStan\Node\StaticMethodCallExpressionNode; use PHPStan\Node\SwitchConditionArm; use PHPStan\Node\SwitchConditionNode; use PHPStan\Node\UnreachableStatementNode; @@ -2804,6 +2807,16 @@ public function processExprNode( if ($exprHandler !== null) { $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); $this->storeExpressionResult($storage, $expr, $expressionResult); + // the call is now processed and stored; emit a virtual node so + // impossible-check rules run on the fully processed call instead of + // asking the scope before the call node itself is processed + if ($expr instanceof FuncCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr), $scope, $storage, $context); + } elseif ($expr instanceof MethodCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr), $scope, $storage, $context); + } elseif ($expr instanceof StaticCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr), $scope, $storage, $context); + } return $expressionResult; } diff --git a/src/Node/FunctionCallExpressionNode.php b/src/Node/FunctionCallExpressionNode.php new file mode 100644 index 00000000000..59e21ba61c4 --- /dev/null +++ b/src/Node/FunctionCallExpressionNode.php @@ -0,0 +1,45 @@ +getAttributes()); + } + + public function getOriginalNode(): FuncCall + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_FunctionCallExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodCallExpressionNode.php b/src/Node/MethodCallExpressionNode.php new file mode 100644 index 00000000000..e2be8de06b2 --- /dev/null +++ b/src/Node/MethodCallExpressionNode.php @@ -0,0 +1,45 @@ +getAttributes()); + } + + public function getOriginalNode(): MethodCall + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_MethodCallExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/NullsafeMethodCallExpressionNode.php b/src/Node/NullsafeMethodCallExpressionNode.php new file mode 100644 index 00000000000..63002b43d01 --- /dev/null +++ b/src/Node/NullsafeMethodCallExpressionNode.php @@ -0,0 +1,59 @@ +getAttributes()); + } + + public function getOriginalNode(): NullsafeMethodCall + { + return $this->originalNode; + } + + public function getCalledOnType(): Type + { + return $this->calledOnType; + } + + public function getCalledOnNativeType(): Type + { + return $this->calledOnNativeType; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_NullsafeMethodCallExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/NullsafePropertyFetchExpressionNode.php b/src/Node/NullsafePropertyFetchExpressionNode.php new file mode 100644 index 00000000000..b279f561c57 --- /dev/null +++ b/src/Node/NullsafePropertyFetchExpressionNode.php @@ -0,0 +1,60 @@ +getAttributes()); + } + + public function getOriginalNode(): NullsafePropertyFetch + { + return $this->originalNode; + } + + public function getCalledOnType(): Type + { + return $this->calledOnType; + } + + public function getCalledOnNativeType(): Type + { + return $this->calledOnNativeType; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_NullsafePropertyFetchExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/StaticMethodCallExpressionNode.php b/src/Node/StaticMethodCallExpressionNode.php new file mode 100644 index 00000000000..2e57a1d8cb5 --- /dev/null +++ b/src/Node/StaticMethodCallExpressionNode.php @@ -0,0 +1,45 @@ +getAttributes()); + } + + public function getOriginalNode(): StaticCall + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_StaticMethodCallExpressionNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 7e755e9e36b..76b470f668a 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -8,13 +8,14 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\FunctionCallExpressionNode; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class ImpossibleCheckTypeFunctionCallRule implements Rule @@ -37,72 +38,73 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\FuncCall::class; + return FunctionCallExpressionNode::class; } public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { - if (!$node->name instanceof Node\Name) { + $funcCall = $node->getOriginalNode(); + if (!$funcCall->name instanceof Node\Name) { return []; } - $functionName = (string) $node->name; + $functionName = (string) $funcCall->name; $reasons = []; - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, $reasons); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $funcCall, $reasons); if ($isAlways === null) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $funcCall); return []; } - $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $funcCall); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $funcCall, $reasons): RuleErrorBuilder { if ($reasons !== []) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); + return $this->possiblyImpureTipHelper->addTip($scope, $funcCall, $ruleErrorBuilder->acceptsReasonsTip($reasons)); } if (!$this->treatPhpDocTypesAsCertain) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $funcCall, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $funcCall, $reasons); if ($isAlways !== null) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $funcCall, $ruleErrorBuilder); } if (!$this->treatPhpDocTypesAsCertainTip) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $funcCall, $ruleErrorBuilder); } $ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $funcCall, $ruleErrorBuilder); }; if (!$isAlways) { $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to function %s()%s will always evaluate to false.', $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $funcCall->getArgs()), ))); $ruleError = $errorBuilder->identifier('function.impossibleType')->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $funcCall, false, $ruleError); return []; } return [$ruleError]; } - $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + $isLast = $funcCall->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $funcCall); return []; } $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to function %s()%s will always evaluate to true.', $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $funcCall->getArgs()), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); @@ -112,7 +114,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $ruleError = $errorBuilder->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $funcCall, true, $ruleError); return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index a5b226387dd..c5236e38366 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\MethodCallExpressionNode; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; @@ -17,7 +18,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class ImpossibleCheckTypeMethodCallRule implements Rule @@ -40,76 +41,77 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\MethodCall::class; + return MethodCallExpressionNode::class; } public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { - if (!$node->name instanceof Node\Identifier) { + $methodCall = $node->getOriginalNode(); + if (!$methodCall->name instanceof Node\Identifier) { return []; } - $methodName = $node->name->name; + $methodName = $methodCall->name->name; $reasons = []; - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, $reasons); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $methodCall, $reasons); if ($isAlways === null) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $methodCall); return []; } - $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $methodCall); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $methodCall, $reasons): RuleErrorBuilder { if ($reasons !== []) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); + return $this->possiblyImpureTipHelper->addTip($scope, $methodCall, $ruleErrorBuilder->acceptsReasonsTip($reasons)); } if (!$this->treatPhpDocTypesAsCertain) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $methodCall, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $methodCall); if ($isAlways !== null) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $methodCall, $ruleErrorBuilder); } if (!$this->treatPhpDocTypesAsCertainTip) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $methodCall, $ruleErrorBuilder); } $ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $methodCall, $ruleErrorBuilder); }; if (!$isAlways) { - $method = $this->getMethod($node->var, $methodName, $scope); + $method = $this->getMethod($methodCall->var, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $methodCall->getArgs()), ))); $ruleError = $errorBuilder->identifier('method.impossibleType')->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $methodCall, false, $ruleError); return []; } return [$ruleError]; } - $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + $isLast = $methodCall->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $methodCall); return []; } - $method = $this->getMethod($node->var, $methodName, $scope); + $method = $this->getMethod($methodCall->var, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $methodCall->getArgs()), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); @@ -119,7 +121,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $ruleError = $errorBuilder->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $methodCall, true, $ruleError); return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index e1ecf70ecdd..7189947acdb 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\StaticMethodCallExpressionNode; use PHPStan\Parser\LastConditionVisitor; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; @@ -17,7 +18,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class ImpossibleCheckTypeStaticMethodCallRule implements Rule @@ -40,77 +41,78 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\StaticCall::class; + return StaticMethodCallExpressionNode::class; } public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { - if (!$node->name instanceof Node\Identifier) { + $staticCall = $node->getOriginalNode(); + if (!$staticCall->name instanceof Node\Identifier) { return []; } - $methodName = $node->name->name; + $methodName = $staticCall->name->name; $reasons = []; - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, $reasons); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $staticCall, $reasons); if ($isAlways === null) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $staticCall); return []; } - $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $node); + $this->functionCallConstantConditionHelper->emitImpossibleCheckReported($scope, $staticCall); - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node, $reasons): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $staticCall, $reasons): RuleErrorBuilder { if ($reasons !== []) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder->acceptsReasonsTip($reasons)); + return $this->possiblyImpureTipHelper->addTip($scope, $staticCall, $ruleErrorBuilder->acceptsReasonsTip($reasons)); } if (!$this->treatPhpDocTypesAsCertain) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $staticCall, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $staticCall); if ($isAlways !== null) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $staticCall, $ruleErrorBuilder); } if (!$this->treatPhpDocTypesAsCertainTip) { - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $staticCall, $ruleErrorBuilder); } $ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); - return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); + return $this->possiblyImpureTipHelper->addTip($scope, $staticCall, $ruleErrorBuilder); }; if (!$isAlways) { - $method = $this->getMethod($node->class, $methodName, $scope); + $method = $this->getMethod($staticCall->class, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to static method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $staticCall->getArgs()), ))); $ruleError = $errorBuilder->identifier('staticMethod.impossibleType')->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $staticCall, false, $ruleError); return []; } return [$ruleError]; } - $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + $isLast = $staticCall->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { - $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $staticCall); return []; } - $method = $this->getMethod($node->class, $methodName, $scope); + $method = $this->getMethod($staticCall->class, $methodName, $scope); $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( 'Call to static method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $staticCall->getArgs()), ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); @@ -120,7 +122,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $ruleError = $errorBuilder->build(); if ($scope->isInTrait()) { - $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $staticCall, true, $ruleError); return []; } diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index a3d3b872f73..e77fa140bec 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -6,13 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\NullsafeMethodCallExpressionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class NullsafeMethodCallRule implements Rule @@ -29,22 +30,23 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\NullsafeMethodCall::class; + return NullsafeMethodCallExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - $calledOnType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($node->var) : $scope->getScopeNativeType($node->var); + $originalNode = $node->getOriginalNode(); + $calledOnType = $this->treatPhpDocTypesAsCertain ? $node->getCalledOnType() : $node->getCalledOnNativeType(); if (!$calledOnType->isNull()->no()) { return []; } - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain || !$this->treatPhpDocTypesAsCertainTip) { return $ruleErrorBuilder; } - $calledOnNativeType = $scope->getScopeNativeType($node->var); + $calledOnNativeType = $node->getCalledOnNativeType(); if ($calledOnNativeType->isNull()->no()) { return $ruleErrorBuilder; } @@ -58,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $calledOnType->describe(VerbosityLevel::typeOnly()), )), ) - ->line($node->name->getStartLine()) + ->line($originalNode->name->getStartLine()) ->identifier('nullsafe.neverNull'); return [$ruleErrorBuilder->build()]; diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 1e11e03f8a8..debad865a30 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -6,13 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\NullsafePropertyFetchExpressionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class NullsafePropertyFetchRule implements Rule @@ -29,26 +30,27 @@ public function __construct( public function getNodeType(): string { - return Node\Expr\NullsafePropertyFetch::class; + return NullsafePropertyFetchExpressionNode::class; } public function processNode(Node $node, Scope $scope): array { - $calledOnType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($node->var) : $scope->getScopeNativeType($node->var); + $originalNode = $node->getOriginalNode(); + $calledOnType = $this->treatPhpDocTypesAsCertain ? $node->getCalledOnType() : $node->getCalledOnNativeType(); if (!$calledOnType->isNull()->no()) { return []; } - if ($scope->isUndefinedExpressionAllowed($node)) { + if ($scope->isUndefinedExpressionAllowed($originalNode)) { return []; } - $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain || !$this->treatPhpDocTypesAsCertainTip) { return $ruleErrorBuilder; } - $calledOnNativeType = $scope->getScopeNativeType($node->var); + $calledOnNativeType = $node->getCalledOnNativeType(); if ($calledOnNativeType->isNull()->no()) { return $ruleErrorBuilder; } @@ -62,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array $calledOnType->describe(VerbosityLevel::typeOnly()), )), ) - ->line($node->name->getStartLine()) + ->line($originalNode->name->getStartLine()) ->identifier('nullsafe.neverNull'); return [$ruleErrorBuilder->build()];