diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 04652767786..3d9d45eb107 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -28,3 +28,5 @@ parameters: checkImportedClassNameCase: true sortWithoutEffect: true unresolvedTemplateArguments: true + unusedVariable: true + unusedParameters: true diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 4206d36d3c1..f437c1e58ff 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -18,6 +18,12 @@ conditionalTags: phpstan.rules.rule: %featureToggles.finiteTypesInHaystack% PHPStan\Rules\Comparison\SwitchConditionRule: phpstan.rules.rule: %featureToggles.switchConditionAlwaysFalse% + PHPStan\Rules\DeadCode\UnusedVariableRule: + phpstan.rules.rule: %featureToggles.unusedVariable% + PHPStan\Rules\Functions\UnusedFunctionParametersRule: + phpstan.rules.rule: %featureToggles.unusedParameters% + PHPStan\Rules\Methods\UnusedMethodParametersRule: + phpstan.rules.rule: %featureToggles.unusedParameters% parameters: checkAdvancedIsset: true @@ -49,3 +55,12 @@ services: class: PHPStan\Rules\Comparison\SwitchConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\DeadCode\UnusedVariableRule + + - + class: PHPStan\Rules\Functions\UnusedFunctionParametersRule + + - + class: PHPStan\Rules\Methods\UnusedMethodParametersRule diff --git a/conf/config.neon b/conf/config.neon index 71222e5ed2c..bdecb624a6e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -59,6 +59,8 @@ parameters: checkImportedClassNameCase: false sortWithoutEffect: false unresolvedTemplateArguments: false + unusedVariable: false + unusedParameters: false fileExtensions: - php checkAdvancedIsset: false @@ -154,6 +156,7 @@ parameters: - ../stubs/ReflectionMethod.stub - ../stubs/ReflectionParameter.stub - ../stubs/ReflectionProperty.stub + - ../stubs/ReflectionType.stub - ../stubs/iterable.stub - ../stubs/ArrayObject.stub - ../stubs/WeakReference.stub diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 7a6dc788adf..0fa5326a3d3 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -57,6 +57,8 @@ parametersSchema: checkImportedClassNameCase: bool() sortWithoutEffect: bool() unresolvedTemplateArguments: bool() + unusedVariable: bool() + unusedParameters: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/e2e/ignore-error-extension/phpstan-baseline.neon b/e2e/ignore-error-extension/phpstan-baseline.neon index 8c53510373c..2a991302649 100644 --- a/e2e/ignore-error-extension/phpstan-baseline.neon +++ b/e2e/ignore-error-extension/phpstan-baseline.neon @@ -1,25 +1,25 @@ parameters: ignoreErrors: - - message: '#^This is an error from a rule that uses a collector$#' + message: '#^This is an error from a rule that uses a collector\: ClassCollector$#' identifier: class.name count: 1 path: src/ClassCollector.php - - message: '#^This is an error from a rule that uses a collector$#' + message: '#^This is an error from a rule that uses a collector\: ClassRule$#' identifier: class.name count: 1 path: src/ClassRule.php - - message: '#^This is an error from a rule that uses a collector$#' + message: '#^This is an error from a rule that uses a collector\: ControllerActionReturnTypeIgnoreExtension$#' identifier: class.name count: 1 path: src/ControllerActionReturnTypeIgnoreExtension.php - - message: '#^This is an error from a rule that uses a collector$#' + message: '#^This is an error from a rule that uses a collector\: ControllerClassNameIgnoreExtension$#' identifier: class.name count: 1 path: src/ControllerClassNameIgnoreExtension.php diff --git a/e2e/ignore-error-extension/src/ClassRule.php b/e2e/ignore-error-extension/src/ClassRule.php index 17283bafe56..5d318461155 100644 --- a/e2e/ignore-error-extension/src/ClassRule.php +++ b/e2e/ignore-error-extension/src/ClassRule.php @@ -29,7 +29,7 @@ public function processNode(Node $node, Scope $scope) : array foreach ($node->get(ClassCollector::class) as $file => $data) { foreach ($data as [$className, $line]) { - $errors[] = RuleErrorBuilder::message('This is an error from a rule that uses a collector') + $errors[] = RuleErrorBuilder::message(sprintf('This is an error from a rule that uses a collector: %s', $className)) ->file($file) ->line($line) ->identifier('class.name') diff --git a/e2e/trait-caching/data/TestClassUsingTrait.php b/e2e/trait-caching/data/TestClassUsingTrait.php index eac1193bedc..0fe84e6d140 100644 --- a/e2e/trait-caching/data/TestClassUsingTrait.php +++ b/e2e/trait-caching/data/TestClassUsingTrait.php @@ -15,7 +15,7 @@ public function doBar() return $this->doFoo(); } - public function doBaz(): void + public function doBaz(): \stdClass { $class = new class() { @@ -29,6 +29,8 @@ public function doBar() return $this->doFoo(); } }; + + return $class->doBar(); } } diff --git a/src/Analyser/ArgsResult.php b/src/Analyser/ArgsResult.php index c158bc36852..8a9ce80c900 100644 --- a/src/Analyser/ArgsResult.php +++ b/src/Analyser/ArgsResult.php @@ -22,11 +22,13 @@ final class ArgsResult /** * @param array $argResults keyed by spl_object_id of each argument's value expression + * @param array $byRefArguments */ public function __construct( private ExpressionResult $expressionResult, private ?ParametersAcceptor $resolvedParametersAcceptor, private array $argResults, + private array $byRefArguments = [], ) { } @@ -72,6 +74,11 @@ public function requireArgResult(Expr $argValue): ExpressionResult return $result; } + public function isPassedByReference(Expr $arg): bool + { + return isset($this->byRefArguments[spl_object_id($arg)]); + } + public function getScope(): MutatingScope { return $this->expressionResult->getScope(); diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 00ff8d28352..b26e3d37819 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -23,6 +23,8 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -84,6 +86,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $varResult->getVariableFlow(), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), @@ -118,6 +121,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), $dimResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $dimResult->hasYield() || $varResult->hasYield(), isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 253f100c8d2..f02c51c187d 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -16,6 +16,8 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; @@ -53,6 +55,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $beforeScope = $scope; $itemNodes = []; $itemResults = []; + $variableFlows = []; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -63,6 +66,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($arrayItem->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep()); $itemResults[spl_object_id($arrayItem->key)] = $keyResult; + $variableFlows[] = $keyResult->getVariableFlow(); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); @@ -72,6 +76,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep()); $itemResults[spl_object_id($arrayItem->value)] = $valueResult; + $variableFlows[] = $valueResult->getVariableFlow(); + if ($arrayItem->byRef) { + $variableFlows[] = VariableFlowBuilder::escapeRoot($arrayItem->value); + } $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); @@ -88,6 +96,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence(...$variableFlows), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 8e9cbaac020..bfd50c64a1e 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -15,7 +15,9 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; +use function is_string; /** * @implements ExprHandler @@ -77,6 +79,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $result->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: $result->getVariableFlow(), hasYield: $result->hasYield(), isAlwaysTerminating: false, throwPoints: [], @@ -88,4 +91,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } + public static function getVariableFlow(ArrowFunction $expr, ExpressionResult $bodyResult): VariableFlow + { + $outputs = []; + foreach ($expr->params as $param) { + if (!$param->byRef || !$param->var instanceof Expr\Variable || !is_string($param->var->name)) { + continue; + } + $outputs[] = VariableFlow::read($param->var->name); + } + + return VariableFlow::arrow($expr, $bodyResult->getVariableFlow(), VariableFlow::sequence(...$outputs)); + } + } diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index c4f5e8af42a..60694494526 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -47,6 +47,8 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VarAnnotationProcessor; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; @@ -56,6 +58,7 @@ use PHPStan\Node\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Node\VariableAssignNode; use PHPStan\Node\VirtualNode; use PHPStan\Php\PhpVersion; @@ -254,10 +257,26 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } + $redundantType = $expr instanceof Assign && $expr->var instanceof Variable && is_string($expr->var->name) + ? self::redundant($assignedExprResult, $expr->var->name) + : null; + $variableFlow = VariableFlow::sequence( + VariableFlowBuilder::targetRead($expr->var, $storage, false), + $assignedExprResult->getVariableFlow(), + VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_ASSIGN, $scope, $storage, $redundantType), + ); + if ($expr instanceof Assign && $expr->expr instanceof Expr\Array_ && self::hasArrayReference($expr->expr)) { + $variableFlow = VariableFlow::sequence($variableFlow, VariableFlowBuilder::escapeRoot($expr->var)); + } + if ($expr instanceof AssignRef) { + $variableFlow = VariableFlow::sequence($variableFlow, VariableFlowBuilder::escapeRoot($expr->var), VariableFlowBuilder::escapeRoot($expr->expr)); + } + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $variableFlow, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), @@ -1097,7 +1116,6 @@ public function applyWrite( $storedAssignedExprResult = $assignedExpr === $target->getAssignedExpr() ? $assignedValueResult ?? $storage->findExpressionResult($assignedExpr) : $storage->findExpressionResult($assignedExpr); - $assignedValueResult = $storedAssignedExprResult; $type = $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval); $conditionalExpressions = []; @@ -1145,7 +1163,7 @@ public function applyWrite( if ($assignedExpr instanceof Match_) { $conditionalExpressions = $this->mergeConditionalExpressions( $conditionalExpressions, - $this->processMatchForConditionalExpressionsAfterAssign($nodeScopeResolver, $scopeBeforeAssignEval, $storage, $var->name, $assignedExpr), + $this->processMatchForConditionalExpressionsAfterAssign($scopeBeforeAssignEval, $var->name, $assignedExpr), ); } @@ -1219,7 +1237,15 @@ public function applyWrite( } $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignVariable($var->name, $type, $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()), TrinaryLogic::createYes()); + + $nativeType = $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()); + $scope = $scope->assignVariable( + $var->name, + $type, + $nativeType, + TrinaryLogic::createYes(), + [], + ); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions((string) $exprString, $holders); } @@ -1227,16 +1253,24 @@ public function applyWrite( if ($assignedExpr instanceof Expr\Array_) { $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $storage, $var->name, $assignedExpr, new Variable($var->name)); } - } elseif ($target->getVariableNameResult() === null) { - // a plain assignment does not read the target, so the dynamic name - // is walked here; read-modify-write targets walked it in - // prepareTarget() and already carry its state - $nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); - $hasYield = $hasYield || $nameExprResult->hasYield(); - $throwPoints = array_merge($throwPoints, $nameExprResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $nameExprResult->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $nameExprResult->isAlwaysTerminating(); - $scope = $nameExprResult->getScope(); + } else { + $nameExprResult = $target->getVariableNameResult(); + if ($nameExprResult === null) { + // Read-modify-write targets already evaluated the dynamic name in prepareTarget(). + $nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); + $hasYield = $hasYield || $nameExprResult->hasYield(); + $throwPoints = array_merge($throwPoints, $nameExprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameExprResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $nameExprResult->isAlwaysTerminating(); + $scope = $nameExprResult->getScope(); + } + $storedAssignedExprResult = $assignedValueResult ?? $storage->findExpressionResult($assignedExpr); + $scope = $this->assignDynamicVariable( + $scope, + $nameExprResult, + $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval), + $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval->doNotTreatPhpDocTypesAsCertain()), + ); } } elseif ($kind === PreparedAssignTarget::KIND_ARRAY_DIM_FETCH) { if (!$var instanceof ArrayDimFetch) { @@ -1320,7 +1354,13 @@ public function applyWrite( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); + $scope = $scope->assignVariable( + $var->name, + $valueToWrite, + $nativeValueToWrite, + TrinaryLogic::createYes(), + [], + ); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); @@ -1784,6 +1824,44 @@ private function resolveContainerTypesAfterAssignedExprEval( return [$varResult->getType(), $varResult->getNativeType()]; } + private function assignDynamicVariable(MutatingScope $scope, ExpressionResult $nameResult, Type $valueType, Type $nativeValueType): MutatingScope + { + $nameType = $nameResult->getType()->toString(); + $nativeNameType = $nameResult->getNativeType()->toString(); + $names = []; + foreach ($nameType->getConstantStrings() as $name) { + $names[$name->getValue()] = $name; + } + foreach (array_merge($scope->getDefinedVariables(), $scope->getMaybeDefinedVariables()) as $name) { + $nameStringType = new ConstantStringType($name); + if ($nameType->isSuperTypeOf($nameStringType)->no()) { + continue; + } + $names[$name] = $nameStringType; + } + + $beforeScope = $scope; + foreach ($names as $nameStringType) { + $name = $nameStringType->getValue(); + if ($name === 'this') { + continue; + } + $certainty = $beforeScope->hasVariableType($name); + $type = $valueType; + $nativeType = $nativeValueType; + if (!$certainty->no()) { + if (!$nameType->equals($nameStringType)) { + $type = TypeCombinator::union($beforeScope->getVariableType($name), $type); + } + if (!$nativeNameType->equals($nameStringType)) { + $nativeType = TypeCombinator::union($beforeScope->doNotTreatPhpDocTypesAsCertain()->getVariableType($name), $nativeType); + } + } + $scope = $scope->assignVariable($name, $type, $nativeType, $nameType->equals($nameStringType) ? TrinaryLogic::createYes() : $certainty->or(TrinaryLogic::createMaybe())); + } + return $scope; + } + private function unwrapAssign(Expr $expr): Expr { if ($expr instanceof Assign) { @@ -1971,9 +2049,7 @@ private function mergeConditionalExpressions(array $conditionalExpressions, arra * @return array */ private function processMatchForConditionalExpressionsAfterAssign( - NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, - ExpressionResultStorage $storage, string $variableName, Match_ $expr, ): array @@ -2149,6 +2225,7 @@ private function processArrayByRefItems(NodeScopeResolver $nodeScopeResolver, Mu } $refVarName = $arrayItem->value->name; + // `$root = [&$ref]` aliases the two slots from now on $dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr); // a plain variable read is scope state - no need to price a synthetic // Variable node on demand (mirrors VariableHandler's typeCallback) @@ -2410,4 +2487,32 @@ private function getOriginalPropertyType(NodeScopeResolver $nodeScopeResolver, P return $originalPropertyType; } + private static function hasArrayReference(Expr\Array_ $array): bool + { + foreach ($array->items as $item) { + if ($item->byRef || ($item->value instanceof Expr\Array_ && self::hasArrayReference($item->value))) { + return true; + } + } + return false; + } + + private static function redundant(ExpressionResult $rhs, string $name): ?Type + { + $scope = $rhs->getScope(); + if (!$scope->hasVariableType($name)->yes()) { + return null; + } + $values = $scope->getVariableType($name)->getFiniteTypes(); + if (count($values) !== 1 || !$values[0]->equals($rhs->getType())) { + return null; + } + $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); + if (!$nativeScope->hasVariableType($name)->yes()) { + return null; + } + $nativeValues = $nativeScope->getVariableType($name)->getFiniteTypes(); + return count($nativeValues) === 1 && $nativeValues[0]->equals($rhs->getNativeType()) ? $rhs->getType() : null; + } + } diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index e4820a6f03c..e587f919568 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -20,8 +20,11 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\CoalesceExpressionNode; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantIntegerType; @@ -280,10 +283,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new CoalesceExpressionNode($expr, $condResult, 'on left side of ??='), $beforeScope, $storage, $context); } + $writeFlow = VariableFlow::sequence( + $rhsResult->getVariableFlow(), + VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_READ_MODIFY_WRITE, $scope, $storage), + ); + $variableFlow = VariableFlow::sequence( + VariableFlowBuilder::targetRead($expr->var, $storage, true), + $expr instanceof Expr\AssignOp\Coalesce ? VariableFlow::choice($writeFlow, null) : $writeFlow, + ); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $variableFlow, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 217edd6a75a..1884ce0e3a1 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -27,6 +27,8 @@ use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Php\PhpVersion; @@ -288,6 +290,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($leftResult->getVariableFlow(), $rightResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index 68c71dff9d4..e8bae880b9f 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -47,6 +47,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 3953731021a..cf828e121e5 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanAndNode; use PHPStan\Type\BooleanType; @@ -59,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftMergedWithRightScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($leftResult->getVariableFlow(), VariableFlow::choice($rightResult->getVariableFlow(), null)), hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 6a35482d073..6709df2595c 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -49,6 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index d1ee34e2d44..185d32d54b7 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanOrNode; use PHPStan\Type\BooleanType; @@ -77,6 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftMergedWithRightScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($leftResult->getVariableFlow(), VariableFlow::choice($rightResult->getVariableFlow(), null)), hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index 1766fa2574b..af5242b29ba 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -61,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index ac796042861..9521ca13486 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -17,6 +17,8 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; @@ -60,6 +62,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 349b0df1de2..0e5d6af9111 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; @@ -51,6 +52,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = false; $classResult = null; + $nameResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); @@ -82,6 +84,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($classResult !== null ? $classResult->getVariableFlow() : null, $nameResult !== null ? $nameResult->getVariableFlow() : null), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index 8f88fc4e7d7..6d9cdb360e8 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -54,6 +54,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index 2b92fcbcf8d..443028f030c 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -15,7 +15,9 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; +use function is_string; /** * @implements ExprHandler @@ -72,6 +74,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $processClosureResult->applyByRefUseScope($processClosureResult->getScope()), beforeScope: $scope, expr: $expr, + variableFlow: self::getVariableFlow($expr), hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -83,4 +86,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } + public static function getVariableFlow(Closure $expr): ?VariableFlow + { + $uses = []; + foreach ($expr->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->byRef ? VariableFlow::escape($use->var->name) : VariableFlow::read($use->var->name); + } + return VariableFlow::sequence(...$uses); + } + } diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 24c3c0c65f3..c3680a3f4f3 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\CoalesceExpressionNode; use PHPStan\Type\Constant\ConstantBooleanType; @@ -85,6 +86,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($condResult->getVariableFlow(), VariableFlow::choice($rightResult->getVariableFlow(), null)), hasYield: $condResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index c0f643e7730..84fa08f4613 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -67,6 +67,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 5ab5282188f..9d3a07268cb 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -44,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult->getScope(), beforeScope: $beforeScope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index 8d91fe55131..18978cf5eed 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -44,15 +45,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); + // the evaluated code may read any variable $scope = $exprResult->getScope()->invalidateVolatileExpressions(); + $throwPoint = InternalThrowPoint::createImplicit($scope, $expr); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlow::all(VariableFlow::READ_ALL), VariableFlow::throwing($throwPoint->getType(), true)), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [$throwPoint]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'eval', 'eval', true)]), typeCallback: static fn (bool $nativeTypesPromoted): Type => new MixedType(), specifyTypesCallback: fn (TypeSpecifierContext $context, bool $nativeTypesPromoted) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index cdd85899af2..a8e085646ae 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\Type; @@ -50,8 +51,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = false; $throwPoints = []; + $variableFlow = null; if ($expr->expr !== null) { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); + $variableFlow = $exprResult->getVariableFlow(); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); @@ -62,6 +65,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($variableFlow, VariableFlow::exit(VariableFlow::STOP)), hasYield: $hasYield, isAlwaysTerminating: true, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 7c357151159..78ad61113b9 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -34,6 +34,8 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -392,7 +394,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); }; $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( - $nodeScopeResolver, $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, $expr, $normalizedExpr, @@ -406,9 +407,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // function call narrows the call itself - the inside-out equivalent of // createForExpr's FuncCall purity gate + tail entry. An impure call narrows to // nothing. - $createTypesCallback = function (Type $type, TypeSpecifierContext $createContext, bool $nativeTypesPromoted) use ($nodeScopeResolver, $expr, $normalizedExpr, $nameResult, $beforeScope, $argsResult): SpecifiedTypes { + $createTypesCallback = function (Type $type, TypeSpecifierContext $createContext, bool $nativeTypesPromoted) use ($expr, $normalizedExpr, $nameResult, $beforeScope, $argsResult): SpecifiedTypes { $s = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; - if (!$this->isFuncCallNarrowable($nodeScopeResolver, $s, $expr, $nameResult)) { + if (!$this->isFuncCallNarrowable($s, $expr, $nameResult)) { return new SpecifiedTypes([], []); } @@ -518,7 +519,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->scopeEffectsHelper->applyCallScopeEffects($nodeScopeResolver, $stmt, $normalizedExpr, $functionReflection, $parametersAcceptor, $argsResult, $scope, $scopeBeforeArgs, $storage, $nodeCallback); - return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $variableFlow = VariableFlow::sequence( + $nameResult !== null ? $nameResult->getVariableFlow() : null, + VariableFlowBuilder::arguments($expr, $argsResult, $storage), + self::getCallVariableFlow($functionReflection !== null ? $functionReflection->getName() : null, $normalizedExpr, $argsResult, $scope), + VariableFlowBuilder::throws($expr, $throwPoints), + $isAlwaysTerminating ? VariableFlow::exit(VariableFlow::STOP) : null, + ); + + return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints, $variableFlow); } private function getFunctionThrowPoint( @@ -748,7 +757,7 @@ private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, Mutatin * * @param FuncCall $expr */ - private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, FuncCall $normalizedExpr, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes + private function specifyTypes(MutatingScope $scope, Expr $expr, FuncCall $normalizedExpr, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes { if ($expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { @@ -793,7 +802,7 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop // evaluated a second time must not read the first call's // truthiness (mirrors the create() gate the old // specifyTypesInCondition() reached for the self key) - return ($this->isFuncCallNarrowable($nodeScopeResolver, $scope, $expr, $nameResult) + return ($this->isFuncCallNarrowable($scope, $expr, $nameResult) ? $specifiedTypes->unionWith($this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context)) : $specifiedTypes) ->setRootExpr($specifiedTypes->getRootExpr()); @@ -801,18 +810,18 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop } } - return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); + return $this->defaultFuncCallNarrowing($scope, $expr, $nameResult, $context); } - $specifiedTypes = $this->specifyTypesFromCallableCall($nodeScopeResolver, $context, $expr, $nameResult, $resolvedParametersAcceptor, $scope); + $specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $nameResult, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } - return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); + return $this->defaultFuncCallNarrowing($scope, $expr, $nameResult, $context); } - private function specifyTypesFromCallableCall(NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context, FuncCall $call, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, MutatingScope $scope): ?SpecifiedTypes + private function specifyTypesFromCallableCall(TypeSpecifierContext $context, FuncCall $call, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, MutatingScope $scope): ?SpecifiedTypes { if (!$call->name instanceof Expr) { return null; @@ -863,16 +872,16 @@ private function specifyTypesFromCallableCall(NodeScopeResolver $nodeScopeResolv * this expression through create(). * */ - private function defaultFuncCallNarrowing(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult, TypeSpecifierContext $context): SpecifiedTypes + private function defaultFuncCallNarrowing(MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult, TypeSpecifierContext $context): SpecifiedTypes { - if (!$this->isFuncCallNarrowable($nodeScopeResolver, $scope, $expr, $nameResult)) { + if (!$this->isFuncCallNarrowable($scope, $expr, $nameResult)) { return (new SpecifiedTypes([], []))->setRootExpr($expr); } return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } - private function isFuncCallNarrowable(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult): bool + private function isFuncCallNarrowable(MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult): bool { if ($expr->name instanceof Name) { if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) { @@ -942,4 +951,60 @@ private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $no return null; } + private static function getCallVariableFlow(?string $functionName, Expr\FuncCall $call, ArgsResult $args, MutatingScope $scope): ?VariableFlow + { + if (in_array($functionName, ['get_defined_vars', 'extract'], true)) { + return VariableFlow::all(VariableFlow::READ_ALL); + } + if (in_array($functionName, ['func_get_arg', 'func_get_args'], true)) { + return VariableFlow::all(VariableFlow::MENTION_ALL); + } + if ($functionName !== 'compact') { + return null; + } + $reads = []; + foreach ($call->getArgs() as $arg) { + if ($arg->unpack) { + return VariableFlow::all(VariableFlow::READ_ALL); + } + $names = self::compactNames($args->requireArgResult($arg->value)->getTypeOnScope($scope, false)); + if ($names === null) { + return VariableFlow::all(VariableFlow::READ_ALL); + } + foreach ($names as $name) { + $reads[] = VariableFlow::read($name); + } + } + return VariableFlow::sequence(...$reads); + } + + /** @return list|null */ + private static function compactNames(Type $type): ?array + { + $strings = $type->getConstantStrings(); + if ($strings !== []) { + return array_map(static fn ($name) => $name->getValue(), $strings); + } + $arrays = $type->getConstantArrays(); + if ($arrays === []) { + return null; + } + $names = []; + foreach ($arrays as $array) { + if ($array->isUnsealed()->yes()) { + return null; + } + foreach ($array->getValueTypes() as $value) { + $values = self::compactNames($value); + if ($values === null) { + return null; + } + foreach ($values as $name) { + $names[] = $name; + } + } + } + return $names; + } + } diff --git a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php index 470520c8202..270ea9897b8 100644 --- a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php +++ b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php @@ -122,7 +122,7 @@ public function specifyIdentical( return $types; } - return $this->specifyGeneral($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); + return $this->specifyGeneral($left, $right, $leftResult, $rightResult, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); } if ($constantName === 'null') { @@ -367,7 +367,6 @@ private function getTypeFromGettypeStringValue(string $value): ?Type * @param callable(): Type $identicalTypeCallback */ private function specifyGeneral( - NodeScopeResolver $nodeScopeResolver, Expr $left, Expr $right, ExpressionResult $leftResult, diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index f56de2f3b48..e481cbb61f4 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -46,15 +47,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $identifier = in_array($expr->type, [Include_::TYPE_INCLUDE, Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require'; + // the included file may read any variable $scope = $exprResult->getScope()->afterExtractCall()->invalidateVolatileExpressions(); + $throwPoint = InternalThrowPoint::createImplicit($scope, $expr); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlow::all(VariableFlow::READ_ALL), VariableFlow::throwing($throwPoint->getType(), true)), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [$throwPoint]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, $identifier, $identifier, true)]), typeCallback: static fn (bool $nativeTypesPromoted): Type => new MixedType(), specifyTypesCallback: fn (TypeSpecifierContext $context, bool $nativeTypesPromoted) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 731c60b634f..5c19222b256 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; @@ -108,6 +109,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), $classResult !== null ? $classResult->getVariableFlow() : null), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 84f6f77e44a..3e937b56591 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Constant\ConstantStringType; @@ -49,6 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $beforeScope = $scope; $hasYield = false; $throwPoints = []; + $variableFlows = []; $impurePoints = []; $isAlwaysTerminating = false; /** @var array $partResults */ @@ -58,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $variableFlows[] = $partResult->getVariableFlow(); $partResults[spl_object_id($part)] = $partResult; $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); @@ -75,6 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence(...$variableFlows), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 02961c56e16..f2d26f381ce 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -23,6 +23,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\IssetExpressionNode; @@ -30,6 +31,7 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function array_map; use function array_merge; use function array_reverse; use function count; @@ -126,6 +128,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence(...array_map(static fn (ExpressionResult $result) => $result->getVariableFlow(), $varResults)), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 4a35250c9c3..0239e02e610 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -28,6 +28,7 @@ use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; @@ -44,7 +45,9 @@ use PHPStan\Type\UnionType; use UnhandledMatchError; use function array_key_exists; +use function array_keys; use function array_merge; +use function array_reverse; use function array_values; use function count; use function ksort; @@ -131,6 +134,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $condResult->isAlwaysTerminating(); $matchScope = $scope->enterMatch($expr, $condType, $condNativeType); $armNodes = []; + $armFlows = []; + $conditionFlows = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false; $arms = $expr->arms; @@ -242,7 +247,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - $nodeScopeResolver->processExprNode($stmt, $cond, $armConditionScope, $storage, $nodeCallback, $deepContext); + $conditionResult = $nodeScopeResolver->processExprNode($stmt, $cond, $armConditionScope, $storage, $nodeCallback, $deepContext); + $conditionFlows[$i][$j] = $conditionResult->getVariableFlow(); $condNodes[] = new MatchExpressionArmCondition( $cond, @@ -283,6 +289,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments()), ); + $armFlows[$i] = $armResult->getVariableFlow(); $armScope = $armResult->getScope(); $scope = $scope->addTemplateArgumentConstraints($armScope->getTemplateArgumentConstraints()); if (!$armResult->isAlwaysTerminating()) { @@ -322,6 +329,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); $armResult = $nodeScopeResolver->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); + $armFlows[$i] = $armResult->getVariableFlow(); $matchScope = $armResult->getScope(); $scope = $scope->addTemplateArgumentConstraints($matchScope->getTemplateArgumentConstraints()); $hasYield = $hasYield || $armResult->hasYield(); @@ -342,7 +350,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $filteringCondData = []; $armCondScope = $matchScope; $condNodes = []; - $armCondResultScope = $matchScope; $bodyScope = null; $condArgResult = $this->identicalNarrowingHelper->captureFirstArgResult($expr->cond, $storage); foreach ($arm->conds as $j => $armCond) { @@ -351,6 +358,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine()); $armCondResult = $nodeScopeResolver->processExprNode($stmt, $armCond, $armCondScope, $storage, $nodeCallback, $deepContext); + $conditionFlows[$i][$j] = $armCondResult->getVariableFlow(); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); @@ -431,6 +439,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments()), ); + $armFlows[$i] = $armResult->getVariableFlow(); $armScope = $armResult->getScope(); $scope = $scope->addTemplateArgumentConstraints($armScope->getTemplateArgumentConstraints()); if (!$armResult->isAlwaysTerminating()) { @@ -499,10 +508,29 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $this->capturedArmResults[spl_object_id($expr)] = [$expr, $armTypeResults]; + $variableFlow = VariableFlow::throwing(new ObjectType(UnhandledMatchError::class), false); + foreach ($expr->arms as $i => $arm) { + if ($arm->conds !== null) { + continue; + } + + $variableFlow = $armFlows[$i] ?? null; + } + foreach (array_reverse($expr->arms, true) as $i => $arm) { + if ($arm->conds === null) { + continue; + } + + foreach (array_reverse(array_keys($arm->conds)) as $j) { + $variableFlow = VariableFlow::sequence($conditionFlows[$i][$j] ?? null, VariableFlow::choice($armFlows[$i] ?? null, $variableFlow)); + } + } + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($condResult->getVariableFlow(), $variableFlow), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index d7f9df021d2..718bbeeba81 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -28,6 +28,8 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\PossiblyImpureCallExpr; @@ -205,7 +207,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $argsResult, ); $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( - $nodeScopeResolver, $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, $expr, $normalizedExpr, @@ -354,7 +355,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - $result = $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $variableFlow = VariableFlow::sequence( + $varResult->getVariableFlow(), + $nameResult !== null ? $nameResult->getVariableFlow() : null, + VariableFlowBuilder::arguments($expr, $argsResult, $storage), + VariableFlowBuilder::throws($expr, $throwPoints), + $isAlwaysTerminating ? VariableFlow::exit(VariableFlow::STOP) : null, + ); + + $result = $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints, $variableFlow); // the var was processed above as the receiver; read its already-computed // result on the original scope instead of re-walking via Scope::getType(). @@ -376,20 +385,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $calledMethodScope = $this->calledMethodProcessor->processCalledMethod($nodeScopeResolver, $methodReflection); if ($calledMethodScope !== null) { $scope = $scope->mergeInitializedProperties($calledMethodScope); - return $this->expressionResultFactory->create( - $scope, - beforeScope: $beforeScope, - expr: $expr, - hasYield: $result->hasYield(), - isAlwaysTerminating: $result->isAlwaysTerminating(), - throwPoints: $result->getThrowPoints(), - impurePoints: $result->getImpurePoints(), - containsNullsafe: $varResult->containsNullsafe(), - typeCallback: $typeCallback, - specifyTypesCallback: $specifyTypesCallback, - createTypesCallback: $createTypesCallback, - argsResult: $argsResult, - ); + return $result->withScope($scope); } } @@ -484,7 +480,7 @@ private function resolveReturnType(MutatingScope $reflectionScope, bool $nativeT * @param MethodCall $expr * @param MethodCall $normalizedExpr */ - private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ExpressionResult $varResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes + private function specifyTypes(MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ExpressionResult $varResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes { if (!$expr->name instanceof Identifier) { return $this->defaultMethodCallNarrowing($scope, $expr, $varResult, $context); diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 152cfb1744a..efa729771f4 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -29,6 +29,8 @@ use PHPStan\Analyser\ThrowPoint; use PHPStan\Analyser\Traverser\GenericTypeTemplateTraverser; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -208,7 +210,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $className = $objectClasses[0]; $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { - $className = null; $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; } @@ -310,7 +311,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->invalidateVolatileExpressions(); } - return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $variableFlow = VariableFlow::sequence( + $classResult !== null ? $classResult->getVariableFlow() : null, + VariableFlowBuilder::arguments($expr, $argsResult, $storage), + VariableFlowBuilder::throws($expr, $throwPoints), + $isAlwaysTerminating ? VariableFlow::exit(VariableFlow::STOP) : null, + ); + + return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints, $variableFlow); } /** diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index e433621aec5..84838fc115e 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -22,6 +22,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\NullsafeMethodCallExpressionNode; use PHPStan\Node\Printer\ExprPrinter; @@ -136,6 +137,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::choice($exprResult->getVariableFlow(), $processedReceiverResult->getVariableFlow()), hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index e1a1269334e..d72181369ec 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -21,6 +21,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\NullsafePropertyFetchExpressionNode; use PHPStan\Node\Printer\ExprPrinter; @@ -115,6 +116,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::choice($exprResult->getVariableFlow(), $processedReceiverResult->getVariableFlow()), hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 53cfc2582eb..a096541a3df 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -19,6 +19,8 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\MethodCallableNode; @@ -85,6 +87,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $scope, expr: $expr->right, + variableFlow: $callableNodeResult->getVariableFlow(), hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -100,6 +103,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $callResult->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence(VariableFlowBuilder::child($expr->left, $storage), VariableFlowBuilder::child($expr->right, $storage), VariableFlowBuilder::throws($expr, $callResult->getThrowPoints())), hasYield: $callResult->hasYield(), isAlwaysTerminating: $callResult->isAlwaysTerminating(), throwPoints: $callResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 2dac637e934..e323b360688 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -17,7 +17,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Type\Type; /** @@ -65,18 +68,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // rule-side asks about it answer from the storage $nodeScopeResolver->storeExpressionResult($storage, $virtualExpr, $virtualExprResult); + $assignedScope = $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $virtualExpr, + $nodeCallback, + $virtualExprResult, + )->getScope(); + return $this->expressionResultFactory->create( - $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $virtualExpr, - $nodeCallback, - $virtualExprResult, - )->getScope(), + $assignedScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_POST_DEC, $assignedScope, $storage)), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 934539ade66..257f52dc87f 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -17,7 +17,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Type\Type; /** @@ -65,18 +68,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // rule-side asks about it answer from the storage $nodeScopeResolver->storeExpressionResult($storage, $virtualExpr, $virtualExprResult); + $assignedScope = $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $virtualExpr, + $nodeCallback, + $virtualExprResult, + )->getScope(); + return $this->expressionResultFactory->create( - $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $virtualExpr, - $nodeCallback, - $virtualExprResult, - )->getScope(), + $assignedScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_POST_INC, $assignedScope, $storage)), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 1097d7e2a82..10d39d39c61 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -16,7 +16,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Variable\VariableWrite; /** * @implements ExprHandler @@ -67,18 +70,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); + $assignedScope = $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + $incDecValueResult, + )->getScope(); + return $this->expressionResultFactory->create( - $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - $incDecValueResult, - )->getScope(), + $assignedScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_PRE_DEC, $assignedScope, $storage)), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index d674f9e56bb..beba3f0b134 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -16,7 +16,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Variable\VariableWrite; /** * @implements ExprHandler @@ -67,18 +70,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); + $assignedScope = $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + $incDecValueResult, + )->getScope(); + return $this->expressionResultFactory->create( - $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - $incDecValueResult, - )->getScope(), + $assignedScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), VariableFlowBuilder::targetWrite($expr->var, VariableWrite::KIND_PRE_INC, $assignedScope, $storage)), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index f22dd0f8d61..d0432f5cf90 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -16,6 +16,8 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Type; @@ -58,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 25f0b7ab20c..8470c337566 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -19,6 +19,8 @@ use PHPStan\Analyser\PropertyHookThrowPointsResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Properties\FoundPropertyReflection; @@ -108,6 +110,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, PropertyFetc $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), $nameResult !== null ? $nameResult->getVariableFlow() : null, VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ShellExecHandler.php b/src/Analyser/ExprHandler/ShellExecHandler.php index fcc71c01eda..fd3233f5554 100644 --- a/src/Analyser/ExprHandler/ShellExecHandler.php +++ b/src/Analyser/ExprHandler/ShellExecHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NullType; @@ -52,6 +53,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $beforeScope = $scope; $hasYield = false; $throwPoints = []; + $variableFlows = []; $impurePoints = []; $isAlwaysTerminating = false; foreach ($expr->parts as $part) { @@ -59,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $variableFlows[] = $partResult->getVariableFlow(); $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); @@ -75,6 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence(...$variableFlows), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 3699a8fc80e..d8816c219fc 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -29,6 +29,8 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\PossiblyImpureCallExpr; @@ -161,16 +163,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nativeThisType = null; if (isset($expr->getArgs()[1])) { $argType = $readArgType($expr->getArgs()[1]->value, false); - if ($argType->isNull()->yes()) { - $thisType = null; - } else { + if (!$argType->isNull()->yes()) { $thisType = $argType; } $nativeArgType = $readArgType($expr->getArgs()[1]->value, true); - if ($nativeArgType->isNull()->yes()) { - $nativeThisType = null; - } else { + if (!$nativeArgType->isNull()->yes()) { $nativeThisType = $nativeArgType; } } @@ -293,7 +291,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) : fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType( - $nodeScopeResolver, $beforeScope, $nativeTypesPromoted, $expr, @@ -303,7 +300,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $argsResult, ); $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( - $nodeScopeResolver, $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, $expr, $normalizedExpr, @@ -316,10 +312,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A type constraint on a (narrowable, i.e. non-side-effecting) static call // narrows the call itself - the inside-out equivalent of createForExpr's // StaticCall purity gate + tail entry. An impure call narrows to nothing. - $createTypesCallback = function (Type $type, TypeSpecifierContext $createContext, bool $nativeTypesPromoted) use ($expr, $classResult, $nodeScopeResolver, $beforeScope): SpecifiedTypes { + $createTypesCallback = function (Type $type, TypeSpecifierContext $createContext, bool $nativeTypesPromoted) use ($expr, $classResult, $beforeScope): SpecifiedTypes { $s = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; - return $this->isStaticCallNarrowable($s, $expr, $classResult, $nodeScopeResolver) + return $this->isStaticCallNarrowable($s, $expr, $classResult) ? $this->defaultNarrowingHelper->createSubjectTypes($s, $expr, null, $type, $createContext) : new SpecifiedTypes([], []); }; @@ -432,7 +428,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $variableFlow = VariableFlow::sequence( + $classResult !== null ? $classResult->getVariableFlow() : null, + $nameResult !== null ? $nameResult->getVariableFlow() : null, + VariableFlowBuilder::arguments($expr, $argsResult, $storage), + VariableFlowBuilder::throws($expr, $throwPoints), + $isAlwaysTerminating ? VariableFlow::exit(VariableFlow::STOP) : null, + ); + + return $preliminaryResult->finalize($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints, $variableFlow); } /** @@ -447,7 +451,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex * branch builds a synthetic StaticCall priced on demand by the resolver. * */ - private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $reflectionScope, bool $nativeTypesPromoted, StaticCall $expr, ?ExpressionResult $classResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor, ?ArgsResult $argsResult): Type + private function resolveReturnType(MutatingScope $reflectionScope, bool $nativeTypesPromoted, StaticCall $expr, ?ExpressionResult $classResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor, ?ArgsResult $argsResult): Type { $classType = $classResult !== null ? ($nativeTypesPromoted ? $classResult->getNativeType() : $classResult->getType()) @@ -546,7 +550,7 @@ private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, Mutatin * @param StaticCall $expr * @param StaticCall $normalizedExpr */ - private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ?ExpressionResult $classResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes + private function specifyTypes(MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ?ExpressionResult $classResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context, ?ArgsResult $argsResult = null): SpecifiedTypes { if (!$expr->name instanceof Identifier) { return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); @@ -609,13 +613,13 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop // see MethodCallHandler::specifyTypes() - the call's own key gets // the purity gate, the asserts narrow their subjects regardless return $specifiedTypes - ->unionWith($this->defaultStaticCallNarrowing($scope, $expr, $classResult, $nodeScopeResolver, $context)) + ->unionWith($this->defaultStaticCallNarrowing($scope, $expr, $classResult, $context)) ->setRootExpr($specifiedTypes->getRootExpr()); } } } - return $this->defaultStaticCallNarrowing($scope, $expr, $classResult, $nodeScopeResolver, $context); + return $this->defaultStaticCallNarrowing($scope, $expr, $classResult, $context); } /** @@ -628,9 +632,9 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop * * @param StaticCall $expr */ - private function defaultStaticCallNarrowing(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context): SpecifiedTypes + private function defaultStaticCallNarrowing(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, TypeSpecifierContext $context): SpecifiedTypes { - if (!$this->isStaticCallNarrowable($scope, $expr, $classResult, $nodeScopeResolver)) { + if (!$this->isStaticCallNarrowable($scope, $expr, $classResult)) { return (new SpecifiedTypes([], []))->setRootExpr($expr); } @@ -638,7 +642,7 @@ private function defaultStaticCallNarrowing(MutatingScope $scope, Expr $expr, ?E } /** @param StaticCall $expr */ - private function isStaticCallNarrowable(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver): bool + private function isStaticCallNarrowable(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult): bool { if (!$expr->name instanceof Identifier) { return true; diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index bf2311d0de6..b66c90d1676 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -19,6 +19,8 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -108,6 +110,7 @@ public function composeResult(StaticPropertyFetch $expr, ?ExpressionResult $clas $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($classResult !== null ? $classResult->getVariableFlow() : null, $nameResult !== null ? $nameResult->getVariableFlow() : null, VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 86af979685a..00c8a6b384d 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -18,6 +18,7 @@ use PHPStan\Analyser\PerFileAnalysisResettable; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -146,6 +147,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $finalScope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($ternaryCondResult->getVariableFlow(), VariableFlow::choice($ifResult !== null ? $ifResult->getVariableFlow() : null, $elseResult->getVariableFlow())), hasYield: $hasYield, isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 0d866b66376..ca7eba1e750 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\Type; @@ -47,6 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $scope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlow::throwing($exprResult->getType(), false)), hasYield: false, isAlwaysTerminating: true, throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getType(), $expr, false)]), diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index 9de17fb0f8c..29c0218a4f2 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 98111e662be..1cbca23fd92 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -47,6 +47,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult->getScope(), beforeScope: $scope, expr: $expr, + variableFlow: $exprResult->getVariableFlow(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 33352dad581..8622c66c1b0 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -21,6 +21,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; @@ -28,6 +29,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; use function count; use function in_array; use function is_string; @@ -146,11 +148,15 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Variable $ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $variableFlow = null; if (is_string($expr->name)) { + $variableFlow = VariableFlow::read($expr->name); if (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); } } elseif ($nameResult !== null) { + $names = $nameResult->getType()->getConstantStrings(); + $variableFlow = VariableFlow::sequence($nameResult->getVariableFlow(), $names === [] ? VariableFlow::all(VariableFlow::READ_ALL) : VariableFlow::sequence(...array_map(static fn ($name) => VariableFlow::read($name->getValue()), $names))); $hasYield = $nameResult->hasYield(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); @@ -162,6 +168,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Variable $ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $variableFlow, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index 4fda86a3730..f264089e2d9 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -56,6 +56,7 @@ public function processExpr( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $innerResult->getVariableFlow(), hasYield: $innerResult->hasYield(), isAlwaysTerminating: $innerResult->isAlwaysTerminating(), throwPoints: $innerResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index d6b83b33a04..87b6210b033 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -63,6 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: $nameResult !== null ? $nameResult->getVariableFlow() : null, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index c47d531691e..b45213cf33d 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\InstantiationCallableNode; use PHPStan\Reflection\InitializerExprContext; @@ -46,6 +47,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $hasYield = false; $isAlwaysTerminating = false; + $classResult = null; if ($expr->getClass() instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $scope = $classResult->getScope(); @@ -59,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($classResult !== null ? $classResult->getVariableFlow() : null), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index fed305786f6..6d7221da401 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\MethodCallableNode; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -51,6 +52,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = $varResult->getThrowPoints(); $impurePoints = $varResult->getImpurePoints(); $isAlwaysTerminating = false; + $nameResult = null; if ($expr->getName() instanceof Expr) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $scope = $nameResult->getScope(); @@ -64,6 +66,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($varResult->getVariableFlow(), $nameResult !== null ? $nameResult->getVariableFlow() : null), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index c7cf02715cd..653b62d3403 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Reflection\InitializerExprContext; @@ -47,6 +48,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $hasYield = false; $isAlwaysTerminating = false; + $classResult = null; + $nameResult = null; if ($expr->getClass() instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $scope = $classResult->getScope(); @@ -68,6 +71,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($classResult !== null ? $classResult->getVariableFlow() : null, $nameResult !== null ? $nameResult->getVariableFlow() : null), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index a7b8061c427..5e137b5b65c 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -48,13 +49,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); + $throwPoint = InternalThrowPoint::createImplicit($scope, $expr); + return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($exprResult->getVariableFlow(), VariableFlow::throwing($throwPoint->getType(), true)), hasYield: true, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [$throwPoint]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'yieldFrom', 'yield from', true)]), typeCallback: static function (bool $nativeTypesPromoted) use ($exprResult): Type { $yieldFromType = ($nativeTypesPromoted ? $exprResult->getNativeType() : $exprResult->getType()); diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index b7622910880..26acf526894 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -17,6 +17,8 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -58,6 +60,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), ]; $isAlwaysTerminating = false; + $keyResult = null; + $valueResult = null; if ($expr->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $expr->key, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); @@ -81,6 +85,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, beforeScope: $beforeScope, expr: $expr, + variableFlow: VariableFlow::sequence($keyResult !== null ? $keyResult->getVariableFlow() : null, $valueResult !== null ? $valueResult->getVariableFlow() : null, VariableFlowBuilder::throws($expr, $throwPoints)), hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index bcee1c9eee5..c155ca82084 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -32,15 +32,10 @@ final class ExpressionResult /** @var (callable(Type, TypeSpecifierContext, bool): SpecifiedTypes)|null */ private $createTypesCallback; - /** @var array */ - private array $specifiedTypes = []; - private ?MutatingScope $truthyScope = null; private ?MutatingScope $falseyScope = null; - private ?Type $cachedType = null; - /** * Whether every ExpressionTypeResolverExtension declined this expression. * One full-null round settles it: a decline is (in practice) a structural @@ -50,19 +45,6 @@ final class ExpressionResult */ private bool $extensionsDeclined = false; - private ?Type $cachedNativeType = null; - - private ?Type $resolvedType = null; - - private ?Type $resolvedNativeType = null; - - private ?Type $projectedType = null; - - private ?Type $projectedNativeType = null; - - /** @var list|null */ - private ?array $readVariableNames = null; - /** * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints @@ -70,6 +52,8 @@ final class ExpressionResult * @param callable(TypeSpecifierContext, bool): SpecifiedTypes $specifyTypesCallback * @param (callable(Type, TypeSpecifierContext, bool): SpecifiedTypes)|null $createTypesCallback * @param ExtensionsCollection $expressionTypeResolverExtensions + * @param array $specifiedTypes + * @param list|null $readVariableNames */ public function __construct( #[AutowiredExtensions(of: ExpressionTypeResolverExtension::class)] @@ -91,16 +75,26 @@ public function __construct( private ?Type $type = null, private ?Type $nativeType = null, private ?ArgsResult $argsResult = null, + private ?VariableFlow $variableFlow = null, + private array $specifiedTypes = [], + private ?Type $cachedType = null, + bool $extensionsDeclined = false, + private ?Type $cachedNativeType = null, + private ?Type $resolvedType = null, + private ?Type $resolvedNativeType = null, + private ?Type $projectedType = null, + private ?Type $projectedNativeType = null, + private ?array $readVariableNames = null, ) { // A precomputed type and a lazy typeCallback are mutually exclusive, but - // exactly one of them must be set - a result with neither cannot answer its - // own type. phpdoc and native types are precomputed together or not at all. + // one must be set unless both callback results have already been memoized. + // PHPDoc and native types are precomputed together or not at all. if ($typeCallback !== null && $type !== null) { throw new ShouldNotHappenException('ExpressionResult cannot have both a typeCallback and a precomputed type.'); } - if ($typeCallback === null && $type === null) { - throw new ShouldNotHappenException('ExpressionResult must have either a precomputed type or a typeCallback.'); + if ($typeCallback === null && $type === null && ($resolvedType === null || $resolvedNativeType === null)) { + throw new ShouldNotHappenException('ExpressionResult must have precomputed types, a typeCallback, or both resolved types.'); } if (($type === null) !== ($nativeType === null)) { throw new ShouldNotHappenException('ExpressionResult type and nativeType must both be set or both be null.'); @@ -109,31 +103,48 @@ public function __construct( $this->typeCallback = $typeCallback; $this->specifyTypesCallback = $specifyTypesCallback; $this->createTypesCallback = $createTypesCallback; + $this->extensionsDeclined = $extensionsDeclined; } /** - * Turns the stored preliminary result (the type/specify callbacks published - * before the call handler's throw-point leg runs) into the final one in - * place: the resolved scope and the effects arrive, every memoized - * own-type/narrowing answer computed through the preliminary is carried - * over, and the truthy/falsey scopes derived from the preliminary scope - * are dropped. Equivalent to overwriting the stored result with a second - * object, minus the allocation and the lost memos. + * Preserve the preliminary result's cached types in a final result carrying + * the resolved scope, effects, and variable flow. * * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints */ - public function finalize(MutatingScope $scope, bool $hasYield, bool $isAlwaysTerminating, array $throwPoints, array $impurePoints): self - { - $this->scope = $scope; - $this->hasYield = $hasYield; - $this->isAlwaysTerminating = $isAlwaysTerminating; - $this->throwPoints = $throwPoints; - $this->impurePoints = $impurePoints; - $this->truthyScope = null; - $this->falseyScope = null; - - return $this; + public function finalize(MutatingScope $scope, bool $hasYield, bool $isAlwaysTerminating, array $throwPoints, array $impurePoints, ?VariableFlow $variableFlow): self + { + return new self( + expressionTypeResolverExtensions: $this->expressionTypeResolverExtensions, + scope: $scope, + beforeScope: $this->beforeScope, + expr: $this->expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: $this->typeCallback, + specifyTypesCallback: $this->specifyTypesCallback, + containsNullsafe: $this->containsNullsafe, + issetabilityDescriptor: $this->issetabilityDescriptor, + truthyScopeOverrideResult: $this->truthyScopeOverrideResult, + falseyScopeOverrideResult: $this->falseyScopeOverrideResult, + createTypesCallback: $this->createTypesCallback, + type: $this->type, + nativeType: $this->nativeType, + argsResult: $this->argsResult, + variableFlow: $variableFlow, + specifiedTypes: $this->specifiedTypes, + cachedType: $this->cachedType, + extensionsDeclined: $this->extensionsDeclined, + cachedNativeType: $this->cachedNativeType, + resolvedType: $this->resolvedType, + resolvedNativeType: $this->resolvedNativeType, + projectedType: $this->projectedType, + projectedNativeType: $this->projectedNativeType, + readVariableNames: $this->readVariableNames, + ); } public function getScope(): MutatingScope @@ -141,16 +152,18 @@ public function getScope(): MutatingScope return $this->scope; } + public function getVariableFlow(): ?VariableFlow + { + return $this->variableFlow; + } + public function withScope(MutatingScope $scope): self { if ($scope === $this->scope) { return $this; } - $result = clone $this; - $result->scope = $scope; - $result->truthyScope = null; - $result->falseyScope = null; - return $result; + + return $this->finalize($scope, $this->hasYield, $this->isAlwaysTerminating, $this->throwPoints, $this->impurePoints, $this->variableFlow); } public function getBeforeScope(): MutatingScope @@ -731,30 +744,36 @@ public function askScopeVariableStateMatches(MutatingScope $scope, bool $useNati */ public function atAskPosition(MutatingScope $scope): self { - $clone = clone $this; - $clone->scope = $scope; - $clone->beforeScope = $scope; - $clone->truthyScope = null; - $clone->falseyScope = null; - $clone->truthyScopeOverrideResult = null; - $clone->falseyScopeOverrideResult = null; - $clone->cachedType = null; - $clone->cachedNativeType = null; - // a scope-authoritative expression's type is pinned eagerly from the ask - // position's state - the original callbacks capture the original - // position's scopes and would answer stale types (e.g. a variable - // receiver consumed on an ensured-non-null scope) - if ($this->type === null && $this->isScopeAuthoritative($scope)) { - $clone->type = $scope->getStateType($this->expr); - $clone->nativeType = $scope->doNotTreatPhpDocTypesAsCertain()->getStateType($this->expr); - $clone->typeCallback = null; - $clone->resolvedType = null; - $clone->resolvedNativeType = null; - $clone->projectedType = null; - $clone->projectedNativeType = null; - } - - return $clone; + // Scope-authoritative types must come from the asking position: the + // original callback captures the original scope. + $fromScope = $this->type === null && $this->isScopeAuthoritative($scope); + + return new self( + expressionTypeResolverExtensions: $this->expressionTypeResolverExtensions, + scope: $scope, + beforeScope: $scope, + expr: $this->expr, + hasYield: $this->hasYield, + isAlwaysTerminating: $this->isAlwaysTerminating, + throwPoints: $this->throwPoints, + impurePoints: $this->impurePoints, + typeCallback: $fromScope ? null : $this->typeCallback, + specifyTypesCallback: $this->specifyTypesCallback, + containsNullsafe: $this->containsNullsafe, + issetabilityDescriptor: $this->issetabilityDescriptor, + createTypesCallback: $this->createTypesCallback, + type: $fromScope ? $scope->getStateType($this->expr) : $this->type, + nativeType: $fromScope ? $scope->doNotTreatPhpDocTypesAsCertain()->getStateType($this->expr) : $this->nativeType, + argsResult: $this->argsResult, + variableFlow: $this->variableFlow, + specifiedTypes: $this->specifiedTypes, + extensionsDeclined: $this->extensionsDeclined, + resolvedType: $fromScope ? null : $this->resolvedType, + resolvedNativeType: $fromScope ? null : $this->resolvedNativeType, + projectedType: $fromScope ? null : $this->projectedType, + projectedNativeType: $fromScope ? null : $this->projectedNativeType, + readVariableNames: $this->readVariableNames, + ); } /** @@ -768,15 +787,34 @@ public function atAskPosition(MutatingScope $scope): self */ public function onNonNullabilityDevicedScopes(MutatingScope $beforeScope, MutatingScope $scope): self { - $clone = clone $this; - $clone->beforeScope = $beforeScope; - $clone->scope = $scope; - $clone->cachedType = null; - $clone->cachedNativeType = null; - $clone->truthyScope = null; - $clone->falseyScope = null; - - return $clone; + return new self( + expressionTypeResolverExtensions: $this->expressionTypeResolverExtensions, + scope: $scope, + beforeScope: $beforeScope, + expr: $this->expr, + hasYield: $this->hasYield, + isAlwaysTerminating: $this->isAlwaysTerminating, + throwPoints: $this->throwPoints, + impurePoints: $this->impurePoints, + typeCallback: $this->typeCallback, + specifyTypesCallback: $this->specifyTypesCallback, + containsNullsafe: $this->containsNullsafe, + issetabilityDescriptor: $this->issetabilityDescriptor, + truthyScopeOverrideResult: $this->truthyScopeOverrideResult, + falseyScopeOverrideResult: $this->falseyScopeOverrideResult, + createTypesCallback: $this->createTypesCallback, + type: $this->type, + nativeType: $this->nativeType, + argsResult: $this->argsResult, + variableFlow: $this->variableFlow, + specifiedTypes: $this->specifiedTypes, + extensionsDeclined: $this->extensionsDeclined, + resolvedType: $this->resolvedType, + resolvedNativeType: $this->resolvedNativeType, + projectedType: $this->projectedType, + projectedNativeType: $this->projectedNativeType, + readVariableNames: $this->readVariableNames, + ); } /** diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index ab19c2bb2b9..6d7a2397293 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -33,6 +33,7 @@ public function create( ?Type $type = null, ?Type $nativeType = null, ?ArgsResult $argsResult = null, + ?VariableFlow $variableFlow = null, ): ExpressionResult; } diff --git a/src/Analyser/InternalStatementResult.php b/src/Analyser/InternalStatementResult.php index 092fd51a277..13583801155 100644 --- a/src/Analyser/InternalStatementResult.php +++ b/src/Analyser/InternalStatementResult.php @@ -23,6 +23,7 @@ public function __construct( private array $throwPoints, private array $impurePoints, private array $endStatements = [], + private ?VariableFlow $variableFlow = null, ) { foreach ($exitPoints as $exitPoint) { @@ -33,6 +34,11 @@ public function __construct( } } + public function getVariableFlow(): ?VariableFlow + { + return $this->variableFlow; + } + public function toPublic(): StatementResult { return new StatementResult( @@ -75,14 +81,14 @@ public function filterOutLoopExitPoints(): self $num = $statement->num; if (!$num instanceof Int_) { - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow); } if ($num->value !== 1) { continue; } - return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints); + return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow); } return $this; diff --git a/src/Analyser/InternalThrowPoint.php b/src/Analyser/InternalThrowPoint.php index a50c4bd0427..a1a690c9721 100644 --- a/src/Analyser/InternalThrowPoint.php +++ b/src/Analyser/InternalThrowPoint.php @@ -30,7 +30,7 @@ public function toPublic(): ThrowPoint return ThrowPoint::createExplicit($this->scope, $this->type, $this->node, $this->canContainAnyThrowable); } - return ThrowPoint::createImplicit($this->scope, $this->node); + return ThrowPoint::createImplicit($this->scope, $this->node, $this->type); } /** @@ -44,9 +44,9 @@ public static function createExplicit(MutatingScope $scope, Type $type, Node $no /** * @param Node\Expr|Node\Stmt $node */ - public static function createImplicit(MutatingScope $scope, Node $node): self + public static function createImplicit(MutatingScope $scope, Node $node, ?Type $type = null): self { - return new self($scope, new ObjectType(Throwable::class), $node, explicit: false, canContainAnyThrowable: true); + return new self($scope, $type ?? new ObjectType(Throwable::class), $node, explicit: false, canContainAnyThrowable: true); } public static function createFromPublic(ThrowPoint $throwPoint, MutatingScope $scope): self diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 64dd29594e7..2235b9dbc78 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -30,7 +30,9 @@ use PhpParser\Node\Stmt\Static_; use PhpParser\Node\Stmt\Switch_; use PhpParser\NodeFinder; +use PHPStan\Analyser\ExprHandler\ArrowFunctionHandler; use PHPStan\Analyser\ExprHandler\AssignHandler; +use PHPStan\Analyser\ExprHandler\ClosureHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -45,6 +47,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ExtensionsCollection; +use PHPStan\File\FileHelper; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\NativeTypeExpr; @@ -111,7 +114,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -use function array_fill_keys; use function array_filter; use function array_key_exists; use function array_keys; @@ -214,6 +216,7 @@ public function __construct( private readonly TemplateArgumentObserver $templateArgumentObserver, private readonly TemplateArgumentResolver $templateArgumentResolver, private readonly ReflectionProvider $reflectionProvider, + private readonly FileHelper $fileHelper, #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] private readonly ExtensionsCollection $functionParameterOutTypeExtensions, #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] @@ -256,12 +259,21 @@ public function __construct( } /** + * The lookups (isAnalysedFile()) are keyed by normalized paths, so the + * given paths are normalized here - a caller-provided unnormalized path + * (mixed directory separators on Windows) must not silently skip the + * in-class-context analysis of a trait. + * * @api * @param string[] $files */ public function setAnalysedFiles(array $files): void { - $this->analysedFiles = array_fill_keys($files, true); + $analysedFiles = []; + foreach ($files as $file) { + $analysedFiles[$this->fileHelper->normalizePath($file)] = true; + } + $this->analysedFiles = $analysedFiles; } /** @@ -747,6 +759,7 @@ private function processStatementStep( $nodeCallback, $context, ); + $state->variableFlows[$i] = $statementResult->getVariableFlow(); $state->scope = $statementResult->getScope(); $state->hasYield = $state->hasYield || $statementResult->hasYield(); @@ -1015,6 +1028,13 @@ private function findOriginalParameterType(ParametersAcceptor $acceptor, Paramet /** Carries what the recorded walk from $from to $to added onto $state. */ private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void { + foreach ($to->variableFlows as $index => $flow) { + if (array_key_exists($index, $from->variableFlows)) { + continue; + } + + $state->variableFlows[$index] = $flow; + } $state->hasYield = $state->hasYield || ($to->hasYield && !$from->hasYield); $state->alreadyTerminated = $state->alreadyTerminated || ($to->alreadyTerminated && !$from->alreadyTerminated); $state->exitPoints = array_merge($state->exitPoints, array_slice($to->exitPoints, count($from->exitPoints))); @@ -1022,6 +1042,22 @@ private function appendRecordedStatementResults(StatementListWalkState $state, S $state->impurePoints = array_merge($state->impurePoints, array_slice($to->impurePoints, count($from->impurePoints))); } + public function getVariableMentionFlow(Node\Stmt $stmt): ?VariableFlow + { + $names = []; + $mentionsEverything = false; + $this->collectMentionedVariables($stmt, $names, $mentionsEverything); + if ($mentionsEverything) { + return VariableFlow::all(VariableFlow::MENTION_ALL); + } + + $flows = []; + foreach (array_keys($names) as $name) { + $flows[] = VariableFlow::mention($name); + } + return VariableFlow::sequence(...$flows); + } + private const MENTIONED_VARIABLES_ATTRIBUTE = 'templateArgumentMentionedVariables'; /** @@ -1187,6 +1223,7 @@ public function processStmtNode( throwPoints: $overridingThrowPoints, impurePoints: $stmtResult->getImpurePoints(), endStatements: $stmtResult->getEndStatements(), + variableFlow: $stmtResult->getVariableFlow(), ); } @@ -1637,6 +1674,7 @@ private function processExprNodeInternal( isAlwaysTerminating: $newExprResult->isAlwaysTerminating(), throwPoints: $newExprResult->getThrowPoints(), impurePoints: $newExprResult->getImpurePoints(), + variableFlow: $newExprResult->getVariableFlow(), // the first-class callable closure type lives on the *CallableNode // result; delegate so getType() of the original CallLike answers from it typeCallback: static fn (bool $nativeTypesPromoted): Type => ($nativeTypesPromoted ? $newExprResult->getNativeType() : $newExprResult->getType()), @@ -1787,15 +1825,6 @@ private function hasContextSensitiveConstruct(Node $node): bool return false; } - /** - * Replays a recorded convergence pass's emissions through the real node - * callback in place of the final loop walk. The pass's storage was merged - * into $storage by the caller; binding it for the whole replay lets the - * recorded scopes answer rule asks from the stored before-scopes, the - * same way the repeated walk's per-emission binding would. - * - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ /** * Opens an engine-feeding gatherer frame for the duration of a body walk. * The caller closes it in a finally block via popNodeGatherer(). @@ -2068,6 +2097,7 @@ private function processClosureNodeInternal( $executionEnds, array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), ), $closureReturnStatementsNodeScope, $storage); + $this->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); return new ProcessClosureResult( $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), @@ -2169,6 +2199,7 @@ private function processClosureNodeInternal( $executionEnds, array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), ), $closureReturnStatementsNodeScope, $storage); + $this->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); return new ProcessClosureResult( $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), @@ -2350,6 +2381,7 @@ public function processArrowFunctionNode( expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), + variableFlow: ArrowFunctionHandler::getVariableFlow($expr, $exprResult), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), typeCallback: static fn () => new MixedType(), @@ -2698,6 +2730,7 @@ public function processArgs( }); $argResults = []; + $byRefArguments = []; $countStableMetadataAcceptor = null; foreach ($processingOrder as $i) { $arg = $args[$i]; @@ -2792,6 +2825,9 @@ public function processArgs( } } + if ($parameter !== null && !$parameter->passedByReference()->no()) { + $byRefArguments[spl_object_id($arg->value)] = true; + } $lookForUnset = false; if ($assignByReference) { $isBuiltin = false; @@ -2899,6 +2935,7 @@ public function processArgs( $closureResult->getScope(), $scopeToPass, $arg->value, + variableFlow: ClosureHandler::getVariableFlow($arg->value), hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -3059,6 +3096,7 @@ public function processArgs( ); $storedArrowResult = $this->expressionResultFactory->create( $arrowFunctionExprResult->getScope(), + variableFlow: $arrowFunctionExprResult->getVariableFlow(), beforeScope: $scopeToPass, expr: $arg->value, hasYield: $arrowFunctionExprResult->hasYield(), @@ -3300,6 +3338,7 @@ public function processArgs( ), $resolvedAcceptor, $argResults, + $byRefArguments, ); } diff --git a/src/Analyser/PropertyHooksProcessor.php b/src/Analyser/PropertyHooksProcessor.php index aef2aa1172d..5b9afdd6a5b 100644 --- a/src/Analyser/PropertyHooksProcessor.php +++ b/src/Analyser/PropertyHooksProcessor.php @@ -147,7 +147,8 @@ public function processPropertyHooks( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }); try { - $statementResult = $nodeScopeResolver->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, $nodeCallback, StatementContext::createTopLevel())->toPublic(); + $internalStatementResult = $nodeScopeResolver->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, $nodeCallback, StatementContext::createTopLevel()); + $statementResult = $internalStatementResult->toPublic(); } finally { $nodeScopeResolver->popNodeGatherer(); } @@ -163,6 +164,7 @@ public function processPropertyHooks( $hookReflection, $propertyReflection, ), $hookScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($hook, $internalStatementResult->getVariableFlow()), $hookScope, $storage); } } diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php index 965fe08105c..d38e0e5cf74 100644 --- a/src/Analyser/ScopeOps.php +++ b/src/Analyser/ScopeOps.php @@ -524,6 +524,9 @@ public static function createConditionalExpressions( if (array_key_exists($exprString, $ourExpressionTypes)) { continue; } + if ($mergedExprTypeHolder->getExpr() instanceof VirtualNode) { + continue; + } foreach ($typeGuards as $guardExprString => $guardHolder) { $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); diff --git a/src/Analyser/StatementListWalkState.php b/src/Analyser/StatementListWalkState.php index 232b984545e..b80654482ba 100644 --- a/src/Analyser/StatementListWalkState.php +++ b/src/Analyser/StatementListWalkState.php @@ -10,6 +10,9 @@ final class StatementListWalkState { + /** @var array */ + public array $variableFlows = []; + public bool $alreadyTerminated = false; public bool $hasYield = false; @@ -36,6 +39,7 @@ public function toResult(): InternalStatementResult $this->exitPoints, $this->throwPoints, $this->impurePoints, + variableFlow: VariableFlow::sequence(...$this->variableFlows), ); } diff --git a/src/Analyser/StmtHandler/BreakContinueHandler.php b/src/Analyser/StmtHandler/BreakContinueHandler.php index b7eef408186..40a999a0dae 100644 --- a/src/Analyser/StmtHandler/BreakContinueHandler.php +++ b/src/Analyser/StmtHandler/BreakContinueHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\StmtHandler; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; @@ -13,6 +14,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; /** @@ -50,7 +52,10 @@ public function processStmt( return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: true, exitPoints: [ new InternalStatementExitPoint($stmt, $scope), - ], throwPoints: $throwPoints, impurePoints: $impurePoints); + ], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: VariableFlow::exit( + $stmt instanceof Break_ ? VariableFlow::BREAK : VariableFlow::CONTINUE, + $stmt->num instanceof Int_ ? $stmt->num->value : 1, + )); } } diff --git a/src/Analyser/StmtHandler/ClassLikeHandler.php b/src/Analyser/StmtHandler/ClassLikeHandler.php index b86547f57e3..1cc07a26e10 100644 --- a/src/Analyser/StmtHandler/ClassLikeHandler.php +++ b/src/Analyser/StmtHandler/ClassLikeHandler.php @@ -84,7 +84,7 @@ public function processStmt( return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); } if (isset($stmt->namespacedName)) { - $classReflection = $this->getCurrentClassReflection($nodeScopeResolver, $stmt, $stmt->namespacedName->toString(), $scope); + $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { @@ -136,7 +136,7 @@ public function processStmt( return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); } - private function getCurrentClassReflection(NodeScopeResolver $nodeScopeResolver, Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection + private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { if (!$this->reflectionProvider->hasClass($className)) { return $this->createAstClassReflection($stmt, $className, $scope); diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index bbbc91930e2..346397d47fc 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -19,6 +19,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableLivenessResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\ExecutionEndNode; @@ -223,7 +224,8 @@ public function processStmt( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }); try { - $statementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $bodyStorage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments()))->toPublic(); + $internalStatementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $bodyStorage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + $statementResult = $internalStatementResult->toPublic(); } finally { $nodeScopeResolver->popNodeGatherer(); } @@ -244,6 +246,7 @@ public function processStmt( $classReflection, $methodReflection, ), $methodScope, $bodyStorage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($stmt, $internalStatementResult->getVariableFlow()), $methodScope, $bodyStorage); } finally { $scope->popExpressionResultStorage(); } diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index b6103523522..82a10c887eb 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\RecordingNodeCallback; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\DoWhileLoopConditionNode; use function array_merge; @@ -175,6 +176,7 @@ public function processStmt( exitPoints: $bodyScopeResult->getExitPointsForOuterLoop(), throwPoints: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), impurePoints: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()), + variableFlow: VariableFlow::loop(null, $bodyScopeResult->getVariableFlow(), $condResult->getVariableFlow(), true, !$alwaysIterates, !$condResult->getType()->toBoolean()->isFalse()->yes()), ); } diff --git a/src/Analyser/StmtHandler/EchoHandler.php b/src/Analyser/StmtHandler/EchoHandler.php index 107c4e52581..72c56963f3a 100644 --- a/src/Analyser/StmtHandler/EchoHandler.php +++ b/src/Analyser/StmtHandler/EchoHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use function array_merge; @@ -46,8 +47,10 @@ public function processStmt( $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $variableFlows = []; foreach ($stmt->exprs as $echoExpr) { $result = $nodeScopeResolver->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); + $variableFlows[] = $result->getVariableFlow(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope, $result); @@ -61,7 +64,7 @@ public function processStmt( $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); $impurePoints[] = new ImpurePoint($scope, $stmt, 'echo', 'echo', true); - return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: VariableFlow::sequence(...$variableFlows)); } } diff --git a/src/Analyser/StmtHandler/ExpressionHandler.php b/src/Analyser/StmtHandler/ExpressionHandler.php index 3aeb14fb24f..9386dbb2506 100644 --- a/src/Analyser/StmtHandler/ExpressionHandler.php +++ b/src/Analyser/StmtHandler/ExpressionHandler.php @@ -102,9 +102,9 @@ public function processStmt( if ($statementType instanceof NeverType && $statementType->isExplicit()) { return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: true, exitPoints: [ new InternalStatementExitPoint($stmt, $scope), - ], throwPoints: $throwPoints, impurePoints: $impurePoints); + ], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: $result->getVariableFlow()); } - return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: $result->getVariableFlow()); } } diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 8269128e9f5..f4ddcd1c2d2 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -23,6 +23,8 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\TrinaryLogic; use function array_last; @@ -126,9 +128,12 @@ public function processStmt( $hasYield = false; $throwPoints = []; $impurePoints = []; + $initFlow = []; + $conditionFlow = []; foreach ($stmt->init as $initExpr) { $initResult = $nodeScopeResolver->processExprNode($stmt, $initExpr, $initScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); $initScope = $initResult->getScope(); + $initFlow[] = $initResult->getVariableFlow(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); @@ -143,9 +148,10 @@ public function processStmt( $storage = $originalStorage->duplicate(); $scope->pushExpressionResultStorage($storage); try { - foreach ($stmt->cond as $condExpr) { + foreach ($stmt->cond as $condIndex => $condExpr) { $condResult = $nodeScopeResolver->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false)); $initScope = $condResult->getScope(); + $conditionFlow[$condIndex] = $condResult->getVariableFlow(); // only the last condition expression is relevant whether the loop continues // see https://www.php.net/manual/en/control-structures.for.php @@ -222,6 +228,7 @@ public function processStmt( // convergence duplicates) that re-priced it on demand $condResult = $nodeScopeResolver->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $alwaysIterates = $alwaysIterates->and($condResult->getType()->toBoolean()->isTrue()); + $conditionFlow[count($stmt->cond) - 1] = $condResult->getVariableFlow(); $bodyScope = $condResult->getTruthyScope(); $bodyScope = $this->inferForLoopExpressions($nodeScopeResolver, $stmt, $lastCondExpr, $bodyScope, $storage); } @@ -288,6 +295,16 @@ public function processStmt( $isAlwaysTerminating = false; } + $updateFlow = []; + foreach ($stmt->loop as $loopExpr) { + $updateFlow[] = VariableFlowBuilder::child($loopExpr, $storage); + } + $condition = VariableFlow::sequence(...$conditionFlow); + $update = VariableFlow::sequence(...$updateFlow); + $loop = $isIterableAtLeastOnce->no() + ? VariableFlow::sequence($condition, VariableFlow::dead(VariableFlow::sequence($finalScopeResult->getVariableFlow(), $update))) + : VariableFlow::loop($condition, $finalScopeResult->getVariableFlow(), $update, $isIterableAtLeastOnce->yes(), !$alwaysIterates->yes()); + $variableFlow = VariableFlow::sequence(...[...$initFlow, $loop]); return new InternalStatementResult( $finalScope->addTemplateArgumentConstraints($loopScope->getTemplateArgumentConstraints()), hasYield: $finalScopeResult->hasYield() || $hasYield, @@ -295,6 +312,7 @@ public function processStmt( exitPoints: $finalScopeResult->getExitPointsForOuterLoop(), throwPoints: array_merge($throwPoints, $finalScopeResult->getThrowPoints()), impurePoints: array_merge($impurePoints, $finalScopeResult->getImpurePoints()), + variableFlow: $variableFlow, ); } diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 25b664bdd62..7ecf69bd2c0 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -35,6 +35,8 @@ use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VarAnnotationProcessor; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; @@ -43,6 +45,7 @@ use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; use PHPStan\Node\InForeachNode; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Node\VariableAssignNode; use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; @@ -468,6 +471,14 @@ static function () use ($condResult, $emptyArrayType): Type { $finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType()); } + $bindingFlow = VariableFlow::sequence( + $stmt->keyVar !== null ? VariableFlowBuilder::targetRead($stmt->keyVar, $storage, false) : null, + $stmt->keyVar !== null ? VariableFlowBuilder::targetWrite($stmt->keyVar, VariableWrite::KIND_FOREACH_KEY, $finalScope, $storage) : null, + VariableFlowBuilder::targetRead($stmt->valueVar, $storage, false), + VariableFlowBuilder::targetWrite($stmt->valueVar, VariableWrite::KIND_FOREACH_VALUE, $finalScope, $storage), + $stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) ? VariableFlow::escape($stmt->valueVar->name) : null, + ); + $loopFlow = VariableFlow::loop($traversableThrowPoint !== null ? VariableFlow::throwing($traversableThrowPoint->getType(), true) : null, VariableFlow::sequence($bindingFlow, $finalScopeResult->getVariableFlow()), null, $isIterableAtLeastOnce->yes() && $nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach(), true); return new InternalStatementResult( $finalScope->addTemplateArgumentConstraints($finalScopeResult->getScope()->getTemplateArgumentConstraints()), hasYield: $finalScopeResult->hasYield() || $condResult->hasYield(), @@ -475,6 +486,7 @@ static function () use ($condResult, $emptyArrayType): Type { exitPoints: $finalScopeResult->getExitPointsForOuterLoop(), throwPoints: $throwPoints, impurePoints: $impurePoints, + variableFlow: VariableFlow::sequence($condResult->getVariableFlow(), $isIterableAtLeastOnce->no() ? VariableFlow::dead($loopFlow) : $loopFlow), ); } @@ -516,6 +528,7 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $originalScope->getIterableValueType($nativeIterateeType), ), $nodeCallback, + null, )->getScope(); $vars = $nodeScopeResolver->getAssignedVariables($stmt->valueVar); if ( @@ -534,13 +547,13 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $originalScope->getIterableKeyType($nativeIterateeType), ), $nodeCallback, + null, )->getScope(); $vars = array_merge($vars, $nodeScopeResolver->getAssignedVariables($stmt->keyVar)); } if ($stmt->valueVar instanceof List_) { $scope = $this->addDestructureTaggedUnionConditionalHolders( - $nodeScopeResolver, $scope, $originalScope->getIterableValueType($iterateeType), $stmt->valueVar, @@ -694,6 +707,7 @@ private function tryProcessUnrolledConstantArrayForeach( $valueType, $nativeValueType, TrinaryLogic::createYes(), + [], ); $iterScope = $iterScope->assignExpression( new OriginalForeachValueExpr($valueVarName), @@ -706,6 +720,7 @@ private function tryProcessUnrolledConstantArrayForeach( $keyType, $nativeKeyType, TrinaryLogic::createYes(), + [], ); $iterScope = $iterScope->assignExpression( new OriginalForeachKeyExpr($keyVarName), @@ -863,7 +878,6 @@ private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $ite * the regular per-variable type tracking. */ private function addDestructureTaggedUnionConditionalHolders( - NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Type $iterableValueType, List_ $list, diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 0869530edb6..600c3aa8f38 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableLivenessResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\FunctionReturnStatementsNode; @@ -142,7 +143,8 @@ public function processStmt( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }); try { - $statementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $bodyStorage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments()))->toPublic(); + $internalStatementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $bodyStorage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + $statementResult = $internalStatementResult->toPublic(); } finally { $nodeScopeResolver->popNodeGatherer(); } @@ -157,6 +159,7 @@ public function processStmt( array_merge($statementResult->getImpurePoints(), $functionImpurePoints), $functionReflection, ), $functionScope, $bodyStorage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($stmt, $internalStatementResult->getVariableFlow()), $functionScope, $bodyStorage); } finally { $scope->popExpressionResultStorage(); } diff --git a/src/Analyser/StmtHandler/GlobalHandler.php b/src/Analyser/StmtHandler/GlobalHandler.php index 665430b4849..f4c217f7284 100644 --- a/src/Analyser/StmtHandler/GlobalHandler.php +++ b/src/Analyser/StmtHandler/GlobalHandler.php @@ -14,6 +14,8 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VarAnnotationProcessor; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -72,12 +74,14 @@ public function processStmt( ), ]; $vars = []; + $variableFlows = []; foreach ($stmt->vars as $var) { if (!$var instanceof Variable) { throw new ShouldNotHappenException(); } $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($scope, $var); $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); + $variableFlows[] = VariableFlow::sequence(VariableFlowBuilder::escapeRoot($var), VariableFlowBuilder::targetRead($var, $storage, false)); $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $var); @@ -91,7 +95,7 @@ public function processStmt( } $scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt); - return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints); + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints, variableFlow: VariableFlow::sequence(...$variableFlows)); } } diff --git a/src/Analyser/StmtHandler/GotoHandler.php b/src/Analyser/StmtHandler/GotoHandler.php index 43c4a3029a1..8eef51b1ec2 100644 --- a/src/Analyser/StmtHandler/GotoHandler.php +++ b/src/Analyser/StmtHandler/GotoHandler.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; /** @@ -34,9 +35,10 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { + // a jump defeats reaching-write tracking for the whole body return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: true, exitPoints: [ new InternalStatementExitPoint($stmt, $scope), - ], throwPoints: [], impurePoints: []); + ], throwPoints: [], impurePoints: [], variableFlow: VariableFlow::all(VariableFlow::OPAQUE)); } } diff --git a/src/Analyser/StmtHandler/IfHandler.php b/src/Analyser/StmtHandler/IfHandler.php index 784f2880769..59526da6eac 100644 --- a/src/Analyser/StmtHandler/IfHandler.php +++ b/src/Analyser/StmtHandler/IfHandler.php @@ -12,8 +12,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use function array_merge; +use function array_reverse; use function count; /** @@ -38,6 +40,8 @@ public function processStmt( ): InternalStatementResult { $entryScope = $scope; + $flowBranches = []; + $elseFlow = null; $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); $conditionType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); @@ -49,9 +53,8 @@ public function processStmt( $finalScope = null; $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); - + $flowBranches[] = [$condResult->getVariableFlow(), $branchScopeStatementResult->getVariableFlow(), $conditionType->isTrue()->yes() ? true : ($conditionType->isFalse()->yes() ? false : null)]; if (!$conditionType->isTrue()->no()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); @@ -80,7 +83,7 @@ public function processStmt( $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $branchScopeStatementResult = $nodeScopeResolver->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); - + $flowBranches[] = [$condResult->getVariableFlow(), $branchScopeStatementResult->getVariableFlow(), $elseIfConditionType->isTrue()->yes() ? true : ($elseIfConditionType->isFalse()->yes() ? false : null)]; if ( !$ifAlwaysTrue && !$lastElseIfConditionIsTrue @@ -120,7 +123,7 @@ public function processStmt( } else { $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt->else, $scope, $storage); $branchScopeStatementResult = $nodeScopeResolver->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $storage, $nodeCallback, $context); - + $elseFlow = $branchScopeStatementResult->getVariableFlow(); if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints()); @@ -147,7 +150,10 @@ public function processStmt( $endStatements[] = new InternalEndStatementResult($stmt, new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints)); } - return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, endStatements: $endStatements); + foreach (array_reverse($flowBranches) as [$conditionFlow, $branchFlow, $truthy]) { + $elseFlow = VariableFlow::conditional($conditionFlow, $branchFlow, $elseFlow, $truthy); + } + return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, endStatements: $endStatements, variableFlow: $elseFlow); } } diff --git a/src/Analyser/StmtHandler/ReturnHandler.php b/src/Analyser/StmtHandler/ReturnHandler.php index c73ac98e59b..b8eefe161e3 100644 --- a/src/Analyser/StmtHandler/ReturnHandler.php +++ b/src/Analyser/StmtHandler/ReturnHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\StmtHandler; +use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\ExpressionContext; @@ -12,7 +13,9 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; +use function is_string; /** * @implements StmtHandler @@ -47,17 +50,22 @@ public function processStmt( $impurePoints = $result->getImpurePoints(); $scope = $result->getScope()->addTemplateArgumentConstraints($varConstraints)->addTemplateArgumentConstraints($nodeScopeResolver->collectReturnSend($stmtScope, $result)); $hasYield = $result->hasYield(); + $variableFlow = $result->getVariableFlow(); } else { $hasYield = false; $throwPoints = []; $impurePoints = []; + $variableFlow = null; } $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage); return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: true, exitPoints: [ new InternalStatementExitPoint($stmt, $scope), - ], throwPoints: $throwPoints, impurePoints: $impurePoints); + ], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: VariableFlow::sequence( + $variableFlow, + VariableFlow::exit(VariableFlow::RETURN, name: $stmt->expr instanceof Variable && is_string($stmt->expr->name) ? $stmt->expr->name : null), + )); } } diff --git a/src/Analyser/StmtHandler/StaticVariableHandler.php b/src/Analyser/StmtHandler/StaticVariableHandler.php index 260efdc2b71..1a332c7064a 100644 --- a/src/Analyser/StmtHandler/StaticVariableHandler.php +++ b/src/Analyser/StmtHandler/StaticVariableHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VarAnnotationProcessor; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -58,6 +59,7 @@ public function processStmt( ]; $vars = []; + $variableFlows = []; foreach ($stmt->vars as $var) { if (!is_string($var->var->name)) { throw new ShouldNotHappenException(); @@ -65,10 +67,12 @@ public function processStmt( if ($var->default !== null) { $defaultExprResult = $nodeScopeResolver->processExprNode($stmt, $var->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); + $variableFlows[] = $defaultExprResult->getVariableFlow(); $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } $scope = $scope->enterExpressionAssign($var->var); + $variableFlows[] = VariableFlow::escape($var->var->name); $varResult = $nodeScopeResolver->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $scope->exitExpressionAssign($var->var); @@ -79,7 +83,7 @@ public function processStmt( $scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt); - return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints); + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints, variableFlow: VariableFlow::sequence(...$variableFlows)); } } diff --git a/src/Analyser/StmtHandler/SwitchHandler.php b/src/Analyser/StmtHandler/SwitchHandler.php index c9ea11a2421..4f94cce08cc 100644 --- a/src/Analyser/StmtHandler/SwitchHandler.php +++ b/src/Analyser/StmtHandler/SwitchHandler.php @@ -17,6 +17,8 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\Node\SwitchConditionArm; @@ -50,6 +52,7 @@ public function processStmt( ): InternalStatementResult { $entryScope = $scope; + $caseFlows = []; $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $scope = $condResult->getScope(); $scopeForBranches = $scope; @@ -111,6 +114,7 @@ public function processStmt( $branchScope = $branchScope->mergeWith($prevScope); $branchScopeResult = $nodeScopeResolver->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $storage, $nodeCallback, $context); + $caseFlows[] = [VariableFlowBuilder::child($caseNode->cond, $storage), $branchScopeResult->getVariableFlow(), $caseNode->cond === null]; $branchScope = $branchScopeResult->getScope(); $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints(); $hasYield = $hasYield || $branchFinalScopeResult->hasYield(); @@ -169,7 +173,7 @@ public function processStmt( $finalScope = $scopeForBranches->mergeWith($finalScope); } - return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPointsForOuterLoop, throwPoints: $throwPoints, impurePoints: $impurePoints); + return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPointsForOuterLoop, throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: VariableFlow::switch($condResult->getVariableFlow(), $caseFlows, $hasDefaultCase || $exhaustive)); } } diff --git a/src/Analyser/StmtHandler/TryCatchHandler.php b/src/Analyser/StmtHandler/TryCatchHandler.php index 0f8fad8f387..5f3426d23b8 100644 --- a/src/Analyser/StmtHandler/TryCatchHandler.php +++ b/src/Analyser/StmtHandler/TryCatchHandler.php @@ -2,6 +2,8 @@ namespace PHPStan\Analyser\StmtHandler; +use Error; +use Exception; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; @@ -9,15 +11,19 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementExitPoint; use PHPStan\Analyser\InternalStatementResult; +use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Node\ReturnAfterFinallyNode; +use PHPStan\Node\Variable\VariableWrite; use PHPStan\Node\VariableAssignNode; use PHPStan\ShouldNotHappenException; use PHPStan\Type\NeverType; @@ -51,6 +57,8 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { + $catchFlows = []; + $finallyFlow = null; $branchScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; @@ -115,6 +123,9 @@ public function processStmt( $onlyExplicitIsThrow = true; if (count($matchingThrowPoints) === 0) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { + if ($throwPoint->getType() instanceof NeverType) { + continue; + } foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) { continue; @@ -138,9 +149,15 @@ public function processStmt( } // implicit only - if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) { + // Broad catches also cover undocumented exceptions when a documented throw matches. + if ( + count($matchingThrowPoints) === 0 + || $onlyExplicitIsThrow + || $originalCatchType->isSuperTypeOf(new ObjectType(Exception::class))->yes() // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException + || $originalCatchType->isSuperTypeOf(new ObjectType(Error::class))->yes() + ) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { - if ($throwPoint->isExplicit()) { + if ($throwPoint->isExplicit() || $throwPoint->getType() instanceof NeverType) { continue; } @@ -177,6 +194,7 @@ public function processStmt( } if (count($matchingThrowPoints) === 0) { + $catchFlows[] = [$originalCatchType, $nodeScopeResolver->getVariableMentionFlow($catchNode)]; continue; } @@ -186,7 +204,12 @@ public function processStmt( $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType); if ($newThrowPoint->getType() instanceof NeverType) { - continue; + if (!$throwPoint->canContainAnyThrowable() || $originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + continue; + } + // Keep the fallback Throwable path for enclosing try blocks without + // introducing any other exception types after the documented ones were caught. + $newThrowPoint = InternalThrowPoint::createImplicit($throwPoint->getScope(), $throwPoint->getNode(), $newThrowPoint->getType()); } $newThrowPoints[] = $newThrowPoint; @@ -214,6 +237,7 @@ public function processStmt( $catchScopeResult = $nodeScopeResolver->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context); $catchScopeForFinally = $catchScopeResult->getScope(); + $catchFlows[] = [$originalCatchType, VariableFlow::sequence($catchNode->var !== null ? VariableFlowBuilder::targetWrite($catchNode->var, VariableWrite::KIND_CATCH, $catchScopeForFinally, $storage) : null, $catchScopeResult->getVariableFlow())]; $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); @@ -258,6 +282,7 @@ public function processStmt( if ($finallyScope !== null) { $originalFinallyScope = $finallyScope; $finallyResult = $nodeScopeResolver->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $storage, $nodeCallback, $context); + $finallyFlow = $finallyResult->getVariableFlow(); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); @@ -295,7 +320,7 @@ public function processStmt( $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } - return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: array_merge($throwPoints, $throwPointsForLater), impurePoints: $impurePoints); + return new InternalStatementResult($finalScope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: array_merge($throwPoints, $throwPointsForLater), impurePoints: $impurePoints, variableFlow: VariableFlow::tryCatch($branchScopeResult->getVariableFlow(), $catchFlows, $finallyFlow)); } } diff --git a/src/Analyser/StmtHandler/UnsetHandler.php b/src/Analyser/StmtHandler/UnsetHandler.php index b404aada5b3..44d90a4ab7a 100644 --- a/src/Analyser/StmtHandler/UnsetHandler.php +++ b/src/Analyser/StmtHandler/UnsetHandler.php @@ -19,6 +19,8 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; +use PHPStan\Analyser\VariableFlow; +use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\Node\Expr\ExistingArrayDimFetch; @@ -27,6 +29,7 @@ use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Type\ObjectType; use function array_merge; +use function is_string; /** * @implements StmtHandler @@ -57,9 +60,14 @@ public function processStmt( $hasYield = false; $throwPoints = []; $impurePoints = []; + $variableFlows = []; foreach ($stmt->vars as $var) { $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($scope, $var); $exprResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); + $variableFlows[] = VariableFlowBuilder::targetRead($var, $storage, true); + if ($var instanceof Expr\Variable && is_string($var->name)) { + $variableFlows[] = VariableFlow::mention($var->name); + } $scope = $exprResult->getScope(); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $var); $hasYield = $hasYield || $exprResult->hasYield(); @@ -127,7 +135,7 @@ public function processStmt( // asks about them answer from the storage $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); - return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: false, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: false, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: VariableFlow::sequence(...$variableFlows)); } } diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index 5154e905962..ec0eeeedd51 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BreaklessWhileLoopNode; use function array_merge; @@ -64,6 +65,7 @@ public function processStmt( exitPoints: [], throwPoints: $condResult->getThrowPoints(), impurePoints: $condResult->getImpurePoints(), + variableFlow: $condResult->getVariableFlow(), ); } $bodyScope = $condResult->getTruthyScope(); @@ -214,6 +216,9 @@ public function processStmt( exitPoints: $finalScopeResult->getExitPointsForOuterLoop(), throwPoints: $throwPoints, impurePoints: $impurePoints, + variableFlow: $neverIterates + ? VariableFlow::sequence($condResult->getVariableFlow(), VariableFlow::dead($finalScopeResult->getVariableFlow())) + : VariableFlow::loop($bodyCondResult->getVariableFlow(), $finalScopeResult->getVariableFlow(), null, $isIterableAtLeastOnce, !$alwaysIterates), ); } diff --git a/src/Analyser/ThrowPoint.php b/src/Analyser/ThrowPoint.php index a3a2a4808a7..8c83b983c80 100644 --- a/src/Analyser/ThrowPoint.php +++ b/src/Analyser/ThrowPoint.php @@ -38,9 +38,9 @@ public static function createExplicit(Scope $scope, Type $type, Node $node, bool /** * @param Node\Expr|Node\Stmt $node */ - public static function createImplicit(Scope $scope, Node $node): self + public static function createImplicit(Scope $scope, Node $node, ?Type $type = null): self { - return new self($scope, new ObjectType(Throwable::class), $node, explicit: false, canContainAnyThrowable: true); + return new self($scope, $type ?? new ObjectType(Throwable::class), $node, explicit: false, canContainAnyThrowable: true); } public function getScope(): Scope diff --git a/src/Analyser/VariableAccessFlow.php b/src/Analyser/VariableAccessFlow.php new file mode 100644 index 00000000000..14bba2a5d56 --- /dev/null +++ b/src/Analyser/VariableAccessFlow.php @@ -0,0 +1,22 @@ + $children + * @param list $catches + * @param list $cases + */ + public function __construct( + string $kind, + public readonly array $children = [], + public readonly ?string $name = null, + public readonly ?Type $type = null, + public readonly int $level = 1, + public readonly bool $atLeastOnce = false, + public readonly bool $canExit = true, + public readonly array $catches = [], + public readonly ?ArrowFunction $arrow = null, + public readonly array $cases = [], + public readonly bool $canRepeat = true, + public readonly bool $canContainAnyThrowable = false, + ) + { + parent::__construct($kind); + } + +} diff --git a/src/Analyser/VariableFlow.php b/src/Analyser/VariableFlow.php new file mode 100644 index 00000000000..40ebd3b60d8 --- /dev/null +++ b/src/Analyser/VariableFlow.php @@ -0,0 +1,155 @@ + $cases */ + public static function switch(?self $condition, array $cases, bool $exhaustive): self + { + return new VariableControlFlow(self::SWITCH, [$condition], canExit: !$exhaustive, cases: $cases); + } + + public static function write(VariableWrite $write, ?Type $redundantType = null): self + { + return new VariableAccessFlow(self::WRITE, $write->getVariableName(), $write, $redundantType); + } + + public static function escape(string $name): self + { + return new VariableAccessFlow(self::ESCAPE, $name); + } + + public static function mention(string $name): self + { + return new VariableAccessFlow(self::MENTION, $name); + } + + /** @param self::READ_ALL|self::MENTION_ALL|self::OPAQUE $kind */ + public static function all(string $kind): self + { + return new VariableControlFlow($kind); + } + + /** @param self::RETURN|self::BREAK|self::CONTINUE|self::STOP $kind */ + public static function exit(string $kind, int $level = 1, ?string $name = null): self + { + return new VariableControlFlow($kind, name: $name, level: $level); + } + + public static function throwing(Type $type, bool $canContinue, bool $canContainAnyThrowable = false): self + { + return new VariableControlFlow(self::THROW, type: $type, canExit: $canContinue, canContainAnyThrowable: $canContainAnyThrowable); + } + + public static function dead(?self $flow): ?self + { + return $flow === null ? null : new VariableControlFlow(self::DEAD, [$flow]); + } + + public static function loop(?self $condition, ?self $body, ?self $update, bool $atLeastOnce, bool $canExit, bool $canRepeat = true): self + { + return new VariableControlFlow(self::LOOP, [$condition, $body, $update], atLeastOnce: $atLeastOnce, canExit: $canExit, canRepeat: $canRepeat); + } + + /** @param list $catches */ + public static function tryCatch(?self $body, array $catches, ?self $finally): self + { + return new VariableControlFlow(self::TRY_CATCH, [$body, $finally], catches: $catches); + } + +} diff --git a/src/Analyser/VariableFlowBuilder.php b/src/Analyser/VariableFlowBuilder.php new file mode 100644 index 00000000000..4e76016b1d4 --- /dev/null +++ b/src/Analyser/VariableFlowBuilder.php @@ -0,0 +1,117 @@ +getNode() !== $expr && ($throw->getNode()->getStartFilePos() !== $expr->getStartFilePos() || $throw->getNode()->getEndFilePos() !== $expr->getEndFilePos())) { + continue; + } + + $throws[] = VariableFlow::throwing($throw->getType(), true, $throw->canContainAnyThrowable()); + } + return VariableFlow::sequence(...$throws); + } + + public static function arguments(Expr\CallLike $call, ArgsResult $argsResult, ExpressionResultStorage $storage): ?VariableFlow + { + $flows = []; + foreach ($call->getArgs() as $arg) { + $result = $argsResult->findArgResult($arg->value) ?? $storage->findExpressionResult($arg->value); + $flows[] = $result !== null ? $result->getVariableFlow() : null; + if (!$arg->byRef && !$argsResult->isPassedByReference($arg->value)) { + continue; + } + + $flows[] = self::escapeRoot($arg->value); + } + return VariableFlow::sequence(...$flows); + } + + public static function child(?Node $node, ExpressionResultStorage $storage): ?VariableFlow + { + if ($node instanceof Expr) { + $result = $storage->findExpressionResult($node); + return $result !== null ? $result->getVariableFlow() : null; + } + return null; + } + + public static function targetRead(Expr $target, ExpressionResultStorage $storage, bool $read): ?VariableFlow + { + if ($target instanceof Expr\Variable) { + return is_string($target->name) ? ($read ? VariableFlow::read($target->name) : null) : self::child($target->name, $storage); + } + if ($target instanceof Expr\List_ || $target instanceof Expr\Array_) { + return null; + } + if ($target instanceof Expr\ArrayDimFetch) { + return VariableFlow::sequence(self::targetRead($target->var, $storage, true), self::child($target->dim, $storage)); + } + if ($target instanceof Expr\PropertyFetch || $target instanceof Expr\NullsafePropertyFetch) { + return VariableFlow::sequence(self::child($target->var, $storage), self::child($target->name, $storage)); + } + if ($target instanceof Expr\StaticPropertyFetch) { + return VariableFlow::sequence(self::child($target->class, $storage), self::child($target->name, $storage)); + } + return self::child($target, $storage); + } + + /** @param VariableWrite::KIND_* $kind */ + public static function targetWrite(Expr $target, int $kind, MutatingScope $scope, ExpressionResultStorage $storage, ?Type $redundant = null): ?VariableFlow + { + if ($target instanceof Expr\List_ || $target instanceof Expr\Array_) { + $writes = []; + foreach ($target->items as $item) { + if ($item === null) { + continue; + } + $writes[] = VariableFlow::sequence(self::child($item->key, $storage), self::targetRead($item->value, $storage, false), self::targetWrite($item->value, VariableWrite::KIND_LIST_ITEM, $scope, $storage), $item->byRef ? self::escapeRoot($item->value) : null); + } + return VariableFlow::sequence(...$writes); + } + if ($target instanceof Expr\ArrayDimFetch) { + do { + $target = $target->var; + } while ($target instanceof Expr\ArrayDimFetch); + if (!$target instanceof Expr\Variable || !is_string($target->name) || $scope->hasVariableType($target->name)->no()) { + return null; + } + $type = $scope->getVariableType($target->name); + if (!$type->isArray()->yes() && !$type->isString()->yes()) { + return null; + } + $kind = VariableWrite::KIND_ARRAY_DIM_WRITE; + } + if (!$target instanceof Expr\Variable || !is_string($target->name)) { + return null; + } + return VariableFlow::sequence( + $kind === VariableWrite::KIND_ARRAY_DIM_WRITE ? VariableFlow::read($target->name) : null, + VariableFlow::write(new VariableWrite($target->name, $target, spl_object_id($target), $kind), $redundant), + ); + } + + public static function escapeRoot(Expr $expr): ?VariableFlow + { + while ($expr instanceof Expr\ArrayDimFetch) { + $expr = $expr->var; + } + return $expr instanceof Expr\Variable && is_string($expr->name) ? VariableFlow::escape($expr->name) : null; + } + +} diff --git a/src/Analyser/VariableFlowContext.php b/src/Analyser/VariableFlowContext.php new file mode 100644 index 00000000000..ba8c30b3ce6 --- /dev/null +++ b/src/Analyser/VariableFlowContext.php @@ -0,0 +1,28 @@ + $return + * @param list> $breaks + * @param list> $continues + * @param list}> $catches + * @param array $uncaught + */ + public function __construct( + public readonly array $return, + public readonly array $breaks = [], + public readonly array $continues = [], + public readonly array $catches = [], + public readonly array $uncaught = [], + ) + { + } + +} diff --git a/src/Analyser/VariableLivenessResolver.php b/src/Analyser/VariableLivenessResolver.php new file mode 100644 index 00000000000..e1965661246 --- /dev/null +++ b/src/Analyser/VariableLivenessResolver.php @@ -0,0 +1,293 @@ + */ + private array $writes = []; + + /** @var array */ + private array $readIds = []; + + /** @var array */ + private array $readNames = []; + + /** @var array */ + private array $mentionedNames = []; + + /** @var array */ + private array $escapedNames = []; + + /** @var array */ + private array $redundantTypes = []; + + private bool $opaque = false; + + private bool $allNamesMentioned = false; + + private bool $returnsByReference = false; + + private function __construct() + { + } + + public static function resolve(Node\FunctionLike $function, ?VariableFlow $flow): VariableWritesNode + { + $self = new self(); + $self->returnsByReference = $function->returnsByRef(); + $imports = []; + foreach ($function->getParams() as $param) { + if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { + continue; + } + if ($param->byRef || $param->flags !== 0) { + $self->escapedNames[$param->var->name] = true; + continue; + } + $imports[] = VariableFlow::write(new VariableWrite($param->var->name, $param->var, spl_object_id($param->var), VariableWrite::KIND_PARAMETER)); + } + if ($function instanceof Node\Expr\Closure) { + foreach ($function->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + if ($use->byRef) { + $self->escapedNames[$use->var->name] = true; + continue; + } + $imports[] = VariableFlow::write(new VariableWrite($use->var->name, $use->var, spl_object_id($use->var), VariableWrite::KIND_CLOSURE_USE)); + } + } + $body = VariableFlow::sequence(...[...$imports, $flow]); + $self->collect($body); + if ($self->writes !== [] && !$self->opaque) { + $self->liveBefore($body, [], new VariableFlowContext([])); + } + + return new VariableWritesNode($function, array_values($self->writes), $self->readIds, $self->readNames, $self->redundantTypes, $self->mentionedNames, $self->escapedNames, $self->opaque, $self->allNamesMentioned); + } + + private function collect(?VariableFlow $flow, bool $dead = false): void + { + if ($flow === null) { + return; + } + if ($flow instanceof VariableAccessFlow && $flow->name !== 'this' && !in_array($flow->name, Scope::SUPERGLOBAL_VARIABLES, true)) { + $this->mentionedNames[$flow->name] = true; + if ($flow->kind === VariableFlow::READ) { + $this->readNames[$flow->name] = true; + } elseif ($flow->kind === VariableFlow::ESCAPE) { + $this->escapedNames[$flow->name] = true; + } + if ($flow->write !== null) { + $id = $flow->write->getId(); + $this->writes[$id] = $flow->write; + if ($flow->type !== null) { + $this->redundantTypes[$id] = $flow->type; + } + if ($dead) { + $this->readIds[$id] = true; + } + } + } + if ($flow->kind === VariableFlow::OPAQUE) { + $this->opaque = true; + } + if (in_array($flow->kind, [VariableFlow::READ_ALL, VariableFlow::MENTION_ALL], true)) { + $this->allNamesMentioned = true; + } + if ($flow instanceof VariableAccessFlow) { + return; + } + if (!$flow instanceof VariableSequenceFlow && !$flow instanceof VariableControlFlow) { + throw new ShouldNotHappenException(); + } + foreach ($flow->children as $child) { + $this->collect($child, $dead || $flow->kind === VariableFlow::DEAD); + } + if (!$flow instanceof VariableControlFlow) { + return; + } + if ($flow->kind === VariableFlow::RETURN && $flow->name !== null && $this->returnsByReference) { + $this->escapedNames[$flow->name] = true; + } + foreach ($flow->cases as [$condition, $body]) { + $this->collect($condition, $dead); + $this->collect($body, $dead); + } + foreach ($flow->catches as [, $catch]) { + $this->collect($catch, $dead); + } + } + + /** + * @param array $next + * @return array + */ + private function liveBefore(?VariableFlow $flow, array $next, VariableFlowContext $context): array + { + if ($flow === null || $flow->kind === VariableFlow::DEAD) { + return $next; + } + if ($flow instanceof VariableAccessFlow) { + if ($flow->kind === VariableFlow::READ) { + $next[$flow->name] = true; + } elseif ($flow->write !== null) { + if (isset($next[$flow->name])) { + $this->readIds[$flow->write->getId()] = true; + } + unset($next[$flow->name]); + } + return $next; + } + if ($flow instanceof VariableSequenceFlow) { + if ($flow->kind === VariableFlow::SEQUENCE) { + foreach (array_reverse($flow->children) as $child) { + $next = $this->liveBefore($child, $next, $context); + } + return $next; + } + $names = []; + foreach ($flow->children as $child) { + $names += $this->liveBefore($child, $next, $context); + } + return $names; + } + if (!$flow instanceof VariableControlFlow) { + throw new ShouldNotHappenException(); + } + if ($flow->kind === VariableFlow::ARROW && $flow->arrow !== null) { + $outputs = $this->liveBefore($flow->children[1], [], new VariableFlowContext([])); + $names = $this->liveBefore($flow->children[0], $outputs, new VariableFlowContext($outputs, uncaught: $outputs)); + foreach ($flow->arrow->params as $param) { + if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { + continue; + } + unset($names[$param->var->name]); + } + return $next + $names; + } + if ($flow->kind === VariableFlow::LOOP) { + $head = []; + do { + $previousCount = count($head); + $update = $this->liveBefore($flow->children[2], $head, $context); + $loopContext = new VariableFlowContext($context->return, [$next, ...$context->breaks], [$update, ...$context->continues], $context->catches, $context->uncaught); + $body = $this->liveBefore($flow->children[1], $update, $loopContext); + $afterCondition = $flow->canRepeat ? ($flow->canExit ? $body + $next : $body) : $next; + $head = $this->liveBefore($flow->children[0], $afterCondition, $context); + } while (count($head) !== $previousCount); + + return $flow->atLeastOnce ? $this->liveBefore($flow->children[0], $body, $context) : $head; + } + if ($flow->kind === VariableFlow::SWITCH) { + $switchContext = new VariableFlowContext($context->return, [$next, ...$context->breaks], [$next, ...$context->continues], $context->catches, $context->uncaught); + $entries = []; + $caseNext = $next; + $unmatched = $flow->canExit ? $next : []; + for ($i = count($flow->cases) - 1; $i >= 0; $i--) { + [, $body, $default] = $flow->cases[$i]; + $caseNext = $this->liveBefore($body, $caseNext, $switchContext); + $entries[$i] = $caseNext; + if (!$default) { + continue; + } + + $unmatched = $caseNext; + } + for ($i = count($flow->cases) - 1; $i >= 0; $i--) { + [$condition, , $default] = $flow->cases[$i]; + if ($default) { + continue; + } + $unmatched = $this->liveBefore($condition, $entries[$i] + $unmatched, $context); + } + return $this->liveBefore($flow->children[0], $unmatched, $context); + } + if ($flow->kind === VariableFlow::RETURN) { + return $context->return; + } + if ($flow->kind === VariableFlow::BREAK) { + return $context->breaks[$flow->level - 1] ?? []; + } + if ($flow->kind === VariableFlow::CONTINUE) { + return $context->continues[$flow->level - 1] ?? []; + } + if ($flow->kind === VariableFlow::STOP) { + return []; + } + if ($flow->kind === VariableFlow::THROW) { + $names = $flow->canExit ? $next : []; + if ($context->catches === []) { + return $names + $context->uncaught; + } + if ($flow->type === null) { + throw new ShouldNotHappenException(); + } + if ($flow->canContainAnyThrowable) { + foreach ($context->catches as [$catchType, $destination]) { + if (!$catchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + continue; + } + $names += $destination; + break; + } + } + foreach ($context->catches as [$catchType, $destination]) { + $accepts = $catchType->isSuperTypeOf($flow->type); + if (!$accepts->no() || !$flow->type->isSuperTypeOf($catchType)->no()) { + $names += $destination; + } + if ($accepts->yes()) { + return $names; + } + } + return $names + $context->uncaught; + } + if ($flow->kind === VariableFlow::TRY_CATCH) { + $finally = $flow->children[1]; + $normal = $this->liveBefore($finally, $next, $context); + $breaks = []; + foreach ($context->breaks as $destination) { + $breaks[] = $this->liveBefore($finally, $destination, $context); + } + $continues = []; + foreach ($context->continues as $destination) { + $continues[] = $this->liveBefore($finally, $destination, $context); + } + $outerCatches = []; + foreach ($context->catches as [$type, $destination]) { + $outerCatches[] = [$type, $this->liveBefore($finally, $destination, $context)]; + } + $catchContext = new VariableFlowContext($this->liveBefore($finally, $context->return, $context), $breaks, $continues, $outerCatches, $this->liveBefore($finally, $context->uncaught, $context)); + $catches = []; + foreach ($flow->catches as [$type, $catch]) { + $catches[] = [$type, $this->liveBefore($catch, $normal, $catchContext)]; + } + return $this->liveBefore($flow->children[0], $normal, new VariableFlowContext($catchContext->return, $breaks, $continues, [...$catches, ...$outerCatches], $catchContext->uncaught)); + } + if ($flow->kind === VariableFlow::READ_ALL) { + $this->readNames += $this->mentionedNames; + return $next + $this->mentionedNames; + } + return $next; + } + +} diff --git a/src/Analyser/VariableSequenceFlow.php b/src/Analyser/VariableSequenceFlow.php new file mode 100644 index 00000000000..965aeea8430 --- /dev/null +++ b/src/Analyser/VariableSequenceFlow.php @@ -0,0 +1,17 @@ + $children + */ + public function __construct(string $kind, public readonly array $children) + { + parent::__construct($kind); + } + +} diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 96f17ea4ce6..398a78e4ab4 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -348,7 +348,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $inceptionResult->handleReturn(1, null, $this->analysisStartTime); } - return $this->runFixer($inceptionResult, $container, $onlyFiles, $input, $output, $files); + return $this->runFixer($inceptionResult, $container, $input, $output, $files); } /** @var AnalyseApplication $application */ @@ -834,7 +834,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult /** * @param string[] $files */ - private function runFixer(InceptionResult $inceptionResult, Container $container, bool $onlyFiles, InputInterface $input, OutputInterface $output, array $files): int + private function runFixer(InceptionResult $inceptionResult, Container $container, InputInterface $input, OutputInterface $output, array $files): int { $ciDetector = new CiDetector(); if ($ciDetector->isCiDetected()) { diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index f9b6f60d628..2ebe31762bd 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -168,11 +168,10 @@ public function run( $mainScript, $projectConfigFile, $input, - $output, $encoder, ); - $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $inceptionResult, $mainScript, $projectConfigFile, $input, $encoder, $output): void { + $this->monitorFileChanges($loop, function (FileMonitorResult $changes) use ($loop, $inceptionResult, $mainScript, $projectConfigFile, $input, $encoder): void { if ($this->processInProgress !== null) { $this->processInProgress->cancel(); $this->processInProgress = null; @@ -190,7 +189,6 @@ public function run( $mainScript, $projectConfigFile, $input, - $output, $encoder, ); }); @@ -430,7 +428,6 @@ private function analyse( string $mainScript, ?string $projectConfigFile, InputInterface $input, - OutputInterface $output, Encoder $phpstanFixerEncoder, ): void { diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index eccb88088a4..8bed9a8cb37 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -366,7 +366,7 @@ private function exportClassStatement(Node\Stmt $node, string $namespacedName, E $node->isPrivateSet(), $virtual, $this->exportAttributeNodes($node->attrGroups), - $this->exportPropertyHooks($node->hooks, $namespacedName, $nameScope), + $this->exportPropertyHooks($node->hooks, $nameScope), ); } @@ -438,7 +438,6 @@ private function exportAttributeNodes(array $attributeGroups): array */ private function exportPropertyHooks( array $hooks, - string $namespacedName, ExportedNameScope $nameScope, ): array { diff --git a/src/Node/Variable/VariableWrite.php b/src/Node/Variable/VariableWrite.php new file mode 100644 index 00000000000..69e2b95effb --- /dev/null +++ b/src/Node/Variable/VariableWrite.php @@ -0,0 +1,68 @@ +variableName; + } + + /** + * The target node of the write - the source of the reported line. + */ + public function getVariable(): Expr\Variable + { + return $this->variable; + } + + public function getId(): int + { + return $this->id; + } + + /** + * @return self::KIND_* + */ + public function getKind(): int + { + return $this->kind; + } + +} diff --git a/src/Node/VariableWritesNode.php b/src/Node/VariableWritesNode.php new file mode 100644 index 00000000000..38593e6b2cd --- /dev/null +++ b/src/Node/VariableWritesNode.php @@ -0,0 +1,154 @@ + $writes + * @param array $readWriteIds + * @param array $readVariableNames + * @param array $redundantWriteTypes + * @param array $referencedVariableNames + * @param array $untrackedVariableNames + */ + public function __construct( + private Node\FunctionLike $functionLike, + private array $writes, + private array $readWriteIds, + private array $readVariableNames, + private array $redundantWriteTypes, + private array $referencedVariableNames, + private array $untrackedVariableNames, + private bool $opaque, + private bool $allVariableNamesReferenced, + ) + { + parent::__construct($functionLike->getAttributes()); + } + + public function getFunctionLike(): Node\FunctionLike + { + return $this->functionLike; + } + + /** + * @return list + */ + public function getWrites(): array + { + return $this->writes; + } + + /** + * The write whose target is this exact node (a parameter's or closure + * use's variable), if it is tracked. + */ + public function getWriteForNode(Node\Expr\Variable $variable): ?VariableWrite + { + foreach ($this->writes as $write) { + if ($write->getVariable() === $variable) { + return $write; + } + } + + return null; + } + + /** + * Whether a construct that can observe every variable by name without + * reading its current value (func_get_args()) appears in the body. + */ + public function areAllVariableNamesReferenced(): bool + { + return $this->allVariableNamesReferenced; + } + + /** + * Whether some path from the write reaches a read of the written value. + */ + public function isRead(VariableWrite $write): bool + { + return isset($this->readWriteIds[$write->getId()]); + } + + /** + * Whether the variable name appears at a read site anywhere in the body, + * regardless of which writes the read observed. + */ + public function isVariableEverRead(string $variableName): bool + { + return isset($this->readVariableNames[$variableName]); + } + + /** + * The type of the assigned value when the write assigns the value the + * variable provably already has, null otherwise. + */ + public function getRedundantType(VariableWrite $write): ?Type + { + return $this->redundantWriteTypes[$write->getId()] ?? null; + } + + /** + * Whether the body mentions the variable at all: a read, a write, a + * statement naming it (global, static, a reference alias), or a construct + * that can observe every variable (eval, include, a dynamic compact() or + * $$name, func_get_args()). + */ + public function isVariableReferenced(string $variableName): bool + { + return $this->allVariableNamesReferenced + || $this->opaque + || isset($this->referencedVariableNames[$variableName]); + } + + /** + * Variables whose writes escape the body (by-ref parameters and uses, + * global/static variables, reference aliases) - every write counts as used. + */ + public function isUntracked(string $variableName): bool + { + return isset($this->untrackedVariableNames[$variableName]); + } + + /** + * The body contains a construct (goto) that defeats reaching-write tracking. + */ + public function isOpaque(): bool + { + return $this->opaque; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_VariableWritesNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index bbf8e4428e7..a2b9062093f 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -174,7 +174,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type return $this->resolveIdentifierTypeNode($typeNode, $nameScope); } elseif ($typeNode instanceof ThisTypeNode) { - return $this->resolveThisTypeNode($typeNode, $nameScope); + return $this->resolveThisTypeNode($nameScope); } elseif ($typeNode instanceof NullableTypeNode) { return $this->resolveNullableTypeNode($typeNode, $nameScope); @@ -588,7 +588,7 @@ private function tryResolvePseudoTypeClassType(IdentifierTypeNode $typeNode, Nam return null; } - private function resolveThisTypeNode(ThisTypeNode $typeNode, NameScope $nameScope): Type + private function resolveThisTypeNode(NameScope $nameScope): Type { $className = $nameScope->getClassName(); if ($className !== null) { diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index d3d1773f802..bb31fbaf615 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -100,7 +100,6 @@ public static function selectFromArgs( } $reorderedArgs = $args; - $parameters = null; $singleParametersAcceptor = null; if (count($parametersAcceptors) === 1) { if (!array_is_list($args)) { diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index f3ad411f46a..f92acf5883b 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -35,6 +35,8 @@ final class ResolvedFunctionVariantWithOriginal implements ResolvedFunctionVaria private ?Type $returnType = null; + private ?bool $hasTemplateOrLateResolvableReturnType = null; + private ?Type $phpDocReturnType = null; /** @@ -189,6 +191,13 @@ public function getReturnType(): Type public function getReturnTypeWithUnresolvedTemplateArguments(Expr $site, TemplateArgumentFrame $frame, bool $allowUnresolved): Type { + // Existing markers pass through both paths; only declared templates can + // create markers for this call or substitute its frame-specific resolutions. + $this->hasTemplateOrLateResolvableReturnType ??= $this->parametersAcceptor->getReturnType()->hasTemplateOrLateResolvableType(); + if (!$this->hasTemplateOrLateResolvableReturnType) { + return $this->getReturnType(); + } + $cached = $this->returnTypeWithUnresolvedTemplateArguments; if ($cached !== null && $cached[0]->get() === $site && $cached[1]->get() === $frame && $cached[2] === $allowUnresolved) { return $cached[3]; diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index eac1a12956e..e0d9327315a 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -84,7 +84,6 @@ private function getErrors(Scope $scope, Node\Expr\List_ $var, Expr $expr): arra continue; } - $keyExpr = null; if ($item->key === null) { $keyType = new ConstantIntegerType($i); $keyExpr = new Node\Scalar\Int_($i); diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index ca734d18673..4fb30320936 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -3,53 +3,65 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Param; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; -use PHPStan\Node\InClassMethodNode; +use PHPStan\Node\VariableWritesNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\UnusedFunctionParametersCheck; -use PHPStan\ShouldNotHappenException; -use function array_filter; -use function array_map; -use function array_values; +use PHPStan\Rules\UnusedParametersCheck; use function count; use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 1)] final class UnusedConstructorParametersRule implements Rule { - public function __construct(private UnusedFunctionParametersCheck $check) + public function __construct( + private UnusedParametersCheck $check, + #[AutowiredParameter(ref: '%featureToggles.reportPreciseLineForUnusedFunctionParameter%')] + private bool $reportExactLine, + ) { } public function getNodeType(): string { - return InClassMethodNode::class; + return VariableWritesNode::class; } public function processNode(Node $node, Scope $scope): array { - $method = $node->getMethodReflection(); - $originalNode = $node->getOriginalNode(); - if (!$method->isConstructor() || $originalNode->stmts === null) { + $originalNode = $node->getFunctionLike(); + if (!$originalNode instanceof Node\Stmt\ClassMethod) { + return []; + } + if ($originalNode->name->toLowerString() !== '__construct' || $originalNode->stmts === null) { return []; } - if (count($originalNode->params) === 0) { return []; } - if ($node->getClassReflection()->isAttributeClass()) { + $method = $scope->getFunction(); + if ($method === null) { + return []; + } + if ($node->isOpaque()) { + return []; + } + if (!$scope->isInClass()) { + return []; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection->isAttributeClass()) { return []; } - foreach ($node->getClassReflection()->getInterfaces() as $interface) { + foreach ($classReflection->getInterfaces() as $interface) { if ($interface->hasConstructor()) { return []; } @@ -57,23 +69,19 @@ public function processNode(Node $node, Scope $scope): array $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - SprintfHelper::escapeFormatString($node->getClassReflection()->getDisplayName()), + SprintfHelper::escapeFormatString($classReflection->getDisplayName()), ); - if ($node->getClassReflection()->isAnonymous()) { + if ($classReflection->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; } - return $this->check->getUnusedParameters( - $scope, - array_map(static function (Param $parameter): Variable { - if (!$parameter->var instanceof Variable) { - throw new ShouldNotHappenException(); - } - return $parameter->var; - }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), - $originalNode->stmts, + return $this->check->getUnusedParameterErrors( + $node, + $method, + $originalNode->params, $message, 'constructor.unusedParameter', + $this->reportExactLine, ); } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index b96c6f58bee..66ccc951b04 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -19,6 +19,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ObjectType; use function array_key_exists; +use function array_keys; use function array_map; use function count; use function is_string; @@ -161,7 +162,7 @@ public function processNode(Node $node, Scope $scope): array $strings = $propertyNameType->getConstantStrings(); if (count($strings) === 0) { // handle subtractions of a dynamic property fetch - foreach ($properties as $propertyName => $data) { + foreach (array_keys($properties) as $propertyName) { if ((new ConstantStringType($propertyName))->isSuperTypeOf($propertyNameType)->no()) { continue; } diff --git a/src/Rules/DeadCode/UnusedVariableRule.php b/src/Rules/DeadCode/UnusedVariableRule.php new file mode 100644 index 00000000000..cfa53957341 --- /dev/null +++ b/src/Rules/DeadCode/UnusedVariableRule.php @@ -0,0 +1,161 @@ + + */ +final class UnusedVariableRule implements Rule +{ + + public function __construct() + { + } + + public function getNodeType(): string + { + return VariableWritesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->isOpaque()) { + return []; + } + + $namesWithReadWrite = []; + foreach ($node->getWrites() as $write) { + if (!$node->isRead($write)) { + continue; + } + $namesWithReadWrite[$write->getVariableName()] = true; + } + + $errors = []; + foreach ($node->getWrites() as $write) { + $name = $write->getVariableName(); + if ($node->isUntracked($name)) { + continue; + } + if (str_starts_with($name, '_')) { + continue; + } + if (in_array($write->getKind(), [VariableWrite::KIND_PARAMETER, VariableWrite::KIND_CLOSURE_USE], true)) { + // reported by UnusedConstructorParametersRule and UnusedClosureUsesRule + continue; + } + if ( + $write->getKind() === VariableWrite::KIND_CATCH + && !$scope->getPhpVersion()->supportsNoncapturingCatches()->yes() + ) { + continue; + } + + if (!$node->isRead($write)) { + // A variable that is never read at all (an unused variable) is a stronger + // finding than a single dead store to a variable the body does read. + $unusedVariable = !isset($namesWithReadWrite[$name]) && !$node->isVariableEverRead($name); + $errors[] = RuleErrorBuilder::message($this->getMessage($write->getKind(), $name, $unusedVariable)) + ->identifier($this->getIdentifier($write->getKind(), $unusedVariable)) + ->line($write->getVariable()->getStartLine()) + ->build(); + continue; + } + + $redundantType = $node->getRedundantType($write); + if ($redundantType === null) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Variable $%s is assigned value %s but it already has that value.', + $name, + $redundantType->describe(VerbosityLevel::value()), + )) + ->identifier('assign.redundant') + ->line($write->getVariable()->getStartLine()) + ->build(); + } + + return $errors; + } + + /** + * @param VariableWrite::KIND_* $kind + */ + private function getMessage(int $kind, string $variableName, bool $unusedVariable): string + { + switch ($kind) { + case VariableWrite::KIND_ASSIGN: + case VariableWrite::KIND_READ_MODIFY_WRITE: + case VariableWrite::KIND_ARRAY_DIM_WRITE: + case VariableWrite::KIND_LIST_ITEM: + if ($unusedVariable) { + return sprintf('Variable $%s is never read.', $variableName); + } + + return sprintf('Value assigned to variable $%s is never read.', $variableName); + case VariableWrite::KIND_PRE_INC: + case VariableWrite::KIND_POST_INC: + return sprintf('Value of variable $%s after ++ is never read.', $variableName); + case VariableWrite::KIND_PRE_DEC: + case VariableWrite::KIND_POST_DEC: + return sprintf('Value of variable $%s after -- is never read.', $variableName); + case VariableWrite::KIND_FOREACH_VALUE: + return sprintf('Foreach value variable $%s is never read.', $variableName); + case VariableWrite::KIND_FOREACH_KEY: + return sprintf('Foreach key variable $%s is never read.', $variableName); + case VariableWrite::KIND_CATCH: + return sprintf('Catch variable $%s is never read.', $variableName); + } + + throw new ShouldNotHappenException(sprintf('Unhandled variable write kind %d', $kind)); + } + + /** + * @param VariableWrite::KIND_* $kind + */ + private function getIdentifier(int $kind, bool $unusedVariable): string + { + switch ($kind) { + case VariableWrite::KIND_ASSIGN: + case VariableWrite::KIND_READ_MODIFY_WRITE: + case VariableWrite::KIND_ARRAY_DIM_WRITE: + case VariableWrite::KIND_LIST_ITEM: + if ($unusedVariable) { + return 'variable.unused'; + } + + return 'assign.unused'; + case VariableWrite::KIND_PRE_INC: + return 'preInc.unused'; + case VariableWrite::KIND_POST_INC: + return 'postInc.unused'; + case VariableWrite::KIND_PRE_DEC: + return 'preDec.unused'; + case VariableWrite::KIND_POST_DEC: + return 'postDec.unused'; + case VariableWrite::KIND_FOREACH_VALUE: + return 'foreach.unusedValue'; + case VariableWrite::KIND_FOREACH_KEY: + return 'foreach.unusedKey'; + case VariableWrite::KIND_CATCH: + return 'catch.unusedVariable'; + } + + throw new ShouldNotHappenException(sprintf('Unhandled variable write kind %d', $kind)); + } + +} diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 8952bd3599f..40ad616d8ec 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -266,7 +266,7 @@ public function check( if (!$hasNamedArguments) { $invokedParametersCount = count($arguments); - foreach ($arguments as [$argumentValue, $argumentValueType, $unpack, $argumentName]) { + foreach ($arguments as [2 => $unpack]) { if ($unpack) { $invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount); break; diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index 8a4bb345b60..05e3509a4f7 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -4,41 +4,69 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Node\VariableWritesNode; use PHPStan\Rules\Rule; -use PHPStan\Rules\UnusedFunctionParametersCheck; -use function array_map; -use function count; +use PHPStan\Rules\RuleErrorBuilder; +use function is_string; +use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 1)] final class UnusedClosureUsesRule implements Rule { - public function __construct(private UnusedFunctionParametersCheck $check) + public function __construct( + #[AutowiredParameter(ref: '%featureToggles.reportPreciseLineForUnusedFunctionParameter%')] + private bool $reportExactLine, + ) { } public function getNodeType(): string { - return Node\Expr\Closure::class; + return VariableWritesNode::class; } public function processNode(Node $node, Scope $scope): array { - if (count($node->uses) === 0) { + $functionLike = $node->getFunctionLike(); + if (!$functionLike instanceof Node\Expr\Closure) { return []; } + if ($node->isOpaque()) { + return []; + } + + $errors = []; + foreach ($functionLike->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + $write = $node->getWriteForNode($use->var); + if ($write !== null) { + // a by-value use imports a value - it is unused unless that + // value is read on some path (overwriting it first is not a use) + if ($node->isRead($write) || $node->areAllVariableNamesReferenced()) { + continue; + } + } elseif ($node->isVariableReferenced($use->var->name)) { + // a by-ref use aliases the outer variable - any mention counts + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf('Anonymous function has an unused use $%s.', $use->var->name)) + ->identifier('closure.unusedUse'); + if ($this->reportExactLine) { + $errorBuilder->line($use->var->getStartLine()); + } + $errors[] = $errorBuilder->build(); + } - return $this->check->getUnusedParameters( - $scope, - array_map(static fn (Node\ClosureUse $use): Node\Expr\Variable => $use->var, $node->uses), - $node->stmts, - 'Anonymous function has an unused use $%s.', - 'closure.unusedUse', - ); + return $errors; } } diff --git a/src/Rules/Functions/UnusedFunctionParametersRule.php b/src/Rules/Functions/UnusedFunctionParametersRule.php new file mode 100644 index 00000000000..ebd8b68d082 --- /dev/null +++ b/src/Rules/Functions/UnusedFunctionParametersRule.php @@ -0,0 +1,56 @@ + + */ +final class UnusedFunctionParametersRule implements Rule +{ + + public function __construct(private UnusedParametersCheck $check) + { + } + + public function getNodeType(): string + { + return VariableWritesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getFunctionLike(); + if (!$originalNode instanceof Node\Stmt\Function_) { + return []; + } + if ($this->check->isNoOpBody($originalNode->stmts)) { + return []; + } + if (count($originalNode->params) === 0) { + return []; + } + $function = $scope->getFunction(); + if ($function === null) { + return []; + } + + return $this->check->getUnusedParameterErrors( + $node, + $function, + $originalNode->params, + sprintf('Function %s() has an unused parameter $%%s.', SprintfHelper::escapeFormatString($function->getName())), + 'function.unusedParameter', + true, + ); + } + +} diff --git a/src/Rules/Methods/UnusedMethodParametersRule.php b/src/Rules/Methods/UnusedMethodParametersRule.php new file mode 100644 index 00000000000..6444f69da0d --- /dev/null +++ b/src/Rules/Methods/UnusedMethodParametersRule.php @@ -0,0 +1,74 @@ + + */ +final class UnusedMethodParametersRule implements Rule +{ + + public function __construct(private UnusedParametersCheck $check) + { + } + + public function getNodeType(): string + { + return VariableWritesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getFunctionLike(); + if (!$originalNode instanceof Node\Stmt\ClassMethod) { + return []; + } + if (!$originalNode->isPrivate() || $originalNode->stmts === null || $this->check->isNoOpBody($originalNode->stmts)) { + return []; + } + if (str_starts_with($originalNode->name->toString(), '__')) { + // a magic method's signature is dictated by the engine, and the + // constructor has its own rule + return []; + } + if (count($originalNode->params) === 0) { + return []; + } + if (!$scope->isInClass()) { + return []; + } + $method = $scope->getFunction(); + if ($method === null) { + return []; + } + + return $this->check->getUnusedParameterErrors( + $node, + $method, + $originalNode->params, + sprintf( + 'Method %s::%s() has an unused parameter $%%s.', + SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()), + SprintfHelper::escapeFormatString($originalNode->name->toString()), + ), + 'method.unusedParameter', + true, + ); + } + +} diff --git a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php index 6885c42817a..088006c1795 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -45,8 +45,6 @@ public function processNode(Node $node, Scope $scope): array { $errors = []; foreach ($node->uses as $use) { - $error = null; - /** @var Node\Name $name */ $name = Node\Name::concat($node->prefix, $use->name, ['startLine' => $use->getStartLine()]); if ( diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php deleted file mode 100644 index a44633caa45..00000000000 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ /dev/null @@ -1,152 +0,0 @@ - - */ - public function getUnusedParameters( - Scope $scope, - array $parameterVars, - array $statements, - string $unusedParameterMessage, - string $identifier, - ): array - { - $unusedParameters = []; - foreach ($parameterVars as $variable) { - if (!is_string($variable->name)) { - throw new ShouldNotHappenException(); - } - - $unusedParameters[$variable->name] = $variable; - } - foreach ($this->getUsedVariables($scope, $statements) as $variableName) { - unset($unusedParameters[$variableName]); - } - - $errors = []; - foreach ($unusedParameters as $name => $variable) { - $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $name))->identifier($identifier); - if ($this->reportExactLine) { - $errorBuilder->line($variable->getStartLine()); - } - $errors[] = $errorBuilder->build(); - } - - return $errors; - } - - /** - * @param Node[]|Node|scalar|null $node - * @return string[] - */ - private function getUsedVariables(Scope $scope, $node): array - { - $variableNames = []; - if ($node instanceof Node) { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); - if (in_array($functionName, ['func_get_args', 'get_defined_vars'], true)) { - return $scope->getDefinedVariables(); - } - } - if ($node instanceof Node\Expr\Include_ || $node instanceof Node\Expr\Eval_) { - return $scope->getDefinedVariables(); - } - if ($node instanceof Variable) { - if (is_string($node->name)) { - if ($node->name !== 'this') { - return [$node->name]; - } - } else { - // a variable-variable's name expression: a literal is priced - // without the scope, a variable read is scope state - neither - // asks the scope about a node this function body's walk has - // not stored yet - if ($node->name instanceof Node\Scalar\String_) { - $nameType = $this->initializerExprTypeResolver->getType($node->name, InitializerExprContext::fromScope($scope)); - } elseif ($node->name instanceof Variable && is_string($node->name->name)) { - $nameType = $scope->hasVariableType($node->name->name)->no() ? new ErrorType() : $scope->getVariableType($node->name->name); - } else { - $nameType = $scope->getType($node->name); - } - if ($nameType->getConstantStrings() === []) { - return $scope->getDefinedVariables(); - } - - foreach ($nameType->getConstantStrings() as $constantString) { - $variableNames[] = $constantString->getValue(); - } - } - } - if ($node instanceof Node\ClosureUse && is_string($node->var->name)) { - return [$node->var->name]; - } - if ( - $node instanceof Node\Expr\FuncCall - && !$node->isFirstClassCallable() - && $node->name instanceof Node\Name - && (string) $node->name === 'compact' - ) { - foreach ($node->getArgs() as $arg) { - // compact('name') takes literal names - price a constant - // argument without asking the scope about a node the walk of - // this very function body has not stored yet - $argType = $arg->value instanceof Node\Scalar\String_ - ? $this->initializerExprTypeResolver->getType($arg->value, InitializerExprContext::fromScope($scope)) - : $scope->getType($arg->value); - foreach ($argType->getConstantStrings() as $constantStringType) { - $variableNames[] = $constantStringType->getValue(); - } - } - } - foreach ($node->getSubNodeNames() as $subNodeName) { - if ($node instanceof Node\Expr\Closure && $subNodeName !== 'uses') { - continue; - } - $subNode = $node->{$subNodeName}; - $variableNames = array_merge($variableNames, $this->getUsedVariables($scope, $subNode)); - } - } elseif (is_array($node)) { - foreach ($node as $subNode) { - $variableNames = array_merge($variableNames, $this->getUsedVariables($scope, $subNode)); - } - } - - return $variableNames; - } - -} diff --git a/src/Rules/UnusedParametersCheck.php b/src/Rules/UnusedParametersCheck.php new file mode 100644 index 00000000000..fdc456b6a74 --- /dev/null +++ b/src/Rules/UnusedParametersCheck.php @@ -0,0 +1,129 @@ + + */ + public function getUnusedParameterErrors( + VariableWritesNode $node, + PhpFunctionFromParserNodeReflection $function, + array $parameters, + string $unusedParameterMessage, + string $identifier, + bool $reportExactLine, + ): array + { + if ($node->isOpaque()) { + return []; + } + + $contractParameterNames = $this->getContractReferencedParameterNames($function); + $errors = []; + foreach ($parameters as $parameter) { + // a promoted parameter is a property - its value is always used + if ($parameter->flags !== 0) { + continue; + } + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + continue; + } + if (isset($contractParameterNames[$parameter->var->name])) { + continue; + } + $write = $node->getWriteForNode($parameter->var); + if ($write !== null) { + if ($node->isRead($write) || $node->areAllVariableNamesReferenced()) { + continue; + } + } elseif ($node->isVariableReferenced($parameter->var->name)) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf($unusedParameterMessage, $parameter->var->name)) + ->identifier($identifier); + if ($reportExactLine) { + $errorBuilder->line($parameter->var->getStartLine()); + } + $errors[] = $errorBuilder->build(); + } + + return $errors; + } + + /** + * Parameters the signature's PhpDoc contract refers to - the subject of a + * phpstan-assert tag, or a parameter a conditional type in the signature + * switches on. They serve the call site even when the body ignores them. + * + * @return array + */ + private function getContractReferencedParameterNames(PhpFunctionFromParserNodeReflection $function): array + { + $names = []; + foreach ($function->getAsserts()->getAll() as $assert) { + $names[ltrim($assert->getParameter()->getParameterName(), '$')] = true; + } + $variant = $function->getOnlyVariant(); + $typesToScan = [$variant->getReturnType()]; + foreach ($variant->getParameters() as $parameterReflection) { + $typesToScan[] = $parameterReflection->getType(); + } + foreach ($typesToScan as $typeToScan) { + TypeTraverser::map($typeToScan, static function (Type $type, callable $traverse) use (&$names): Type { + if ($type instanceof ConditionalTypeForParameter) { + $names[ltrim($type->getParameterName(), '$')] = true; + } + + return $traverse($type); + }); + } + + return $names; + } + + /** + * A body with no real statements - empty, or only comments (a comment + * parses into a Nop statement) - is a deliberate no-op stub; its + * parameters exist to be ignored. + * + * @param Stmt[] $stmts + */ + public function isNoOpBody(array $stmts): bool + { + foreach ($stmts as $stmt) { + if (!$stmt instanceof Stmt\Nop) { + return false; + } + } + + return true; + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index ed9140b53dd..3888ef15c45 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -22,6 +22,7 @@ use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\DependencyInjection\DirectExtensionsCollection; +use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Fixable\Patcher; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; @@ -92,6 +93,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getByType(TemplateArgumentObserver::class), self::getContainer()->getByType(TemplateArgumentResolver::class), $reflectionProvider, + self::getContainer()->getByType(FileHelper::class), self::getContainer()->getExtensionsCollection(FunctionParameterOutTypeExtension::class), self::getContainer()->getExtensionsCollection(MethodParameterOutTypeExtension::class), self::getContainer()->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 72d8e567694..77203859e35 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -68,6 +68,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getByType(TemplateArgumentObserver::class), $container->getByType(TemplateArgumentResolver::class), $reflectionProvider, + $container->getByType(FileHelper::class), $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), diff --git a/src/Testing/functions.php b/src/Testing/functions.php index 241d78d31fe..1d677b906ae 100644 --- a/src/Testing/functions.php +++ b/src/Testing/functions.php @@ -13,6 +13,8 @@ * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function assertType(string $type, $value) // phpcs:ignore { return null; @@ -30,6 +32,8 @@ function assertType(string $type, $value) // phpcs:ignore * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function assertNativeType(string $type, $value) // phpcs:ignore { return null; @@ -44,6 +48,8 @@ function assertNativeType(string $type, $value) // phpcs:ignore * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function assertSuperType(string $superType, $value) // phpcs:ignore { return null; @@ -56,6 +62,8 @@ function assertSuperType(string $superType, $value) // phpcs:ignore * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function assertVariableCertainty(TrinaryLogic $certainty, $variable) // phpcs:ignore { return null; diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 54b3d50b065..2c6ffe08164 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -23,7 +23,7 @@ final class TurboExtensionEnabler * version is the short SHA of the last commit touching turbo-ext/src/, * enforced by the phar.yml turbo-version job. */ - public const EXPECTED_EXTENSION_VERSION = 'b48812e'; + public const EXPECTED_EXTENSION_VERSION = 'b1c223b'; private static bool $typeCombinatorCacheEnabled = false; diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 709ed1fb610..530ebbd3265 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\TrinaryLogic; use function array_key_exists; +use function array_keys; use function array_merge; use function count; use function sprintf; @@ -33,7 +34,7 @@ public static function isParametersAcceptorSuperTypeOf( && $lastParameter->isVariadic() && $theirParameterCount < $ourParameterCount ) { - foreach ($ourParameters as $i => $ourParameter) { + foreach (array_keys($ourParameters) as $i) { if (array_key_exists($i, $theirParameters)) { continue; } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 5f95b0d6ce0..fcb35f63c85 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2866,7 +2866,7 @@ public function isKeysSupersetOf(self $otherArray): bool $otherHasExtras = $otherArray->isUnsealed()->yes(); $otherHasRequiredKeys = false; - foreach ($otherArray->keyTypes as $j => $keyType) { + foreach (array_keys($otherArray->keyTypes) as $j) { if ($otherArray->isOptionalKey($j)) { continue; } @@ -2878,7 +2878,7 @@ public function isKeysSupersetOf(self $otherArray): bool // already accepts []. i.e., all of $this's known keys are optional. Otherwise // merge would add [] as a new instance. if (!$otherHasRequiredKeys && !$otherHasExtras && count($otherArray->keyTypes) === 0) { - foreach ($this->keyTypes as $i => $keyType) { + foreach (array_keys($this->keyTypes) as $i) { if (!$this->isOptionalKey($i)) { return false; } @@ -3028,7 +3028,6 @@ public function mergeWith(self $otherArray): self $keyTypes = []; $valueTypes = []; $optionalKeys = []; - $nextAutoIndexes = [0]; $otherKeyIndexMap = $otherArray->getKeyIndexMap(); $processed = []; diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 205de4f63e2..d5d15a26abf 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -71,11 +71,9 @@ public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { // @phpstan-ignore booleanAnd.alwaysFalse, instanceof.alwaysFalse, booleanAnd.alwaysFalse, instanceof.alwaysFalse, instanceof.alwaysTrue - if ($this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType) { - $boundDescription = ''; - } else { - $boundDescription = sprintf(' of %s', $this->bound->describe($level)); - } + $boundDescription = $this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType + ? '' + : sprintf(' of %s', $this->bound->describe($level)); $defaultDescription = ''; if ($this->default !== null) { $recursionGuard = RecursionGuard::runOnObjectIdentity($this->default, fn () => $this->default->describe($level)); diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index b82ce7bb971..0889971e490 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -181,7 +181,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]; } } else { - foreach ($offsetTypes as $key => [$hasOffsetValue, $offsetValueType]) { + foreach ($offsetTypes as $key => [$hasOffsetValue]) { // more precise values-types will be calculated elsewhere. // just remember the offset key. $offsetTypes[$key] = [ diff --git a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php index 834175222a2..642894bf872 100644 --- a/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php @@ -170,7 +170,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]; } } else { - foreach ($offsetTypes as $key => [$hasOffsetValue, $offsetValueType]) { + foreach ($offsetTypes as $key => [$hasOffsetValue]) { // more precise values-types will be calculated elsewhere. // just remember the offset key. $offsetTypes[$key] = [ diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 971f593bfc8..da3cc191234 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; use function count; #[AutowiredService] @@ -40,7 +41,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($constantStrings as $constantString) { try { new DateInterval($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore + } catch (Throwable) { return $this->exceptionType(); } diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 2facd039453..45cdb606a3a 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -14,6 +14,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; use function count; use function in_array; @@ -42,7 +43,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($constantStrings as $constantString) { try { new DateTime($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore + } catch (Throwable) { return $this->exceptionType(); } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index 02c0099c4ee..f3b91b8b358 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -14,6 +14,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; use function count; use function in_array; @@ -47,7 +48,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M try { $dateTime = new DateTime(); $dateTime->modify($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore + } catch (Throwable) { return $this->exceptionType(); } diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php index 0c4c0bd9dd9..d58e183886a 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; use function count; #[AutowiredService] @@ -40,7 +41,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($constantStrings as $constantString) { try { new DateTimeZone($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore + } catch (Throwable) { return $this->exceptionType(); } diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index 78faf2d1d33..018f9be27ca 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use SimpleXMLElement; +use Throwable; use function count; use function extension_loaded; use function libxml_use_internal_errors; @@ -41,7 +42,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect foreach ($constantStrings as $constantString) { try { new SimpleXMLElement($constantString->getValue()); - } catch (\Exception $e) { // phpcs:ignore + } catch (Throwable) { return $methodReflection->getThrowType(); } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 39ce5efa661..b677932e8e7 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -2043,7 +2043,7 @@ public static function doIntersect(Type ...$types): Type if ($constArrayIsI) { $types[$i] = $newArrayType; - array_splice($types, $j--, 1); + array_splice($types, $j, 1); } else { $types[$j] = $newArrayType; array_splice($types, $i--, 1); diff --git a/src/dumpType.php b/src/dumpType.php index 5fe276ceaff..943ca7e124c 100644 --- a/src/dumpType.php +++ b/src/dumpType.php @@ -10,6 +10,8 @@ * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function dumpType($value, ...$values) // phpcs:ignore Squiz.Functions.GlobalFunction.Found { return null; @@ -23,6 +25,8 @@ function dumpType($value, ...$values) // phpcs:ignore Squiz.Functions.GlobalFunc * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function dumpNativeType($value, ...$values) // phpcs:ignore Squiz.Functions.GlobalFunction.Found { return null; @@ -36,6 +40,8 @@ function dumpNativeType($value, ...$values) // phpcs:ignore Squiz.Functions.Glob * * @throws void */ +// the parameters are consumed by the analyser, not by the function body +// @phpstan-ignore function.unusedParameter, function.unusedParameter function dumpPhpDocType($value, ...$values) // phpcs:ignore Squiz.Functions.GlobalFunction.Found { return null; diff --git a/stubs/ReflectionParameter.stub b/stubs/ReflectionParameter.stub index 12e7c1467eb..b3a1c885840 100644 --- a/stubs/ReflectionParameter.stub +++ b/stubs/ReflectionParameter.stub @@ -10,10 +10,19 @@ class ReflectionParameter } /** + * @return bool * @phpstan-assert-if-true !null $this->getType() */ public function hasType(): bool { } + /** + * @return bool + * @throws void + */ + public function isOptional(): bool + { + } + } diff --git a/stubs/ReflectionType.stub b/stubs/ReflectionType.stub new file mode 100644 index 00000000000..a2e66a6ced2 --- /dev/null +++ b/stubs/ReflectionType.stub @@ -0,0 +1,13 @@ +runAnalyse(__DIR__ . '/data/undefined-variable-assign.php'); - $this->assertCount(2, $errors); + $this->assertCount(3, $errors); $error = $errors[0]; $this->assertSame('Undefined variable: $bar', $error->getMessage()); $this->assertSame(3, $error->getLine()); $error = $errors[1]; + $this->assertSame('Variable $foo is never read.', $error->getMessage()); + $this->assertSame(3, $error->getLine()); + + $error = $errors[2]; $this->assertSame('Variable $foo might not be defined.', $error->getMessage()); $this->assertSame(6, $error->getLine()); } @@ -57,7 +61,11 @@ public function testMissingFunctionErrorAboutMisconfiguredAutoloader(): void public function testAnonymousClassWithInheritedConstructor(): void { $errors = $this->runAnalyse(__DIR__ . '/data/anonymous-class-with-inherited-constructor.php'); - $this->assertNoErrors($errors); + $this->assertCount(2, $errors); + $this->assertSame('Variable $a is never read.', $errors[0]->getMessage()); + $this->assertSame(17, $errors[0]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[1]->getMessage()); + $this->assertSame(33, $errors[1]->getLine()); } public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): void @@ -167,14 +175,20 @@ public function testExtendsPdoStatementCrash(): void public function testBug14548(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-14548.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Foreach value variable $priorityName is never read.', $errors[0]->getMessage()); + $this->assertSame(18, $errors[0]->getLine()); } public function testBug12803(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-12803.php'); - $this->assertNoErrors($errors); + $this->assertCount(2, $errors); + $this->assertSame('Variable $a is never read.', $errors[0]->getMessage()); + $this->assertSame(14, $errors[0]->getLine()); + $this->assertSame('Variable $b is never read.', $errors[1]->getMessage()); + $this->assertSame(15, $errors[1]->getLine()); } public function testArrayDestructuringArrayDimFetch(): void @@ -225,16 +239,22 @@ public function testBug14604(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-14604.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $locations is never read.', $errors[0]->getMessage()); + $this->assertSame(17, $errors[0]->getLine()); } public function testBug13424(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-13424.php'); - $this->assertCount(1, $errors); - $this->assertSame('Instantiated class Bug13424\Hello not found.', $errors[0]->getMessage()); - $this->assertSame(14, $errors[0]->getLine()); + $this->assertCount(3, $errors); + $this->assertSame('Variable $hello is never read.', $errors[0]->getMessage()); + $this->assertSame(10, $errors[0]->getLine()); + $this->assertSame('Instantiated class Bug13424\Hello not found.', $errors[1]->getMessage()); + $this->assertSame(14, $errors[1]->getLine()); + $this->assertSame('Variable $hello is never read.', $errors[2]->getMessage()); + $this->assertSame(14, $errors[2]->getLine()); } public function testTwoSameClassesInSingleFile(): void @@ -290,7 +310,9 @@ public function testBug3468(): void { // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-3468.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $element is never read.', $errors[0]->getMessage()); + $this->assertSame(18, $errors[0]->getLine()); } public function testBug3379(): void @@ -363,7 +385,11 @@ public function testBug3769(): void // false positive require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; $errors = $this->runAnalyse(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); - $this->assertNoErrors($errors); + $this->assertCount(11, $errors); + foreach ([13, 29, 30, 31, 40, 75, 76, 77, 78, 108, 111] as $i => $line) { + $this->assertSame('Variable $a is never read.', $errors[$i]->getMessage()); + $this->assertSame($line, $errors[$i]->getLine()); + } } public function testBug6301(): void @@ -400,8 +426,10 @@ public function testBug4713(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-4713.php'); - $this->assertCount(1, $errors); + $this->assertCount(2, $errors); $this->assertSame('Method Bug4713\Service::createInstance() should return Bug4713\Service but returns object.', $errors[0]->getMessage()); + $this->assertSame('Variable $service is never read.', $errors[1]->getMessage()); + $this->assertSame(14, $errors[1]->getLine()); $reflectionProvider = self::createReflectionProvider(); $class = $reflectionProvider->getClass(Service::class); @@ -586,10 +614,14 @@ public function testBug6253(): void [ __DIR__ . '/data/bug-6253.php', __DIR__ . '/data/bug-6253-app-scope-trait.php', - __DIR__ . '/data/bug-6253-collection-trait.php', + // deliberately unnormalized: setAnalysedFiles() must normalize it, or the + // trait is silently not analysed in class context (mixed separators on Windows) + __DIR__ . '/data/../data/bug-6253-collection-trait.php', ], ); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $c is never read.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); } public function testBug13057(): void @@ -713,11 +745,21 @@ public function testBug6160(): void { // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-6160.php'); - $this->assertCount(2, $errors); + $this->assertCount(7, $errors); $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, 94561 given.', $errors[0]->getMessage()); $this->assertSame(19, $errors[0]->getLine()); - $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, \'sdf\' given.', $errors[1]->getMessage()); - $this->assertSame(23, $errors[1]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[1]->getMessage()); + $this->assertSame(19, $errors[1]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[2]->getMessage()); + $this->assertSame(20, $errors[2]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[3]->getMessage()); + $this->assertSame(21, $errors[3]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[4]->getMessage()); + $this->assertSame(22, $errors[4]->getLine()); + $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, \'sdf\' given.', $errors[5]->getMessage()); + $this->assertSame(23, $errors[5]->getLine()); + $this->assertSame('Variable $a is never read.', $errors[6]->getMessage()); + $this->assertSame(23, $errors[6]->getLine()); } public function testBug6979(): void @@ -739,9 +781,10 @@ public function testBug7030(): void #[RequiresPhp('>= 8.1.0')] public function testBug7012(): void { - // false positive $errors = $this->runAnalyse(__DIR__ . '/data/bug-7012.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Function Bug7012\test() has an unused parameter $f.', $errors[0]->getMessage()); + $this->assertSame(10, $errors[0]->getLine()); } #[RequiresPhp('>= 8.1.0')] @@ -904,9 +947,12 @@ public function testBug7381(): void public function testBug7153(): void { - // false negative $errors = $this->runAnalyse(__DIR__ . '/nsrt/bug-7153.php'); - $this->assertNoErrors($errors); + $this->assertCount(2, $errors); + $this->assertSame('Function Bug7153\blih() has an unused parameter $blah.', $errors[0]->getMessage()); + $this->assertSame(18, $errors[0]->getLine()); + $this->assertSame('Function Bug7153\blih() has an unused parameter $bleh.', $errors[1]->getMessage()); + $this->assertSame(18, $errors[1]->getLine()); } public function testBug7275(): void @@ -929,9 +975,9 @@ public function testBug12767(): void $errors = $this->runAnalyse(__DIR__ . '/data/bug-12767.php'); $this->assertCount(3, $errors); - $this->assertSame('Expected type int, actual: *ERROR*', $errors[0]->getMessage()); - $this->assertSame('Undefined variable: $field1', $errors[1]->getMessage()); - $this->assertSame('Undefined variable: $field2', $errors[2]->getMessage()); + $this->assertSame('Expected type int, actual: int<1, max>', $errors[0]->getMessage()); + $this->assertSame('Variable $field1 might not be defined.', $errors[1]->getMessage()); + $this->assertSame('Variable $field2 might not be defined.', $errors[2]->getMessage()); } public function testBug7554(): void @@ -986,7 +1032,15 @@ public function testBug7918(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-7918.php'); - $this->assertNoErrors($errors); + $this->assertCount(4, $errors); + $this->assertSame('Foreach key variable $id is never read.', $errors[0]->getMessage()); + $this->assertSame(33, $errors[0]->getLine()); + $this->assertSame('Foreach value variable $arr2 is never read.', $errors[1]->getMessage()); + $this->assertSame(33, $errors[1]->getLine()); + $this->assertSame('Foreach key variable $id is never read.', $errors[2]->getMessage()); + $this->assertSame(91, $errors[2]->getLine()); + $this->assertSame('Foreach value variable $arr2 is never read.', $errors[3]->getMessage()); + $this->assertSame(91, $errors[3]->getLine()); } public function testArrayUnion(): void @@ -1014,7 +1068,9 @@ public function testBug8078(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-8078.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $closure is never read.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); } #[RequiresPhp('>= 8.1.0')] @@ -1101,7 +1157,13 @@ public function testBug12934(): void public function testPr2030(): void { $errors = $this->runAnalyse(__DIR__ . '/data/pr-2030.php'); - $this->assertNoErrors($errors); + $this->assertCount(3, $errors); + $this->assertSame('Foreach key variable $index is never read.', $errors[0]->getMessage()); + $this->assertSame(24, $errors[0]->getLine()); + $this->assertSame('Variable $noteTitle is never read.', $errors[1]->getMessage()); + $this->assertSame(25, $errors[1]->getLine()); + $this->assertSame('Variable $noteSource is never read.', $errors[2]->getMessage()); + $this->assertSame(26, $errors[2]->getLine()); } #[RequiresPhp('>= 8.0.0')] @@ -1224,7 +1286,9 @@ public function testBug13492(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-13492.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $customer is never read.', $errors[0]->getMessage()); + $this->assertSame(56, $errors[0]->getLine()); } #[RequiresPhp('>= 8.0.0')] @@ -1352,7 +1416,9 @@ public function testBug11026(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-11026.php'); - $this->assertNoErrors($errors); + $this->assertCount(1, $errors); + $this->assertSame('Variable $a is never read.', $errors[0]->getMessage()); + $this->assertSame(6, $errors[0]->getLine()); } public function testBug10867(): void @@ -1406,7 +1472,11 @@ public function testBug11598(): void { // false negative $errors = $this->runAnalyse(__DIR__ . '/data/bug-11598.php'); - $this->assertNoErrors($errors); + $this->assertCount(2, $errors); + $this->assertSame('Variable $foo is never read.', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Variable $foo is never read.', $errors[1]->getMessage()); + $this->assertSame(14, $errors[1]->getLine()); } public function testBug11640(): void diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index eec4d18bb64..a9fe9e9b25c 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -864,6 +864,7 @@ private function createAnalyser(): Analyser $container->getByType(TemplateArgumentObserver::class), $container->getByType(TemplateArgumentResolver::class), $reflectionProvider, + $fileHelper, $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index eb5fbc4d67a..1f5c87dd098 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -41,7 +41,7 @@ public function testMethodDoesNotExist(): void __DIR__ . '/traits/Bar.php', __DIR__ . '/traits/FooTrait.php', ]); - $this->assertCount(1, $errors); + $this->assertCount(2, $errors); $error = $errors[0]; $this->assertSame('Call to an undefined method AnalyseTraits\Bar::doFoo().', $error->getMessage()); $this->assertSame( @@ -49,6 +49,14 @@ public function testMethodDoesNotExist(): void $error->getFile(), ); $this->assertSame(10, $error->getLine()); + + $error = $errors[1]; + $this->assertSame('Variable $r is never read.', $error->getMessage()); + $this->assertSame( + sprintf('%s (in context of class AnalyseTraits\Bar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), + $error->getFile(), + ); + $this->assertSame(15, $error->getLine()); } public function testNestedTraits(): void @@ -58,7 +66,7 @@ public function testNestedTraits(): void __DIR__ . '/traits/NestedFooTrait.php', __DIR__ . '/traits/FooTrait.php', ]); - $this->assertCount(2, $errors); + $this->assertCount(3, $errors); $firstError = $errors[0]; $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doFoo().', $firstError->getMessage()); $this->assertSame( @@ -67,7 +75,15 @@ public function testNestedTraits(): void ); $this->assertSame(10, $firstError->getLine()); - $secondError = $errors[1]; + $unusedError = $errors[1]; + $this->assertSame('Variable $r is never read.', $unusedError->getMessage()); + $this->assertSame( + sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), + $unusedError->getFile(), + ); + $this->assertSame(15, $unusedError->getLine()); + + $secondError = $errors[2]; $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doNestedFoo().', $secondError->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/NestedFooTrait.php')), diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index 4da555b362f..1495be9f24d 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; use PHPStan\Parser\Parser; use PHPStan\ShouldNotHappenException; @@ -10,6 +12,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; use PHPUnit\Framework\Attributes\DataProvider; use function count; use function get_class; @@ -216,4 +219,117 @@ static function (): void { $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); } + public function testFinalizePreservesPreliminaryResult(): void + { + $scope = self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create('test.php')); + $expr = new Variable('value'); + $type = new IntegerType(); + $result = self::getContainer()->getByType(ExpressionResultFactory::class)->create( + $scope, + $scope, + $expr, + false, + false, + [], + [], + static fn (): IntegerType => $type, + SpecifiedTypes::emptySpecifyCallback(), + ); + $finalScope = $scope->assignVariable('value', $type, $type, TrinaryLogic::createYes()); + $throwPoints = [InternalThrowPoint::createImplicit($finalScope, $expr)]; + $flow = VariableFlow::read('value'); + $final = $result->finalize($finalScope, true, true, $throwPoints, [], $flow); + + $this->assertNotSame($result, $final); + $this->assertSame($scope, $result->getScope()); + $this->assertFalse($result->hasYield()); + $this->assertFalse($result->isAlwaysTerminating()); + $this->assertSame([], $result->getThrowPoints()); + $this->assertNull($result->getVariableFlow()); + $this->assertSame($finalScope, $final->getScope()); + $this->assertTrue($final->hasYield()); + $this->assertTrue($final->isAlwaysTerminating()); + $this->assertSame($throwPoints, $final->getThrowPoints()); + $this->assertSame($flow, $final->getVariableFlow()); + $this->assertSame($type, $final->getType()); + } + + /** @return iterable */ + public static function dataCopiesPreserveMemoizedAnswers(): iterable + { + foreach (['finalize', 'withScope', 'atAskPosition', 'onNonNullabilityDevicedScopes'] as $method) { + foreach ([0, 1, 2, 3] as $resolved) { + yield [$method, $resolved]; + } + } + } + + #[DataProvider('dataCopiesPreserveMemoizedAnswers')] + public function testCopiesPreserveMemoizedAnswers(string $method, int $resolved): void + { + $scope = self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create('test.php')); + $type = new IntegerType(); + $nativeType = new MixedType(); + $calls = new class { + + public int $types = 0; + + public int $narrowing = 0; + + }; + $result = self::getContainer()->getByType(ExpressionResultFactory::class)->create( + $scope, + $scope, + new Int_(1), + false, + false, + [], + [], + static function (bool $native) use ($calls, $type, $nativeType): Type { + $calls->types++; + return $native ? $nativeType : $type; + }, + static function () use ($calls): SpecifiedTypes { + $calls->narrowing++; + return new SpecifiedTypes([], []); + }, + ); + if (($resolved & 1) !== 0) { + $this->assertSame($type, $result->getType()); + } + if (($resolved & 2) !== 0) { + $this->assertSame($nativeType, $result->getNativeType()); + } + $context = TypeSpecifierContext::createTruthy(); + $specifiedTypes = $result->getSpecifiedTypes($context); + $newScope = $scope->assignVariable('other', $type, $type, TrinaryLogic::createYes()); + switch ($method) { + case 'finalize': + $copy = $result->finalize($newScope, false, false, [], [], null); + break; + case 'withScope': + $copy = $result->withScope($newScope); + break; + case 'atAskPosition': + $copy = $result->atAskPosition($newScope); + break; + case 'onNonNullabilityDevicedScopes': + $copy = $result->onNonNullabilityDevicedScopes($newScope, $newScope); + break; + default: + throw new ShouldNotHappenException(); + } + + $this->assertNotSame($result, $copy); + $this->assertSame($scope, $result->getScope()); + $this->assertSame($newScope, $copy->getScope()); + $this->assertSame($type, $copy->getType()); + $this->assertSame($nativeType, $copy->getNativeType()); + $this->assertSame($type, $copy->getKeepVoidType(false)); + $this->assertSame($nativeType, $copy->getKeepVoidType(true)); + $this->assertSame($specifiedTypes, $copy->getSpecifiedTypes($context)); + $this->assertSame(2, $calls->types); + $this->assertSame(1, $calls->narrowing); + } + } diff --git a/tests/PHPStan/Analyser/Generics/TemplateArgumentResolverTest.php b/tests/PHPStan/Analyser/Generics/TemplateArgumentResolverTest.php index 02babade308..eb6e2d2ae33 100644 --- a/tests/PHPStan/Analyser/Generics/TemplateArgumentResolverTest.php +++ b/tests/PHPStan/Analyser/Generics/TemplateArgumentResolverTest.php @@ -284,6 +284,29 @@ public function testReturnTypeCacheDistinguishesImmutableResolutions(): void $this->assertNull($siteReference->get(), 'The resolved return-type cache must not retain the call AST.'); } + public function testConcreteReturnTypeKeepsExistingMarkers(): void + { + [$constraints, $site, $type] = self::constraintsWithA(new ConstantIntegerType(1)); + $variant = new ResolvedFunctionVariantWithOriginal( + new ExtendedFunctionVariant(TemplateTypeMap::createEmpty(), null, [], false, $type, $type, new MixedType()), + TemplateTypeMap::createEmpty(), + TemplateTypeVarianceMap::createEmpty(), + [], + ); + $observer = new TemplateArgumentObserver(); + $resolved = (new TemplateArgumentResolver())->resolve($constraints->merge($observer->collectSend(new GenericObjectType(A\A::class, [new IntegerType()]), $type)), null, []); + self::assertFalse($type->hasTemplateOrLateResolvableType()); + + foreach ([new TemplateArgumentFrame(null), $resolved] as $frame) { + foreach ([true, false] as $allowUnresolved) { + $result = $variant->getReturnTypeWithUnresolvedTemplateArguments($site, $frame, $allowUnresolved); + self::assertTrue($type->equals($result)); + $observed = (new TemplateArgumentResolver())->resolve($observer->collectSites($result), null, []); + self::assertSame('1', self::describe($observed->resolve($site, 'T'))); + } + } + } + public function testSiteAttributionByTokenPosition(): void { $constraints = TemplateArgumentConstraints::createEmpty(); diff --git a/tests/PHPStan/Analyser/NodeCallbackScopeResolverRuleTest.php b/tests/PHPStan/Analyser/NodeCallbackScopeResolverRuleTest.php index 2b6c7708fc5..770a98375d2 100644 --- a/tests/PHPStan/Analyser/NodeCallbackScopeResolverRuleTest.php +++ b/tests/PHPStan/Analyser/NodeCallbackScopeResolverRuleTest.php @@ -55,8 +55,8 @@ static function (Node $node, Scope $scope) { return []; } + $scope->getType($node->getArgs()[0]->value); // on purpose to hit the cache $arg0 = $scope->getType($node->getArgs()[0]->value); - $arg0 = $scope->getType($node->getArgs()[0]->value); // on purpose to hit the cache return [ RuleErrorBuilder::message($arg0->describe(VerbosityLevel::precise()))->identifier('fnsr.rule')->build(), diff --git a/tests/PHPStan/Analyser/ReflectionParameterPhp7Test.php b/tests/PHPStan/Analyser/ReflectionParameterPhp7Test.php new file mode 100644 index 00000000000..a16b513ed8b --- /dev/null +++ b/tests/PHPStan/Analyser/ReflectionParameterPhp7Test.php @@ -0,0 +1,32 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 846c9fe15af..ca57edd2108 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1389,7 +1389,7 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array { $typesDescription = []; - foreach ($specifiedTypes->getSureTypes() as $exprString => [$exprNode, $exprType]) { + foreach ($specifiedTypes->getSureTypes() as $exprString => [1 => $exprType]) { $typesDescription[$exprString][] = $exprType->describe(VerbosityLevel::precise()); } @@ -1405,7 +1405,7 @@ private function toReadableResult(SpecifiedTypes $specifiedTypes): array $typesDescription[$exprString][] = TypeCombinator::union(...$parts)->describe(VerbosityLevel::precise()); } - foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$exprNode, $exprType]) { + foreach ($specifiedTypes->getSureNotTypes() as $exprString => [1 => $exprType]) { $typesDescription[$exprString][] = '~' . $exprType->describe(VerbosityLevel::precise()); } diff --git a/tests/PHPStan/Analyser/nsrt/array-shift-while-count-loop.php b/tests/PHPStan/Analyser/nsrt/array-shift-while-count-loop.php new file mode 100644 index 00000000000..17b09032d65 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-shift-while-count-loop.php @@ -0,0 +1,34 @@ + $items */ + public function run(array $items): void + { + foreach ([1, 2] as $_) { + $toCurrent = 0; + if ($items !== []) { + $toCurrent = count($items); + } + + while ($toCurrent > 0) { + // array_shift() empties $items across iterations: the loop must + // not keep the guard's non-emptiness, so the shifted item is nullable + assertType('list', $items); + $item = array_shift($items); + assertType('ArrayShiftWhileCountLoop\Item|null', $item); + $toCurrent--; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/broad-catch-implicit-throws.php b/tests/PHPStan/Analyser/nsrt/broad-catch-implicit-throws.php new file mode 100644 index 00000000000..69cf2484219 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/broad-catch-implicit-throws.php @@ -0,0 +1,50 @@ +getMethod('set' . ucfirst($propertyName)); + $reflectionParams = $reflectionMethod->getParameters(); + if (isset($reflectionParams[0])) { + $paramType = $reflectionParams[0]->getType(); + if (($paramType !== null) && $reflectionParams[0]->isOptional()) { + $paramType = '?' . $paramType; + } + } + + if ($paramType !== null) { + $paramType = (string) $paramType; + } + } catch (\Exception $e) { + } + + assertType('string|null', $paramType); +} + +function stringifyType(\ReflectionType $type): void +{ + $string = null; + try { + $string = (string) $type; + } catch (\Exception $e) { + } + + assertType('string', $string); +} + +function checkOptional(\ReflectionParameter $parameter): void +{ + $optional = null; + try { + $optional = $parameter->isOptional(); + } catch (\Exception $e) { + } + + assertType('bool', $optional); +} + +function checkHasType(\ReflectionParameter $parameter): void +{ + assertType('bool', $parameter->hasType()); +} diff --git a/tests/PHPStan/Analyser/nsrt/variable-variable-assign.php b/tests/PHPStan/Analyser/nsrt/variable-variable-assign.php new file mode 100644 index 00000000000..a6fda38e434 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/variable-variable-assign.php @@ -0,0 +1,51 @@ +runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon', 'json', $baselineFile); + $this->runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon', 'json', $baselineFile); $output = $this->runPhpStan(__DIR__ . '/data/', $baselineFile); @unlink($baselineFile); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index c9463f9675a..66eef36fbde 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -378,7 +378,7 @@ public function testOutputOrdering(array $errors): void ], ], ], Neon::BLOCK)), - $f = trim($this->getOutputContent()), + trim($this->getOutputContent()), ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index 8396630f7c6..bf31b347be7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -466,7 +466,6 @@ public function testErrorLimit( $this->fail('showAllErrors cannot be true when errorsBudget is set'); } putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1'); - $errorsBudget = null; } else { putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS'); } diff --git a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php index 81f9485fe46..03d6357703d 100644 --- a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php +++ b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php @@ -6,7 +6,6 @@ use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; use RuntimeException; -use Throwable; final class BleedingEdgeToggleTest extends TestCase { @@ -56,17 +55,15 @@ public function testRestoresPreviousValueWhenCallbackThrows(): void { BleedingEdgeToggle::setBleedingEdge(false); - $thrown = false; + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('boom'); try { BleedingEdgeToggle::withBleedingEdge(true, static function (): void { throw new RuntimeException('boom'); }); - } catch (Throwable $e) { - $thrown = $e instanceof RuntimeException && $e->getMessage() === 'boom'; + } finally { + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); } - - $this->assertTrue($thrown); - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); } public function testThrowsAndRestoresWhenCallbackYields(): void diff --git a/tests/PHPStan/Generics/data/invalidReturn-7.json b/tests/PHPStan/Generics/data/invalidReturn-7.json index 44706c08a45..a7ce09a11c9 100644 --- a/tests/PHPStan/Generics/data/invalidReturn-7.json +++ b/tests/PHPStan/Generics/data/invalidReturn-7.json @@ -1,17 +1,17 @@ [ { "message": "Function PHPStan\\Generics\\InvalidReturn\\invalidReturnA() should return T but returns int.", - "line": 11, + "line": 13, "ignorable": true }, { "message": "Function PHPStan\\Generics\\InvalidReturn\\invalidReturnB() should return T of DateTimeInterface but returns DateTime.", - "line": 20, + "line": 24, "ignorable": true }, { "message": "Function PHPStan\\Generics\\InvalidReturn\\invalidReturnC() should return T of DateTime but returns DateTime.", - "line": 29, + "line": 35, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/invalidReturn.php b/tests/PHPStan/Generics/data/invalidReturn.php index bd69b6741d1..2b7bf782b6e 100644 --- a/tests/PHPStan/Generics/data/invalidReturn.php +++ b/tests/PHPStan/Generics/data/invalidReturn.php @@ -8,6 +8,8 @@ * @return T */ function invalidReturnA($a) { + var_dump($a); + return 1; } @@ -17,6 +19,8 @@ function invalidReturnA($a) { * @return T */ function invalidReturnB($a) { + var_dump($a); + return new \DateTime(); } @@ -26,5 +30,7 @@ function invalidReturnB($a) { * @return T */ function invalidReturnC($a) { + var_dump($a); + return new \DateTime(); } diff --git a/tests/PHPStan/Generics/data/variance-0.json b/tests/PHPStan/Generics/data/variance-0.json index 9b097eebd1a..6d6df01334f 100644 --- a/tests/PHPStan/Generics/data/variance-0.json +++ b/tests/PHPStan/Generics/data/variance-0.json @@ -1,12 +1,12 @@ [ { "message": "Template type T of function PHPStan\\Generics\\Variance\\returnOut() is not referenced in a parameter.", - "line": 109, + "line": 111, "ignorable": true }, { "message": "Template type T of function PHPStan\\Generics\\Variance\\returnInvariant() is not referenced in a parameter.", - "line": 117, + "line": 119, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-2.json b/tests/PHPStan/Generics/data/variance-2.json index 3c7d9da4d4a..4f943142639 100644 --- a/tests/PHPStan/Generics/data/variance-2.json +++ b/tests/PHPStan/Generics/data/variance-2.json @@ -46,32 +46,32 @@ }, { "message": "Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function PHPStan\\Generics\\Variance\\returnOut().", - "line": 109, + "line": 111, "ignorable": true }, { "message": "Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type T in in function PHPStan\\Generics\\Variance\\returnInvariant().", - "line": 117, + "line": 119, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter v of method PHPStan\\Generics\\Variance\\CovariantOfOBject::set().", - "line": 124, + "line": 126, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter t of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", - "line": 154, + "line": 156, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in contravariant position in parameter w of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", - "line": 154, + "line": 156, "ignorable": true }, { "message": "Template type T is declared as covariant, but occurs in invariant position in parameter v of method PHPStan\\Generics\\Variance\\ConstructorAndStatic::create().", - "line": 154, + "line": 156, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-4.json b/tests/PHPStan/Generics/data/variance-4.json index 7757cc3dead..e9386b4bd07 100644 --- a/tests/PHPStan/Generics/data/variance-4.json +++ b/tests/PHPStan/Generics/data/variance-4.json @@ -1,7 +1,7 @@ [ { "message": "Property PHPStan\\Generics\\Variance\\ConstructorAndStatic::$data is never read, only written.", - "line": 135, + "line": 137, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance-5.json b/tests/PHPStan/Generics/data/variance-5.json index a16b075630e..39315cf1b5f 100644 --- a/tests/PHPStan/Generics/data/variance-5.json +++ b/tests/PHPStan/Generics/data/variance-5.json @@ -1,7 +1,7 @@ [ { "message": "Parameter #1 $it of function PHPStan\\Generics\\Variance\\acceptInvariantIterOfDateTimeInterface expects PHPStan\\Generics\\Variance\\InvariantIter, PHPStan\\Generics\\Variance\\InvariantIter given.", - "line": 165, + "line": 167, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Generics/data/variance.php b/tests/PHPStan/Generics/data/variance.php index 8b68847f77d..86be2b7bffa 100644 --- a/tests/PHPStan/Generics/data/variance.php +++ b/tests/PHPStan/Generics/data/variance.php @@ -99,6 +99,8 @@ class Quuz extends Quux { * @return T */ function x($a, $b, $c, $d, $e) { + var_dump($b, $c, $d, $e); + return $a; } diff --git a/tests/PHPStan/Generics/data/varyingAcceptor.php b/tests/PHPStan/Generics/data/varyingAcceptor.php index 054bc020791..a67e3384f0e 100644 --- a/tests/PHPStan/Generics/data/varyingAcceptor.php +++ b/tests/PHPStan/Generics/data/varyingAcceptor.php @@ -48,7 +48,7 @@ function testApply() * @param callable(callable():T):T $closure * @return T */ -function bar(callable $closure) { throw new \Exception(); } +function bar(callable $closure) { var_dump($closure); throw new \Exception(); } /** @param callable(callable():int):string $callable */ function testBar($callable): void { diff --git a/tests/PHPStan/Levels/data/acceptTypes-10.json b/tests/PHPStan/Levels/data/acceptTypes-10.json index 8a1b7a3992b..2afb2cec1b0 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-10.json +++ b/tests/PHPStan/Levels/data/acceptTypes-10.json @@ -6,12 +6,12 @@ }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", - "line": 325, + "line": 326, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(mixed): mixed given.", - "line": 326, + "line": 327, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-4.json b/tests/PHPStan/Levels/data/acceptTypes-4.json index fbcb96fc5a4..5cb2f79d3b6 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-4.json +++ b/tests/PHPStan/Levels/data/acceptTypes-4.json @@ -1,22 +1,22 @@ [ { "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", - "line": 531, + "line": 532, "ignorable": true }, { "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", - "line": 532, + "line": 533, "ignorable": true }, { "message": "Call to method Levels\\AcceptTypes\\Baz::requireArray() on a separate line has no effect.", - "line": 542, + "line": 543, "ignorable": true }, { "message": "Call to method Levels\\AcceptTypes\\Baz::requireFoo() on a separate line has no effect.", - "line": 543, + "line": 544, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index d64081a6c04..a57678714b5 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -66,147 +66,147 @@ }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl given.", - "line": 226, + "line": 227, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl given.", - "line": 227, + "line": 228, "ignorable": true }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl given.", - "line": 238, + "line": 239, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl given.", - "line": 239, + "line": 240, "ignorable": true }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 250, + "line": 251, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface given.", - "line": 251, + "line": 252, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float given.", - "line": 412, + "line": 413, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|string given.", - "line": 413, + "line": 414, "ignorable": true }, { "message": "Parameter #1 $a of method Levels\\AcceptTypes\\Baz::doLorem() expects int|resource, float|string given.", - "line": 425, + "line": 426, "ignorable": true }, { "message": "Parameter #1 $a of method Levels\\AcceptTypes\\Baz::doLorem() expects int|resource, float|string|null given.", - "line": 426, + "line": 427, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, null given.", - "line": 431, + "line": 432, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", - "line": 493, + "line": 494, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", - "line": 494, + "line": 495, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", - "line": 577, + "line": 578, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", - "line": 578, + "line": 579, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", - "line": 579, + "line": 580, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{} given.", - "line": 580, + "line": 581, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 1} given.", - "line": 582, + "line": 583, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 'date', bar: 'date'} given.", - "line": 583, + "line": 584, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 'nonexistent'} given.", - "line": 584, + "line": 585, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{bar: 'date'} given.", - "line": 585, + "line": 586, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, non-empty-array given.", - "line": 588, + "line": 589, "ignorable": true }, { "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects static(Levels\\AcceptTypes\\RequireObjectWithoutClassType), object given.", - "line": 648, + "line": 649, "ignorable": true }, { "message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).", - "line": 671, + "line": 672, "ignorable": true }, { "message": "Parameter #1 $min (int<11, max>) of function random_int expects lower number than parameter #2 $max (10).", - "line": 678, + "line": 679, "ignorable": true }, { "message": "Parameter #1 $min (340) of function random_int expects lower number than parameter #2 $max (int).", - "line": 685, + "line": 686, "ignorable": true }, { "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, 'foo' given.", - "line": 707, + "line": 708, "ignorable": true }, { "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", - "line": 733, + "line": 734, "ignorable": true }, { "message": "Parameter #2 $array of function implode expects array, int given.", - "line": 763, + "line": 766, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-6.json b/tests/PHPStan/Levels/data/acceptTypes-6.json index 35bd142bb91..27cbc796d03 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-6.json +++ b/tests/PHPStan/Levels/data/acceptTypes-6.json @@ -66,102 +66,102 @@ }, { "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFoo() has no return type specified.", - "line": 208, + "line": 209, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFooUnionClosures() has no return type specified.", - "line": 254, + "line": 255, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBar() has no return type specified.", - "line": 332, + "line": 333, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBaz() has no return type specified.", - "line": 342, + "line": 343, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doFoo() has no return type specified.", - "line": 409, + "line": 410, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doBar() has no return type specified.", - "line": 418, + "line": 419, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doBaz() has no return type specified.", - "line": 423, + "line": 424, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doLorem() has no return type specified.", - "line": 437, + "line": 438, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doIpsum() has no return type specified.", - "line": 445, + "line": 446, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doFooArray() has no return type specified.", - "line": 490, + "line": 491, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::doBarArray() has no return type specified.", - "line": 502, + "line": 503, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::testUnions() has no return type specified.", - "line": 524, + "line": 525, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::testUnions2() has no return type specified.", - "line": 535, + "line": 536, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::requireArray() has no return type specified.", - "line": 549, + "line": 550, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Baz::requireFoo() has no return type specified.", - "line": 554, + "line": 555, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\ArrayShapes::doFoo() has no return type specified.", - "line": 570, + "line": 571, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return type specified.", - "line": 603, + "line": 604, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has no return type specified.", - "line": 755, + "line": 756, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has parameter $union with no value type specified in iterable type array.", - "line": 755, + "line": 756, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Implode::invalidType() has no return type specified.", - "line": 762, + "line": 765, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index c9bcbcd7517..4c13523e8b4 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -36,127 +36,127 @@ }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", - "line": 283, + "line": 284, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface, mixed, mixed): Levels\\AcceptTypes\\FooImpl) given.", - "line": 284, + "line": 285, "ignorable": true }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", - "line": 301, + "line": 302, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooImpl): Levels\\AcceptTypes\\FooImpl) given.", - "line": 302, + "line": 303, "ignorable": true }, { "message": "Parameter #1 $closure of method Levels\\AcceptTypes\\ClosureAccepts::doBar() expects Closure(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", - "line": 319, + "line": 320, "ignorable": true }, { "message": "Parameter #1 $callable of method Levels\\AcceptTypes\\ClosureAccepts::doBaz() expects callable(Levels\\AcceptTypes\\FooInterface, int): Levels\\AcceptTypes\\FooInterface, (Closure(): Levels\\AcceptTypes\\FooInterface)|(Closure(Levels\\AcceptTypes\\FooInterface): Levels\\AcceptTypes\\ParentFooInterface) given.", - "line": 320, + "line": 321, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", - "line": 415, + "line": 416, "ignorable": true }, { "message": "Parameter #1 $a of method Levels\\AcceptTypes\\Baz::doIpsum() expects float, float|string given.", - "line": 429, + "line": 430, "ignorable": true }, { "message": "Parameter #1 $a of method Levels\\AcceptTypes\\Baz::doIpsum() expects float, float|string|null given.", - "line": 430, + "line": 431, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", - "line": 496, + "line": 497, "ignorable": true }, { "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", - "line": 531, + "line": 532, "ignorable": true }, { "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", - "line": 532, + "line": 533, "ignorable": true }, { "message": "Parameter #1 $array of method Levels\\AcceptTypes\\Baz::requireArray() expects array, array|Levels\\AcceptTypes\\Foo given.", - "line": 542, + "line": 543, "ignorable": true }, { "message": "Parameter #1 $foo of method Levels\\AcceptTypes\\Baz::requireFoo() expects Levels\\AcceptTypes\\Foo, array|Levels\\AcceptTypes\\Foo given.", - "line": 543, + "line": 544, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{}|array{foo: 'date'} given.", - "line": 596, + "line": 597, "ignorable": true }, { "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, iterable given.", - "line": 597, + "line": 598, "ignorable": true }, { "message": "Parameter #1 $s of method Levels\\AcceptTypes\\RequireClassString::requireClassString() expects class-string, string given.", - "line": 617, + "line": 618, "ignorable": true }, { "message": "Parameter #1 $s of method Levels\\AcceptTypes\\RequireClassString::requireGenericClassString() expects class-string, string given.", - "line": 618, + "line": 619, "ignorable": true }, { "message": "Parameter #1 $stdClass of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStdClass() expects stdClass, object given.", - "line": 647, + "line": 648, "ignorable": true }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", - "line": 690, + "line": 691, "ignorable": true }, { "message": "Parameter #1 $min (int<-1, 0>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", - "line": 691, + "line": 692, "ignorable": true }, { "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", - "line": 692, + "line": 693, "ignorable": true }, { "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", - "line": 708, + "line": 709, "ignorable": true }, { "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", - "line": 735, + "line": 736, "ignorable": true }, { "message": "Parameter #2 $array of function implode expects array, array|int|string given.", - "line": 756, + "line": 757, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-8.json b/tests/PHPStan/Levels/data/acceptTypes-8.json index 10728d1f255..0f9078489d1 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-8.json +++ b/tests/PHPStan/Levels/data/acceptTypes-8.json @@ -21,27 +21,27 @@ }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, int|null given.", - "line": 414, + "line": 415, "ignorable": true }, { "message": "Parameter #1 $a of method Levels\\AcceptTypes\\Baz::doLorem() expects int|resource, int|null given.", - "line": 428, + "line": 429, "ignorable": true }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBarArray() expects array, array given.", - "line": 495, + "line": 496, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Discussion8209::test1() should return int but returns int|null.", - "line": 771, + "line": 776, "ignorable": true }, { "message": "Method Levels\\AcceptTypes\\Discussion8209::test2() should return array but returns array.", - "line": 779, + "line": 784, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index 61b2c1fbbbb..c1aad4ccdb0 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -177,6 +177,7 @@ public function benevolentUnionNotReported(array $strings) { foreach ($strings as $key => $val) { $this->doBar($key); + var_dump($val); } } @@ -754,6 +755,8 @@ class Implode { */ public function partlySupportedUnion($union) { $imploded = implode('abc', $union); + + return $imploded; } /** @@ -761,6 +764,8 @@ public function partlySupportedUnion($union) { */ public function invalidType($invalid) { $imploded = implode('abc', $invalid); + + return $imploded; } } diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-3.json b/tests/PHPStan/Levels/data/arrayDestructuring-3.json index a97c71f0f5e..beb9ae2b9e4 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-3.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-3.json @@ -1,12 +1,12 @@ [ { "message": "Cannot use array destructuring on iterable.", - "line": 23, + "line": 25, "ignorable": true }, { "message": "Offset 3 does not exist on array{'a', 'b', 'c'}.", - "line": 30, + "line": 34, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-8.json b/tests/PHPStan/Levels/data/arrayDestructuring-8.json index 3b033abd6d1..853eca8e906 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-8.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-8.json @@ -1,7 +1,7 @@ [ { "message": "Cannot use array destructuring on array|null.", - "line": 15, + "line": 16, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDestructuring.php b/tests/PHPStan/Levels/data/arrayDestructuring.php index c97a3e6328a..7ec0d5838d3 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring.php +++ b/tests/PHPStan/Levels/data/arrayDestructuring.php @@ -12,7 +12,9 @@ class Foo public function doFoo(array $array, ?array $arrayOrNull): void { [$a, $b, $c] = $array; + var_dump($a, $b, $c); [$a, $b, $c] = $arrayOrNull; + var_dump($a, $b, $c); } /** @@ -21,13 +23,16 @@ public function doFoo(array $array, ?array $arrayOrNull): void public function doBar(iterable $it): void { [$a] = $it; + var_dump($a); } public function doBaz(): void { $array = ['a', 'b', 'c']; [$a] = $array; + var_dump($a); [$a, , , $d] = $array; + var_dump($a, $d); } } diff --git a/tests/PHPStan/Levels/data/binaryOps-2.json b/tests/PHPStan/Levels/data/binaryOps-2.json index c94601f913f..e66171b898b 100644 --- a/tests/PHPStan/Levels/data/binaryOps-2.json +++ b/tests/PHPStan/Levels/data/binaryOps-2.json @@ -1,32 +1,32 @@ [ { "message": "Binary operation \"+\" between int and object|string results in an error.", - "line": 23, + "line": 24, "ignorable": true }, { "message": "Binary operation \"+\" between int and string results in an error.", - "line": 24, + "line": 25, "ignorable": true }, { "message": "Binary operation \"+\" between string and string results in an error.", - "line": 25, + "line": 26, "ignorable": true }, { "message": "Binary operation \"+\" between int|string and object|string results in an error.", - "line": 26, + "line": 27, "ignorable": true }, { "message": "Binary operation \"+\" between int|string and string results in an error.", - "line": 27, + "line": 28, "ignorable": true }, { "message": "Binary operation \"+\" between object|string and object|string results in an error.", - "line": 28, + "line": 29, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/binaryOps-7.json b/tests/PHPStan/Levels/data/binaryOps-7.json index e3491aa1e45..a9245492db0 100644 --- a/tests/PHPStan/Levels/data/binaryOps-7.json +++ b/tests/PHPStan/Levels/data/binaryOps-7.json @@ -1,7 +1,7 @@ [ { "message": "Binary operation \"+\" between int and int|string results in an error.", - "line": 22, + "line": 23, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/binaryOps.php b/tests/PHPStan/Levels/data/binaryOps.php index fd53d3f9462..ef288ca0df5 100644 --- a/tests/PHPStan/Levels/data/binaryOps.php +++ b/tests/PHPStan/Levels/data/binaryOps.php @@ -18,14 +18,16 @@ public function doFoo( $stringOrObject ) { - $result = $int + $int; - $result = $int + $intOrString; - $result = $int + $stringOrObject; - $result = $int + $string; - $result = $string + $string; - $result = $intOrString + $stringOrObject; - $result = $intOrString + $string; - $result = $stringOrObject + $stringOrObject; + $results = []; + $results[] = $int + $int; + $results[] = $int + $intOrString; + $results[] = $int + $stringOrObject; + $results[] = $int + $string; + $results[] = $string + $string; + $results[] = $intOrString + $stringOrObject; + $results[] = $intOrString + $string; + $results[] = $stringOrObject + $stringOrObject; + var_dump($results); } } diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index e2810b0e424..918dcdd3e0d 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,12 +1,12 @@ [ { "message": "Cannot cast array|(callable(): mixed) to int.", - "line": 20, + "line": 21, "ignorable": true }, { "message": "Cannot cast array|float|int to string.", - "line": 21, + "line": 22, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/casts.php b/tests/PHPStan/Levels/data/casts.php index 67cf2dacd81..5e02d3f83ce 100644 --- a/tests/PHPStan/Levels/data/casts.php +++ b/tests/PHPStan/Levels/data/casts.php @@ -16,9 +16,11 @@ public function doFoo( $arrayOrFloatOrInt ) { - $test = (int) $array; - $test = (int) $arrayOrCallable; - $test = (string) $arrayOrFloatOrInt; + $tests = []; + $tests[] = (int) $array; + $tests[] = (int) $arrayOrCallable; + $tests[] = (string) $arrayOrFloatOrInt; + var_dump($tests); } } diff --git a/tests/PHPStan/Levels/data/clone-10-missing.json b/tests/PHPStan/Levels/data/clone-10-missing.json index 40e1203120d..315d0f46ab3 100644 --- a/tests/PHPStan/Levels/data/clone-10-missing.json +++ b/tests/PHPStan/Levels/data/clone-10-missing.json @@ -1,12 +1,12 @@ [ { "message": "Cannot clone non-object variable $nullableInt of type int.", - "line": 34, + "line": 35, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", - "line": 35, + "line": 36, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-3.json b/tests/PHPStan/Levels/data/clone-3.json index 6986f61d4b8..a7b06c93241 100644 --- a/tests/PHPStan/Levels/data/clone-3.json +++ b/tests/PHPStan/Levels/data/clone-3.json @@ -1,17 +1,17 @@ [ { "message": "Cannot clone non-object variable $int of type int.", - "line": 29, + "line": 30, "ignorable": true }, { "message": "Cannot clone non-object variable $intOrString of type int|string.", - "line": 30, + "line": 31, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableInt of type int.", - "line": 34, + "line": 35, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-7.json b/tests/PHPStan/Levels/data/clone-7.json index 8bf68da46aa..9d0e6d601a7 100644 --- a/tests/PHPStan/Levels/data/clone-7.json +++ b/tests/PHPStan/Levels/data/clone-7.json @@ -1,12 +1,12 @@ [ { "message": "Cannot clone non-object variable $fooOrInt of type int|Levels\\Cloning\\Foo.", - "line": 33, + "line": 34, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", - "line": 35, + "line": 36, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-8-missing.json b/tests/PHPStan/Levels/data/clone-8-missing.json index 40e1203120d..315d0f46ab3 100644 --- a/tests/PHPStan/Levels/data/clone-8-missing.json +++ b/tests/PHPStan/Levels/data/clone-8-missing.json @@ -1,12 +1,12 @@ [ { "message": "Cannot clone non-object variable $nullableInt of type int.", - "line": 34, + "line": 35, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", - "line": 35, + "line": 36, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-8.json b/tests/PHPStan/Levels/data/clone-8.json index 9cd1fa0a55c..c83592921c3 100644 --- a/tests/PHPStan/Levels/data/clone-8.json +++ b/tests/PHPStan/Levels/data/clone-8.json @@ -1,17 +1,17 @@ [ { "message": "Cannot clone non-object variable $nullableFoo of type Levels\\Cloning\\Foo|null.", - "line": 32, + "line": 33, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableInt of type int|null.", - "line": 34, + "line": 35, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo|null.", - "line": 35, + "line": 36, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-9-missing.json b/tests/PHPStan/Levels/data/clone-9-missing.json index 40e1203120d..315d0f46ab3 100644 --- a/tests/PHPStan/Levels/data/clone-9-missing.json +++ b/tests/PHPStan/Levels/data/clone-9-missing.json @@ -1,12 +1,12 @@ [ { "message": "Cannot clone non-object variable $nullableInt of type int.", - "line": 34, + "line": 35, "ignorable": true }, { "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", - "line": 35, + "line": 36, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-9.json b/tests/PHPStan/Levels/data/clone-9.json index 8148322a425..dc236c4a0d2 100644 --- a/tests/PHPStan/Levels/data/clone-9.json +++ b/tests/PHPStan/Levels/data/clone-9.json @@ -1,7 +1,7 @@ [ { "message": "Cannot clone non-object variable $mixed of type mixed.", - "line": 36, + "line": 37, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone.php b/tests/PHPStan/Levels/data/clone.php index 4697debb13f..6d2dd7af862 100644 --- a/tests/PHPStan/Levels/data/clone.php +++ b/tests/PHPStan/Levels/data/clone.php @@ -26,14 +26,16 @@ public function doFoo( $mixed ) { - $result = clone $int; - $result = clone $intOrString; - $result = clone $foo; - $result = clone $nullableFoo; - $result = clone $fooOrInt; - $result = clone $nullableInt; - $result = clone $nullableUnion; - $result = clone $mixed; + $results = []; + $results[] = clone $int; + $results[] = clone $intOrString; + $results[] = clone $foo; + $results[] = clone $nullableFoo; + $results[] = clone $fooOrInt; + $results[] = clone $nullableInt; + $results[] = clone $nullableUnion; + $results[] = clone $mixed; + var_dump($results); } } diff --git a/tests/PHPStan/Levels/data/comparison-2.json b/tests/PHPStan/Levels/data/comparison-2.json index fbee8d78eea..51321c7b5b0 100644 --- a/tests/PHPStan/Levels/data/comparison-2.json +++ b/tests/PHPStan/Levels/data/comparison-2.json @@ -1,12 +1,12 @@ [ { "message": "Comparison operation \"==\" between stdClass and int results in an error.", - "line": 27, + "line": 28, "ignorable": true }, { "message": "Comparison operation \"==\" between stdClass and float results in an error.", - "line": 28, + "line": 29, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/comparison-4.json b/tests/PHPStan/Levels/data/comparison-4.json index d093a9fbccb..f8f190089ae 100644 --- a/tests/PHPStan/Levels/data/comparison-4.json +++ b/tests/PHPStan/Levels/data/comparison-4.json @@ -1,7 +1,7 @@ [ { "message": "Strict comparison using === between string and 1 will always evaluate to false.", - "line": 38, + "line": 40, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/comparison-7.json b/tests/PHPStan/Levels/data/comparison-7.json index a5f6ca31fa8..c27f13c8ae4 100644 --- a/tests/PHPStan/Levels/data/comparison-7.json +++ b/tests/PHPStan/Levels/data/comparison-7.json @@ -1,12 +1,12 @@ [ { "message": "Comparison operation \"==\" between stdClass and int|string results in an error.", - "line": 30, + "line": 31, "ignorable": true }, { "message": "Comparison operation \"==\" between stdClass and int|stdClass results in an error.", - "line": 31, + "line": 32, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/comparison.php b/tests/PHPStan/Levels/data/comparison.php index ee10f825be5..27c7be0b888 100644 --- a/tests/PHPStan/Levels/data/comparison.php +++ b/tests/PHPStan/Levels/data/comparison.php @@ -24,13 +24,15 @@ public function doFoo( $intOrObject ) { - $result = $object == $int; - $result = $object == $float; - $result = $object == $string; - $result = $object == $intOrString; - $result = $object == $intOrObject; + $results = []; + $results[] = $object == $int; + $results[] = $object == $float; + $results[] = $object == $string; + $results[] = $object == $intOrString; + $results[] = $object == $intOrObject; - $result = self::FOO_CONST === 'bar'; + $results[] = self::FOO_CONST === 'bar'; + var_dump($results); } public function doBar(\ffmpeg_movie $movie): void diff --git a/tests/PHPStan/Levels/data/iterable.php b/tests/PHPStan/Levels/data/iterable.php index 57815aa97cb..7fa6db7ecc1 100644 --- a/tests/PHPStan/Levels/data/iterable.php +++ b/tests/PHPStan/Levels/data/iterable.php @@ -21,19 +21,19 @@ public function doFoo( ) { foreach ($array as $val) { - + var_dump($val); } foreach ($arrayOrNull as $val) { - + var_dump($val); } foreach ($int as $val) { - + var_dump($val); } foreach ($intOrFloat as $val) { - + var_dump($val); } foreach ($arrayOrFalse as $val) { - + var_dump($val); } } diff --git a/tests/PHPStan/Levels/data/propertyAccesses-0.json b/tests/PHPStan/Levels/data/propertyAccesses-0.json index 953c3a89d29..256506a7bc8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-0.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-0.json @@ -1,12 +1,12 @@ [ { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 14, + "line": 15, "ignorable": true }, { "message": "Access to static property $bar on an unknown class Levels\\PropertyAccesses\\Lorem.", - "line": 32, + "line": 34, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-1.json b/tests/PHPStan/Levels/data/propertyAccesses-1.json index 252dee98bd1..163fac35ffc 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-1.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-1.json @@ -1,12 +1,12 @@ [ { "message": "Access to an undefined property Levels\\PropertyAccesses\\ClassWithMagicMethod::$test.", - "line": 76, + "line": 82, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::$test.", - "line": 95, + "line": 101, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json index fd6f669c7c1..67fe7068ba8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-10-missing.json @@ -1,52 +1,52 @@ [ { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 61, + "line": 64, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 166, + "line": 173, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 170, + "line": 178, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 63, + "line": 67, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 169, + "line": 177, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 170, + "line": 178, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-10.json b/tests/PHPStan/Levels/data/propertyAccesses-10.json index 9581e25ad9a..6324e67f6aa 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-10.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-10.json @@ -1,57 +1,57 @@ [ { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 14, + "line": 15, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 18, + "line": 19, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 32, + "line": 34, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 36, + "line": 38, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 95, + "line": 101, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 186, + "line": 196, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 187, + "line": 197, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 188, + "line": 198, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 198, + "line": 208, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 199, + "line": 209, "ignorable": true }, { "message": "Parameter #1 (mixed) of echo cannot be converted to string.", - "line": 200, + "line": 210, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-2.json b/tests/PHPStan/Levels/data/propertyAccesses-2.json index 1d3376b7811..bc24da083af 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-2.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-2.json @@ -1,52 +1,52 @@ [ { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 18, + "line": 19, "ignorable": true }, { "message": "Access to an undefined static property Levels\\PropertyAccesses\\Bar::$foo.", - "line": 36, + "line": 38, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 61, + "line": 64, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", - "line": 66, + "line": 71, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 166, + "line": 173, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 170, + "line": 178, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Baz::$foo.", - "line": 173, + "line": 182, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-6.json b/tests/PHPStan/Levels/data/propertyAccesses-6.json index b62abeefd83..b59d3862adb 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -6,32 +6,32 @@ }, { "message": "Method Levels\\PropertyAccesses\\Bar::doBar() has no return type specified.", - "line": 29, + "line": 30, "ignorable": true }, { "message": "Method Levels\\PropertyAccesses\\Baz::doBaz() has no return type specified.", - "line": 50, + "line": 52, "ignorable": true }, { "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::doFoo() has no return type specified.", - "line": 74, + "line": 80, "ignorable": true }, { "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", - "line": 93, + "line": 99, "ignorable": true }, { "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::__get() has no return type specified.", - "line": 98, + "line": 104, "ignorable": true }, { "message": "Method Levels\\PropertyAccesses\\Ipsum::doBaz() has no return type specified.", - "line": 158, + "line": 164, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json index 7d9c064f4b8..d5decc371e3 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-7-missing.json @@ -1,22 +1,22 @@ [ { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 170, + "line": 178, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-7.json b/tests/PHPStan/Levels/data/propertyAccesses-7.json index b6df87becb3..431465f40a2 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-7.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-7.json @@ -1,47 +1,47 @@ [ { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 57, + "line": 59, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 63, + "line": 67, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 161, + "line": 167, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 169, + "line": 177, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 170, + "line": 178, "ignorable": true }, { "message": "Access to an undefined property object::$baz.", - "line": 200, + "line": 210, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json index fd6f669c7c1..67fe7068ba8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-8-missing.json @@ -1,52 +1,52 @@ [ { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 61, + "line": 64, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 166, + "line": 173, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 170, + "line": 178, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 63, + "line": 67, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 169, + "line": 177, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 170, + "line": 178, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-8.json b/tests/PHPStan/Levels/data/propertyAccesses-8.json index d76d3fd5140..236bec3170a 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-8.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-8.json @@ -1,42 +1,42 @@ [ { "message": "Cannot access property $foo on Levels\\PropertyAccesses\\Foo|null.", - "line": 60, + "line": 63, "ignorable": true }, { "message": "Cannot access property $bar on Levels\\PropertyAccesses\\Foo|null.", - "line": 61, + "line": 64, "ignorable": true }, { "message": "Cannot access property $foo on Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo|null.", - "line": 63, + "line": 67, "ignorable": true }, { "message": "Cannot access property $bar on Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo|null.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Cannot access property $foo on Levels\\PropertyAccesses\\Foo|null.", - "line": 165, + "line": 172, "ignorable": true }, { "message": "Cannot access property $bar on Levels\\PropertyAccesses\\Foo|null.", - "line": 166, + "line": 173, "ignorable": true }, { "message": "Cannot access property $foo on Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo|null.", - "line": 169, + "line": 177, "ignorable": true }, { "message": "Cannot access property $bar on Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo|null.", - "line": 170, + "line": 178, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json index fd6f669c7c1..67fe7068ba8 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json @@ -1,52 +1,52 @@ [ { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 58, + "line": 60, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 61, + "line": 64, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 162, + "line": 168, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", - "line": 166, + "line": 173, "ignorable": true }, { "message": "Non-static access to static property Levels\\PropertyAccesses\\Bar::$bar.", - "line": 170, + "line": 178, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 63, + "line": 67, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 64, + "line": 68, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", - "line": 169, + "line": 177, "ignorable": true }, { "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", - "line": 170, + "line": 178, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9.json b/tests/PHPStan/Levels/data/propertyAccesses-9.json index c7c6ae5a96a..bc534e0f04a 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-9.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-9.json @@ -1,7 +1,7 @@ [ { "message": "Cannot access property $foo on mixed.", - "line": 197, + "line": 207, "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses.php b/tests/PHPStan/Levels/data/propertyAccesses.php index 1c006ab6dce..b68164953dd 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses.php +++ b/tests/PHPStan/Levels/data/propertyAccesses.php @@ -11,6 +11,7 @@ class Foo public function doFoo(int $i) { $foo = $this->foo; + var_dump($foo); echo $this->bar; $foo = new self(); @@ -29,6 +30,7 @@ class Bar public static function doBar(int $i) { $bar = Bar::$bar; + var_dump($bar); echo Lorem::$bar; $bar = new Bar(); @@ -56,14 +58,18 @@ public function doBaz( { $foo = $fooOrBar->foo; $bar =$fooOrBar->bar; + var_dump($foo, $bar); $foo = $fooOrNull->foo; $bar = $fooOrNull->bar; + var_dump($foo, $bar); $foo = $fooOrBarOrNull->foo; $bar = $fooOrBarOrNull->bar; + var_dump($foo, $bar); $foo = $barOrBaz->foo; + var_dump($foo); } } @@ -160,17 +166,21 @@ public function doBaz() $fooOrBar = $this->makeFooOrBar(); $foo = $fooOrBar->foo; $bar =$fooOrBar->bar; + var_dump($foo, $bar); $fooOrNull = $this->makeFooOrNull(); $foo = $fooOrNull->foo; $bar = $fooOrNull->bar; + var_dump($foo, $bar); $fooOrBarOrNull = $this->makeFooOrBarOrNull(); $foo = $fooOrBarOrNull->foo; $bar = $fooOrBarOrNull->bar; + var_dump($foo, $bar); $barOrBaz = $this->makeBarOrBaz(); $foo = $barOrBaz->foo; + var_dump($foo); } } diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-4.json b/tests/PHPStan/Levels/data/stringOffsetAccess-4.json new file mode 100644 index 00000000000..214888bd826 --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-4.json @@ -0,0 +1,17 @@ +[ + { + "message": "Variable $string is assigned value 'foo' but it already has that value.", + "line": 12, + "ignorable": true + }, + { + "message": "Variable $string is assigned value 'foo' but it already has that value.", + "line": 15, + "ignorable": true + }, + { + "message": "Variable $string is assigned value 'foo' but it already has that value.", + "line": 18, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php index c9f990d1cdf..32fd46d18ed 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderGoldenTest.php @@ -252,7 +252,7 @@ private static function generateClassDescription(string $className): string $keyword = 'class'; break; default: - $keyword = self::fail(); + self::fail(); } $verbosityLevel = VerbosityLevel::precise(); diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index 98b47e6cb09..9d9e41a0be1 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -529,7 +529,7 @@ public function testParseAll(int $phpVersionId): void } else { $reflectionFunction = new ReflectionFunction($reflector->reflectFunction($realFunctionName)); } - } catch (IdentifierNotFound | OutOfBoundsException $e) { + } catch (IdentifierNotFound | OutOfBoundsException) { // pass } diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index 3d2ea76ad89..63644db9729 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -2,9 +2,8 @@ namespace PHPStan\Rules\Classes; -use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Rule; -use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\Rules\UnusedParametersCheck; use PHPStan\Testing\RuleTestCase; /** @@ -17,11 +16,7 @@ class UnusedConstructorParametersRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck( - self::createReflectionProvider(), - self::getContainer()->getByType(InitializerExprTypeResolver::class), - $this->reportExactLine, - )); + return new UnusedConstructorParametersRule(self::getContainer()->getByType(UnusedParametersCheck::class), $this->reportExactLine); } public function testUnusedConstructorParametersNoExactLine(): void @@ -83,4 +78,18 @@ public function testParameterUsedInIncludedFile(): void $this->analyse([__DIR__ . '/data/unused-constructor-parameters-include.php'], []); } + public function testResolvedDynamicUsages(): void + { + $this->analyse([__DIR__ . '/data/unused-constructor-parameters-resolved-dynamic.php'], [ + [ + 'Constructor of class UnusedConstructorParametersResolvedDynamic\VariableVariable has an unused parameter $other.', + 8, + ], + [ + 'Constructor of class UnusedConstructorParametersResolvedDynamic\OverwrittenParameter has an unused parameter $x.', + 43, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-resolved-dynamic.php b/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-resolved-dynamic.php new file mode 100644 index 00000000000..d3075dd7cae --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-resolved-dynamic.php @@ -0,0 +1,81 @@ + */ + private $data; + + /** + * @param string[] $keys + */ + public function __construct($x, array $keys) + { + // a compact() argument that is not statically known can name any + // variable - the syntactic check reported $x here + $this->data = compact($keys); + } + +} + +class OverwrittenParameter +{ + + /** @var int */ + private $sum; + + public function __construct($x, $used) + { + // the incoming value is overwritten before it is ever read - the + // parameter is unused even though the name appears in the body + $x = 1; + $this->sum = $x + $used; + } + +} + +class ParameterOverwrittenInOneBranch +{ + + /** @var int */ + private $value; + + public function __construct($x) + { + if (rand(0, 1) === 0) { + $x = 1; + } + $this->value = $x; + } + +} + +class OverwrittenParameterObservedByFuncGetArgs +{ + + /** @var array */ + private $args; + + public function __construct($x) + { + $x = 1; + $this->args = [func_get_args(), $x]; + } + +} diff --git a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php index 83aac759b44..301185e8405 100644 --- a/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php +++ b/tests/PHPStan/Rules/Constants/DynamicClassConstantFetchRuleTest.php @@ -33,7 +33,6 @@ protected function getRule(): TRule public function testRule(): void { - $errors = []; if (PHP_VERSION_ID < 80300) { $errors = [ [ diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php new file mode 100644 index 00000000000..db71d668366 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -0,0 +1,397 @@ + + */ +class UnusedVariableRuleTest extends RuleTestCase +{ + + private bool $polluteScopeWithAlwaysIterableForeach = true; + + protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool + { + return $this->polluteScopeWithAlwaysIterableForeach; + } + + protected function getRule(): Rule + { + return new UnusedVariableRule(); + } + + public function testThrowableCatchAfterDocumentedException(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-throwable-catch.php'], []); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/unused-variable.php'], [ + [ + 'Variable $a is never read.', + 27, + ], + [ + 'Value assigned to variable $a is never read.', + 32, + ], + [ + 'Variable $a is never read.', + 40, + ], + [ + 'Value assigned to variable $x is never read.', + 46, + ], + [ + 'Variable $a is never read.', + 70, + ], + [ + 'Variable $a is never read.', + 76, + ], + [ + 'Variable $a is never read.', + 93, + ], + [ + 'Variable $a is never read.', + 95, + ], + [ + 'Variable $a is never read.', + 101, + ], + [ + 'Variable $a is never read.', + 113, + ], + [ + 'Foreach key variable $k is never read.', + 119, + ], + [ + 'Foreach value variable $v is never read.', + 126, + ], + [ + 'Foreach value variable $v is never read.', + 133, + ], + [ + 'Variable $a is never read.', + 148, + ], + [ + 'Value of variable $i after ++ is never read.', + 157, + ], + [ + 'Variable $x is never read.', + 223, + ], + [ + 'Value assigned to variable $x is never read.', + 251, + ], + [ + 'Value assigned to variable $s is never read.', + 264, + ], + [ + 'Variable $a is never read.', + 276, + ], + [ + 'Variable $f is never read.', + 283, + ], + [ + 'Variable $a is never read.', + 303, + ], + [ + 'Variable $x is never read.', + 337, + ], + [ + 'Value assigned to variable $title is never read.', + 422, + ], + [ + 'Variable $b is never read.', + 614, + ], + [ + 'Variable $a is never read.', + 632, + ], + [ + 'Variable $a is never read.', + 637, + ], + [ + 'Value assigned to variable $a is never read.', + 703, + ], + [ + 'Variable $a is never read.', + 739, + ], + [ + 'Value assigned to variable $a is never read.', + 744, + ], + [ + 'Value assigned to variable $tags is never read.', + 840, + ], + [ + 'Value of variable $i after -- is never read.', + 864, + ], + [ + 'Value assigned to variable $x is never read.', + 870, + ], + [ + 'Foreach value variable $v is never read.', + 877, + ], + [ + 'Value of variable $i after ++ is never read.', + 885, + ], + [ + 'Value of variable $i after -- is never read.', + 892, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testPhp8(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-php8.php'], [ + [ + 'Catch variable $e is never read.', + 23, + ], + [ + 'Value assigned to variable $nightsFrom is never read.', + 98, + ], + ]); + } + + public function testBug12789(): void + { + $this->analyse([__DIR__ . '/data/bug-12789.php'], [ + [ + 'Variable $RetVal is never read.', + 12, + ], + ]); + } + + public function testBug13472(): void + { + $this->analyse([__DIR__ . '/data/bug-13472.php'], [ + [ + 'Value assigned to variable $v is never read.', + 14, + ], + [ + 'Foreach value variable $item is never read.', + 41, + ], + ]); + } + + public function testBug14258(): void + { + $this->analyse([__DIR__ . '/data/bug-14258.php'], [ + [ + 'Variable $cutsomerId is never read.', + 15, + ], + ]); + } + + public function testBug12012(): void + { + $this->analyse([__DIR__ . '/data/bug-12012.php'], [ + [ + 'Value assigned to variable $s1 is never read.', + 10, + ], + [ + 'Value assigned to variable $s1 is never read.', + 12, + ], + ]); + } + + public function testBug11483(): void + { + $this->analyse([__DIR__ . '/data/bug-11483.php'], [ + [ + 'Value assigned to variable $hello is never read.', + 9, + ], + ]); + } + + public function testBug10202(): void + { + $this->analyse([__DIR__ . '/data/bug-10202.php'], [ + [ + 'Variable $x is never read.', + 9, + ], + [ + 'Variable $x is never read.', + 12, + ], + [ + 'Variable $x is never read.', + 14, + ], + ]); + } + + #[RequiresPhp('< 8.0.0')] + public function testCatchVariableNotReportedBeforePhp80(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-catch.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testCatchVariableReportedSincePhp80(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-catch.php'], [ + [ + 'Catch variable $e is never read.', + 9, + ], + ]); + } + + public function testRedundantAssignment(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-redundant.php'], [ + [ + 'Variable $x is assigned value true but it already has that value.', + 26, + ], + [ + 'Value assigned to variable $x is never read.', + 42, + ], + [ + 'Variable $x is assigned value 1 but it already has that value.', + 43, + ], + [ + 'Variable $x is assigned value null but it already has that value.', + 51, + ], + [ + 'Variable $s is assigned value \'a\' but it already has that value.', + 60, + ], + [ + 'Variable $a is assigned value array{k: 1} but it already has that value.', + 69, + ], + [ + 'Value assigned to variable $x is never read.', + 95, + ], + [ + 'Variable $x is assigned value 1 but it already has that value.', + 118, + ], + ]); + } + + public function testByRefReturn(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-by-ref-return.php'], [ + [ + 'Value assigned to variable $x is never read.', + 26, + ], + [ + 'Variable $unused is never read.', + 32, + ], + [ + 'Value assigned to variable $x is never read.', + 68, + ], + ]); + } + + public function testDeadBranchWrites(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-dead-branch.php'], [ + [ + 'Value assigned to variable $x is never read.', + 79, + ], + ]); + } + + public function testResultFlow(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-result-flow.php'], [ + ['Value assigned to variable $shadowed is never read.', 15], + ['Value assigned to variable $value is never read.', 16], + ['Value assigned to variable $value is never read.', 73], + ['Value assigned to variable $value is never read.', 98], + ['Value assigned to variable $value is never read.', 128], + ['Variable $value is never read.', 136], + ['Value assigned to variable $value is never read.', 148], + ['Value assigned to variable $value is never read.', 160], + ['Value assigned to variable $value is never read.', 167], + ]); + } + + public function testForeachWithoutPollution(): void + { + $this->polluteScopeWithAlwaysIterableForeach = false; + $this->analyse([__DIR__ . '/data/unused-variable-foreach-pollution.php'], []); + } + + public function testUsageFlow(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-usage-flow.php'], []); + } + + public function testNestedOffsetWrites(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-nested-offset-writes.php'], []); + } + + public function testArrowReferenceParameters(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-arrow-reference.php'], [ + ['Value assigned to variable $value is never read.', 14], + ]); + } + + public function testBroadCatchesIncludeImplicitThrows(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-broad-catch.php'], [ + ['Value assigned to variable $file is never read.', 71], + ]); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10202.php b/tests/PHPStan/Rules/DeadCode/data/bug-10202.php new file mode 100644 index 00000000000..6223e5ae0b7 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10202.php @@ -0,0 +1,17 @@ +text'; + + $s1 = 'something else'; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12789.php b/tests/PHPStan/Rules/DeadCode/data/bug-12789.php new file mode 100644 index 00000000000..6135363fe45 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12789.php @@ -0,0 +1,16 @@ + 4) { + // Typo in next line - should be $retVal rather than $RetVal + $RetVal = $str2 . $str1; + } + return $retVal; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-13472.php b/tests/PHPStan/Rules/DeadCode/data/bug-13472.php new file mode 100644 index 00000000000..63e2430a23a --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-13472.php @@ -0,0 +1,47 @@ +dummyConsume($v); + $v = 10; + + return $v; + } + + public function testOverwrittenButUsed2(): int + { + $v = 1; + $v = $v + 1; + + return $v; + } + + /** @param list $possiblyEmptyList */ + public function testOverwrittenButUsed3(array $possiblyEmptyList): int + { + $v = 1; + foreach ($possiblyEmptyList as $item) { + $v = 2; + } + + return $v; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-14258.php b/tests/PHPStan/Rules/DeadCode/data/bug-14258.php new file mode 100644 index 00000000000..70631728f5a --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-14258.php @@ -0,0 +1,19 @@ += 8.0 + + +namespace UnusedVariableArrowReference; + +function normalize(array $values): array +{ + array_walk_recursive($values, fn (&$value) => $value = trim($value)); + return $values; +} + +function callback(): \Closure +{ + $value = 1; + return fn (&$value) => $value = 2; +} + +function terminatingCallback(): \Closure +{ + return fn (&$value) => throw new \RuntimeException($value = 'changed'); +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-broad-catch.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-broad-catch.php new file mode 100644 index 00000000000..da0b27ad9f2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-broad-catch.php @@ -0,0 +1,79 @@ += 8.0 + +namespace UnusedVariableBroadCatch; + +function readDocumentation(\ReflectionParameter $parameter): void +{ + if ($parameter->getName() === 'required') { + throw new \RuntimeException('Missing documentation'); + } +} + +function defaultValue(\ReflectionParameter $parameter): mixed +{ + try { + readDocumentation($parameter); + return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + } catch (\Exception) { + $default = null; + if ($parameter->isDefaultValueAvailable()) { + $default = $parameter->getDefaultValue(); + } + return $default; + } +} + +function readDefinition(bool $fail): void +{ + if ($fail) { + throw new \Error('Missing definition'); + } +} + +/** @throws \TypeError */ +function checkType(): void +{ + throw new \TypeError(); +} + +function definition(bool $fail, bool $check): ?int +{ + try { + readDefinition($fail); + if ($check) { + checkType(); + } + return 1; + } catch (\Error) { + $fallback = null; + if ($check) { + $fallback = 0; + } + return $fallback; + } +} + +/** @throws \RuntimeException */ +function loadFile(): object +{ + return new \stdClass(); +} + +function validateFile(object $file): void +{ + if (!isset($file->valid)) { + throw new \RuntimeException('Invalid file'); + } +} + +function validatedFile(): ?object +{ + $file = null; + try { + $file = loadFile(); + validateFile($file); + } catch (\Exception) { + $file = null; + } + return $file; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-by-ref-return.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-by-ref-return.php new file mode 100644 index 00000000000..d7e1bdfd987 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-by-ref-return.php @@ -0,0 +1,73 @@ + 0) { + sink($x); + } elseif (false) { + $x = 3; + } + sink($x); +} + +function writeInElseifAfterAlwaysTrue(int $i): void +{ + $x = 1; + if (true) { + sink($x); + } elseif ($i > 0) { + $x = 3; + } + sink($x); +} + +function deadBranchInsideLoop(array $items): void +{ + $result = []; + foreach ($items as $item) { + if (false) { + $result[] = 1; + } else { + $result[] = 2; + } + } + sink($result); + sink($item ?? null); +} + +function liveBranchesStillReport(int $i): void +{ + $x = 1; + if ($i > 0) { + $x = 2; + $x = 3; + } + sink($x); +} + +/** + * @param mixed $maybeint + */ +function absint($maybeint): int +{ + if (!is_numeric($maybeint)) { + return 0; + } + $value = abs((int) $maybeint); + if (is_float($value)) { + $value = PHP_INT_MAX; + } + + return $value; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-foreach-pollution.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-foreach-pollution.php new file mode 100644 index 00000000000..524347f208a --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-foreach-pollution.php @@ -0,0 +1,12 @@ + 1]); + return $data; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-php8.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-php8.php new file mode 100644 index 00000000000..af4f6c58c73 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-php8.php @@ -0,0 +1,103 @@ += 8.0 + +namespace UnusedVariableRulePhp8; + +/** @param mixed $v */ +function sink($v): void +{ +} + +/** + * @return mixed + * @phpstan-impure + */ +function source() +{ + return rand(); +} + +function catchUnused(): void +{ + try { + sink(1); + } catch (\Exception $e) { // unused $e + } +} + +function catchUsed(): void +{ + try { + sink(1); + } catch (\Exception $e) { + sink($e); + } +} + +function matchRead(int $i): int +{ + $a = source(); + return match ($i) { + 1 => $a, + default => 0, + }; +} + +function nullsafeRead(?\stdClass $o): void +{ + $x = $o?->foo; + sink($x); +} + +function namedArgs(): void +{ + $v = 1; + sink(v: $v); +} + +class NullsafeReads +{ + + public function __construct(private ?\DateTimeImmutable $maxDate, private ?NullsafeReads $inner, private ?\ArrayObject $bag) + { + } + + public function argumentOfNullsafeCall(): ?\DateTimeImmutable + { + $nightsFrom = 1; + + return $this->maxDate?->modify(sprintf('-%d days', $nightsFrom)); + } + + public function argumentOfNestedNullsafeCall(): void + { + $weekDay = 3; + sink($this->inner?->argumentOfNullsafeCallWith($weekDay)); + } + + public function argumentOfNullsafeCallWith(int $weekDay): int + { + return $weekDay; + } + + public function nullsafeInCondition(): void + { + $time = new \DateTimeImmutable(); + if ($this->maxDate?->getTimestamp() === $time->getTimestamp()) { + sink(1); + } + } + + public function nullsafeOffsetOnPropertyFetch(): void + { + $key = 'k'; + sink($this->bag?->offsetGet($key)); + } + + public function unreadArgumentVariable(): void + { + $nightsFrom = 1; // unused $nightsFrom + $nightsFrom = 2; + sink($this->maxDate?->modify(sprintf('-%d days', $nightsFrom))); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant.php new file mode 100644 index 00000000000..5b8d93fc590 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-redundant.php @@ -0,0 +1,134 @@ + 1]; + if (cond()) { + $a = ['k' => 1]; + } + sink($a); +} + +function wideBoolIsNotRedundant(): void +{ + $x = cond(); + if (cond()) { + $x = true; + } + sink($x); +} + +function nativeTypeMustAgree(): void +{ + $x = true; + if (cond()) { + $x = alwaysTrue(); + } + sink($x); +} + +function maybeDefinedIsNotRedundant(): void +{ + if (cond()) { + $x = true; + } + $x = true; + sink($x); +} + +function firstAssignmentIsNotRedundant(): void +{ + $x = true; + sink($x); +} + +function readModifyWriteIsNotConsidered(): void +{ + $i = 1; + $i += 0; + sink($i); +} + +function redundantInEveryLoopIteration(): void +{ + $x = 1; + while (cond()) { + $x = 1; + } + sink($x); +} + +function redundantOnFirstPassOnly(): void +{ + $x = 1; + while (cond()) { + sink($x); + $x = $x + 1; + if (cond()) { + $x = 1; + } + } + sink($x); +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-result-flow.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-result-flow.php new file mode 100644 index 00000000000..7ec03393631 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-result-flow.php @@ -0,0 +1,201 @@ += 8.1 + +namespace UnusedVariableResultFlow; + +function capturesAsArguments(): array +{ + $arrow = 1; + $closure = 2; + return array_map(static fn ($x) => $x + $arrow, array_map(static function ($x) use ($closure) { return $x + $closure; }, [1])); +} + +function arrowIsolation(): array +{ + $value = 1; + $shadowed = 2; + return [fn () => $value = 3, fn ($shadowed) => $shadowed, $value]; +} + +function arrowThrow(): int +{ + $value = 1; + $callback = fn () => throw new \Exception(); + echo $callback::class; + return $value; +} + +function loopExit(): int +{ + $values = [1, 2]; + while ($values !== []) { + array_pop($values); + } + $result = 3; + return $result; +} + +function forExpressions(): int +{ + $init = 1; + $condition = 2; + $update = 3; + for ($i = $init; $condition, $i < 4; $i += $update) { + echo $i; + } + return $i; +} + +function switchConditions(int $subject): int +{ + $value = 1; + switch ($subject) { + case 0: + return $value; + case ($value = 2): + return $value; + default: + return $value; + } +} + +function matchConditions(int $subject): int +{ + $value = 1; + return match ($subject) { + 0 => $value, + ($value = 2), ($value = 3) => $value, + default => $value, + }; +} + +function branchOverwrite(bool $flag): int +{ + $value = 1; + if ($flag) { + $value = 2; + } else { + $value = 3; + } + return $value; +} + +function finallyReturn(bool $flag): int +{ + $value = 1; + try { + if ($flag) { + return $value; + } + $value = 2; + throw new \Exception(); + } finally { + echo $value; + } +} + +function finallyOverride(): int +{ + $value = 1; + try { + return 0; + } finally { + $value = 2; + return $value; + } +} + +function nestedContinue(): int +{ + $value = 0; + for ($i = 0; $i < 2; $i++) { + try { + while (rand(0, 1)) { + $value = 1; + continue 2; + } + } finally { + echo $value; + } + } + return $value; +} + +function catchReads(callable $callback): void +{ + $value = 1; + try { + $callback(); + $value = 2; + } catch (\RuntimeException $e) { + echo $value, $e->getMessage(); + } +} + +function namespacedCompact(): int +{ + $value = 1; + compact('value'); + return 0; +} + +function compact(string $value): void +{ + echo $value; +} + +function overwrittenInNonEmptyLoop(): int +{ + $value = 1; + foreach ([2, 3] as $item) { + $value = $item; + } + return $value; +} + +function doOnce(): int +{ + $value = 1; + do { + echo $value; + $value = 2; + } while (false); + return 0; +} + +function neverForLoop(): void +{ + $value = 1; + for (; false;) { + echo $value; + } +} + +function referenceArgument(callable $reader): void +{ + $value = 1; + readReference($value); + $value = 2; + $reader(); +} + +function readReference(int &$value): void +{ + echo $value; +} + +function arrayReference(): void +{ + $value = 1; + $array = [[&$value]]; + $array[0][0] = 2; + echo $value; +} + +function firstClassCallables(): array +{ + $object = new \ArrayObject(); + $method = 'getIterator'; + $function = 'strlen'; + $class = \DateTimeImmutable::class; + return [$object->$method(...), $function(...), $class::createFromFormat(...)]; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-throwable-catch.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-throwable-catch.php new file mode 100644 index 00000000000..4fc42a76b5f --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-throwable-catch.php @@ -0,0 +1,41 @@ +getDisplayName(); + try { + $session->handleEncoded($packet); + } catch (\DomainException $e) { + echo $e->getMessage(); + } catch (\Throwable $e) { + echo "Crash occurred while handling a packet from session: $name"; + throw $e; + } +} + +function receiveNested(Session $session, string $packet): void +{ + $name = $session->getDisplayName(); + try { + try { + $session->handleEncoded($packet); + } catch (\DomainException $e) { + echo $e->getMessage(); + } + } catch (\Throwable $e) { + echo "Crash occurred while handling a packet from session: $name"; + throw $e; + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable-usage-flow.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable-usage-flow.php new file mode 100644 index 00000000000..3f2affc6630 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable-usage-flow.php @@ -0,0 +1,71 @@ += 8.3 + +namespace UnusedVariableUsageFlow; + +function dynamicGlobal(): void +{ + $name = 'shared'; + global $$name; +} + +function staticInitializer(): void +{ + $value = 123; + static $cached = $value; +} + +function terminatingOperands(bool $condition): void +{ + $error = new \RuntimeException(); + $condition && throw $error; + $condition || throw $error; +} + +function terminatingBranches(int $value): void +{ + $error = new \RuntimeException(); + switch ($value) { + case 0: + throw $error; + default: + return; + } +} + +function finallyReads(): void +{ + $value = 123; + try { + return; + } finally { + echo $value; + } +} + +function nullsafeCall(?\Closure $callback): void +{ + $value = 123; + $callback?->__invoke($value); +} + +function nestedCaptures(): \Closure +{ + $value = 123; + return function () use ($value): \Closure { + return fn () => $value; + }; +} + +function nullsafeDynamicMethod(?object $object): void +{ + $method = 'before'; + $object?->{$method = 'after'}(); + echo $method; +} + +function nullsafeDynamicProperty(?object $object): void +{ + $property = 'before'; + $object?->{$property = 'after'}; + echo $property; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-variable.php b/tests/PHPStan/Rules/DeadCode/data/unused-variable.php new file mode 100644 index 00000000000..9cb48ef6fd1 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-variable.php @@ -0,0 +1,893 @@ + $v) { // unused $k + sink($v); + } +} + +function foreachValueUnused(array $arr): void +{ + foreach ($arr as $k => $v) { // unused $v + sink($k); + } +} + +function foreachValueOnlyUnused(array $arr): void +{ + foreach ($arr as $v) { // unused $v + sink(1); + } +} + +function foreachListUsed(array $arr): void +{ + foreach ($arr as [$a, $b]) { + sink($a); + sink($b); + } +} + +function foreachListPartiallyUsed(array $arr): void +{ + foreach ($arr as [$a, $b]) { // unused $a + sink($b); + } +} + +function incrementLast(): void +{ + $i = 0; + sink($i); + $i++; // unused $i +} + +function forLoop(): void +{ + for ($i = 0; $i < 3; $i++) { + sink($i); + } +} + +function forLoopCounterOnlyInCondition(): void +{ + for ($i = 0; $i < 3; $i++) { + sink(1); + } +} + +function whileAssignInCondition(): void +{ + while (($line = source()) !== false) { + sink($line); + } +} + +function doWhile(): void +{ + $i = 5; + do { + sink(1); + } while (--$i > 0); + sink($i); +} + +function doWhileNoReadAfter(): void +{ + $i = 5; + do { + sink(1); + } while (--$i > 0); +} + +function backEdgeRead(): void +{ + $x = 0; + while (cond()) { + sink($x); + $x = source(); + } +} + +function backEdgeReadWithContinue(): void +{ + $x = 0; + while (cond()) { + if (cond()) { + $x = 1; + continue; + } + sink($x); + $x = 2; + } +} + +function loopWriteNeverRead(): void +{ + while (cond()) { + $x = source(); // unused $x + } +} + +function loopWriteReadNextIterationOnly(): void +{ + while (cond()) { + if (cond()) { + $x = 1; + } + if (isset($x)) { + sink($x); + } + } +} + +function arrayBuildReturned(): array +{ + $x = []; + $x[] = 1; + $x['k'] = 2; + return $x; +} + +function arrayBuildUnused(): void +{ + $x = []; + $x[] = 1; + $x['k'] = 2; // unused $x +} + +function stringAppendReturned(): string +{ + $s = 'a'; + $s .= 'b'; + return $s; +} + +function stringAppendUnused(): void +{ + $s = 'a'; + $s .= 'b'; // unused $s +} + +function unsetAfterWrite(): void +{ + $a = 1; // known false negative: unset() walks the variable as a read + unset($a); +} + +function closureBodyDeadWrite(): void +{ + $f = function (): void { + $a = 1; // unused $a + }; + $f(); +} + +function closureAssignedUnused(): void +{ + $f = function (): void { // unused $f + }; +} + +function switchBranchesRead(): void +{ + switch (rand(0, 2)) { + case 0: + $a = true; + break; + default: + $a = false; + } + sink($a); +} + +function switchBranchDeadWrite(): void +{ + switch (rand(0, 2)) { + case 0: + $a = 1; // unused $a + break; + default: + sink(1); + } +} + +function tryFinallyRead(): void +{ + $var = ''; + try { + if (cond()) { + throw new \Exception(); + } + $var = 'hello'; + } finally { + sink($var); + } +} + +function tryCatchRead(): void +{ + try { + $x = source(); + } catch (\Exception $e) { + sink($e); + $x = null; + } + sink($x); +} + +function tryDeadWrite(): void +{ + try { + $x = source(); // unused $x + } catch (\Exception $e) { + sink($e); + } +} + +function issetRead(): void +{ + if (cond()) { + $j = 'hello'; + } + if (isset($j)) { + sink($j); + } +} + +function emptyRead(): void +{ + $j = source(); + if (empty($j)) { + sink(1); + } +} + +function coalesceRead(): void +{ + $j = source(); + sink($j ?? 1); +} + +function coalesceAssign(?int $b, int $c): void +{ + $b ??= $c; + sink($b); +} + +function compactRead(): array +{ + $a = 1; + $b = 2; + return compact('a', 'b'); +} + +function compactDynamic(string $name): array +{ + $a = 1; + return compact($name); +} + +function variableVariableRead(string $name): void +{ + $a = 1; + sink($$name); +} + +function variableVariableConstantRead(): void +{ + $a = 1; + $name = 'a'; + sink(${$name}); +} + +function extractAfterWrite(array $arr): void +{ + $a = 1; + extract($arr); + sink($a); +} + +function getDefinedVarsRead(): array +{ + $a = 1; + return get_defined_vars(); +} + +function includeReadsEverything(): void +{ + $title = 'x'; + include 'template.php'; +} + +function includeThenOverwrite(): void +{ + $title = 'x'; + include 'template.php'; + $title = 'y'; // unused $title +} + +function evalReadsEverything(): void +{ + $title = 'x'; + eval('echo $title;'); +} + +function gotoOpaque(): void +{ + $a = 1; + goto end; + end: + sink(1); +} + +function staticVar(): void +{ + static $token; + $token = source(); +} + +function staticVarRead(): int +{ + static $token; + if (!$token) { + $token = rand(1, 10); + } + return $token; +} + +function globalVar(): void +{ + global $a; + $a = 'hello'; +} + +function byRefParam(array &$p): void +{ + $p = [0]; +} + +function pregMatchByRef(): array +{ + preg_match('/x/', 'x', $m); + return $m; +} + +function pregMatchByRefUnused(): void +{ + preg_match('/x/', 'x', $m); +} + +function sortByRef(array $arr): void +{ + sort($arr); +} + +function arrayPushByRef(): array +{ + $arr = []; + array_push($arr, 1); + return $arr; +} + +function foreachByRef(array $a): array +{ + foreach ($a as &$v) { + $v = 1; + } + return $a; +} + +function assignRef(): void +{ + $b = 1; + $a = &$b; + $a = 2; +} + +function closureUseByValue(): void +{ + $i = 0; + $f = function () use ($i): int { + return $i + 1; + }; + $f(); +} + +function closureUseByRef(): void +{ + $i = 0; + $f = function () use (&$i): void { + $i = 1; + }; + $f(); + sink($i); +} + +function closureUseByRefNoReadAfter(): void +{ + $i = 0; + $f = function () use (&$i): void { + $i = 1; + }; + $f(); +} + +function recursiveClosure(): void +{ + $f = function () use (&$f): void { + $f(); + }; + $f(); +} + +function arrowFunctionCapture(): void +{ + $x = 1; + $f = fn (): int => $x + 1; + $f(); +} + +function dynamicMethodName(object $o): void +{ + $name = 'foo'; + $o->$name(); +} + +function dynamicClassConst(): void +{ + $class = \stdClass::class; + sink($class::FOO); +} + +function dynamicNew(): void +{ + $class = \stdClass::class; + new $class(); +} + +function dynamicStaticProperty(): void +{ + $class = \stdClass::class; + $class::$prop = 1; +} + +function dimWriteOnObject(\ArrayAccess $o): void +{ + $o['k'] = 1; +} + +function dimWriteOnObjectFromNew(): void +{ + $o = new \ArrayObject(); + $o['k'] = 1; +} + +/** @param mixed $m */ +function dimWriteOnMixed($m): void +{ + $m['k'] = 1; +} + +function superglobalDimWrite(): void +{ + $_SESSION['k'] = 1; +} + +function propertyWriteThroughLocal(): void +{ + $o = new \stdClass(); + $o->p = 1; +} + +/** @param mixed $y */ +function varAnnotation($y): void +{ + /** @var \stdClass $y */ + $y->m(); +} + +function varAnnotationAfterWrite(): void +{ + /** @var \stdClass $y */ + $y = source(); + $y->m(); +} + +function chainedAssign(): void +{ + $a = $b = 1; // unused $b + sink($a); +} + +function underscorePrefix(): void +{ + $_ = source(); + $_unused = source(); +} + +function parameterOverwritten($a): int +{ + $a = 1; + return $a; +} + +function parameterOverwrittenUnread($a): void +{ + $a = 1; // unused $a +} + +function nestedFunctionScopes(): void +{ + $a = 1; // unused $a + $f = function (): void { + $a = 2; + sink($a); + }; + $f(); +} + +function ternaryRead(): int +{ + $a = source(); + return cond() ? $a : 0; +} + +function instanceofRead(): bool +{ + $a = source(); + return $a instanceof \stdClass; +} + +function usedAsArrayKey(): array +{ + $k = 'a'; + return [$k => 1]; +} + +function usedInStringInterpolation(): string +{ + $name = 'x'; + return "hello $name"; +} + +function yieldRead(): \Generator +{ + $a = 1; + yield $a; +} + +function throwRead(): void +{ + $e = new \Exception(); + throw $e; +} + +function echoRead(): void +{ + $a = 1; + echo $a; +} + +function castRead(): int +{ + $a = '1'; + return (int) $a; +} + +function cloneRead(): object +{ + $o = new \stdClass(); + return clone $o; +} + +function selfReferentialChain(): void +{ + // the Psalm layer will also report the first write; phase 1 sees it read by the second + $a = 5; + $a = $a + 1; // unused $a +} + +function articleExample(): void +{ + // Psalm reports every write of $b; phase 1 sees each read by the self-chain + $b = $a = 0; + while (cond()) { + if (cond() && cond()) { + $a = 5; + break; + } + if (cond()) { + continue; + } + $a = $a + 1; + $b = $b + 1; + } + sink($a); +} + +class Foo +{ + + /** @var mixed */ + private $prop; + + public function __construct() + { + $x = 1; + $this->prop = $x; + $this->init(); + } + + private function init(): void + { + $a = 1; // unused $a + } + + public function overwritten(): void + { + $a = 1; // unused $a + $a = 2; + sink($a); + } + + public static function staticMethod(): void + { + $x = 1; + self::helper($x); + } + + /** @param mixed $x */ + private static function helper($x): void + { + } + + public function thisPropertyWrite(): void + { + $this->prop = 1; + } + +} + +/** + * @param list $tokens + * @return list + */ +function nestedWritesReadOnNextIteration(array $tokens): array +{ + $comment = null; + $expected = null; + $open = 0; + $ids = []; + foreach ($tokens as [$type, $content]) { + if ($type === 1) { + if ($open > 0) { + $comment .= $content; + } + $open++; + $expected = null; + continue; + } + if ($type === 2) { + $open--; + if ($open === 0) { + $key = array_key_last($ids); + if ($key !== null) { + $ids[$key]['comment'] = $comment; + $comment = null; + } + $expected = [3, 4]; + } else { + $comment .= $content; + } + continue; + } + if ($open > 0) { + $comment .= $content; + continue; + } + if ($expected !== null && !in_array($type, $expected, true)) { + throw new \Exception(); + } + $ids[] = ['comment' => null]; + $expected = [1]; + } + + return $ids; +} + +/** + * @param list<\stdClass> $xs + */ +function branchDeadInFirstIteration(array $xs): array +{ + $winners = []; + $winning = null; + foreach ($xs as $x) { + if ($winning === null) { + $winners[] = $x; + $winning = $x; + } else { + $c = $winning == $x; + if ($c) { + $winners = [$x]; + $winning = $x; + } + } + } + + return $winners; +} + +function recursiveClosureReportsOnce(): void +{ + $check = function (int $n) use (&$check): void { + $tags = []; // unused $tags + if ($n > 0) { + $tags = [$n]; + sink($tags); + } + $check($n - 1); + }; + $check(3); +} + +function closureBodyWritesStayInClosure(): void +{ + $outer = 1; + $f = function () use ($outer): int { + $inner = 2; + return $outer + $inner; + }; + sink($f()); +} + +function decrementLast(): void +{ + $i = 10; + sink($i); + $i--; // unused $i +} + +function readBeforeOnlyWrite(): void +{ + sink($x ?? null); + $x = 2; // a dead store, not an unused variable: $x is read above +} + +function foreachValueOverwritesUsedVariable(): void +{ + $v = 1; + sink($v); + foreach ([1, 2] as $v) { // unused $v + } +} + +function preIncrementLast(): void +{ + $i = 0; + sink($i); + ++$i; // unused $i +} + +function preDecrementLast(): void +{ + $i = 10; + sink($i); + --$i; // unused $i +} diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 9d2db0fb2d1..b29ffd478e5 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -2,9 +2,7 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\Rule; -use PHPStan\Rules\UnusedFunctionParametersCheck; use PHPStan\Testing\RuleTestCase; /** @@ -15,7 +13,12 @@ class UnusedClosureUsesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck(self::createReflectionProvider(), self::getContainer()->getByType(InitializerExprTypeResolver::class), true)); + return new UnusedClosureUsesRule(true); + } + + public function testCapturesAssignedThroughVariableVariables(): void + { + $this->analyse([__DIR__ . '/data/unused-closure-uses-variable-variables.php'], []); } public function testUnusedClosureUses(): void @@ -40,4 +43,25 @@ public function testUnusedClosureUses(): void ]); } + public function testResolvedDynamicUsages(): void + { + $this->analyse([__DIR__ . '/data/unused-closure-uses-resolved-dynamic.php'], [ + [ + 'Anonymous function has an unused use $other.', + 6, + ], + [ + 'Anonymous function has an unused use $overwritten.', + 22, + ], + ]); + } + + public function testReferenceCapturedInSkippedCatch(): void + { + $this->analyse([__DIR__ . '/data/unused-closure-uses-skipped-catch.php'], [ + ['Anonymous function has an unused use $unused.', 15], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/UnusedFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedFunctionParametersRuleTest.php new file mode 100644 index 00000000000..3bd7b6b7b36 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/UnusedFunctionParametersRuleTest.php @@ -0,0 +1,46 @@ + + */ +class UnusedFunctionParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UnusedFunctionParametersRule(self::getContainer()->getByType(UnusedParametersCheck::class)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/unused-function-parameters.php'], [ + [ + 'Function UnusedFunctionParameters\completelyUnused() has an unused parameter $unused.', + 5, + ], + [ + 'Function UnusedFunctionParameters\immediatelyReassigned() has an unused parameter $x.', + 10, + ], + [ + 'Function UnusedFunctionParameters\byRefUnused() has an unused parameter $x.', + 38, + ], + [ + 'Function UnusedFunctionParameters\variadicUnused() has an unused parameter $rest.', + 50, + ], + [ + 'Function UnusedFunctionParameters\usedViaResolvedVariableVariable() has an unused parameter $other.', + 79, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/unused-closure-uses-resolved-dynamic.php b/tests/PHPStan/Rules/Functions/data/unused-closure-uses-resolved-dynamic.php new file mode 100644 index 00000000000..f825a3be151 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/unused-closure-uses-resolved-dynamic.php @@ -0,0 +1,51 @@ +click($xpath); + return true; + } catch (\RuntimeException $caught) { + $exception = $caught; + return null; + } + }; + if ($callback() !== true) { + throw $exception; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/unused-closure-uses-variable-variables.php b/tests/PHPStan/Rules/Functions/data/unused-closure-uses-variable-variables.php new file mode 100644 index 00000000000..2a608ab3237 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/unused-closure-uses-variable-variables.php @@ -0,0 +1,22 @@ + $word) { + if (preg_match("/^$word\$/iu", $chunk)) { + return $toTranslations[$index] ?? ''; + } + } + return $chunk; + }; +} diff --git a/tests/PHPStan/Rules/Functions/data/unused-function-parameters.php b/tests/PHPStan/Rules/Functions/data/unused-function-parameters.php new file mode 100644 index 00000000000..04aab07cffc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/unused-function-parameters.php @@ -0,0 +1,115 @@ + $x; +} + +function closureParametersAreNotReported(): callable +{ + return function (int $x): int { + return 0; + }; +} + +function usedViaCompact(string $key): array +{ + return compact('key'); +} + +function usedViaResolvedVariableVariable(int $secret, int $other): int +{ + $name = 'secret'; + + return $$name; +} + +/** + * @param mixed[] $arr + * @phpstan-assert-if-true string[] $arr + */ +function assertedParameterIsContract(array $arr): bool +{ + return true; +} + +/** + * @template T of array + * @param T $array + * @return T[($maybeZero is 0 ? 0 : key-of)] + */ +function parameterInConditionalReturnType(array $array, int $maybeZero): int +{ + return $array[0]; +} + +function commentOnlyBody(int $color): void +{ + // Omitted +} + +function observedByFuncGetArg(int $x): array +{ + $x = 1; + + return [func_get_arg(0), $x]; +} diff --git a/tests/PHPStan/Rules/Methods/UnusedMethodParametersRuleTest.php b/tests/PHPStan/Rules/Methods/UnusedMethodParametersRuleTest.php new file mode 100644 index 00000000000..aaf73a939fe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/UnusedMethodParametersRuleTest.php @@ -0,0 +1,42 @@ + + */ +class UnusedMethodParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UnusedMethodParametersRule(self::getContainer()->getByType(UnusedParametersCheck::class)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/unused-method-parameters.php'], [ + [ + 'Method UnusedMethodParameters\Foo::completelyUnused() has an unused parameter $unused.', + 24, + ], + [ + 'Method UnusedMethodParameters\Foo::immediatelyReassigned() has an unused parameter $x.', + 29, + ], + [ + 'Method UnusedMethodParameters\Foo::byRefUnused() has an unused parameter $x.', + 57, + ], + [ + 'Method UnusedMethodParameters\Foo::staticCompletelyUnused() has an unused parameter $unused.', + 69, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/unused-method-parameters.php b/tests/PHPStan/Rules/Methods/data/unused-method-parameters.php new file mode 100644 index 00000000000..7b06686ad1e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/unused-method-parameters.php @@ -0,0 +1,114 @@ +y = $constructorParameter; + } + + public function publicMethod(int $unused): void + { + } + + protected function protectedMethod(int $unused): void + { + } + + private function completelyUnused(int $used, int $unused): int + { + return $used; + } + + private function immediatelyReassigned(int $x): int + { + $x = 1; + + return $x; + } + + private function readThenReassigned(int $x): int + { + $x = $x + 1; + + return $x; + } + + private function reassignedInOneBranch(int $x): int + { + if (rand(0, 1) === 0) { + $x = 1; + } + + return $x; + } + + private function byRefWritten(int &$x): void + { + $x = 1; + } + + private function byRefUnused(int &$x): void + { + echo 'no use of the reference'; + } + + private function observedByFuncGetArgs(int $x): array + { + $x = 1; + + return [func_get_args(), $x]; + } + + private static function staticCompletelyUnused(int $unused): void + { + echo 'side effect only'; + } + + private function usedOnlyInClosureUse(int $x): callable + { + return function () use ($x): int { + return $x; + }; + } + + private function __invoke(int $magicParameter): void + { + } + + /** + * @param mixed $value + * @phpstan-assert-if-true int $value + */ + private function assertedParameterIsContract($value): bool + { + return true; + } + + private function commentOnlyBody(int $color): void + { + // Omitted + } + + public function callThemAll(): void + { + $this->completelyUnused(1, 2); + $this->immediatelyReassigned(1); + $this->readThenReassigned(1); + $this->reassignedInOneBranch(1); + $a = 1; + $this->byRefWritten($a); + $this->byRefUnused($a); + $this->observedByFuncGetArgs(1); + self::staticCompletelyUnused(1); + $this->usedOnlyInClosureUse(1); + $this->assertedParameterIsContract(1); + } + +} diff --git a/tests/e2e/anon-class/Granularity.php b/tests/e2e/anon-class/Granularity.php index 9762130417d..7e95c30f67f 100644 --- a/tests/e2e/anon-class/Granularity.php +++ b/tests/e2e/anon-class/Granularity.php @@ -10,7 +10,7 @@ protected static function provideInstances(): array { $myclass = new class() extends Granularity { }; - return []; + return [$myclass]; } } diff --git a/tests/e2e/data/soap.php b/tests/e2e/data/soap.php index d75edf768ed..cf41d88ec14 100644 --- a/tests/e2e/data/soap.php +++ b/tests/e2e/data/soap.php @@ -37,8 +37,10 @@ public function __construct($wsdl, array $options = null) function () { $soap = new MySoapClient('some.wsdl', ['soap_version' => SOAP_1_2]); - $soap = new MySoapClient2('some.wsdl', ['soap_version' => SOAP_1_2]); - $soap = new MySoapClient3('some.wsdl', ['soap_version' => SOAP_1_2]); + $soap2 = new MySoapClient2('some.wsdl', ['soap_version' => SOAP_1_2]); + $soap3 = new MySoapClient3('some.wsdl', ['soap_version' => SOAP_1_2]); + + return [$soap, $soap2, $soap3]; }; class MySoapHeader extends \SoapHeader @@ -53,6 +55,8 @@ public function __construct(string $username, string $password) function () { $header = new MySoapHeader('user', 'passw0rd'); + + return $header; }; function (\SoapFault $fault) { diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp index 81b321419bc..23f8c77c01a 100644 --- a/turbo-ext/src/ScopeOps.cpp +++ b/turbo-ext/src/ScopeOps.cpp @@ -748,6 +748,9 @@ class ScopeOps if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) { return zv::Val(); } + if (instanceof_function(holderExpr(mergedHolder)->ce, virtualNodeCe)) { + continue; + } for (auto guardEntry : zv::TableRef(typeGuards.table())) { zv::Val noHolder = createNoErrorHolder(zv::ObjRef(mergedHolder.asObject()).propAt(PT_ETH_PROP_EXPR).raw());