diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index 7c5f267e4a6..69238150079 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -27,6 +27,11 @@ final class NoScopeTypeReadInEngineRule implements Rule private const ENGINE_NAMESPACES = [ 'PHPStan\\Analyser\\NodeScopeResolver', + 'PHPStan\\Analyser\\ArgumentsHandler', + 'PHPStan\\Analyser\\AttributesHandler', + 'PHPStan\\Analyser\\ClosureProcessor', + 'PHPStan\\Analyser\\ParametersProcessor', + 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', ]; diff --git a/src/Analyser/ArgsResult.php b/src/Analyser/ArgsResult.php index 8a9ce80c900..b08497edaa8 100644 --- a/src/Analyser/ArgsResult.php +++ b/src/Analyser/ArgsResult.php @@ -10,7 +10,7 @@ use function sprintf; /** - * Result of NodeScopeResolver::processArgs(): the scope/throw/impure state after + * Result of ArgumentsHandler::processArgs(): the scope/throw/impure state after * processing all arguments (wrapped ExpressionResult) plus the ParametersAcceptor * resolved from the arg types gathered on the arg-to-arg evolving scope. The * resolved acceptor is type-driven (selectFromTypes) so its generics are resolved diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php new file mode 100644 index 00000000000..718d1b8826a --- /dev/null +++ b/src/Analyser/ArgumentsHandler.php @@ -0,0 +1,1260 @@ + $functionParameterOutTypeExtensions + * @param ExtensionsCollection $methodParameterOutTypeExtensions + * @param ExtensionsCollection $staticMethodParameterOutTypeExtensions + * @param ExtensionsCollection $functionParameterClosureThisExtensions + * @param ExtensionsCollection $methodParameterClosureThisExtensions + * @param ExtensionsCollection $staticMethodParameterClosureThisExtensions + * @param ExtensionsCollection $functionParameterClosureTypeExtensions + * @param ExtensionsCollection $methodParameterClosureTypeExtensions + * @param ExtensionsCollection $staticMethodParameterClosureTypeExtensions + */ + public function __construct( + private TemplateArgumentObserver $templateArgumentObserver, + private ExpressionResultFactory $expressionResultFactory, + private ClosureProcessor $closureProcessor, + #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] + private ExtensionsCollection $functionParameterOutTypeExtensions, + #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] + private ExtensionsCollection $methodParameterOutTypeExtensions, + #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] + private ExtensionsCollection $staticMethodParameterOutTypeExtensions, + #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] + private ExtensionsCollection $functionParameterClosureThisExtensions, + #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)] + private ExtensionsCollection $methodParameterClosureThisExtensions, + #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)] + private ExtensionsCollection $staticMethodParameterClosureThisExtensions, + #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)] + private ExtensionsCollection $functionParameterClosureTypeExtensions, + #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)] + private ExtensionsCollection $methodParameterClosureTypeExtensions, + #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] + private ExtensionsCollection $staticMethodParameterClosureTypeExtensions, + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private bool $implicitThrows, + private AssignHandler $assignHandler, + private ClosureTypeResolver $closureTypeResolver, + private ClosureParameterResolver $closureParameterResolver, + ) + { + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + * @param callable(Node $node, Scope $scope): void $nodeCallback + * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory + */ + public function processArgs( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, + array $parametersAcceptors, + ?array $namedArgumentsVariants, + CallLike $callLike, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ?callable $closureBindScopeFactory = null, + ): ArgsResult + { + $args = $callLike->getArgs(); + + // Evolving-scope arg types: gathered as each argument is processed on the + // scope that evolves arg-to-arg. They select the FINAL resolved acceptor + // (the call's return type, by-ref OUT types), which type-resolves generics + // from the actual argument types. + $gatheredTypes = []; + $gatheredUnpack = false; + $gatheredHasName = false; + $gatheredArgTypeByIndex = []; + + // Metadata acceptor base - NO forward read. The per-argument resolution below picks the + // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is + // keyed off the argument count, e.g. sscanf - and the count is known structurally) and + // resolves generic parameter types from the args gathered so far; the call's return type + // comes from the post-loop resolved acceptor. + $metadataAcceptor = $parametersAcceptors[0] ?? null; + + // Whether selecting an acceptor is type-driven at all: multiple variants to + // choose between, templates or conditionals to resolve from the arg types, + // or named-argument variants. When it is not, the gathered arg types can + // never influence the selected acceptor, so the faithful-return gather walk + // of a closure/arrow argument (gatherClosureArgType()) would be pure waste - + // its signature-only shallow type keeps the count/name bookkeeping correct. + $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1 + || $namedArgumentsVariants !== null + || ($metadataAcceptor !== null && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($metadataAcceptor)); + + // Both predicates are hoisted out of the per-argument loop - they traverse + // the acceptor's parameter types. + $hasTemplateParameterType = $metadataAcceptor !== null + && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor); + $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType; + + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + /** @var list $deferredInvalidateExpressions */ + $deferredInvalidateExpressions = []; + /** @var ProcessClosureResult[] $deferredByRefClosureResults */ + $deferredByRefClosureResults = []; + + $processingOrder = array_keys($args); + usort($processingOrder, static function (int $a, int $b) use ($args): int { + $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; + $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; + $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction; + $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction; + if ($aIsClosure !== $bIsClosure) { + // closures sort after non-closures so every sibling feeding an + // intrinsic override / generic callable(T) is in scope first + return $aIsClosure ? 1 : -1; + } + + $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + if ($aOriginal === null && $bOriginal === null) { + return $a <=> $b; + } + if ($aOriginal === null) { + return 1; + } + if ($bOriginal === null) { + return -1; + } + + return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); + }); + + $argResults = []; + $byRefArguments = []; + $countStableMetadataAcceptor = null; + foreach ($processingOrder as $i) { + $arg = $args[$i]; + + if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { + // Gather the closure/arrow type for the FINAL resolved acceptor on + // the evolving scope, BEFORE the body is processed with a possibly + // generic-resolved parameter injected, so the inferred return type + // stays faithful to the closure's own declaration and its own + // contribution (a TValue from its return) participates in the final + // resolution (see gatherClosureArgType()). + $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection + ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) + : $this->closureTypeResolver->getClosureType($scope, $arg->value, true, $storage); + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); + } + + $argMetadataAcceptor = $metadataAcceptor; + if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { + if ($this->argConsumesResolvedParameterType($arg->value)) { + // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the + // full argument count with mixed. Closures sort last and by-ref out-params follow the + // args that pin them, so determining siblings are already processed; the mixed pad keeps + // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), + // while processed siblings resolve a generic callable(T) parameter. No forward read. + $paddedTypes = []; + $paddedUnpack = false; + $paddedHasName = false; + foreach ($args as $j => $paddedArg) { + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); + } + $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); + } else { + // Only a closure/arrow function consumes the generic-RESOLVED + // parameter type: its body is inferred from the resolved + // callable(T) - directly, or through the in-function-call stack + // when nested anywhere inside the argument. Every other argument + // reads variant-stable facts off its parameter (by-ref flag, + // callable bookkeeping), so one all-mixed count-stable selection + // serves them all instead of a full template inference per argument. + if ($countStableMetadataAcceptor === null) { + $paddedTypes = []; + $paddedUnpack = false; + $paddedHasName = false; + foreach ($args as $j => $paddedArg) { + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); + } + $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); + } + $argMetadataAcceptor = $countStableMetadataAcceptor; + } + } + $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; + + $assignByReference = false; + $parameter = null; + $parameterType = null; + $parameterNativeType = null; + if ($parameters !== null) { + $matchedParameter = null; + if ($arg->name !== null) { + foreach ($parameters as $p) { + if ($p->getName() === $arg->name->toString()) { + $matchedParameter = $p; + break; + } + } + } elseif (isset($parameters[$i])) { + $matchedParameter = $parameters[$i]; + } + + if ($matchedParameter !== null) { + $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); + $parameterType = $matchedParameter->getType(); + + if ($matchedParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $matchedParameter->getNativeType(); + } + $parameter = $matchedParameter; + } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { + $lastParameter = array_last($parameters); + $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); + $parameterType = $lastParameter->getType(); + + if ($lastParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $lastParameter->getNativeType(); + } + $parameter = $lastParameter; + } + } + + if ($parameter !== null && !$parameter->passedByReference()->no()) { + $byRefArguments[spl_object_id($arg->value)] = true; + } + $lookForUnset = false; + if ($assignByReference) { + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) + ) { + $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; + } + } + + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + if ($calleeReflection !== null) { + $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction; + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); + + $originalScope = $scope; + $scopeToPass = $scope; + if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) { + $scopeToPass = $closureBindScopeFactory($scope); + } + + if ($arg->value instanceof Expr\Closure) { + + $storedClosureArgResult = null; + if ($nodeScopeResolver->isReturningStoredExpressionResults() || $nodeScopeResolver->isConsumingStoredExpressionResults()) { + // an on-demand re-walk of the enclosing call must not re-run the + // closure's whole by-ref convergence: consume the main walk's + // stored result, or (when the body release already dropped it) + // price the closure through getClosureType's per-node cache - + // a single body walk on miss, none on repeat asks + $storedClosureArgResult = $storage->findExpressionResult($arg->value); + // consume mode alone (the nullsafe call's plain-twin walk) is the + // FIRST and only walk of these arguments - nothing stored them, so + // a miss means "walk it", not "price it silently": pricing skips + // the body walk and never fires the closure's node callbacks + if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { + $storedClosureArgResult = $this->expressionResultFactory->create( + $scopeToPass, + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); + } + } + if ($storedClosureArgResult !== null) { + $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; + } else { + $restoreThisScope = null; + if ( + $closureBindScopeFactory === null + && $parameter instanceof ExtendedParameterReflection + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } + } + } + + $closureResult = $this->closureProcessor->processClosureNode($nodeScopeResolver, $stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + } + + $storedClosureResult = $this->expressionResultFactory->create( + $closureResult->getScope(), + $scopeToPass, + $arg->value, + variableFlow: ClosureHandler::getVariableFlow($arg->value), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $this->closureTypeResolver->buildClosureTypeForClosure( + $scopeToPass, + $arg->value, + $closureResult->getGatheredReturnStatements(), + $closureResult->getGatheredYieldStatements(), + $closureResult->getExecutionEnds(), + $closureResult->getThrowPoints(), + $closureResult->getClosureTypeImpurePoints(), + $closureResult->getInvalidateExpressions(), + false, + $storage, + ), + // the native flavour reads the stored native types off the same + // single body walk - no second walk on the promoted scope + nativeType: $this->closureTypeResolver->buildClosureTypeForClosure( + $scopeToPass, + $arg->value, + $closureResult->getGatheredReturnStatements(), + $closureResult->getGatheredYieldStatements(), + $closureResult->getExecutionEnds(), + $closureResult->getThrowPoints(), + $closureResult->getClosureTypeImpurePoints(), + $closureResult->getInvalidateExpressions(), + true, + $storage, + ), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureResult); + // the closure node's own callback fires after its result is + // stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + // the arg result must be the properly-typed stored result - + // ArgsResult readers price array_push() & co. from it + $argResults[spl_object_id($arg->value)] = $storedClosureResult; + + $uses = []; + foreach ($arg->value->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->var->name; + } + + $scope = $closureResult->getScope(); + $deferredByRefClosureResults[] = $closureResult; + // Prefer the invalidate expressions collected on the ClosureType - + // they also cover writes the closure's own body walk observed, + // unlike $closureResult->getInvalidateExpressions(). + $closureExprType = $storedClosureResult->getType(); + $invalidateExpressions = $closureExprType instanceof ClosureType + ? $closureExprType->getInvalidateExpressions() + : $closureResult->getInvalidateExpressions(); + if ($restoreThisScope !== null) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($invalidateExpressions as $j => $invalidateExprNode) { + $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); + if ($foundThis === null) { + continue; + } + + unset($invalidateExpressions[$j]); + } + $invalidateExpressions = array_values($invalidateExpressions); + $scope = $scope->restoreThis($restoreThisScope); + } + + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses]; + } + } + } elseif ($arg->value instanceof Expr\ArrowFunction) { + + $storedClosureArgResult = null; + if ($nodeScopeResolver->isReturningStoredExpressionResults() || $nodeScopeResolver->isConsumingStoredExpressionResults()) { + // see the Closure branch above - consume or price via the cache + $storedClosureArgResult = $storage->findExpressionResult($arg->value); + // consume mode alone (the nullsafe call's plain-twin walk) is the + // first and only walk of the argument - a miss means walk it + if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { + $storedClosureArgResult = $this->expressionResultFactory->create( + $scopeToPass, + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); + } + } + if ($storedClosureArgResult !== null) { + $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; + } else { + if ( + $closureBindScopeFactory === null + && $parameter instanceof ExtendedParameterReflection + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } + } + } + + $arrowFunctionResult = $this->closureProcessor->processArrowFunctionNode($nodeScopeResolver, $stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); + $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionExprResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); + } + $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); + // both flavours are built from the single body walk (see + // ArrowFunctionHandler); the built type also answers the + // invalidate-expressions read below without re-walking the + // still-unstored node through Scope::getType() + $arrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction( + $scopeToPass, + $arg->value, + $arrowFunctionScope, + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + false, + $storage, + ); + $storedArrowResult = $this->expressionResultFactory->create( + $arrowFunctionExprResult->getScope(), + variableFlow: $arrowFunctionExprResult->getVariableFlow(), + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: $arrowFunctionExprResult->hasYield(), + isAlwaysTerminating: $arrowFunctionExprResult->isAlwaysTerminating(), + throwPoints: $arrowFunctionExprResult->getThrowPoints(), + impurePoints: $arrowFunctionExprResult->getImpurePoints(), + type: $arrowFunctionType, + nativeType: $this->closureTypeResolver->buildClosureTypeForArrowFunction( + $scopeToPass, + $arg->value, + $arrowFunctionScope, + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + true, + $storage, + ), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedArrowResult); + // the arrow function node's own callback fires after its result + // is stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + // the arg result must be the properly-typed stored result, not + // the body walk's placeholder (whose typeCallback answers mixed) - + // ArgsResult readers price array_push() & co. from it + $argResults[spl_object_id($arg->value)] = $storedArrowResult; + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()]; + } + } + $scope = $scope->addTemplateArgumentConstraints($argResults[spl_object_id($arg->value)]->getScope()->getTemplateArgumentConstraints()); + } else { + $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; + if ($enterExpressionAssignForByRef) { + $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); + } + $argContext = $context->enterDeep(); + if (!$arg->unpack && $arg->value instanceof Expr\Array_) { + $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); + } + $exprResult = $nodeScopeResolver->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $argContext); + $argResults[spl_object_id($arg->value)] = $exprResult; + $exprType = $exprResult->getType(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); + $scope = $exprResult->getScope(); + if ($enterExpressionAssignForByRef) { + $scope = $scope->exitExpressionAssign($arg->value); + } + $hasYield = $hasYield || $exprResult->hasYield(); + + if ($exprType->isCallable()->yes()) { + $acceptors = $exprType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()]; + } + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + } + } + } + + $gatheredArgTypeByIndex[$i] = $exprResult->getType(); + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); + $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($templateArgumentFrame !== null && $parameter !== null) { + // the metadata acceptor is resolved against the arguments gathered + // before this one, so a template this argument itself decides is + // still its bound there - observe the declared parameter type, + // where such a template is uninformative and the receiver's + // class-level arguments are already in place + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectArgument( + $this->findOriginalParameterType($argMetadataAcceptor, $parameter) ?? $parameter->getType(), + $gatheredArgTypeByIndex[$i], + ($calleeReflection instanceof FunctionReflection || $calleeReflection instanceof ExtendedMethodReflection) && $calleeReflection->isPure()->yes(), + )); + } + } + + if ($assignByReference && $lookForUnset) { + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + + if ($i !== 0 || $closureBindScopeFactory === null) { + continue; + } + + $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); + } + + foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { + $scope = $this->closureProcessor->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); + } + + foreach ($deferredByRefClosureResults as $deferredClosureResult) { + $scope = $deferredClosureResult->applyByRefUseScope($scope); + } + + // Type-driven resolved acceptor: the arg types gathered on the evolving + // scope select (and generic-resolve) the acceptor that drives the call's + // return type. Intrinsic overrides are applied on the final scope, + // mirroring the original selectFromArgs(). + // When the selection is not type-driven, the single acceptor IS the + // resolved acceptor - the fast path selectFromArgs() used to take. + $resolvedAcceptor = null; + if ($parametersAcceptors !== []) { + $resolvedAcceptor = $typeDrivenAcceptorSelection + ? $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope) + : $metadataAcceptor; + } + + if ($resolvedAcceptor !== null && $nodeScopeResolver->observingTemplateArgumentFrame($scope) !== null) { + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectCall( + $callLike, + $resolvedAcceptor, + $gatheredTypes, + $callLike instanceof New_ && $calleeReflection instanceof MethodReflection ? $calleeReflection->getDeclaringClass()->getTemplateTypeMap() : null, + )); + } + + // The by-ref OUT writeback reads the metadata acceptor: it is selected from + // the full argument count (stable variant). When that single acceptor still + // carries templates (fast path), its OUT types need generic-resolving from the + // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly + // that (same variant, resolved); otherwise the metadata acceptor is already resolved. + $writebackAcceptor = $metadataAcceptor; + if ( + $metadataAcceptor !== null + && $argMetadataIsTypeDriven + ) { + $writebackAcceptor = $resolvedAcceptor; + } + $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null; + if ($writebackParameters !== null) { + foreach ($args as $i => $arg) { + $assignByReference = false; + $currentParameter = null; + if (isset($writebackParameters[$i])) { + $currentParameter = $writebackParameters[$i]; + } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) { + $currentParameter = array_last($writebackParameters); + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + } + + if ($assignByReference) { + $argValue = $arg->value; + if (!$argValue instanceof Variable || $argValue->name !== 'this') { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + $byRefType = $paramOutType; + } elseif ( + $currentParameter instanceof ExtendedParameterReflection + && $currentParameter->getOutType() !== null + ) { + $byRefType = $currentParameter->getOutType(); + } elseif ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } elseif ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } else { + $byRefType = new MixedType(); + } + + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, + $scope, + $storage, + $stmt, + $argValue, + new TypeExpr($byRefType), + $nodeCallback, + )->getScope(); + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); + } + } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { + $argType = $this->readArgResult($argResults, $arg->value)->getTypeOnScope($scope, false); + if (!$argType->isObject()->no()) { + $nakedReturnType = null; + if ($nakedMethodReflection !== null) { + $nakedParametersAcceptor = $this->selectArgsAcceptor( + $gatheredTypes, + $nakedMethodReflection->getVariants(), + $nakedMethodReflection->getNamedArgumentsVariants(), + $gatheredHasName, + $gatheredUnpack, + ); + $nakedReturnType = $nakedParametersAcceptor->getReturnType(); + } + if ( + $nakedReturnType === null + || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() + || $nakedMethodReflection->isPure()->no() + ) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); + $scope = $scope->invalidateExpression($arg->value, true); + } + } + } + } + + // not storing this, it's scope after processing all args + return new ArgsResult( + $this->expressionResultFactory->create( + $scope, + $scope, + $callLike, + $hasYield, + $isAlwaysTerminating, + $throwPoints, + $impurePoints, + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $resolvedAcceptor, + $argResults, + $byRefArguments, + ); + } + + /** + * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs(): + * indexes the gathered arg type by name (sets $hasName) vs position, and + * expands unpacked constant arrays / falls back to the iterable value type + * (sets $unpack), so selectFromTypes() picks the matching variant. + * + * @param array $types + */ + private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void + { + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); + $hasName = true; + } else { + $index = $i; + } + + if ($originalArg->unpack) { + $unpack = true; + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + $values = $constantArray->getValueTypes(); + foreach ($constantArray->getKeyTypes() as $j => $keyType) { + $valueType = $values[$j]; + $valueIndex = $keyType->getValue(); + if (is_string($valueIndex)) { + $hasName = true; + } else { + $valueIndex = $i + $j; + } + + $types[$valueIndex] = isset($types[$valueIndex]) + ? TypeCombinator::union($types[$valueIndex], $valueType) + : $valueType; + } + } + } else { + $types[$index] = $type->getIterableValueType(); + } + } else { + $types[$index] = $type; + } + } + + /** + * Whether processing this argument consumes the generic-RESOLVED parameter + * type: a closure/arrow function does - its parameters and body scope are + * typed from the resolved callable(T) - whether it IS the argument or is + * nested anywhere inside it (the enclosing parameter is pushed on the + * in-function-call stack and the nested closure types itself from there). + * Every other argument only reads variant-stable facts off its parameter. + */ + private function argConsumesResolvedParameterType(Expr $value): bool + { + if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) { + return true; + } + + // cached on the node - args are re-processed across convergence passes + $cached = $value->getAttribute('phpstanArgContainsClosure'); + if ($cached !== null) { + return $cached; + } + + $contains = (new NodeFinder())->findFirst( + [$value], + static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction, + ) !== null; + $value->setAttribute('phpstanArgContainsClosure', $contains); + + return $contains; + } + + /** + * Resolves the type of a closure/arrow function argument for the generic + * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure + * type is read with the RAW (un-generic-resolved) acceptor parameter pushed + * onto the in-function-call stack, so its body sees the template parameter + * (effectively mixed for an untyped param) rather than a parameter already + * resolved from sibling args. That keeps the inferred return type (the U in + * callable(T): U) faithful to the closure's own declaration. + * + * @param ParametersAcceptor[] $parametersAcceptors + */ + private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type + { + $rawParameter = null; + if (count($parametersAcceptors) === 1) { + $rawParameters = $parametersAcceptors[0]->getParameters(); + if (isset($rawParameters[$i])) { + $rawParameter = $rawParameters[$i]; + } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) { + $rawParameter = array_last($rawParameters); + } + } + + if ($rawParameter !== null) { + $scope = $scope->pushInFunctionCall(null, $rawParameter, false); + } + + return $this->closureParameterResolver->resolveCallableTypeForScope($closureExpr, $scope); + } + + /** + * @param array $types + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor + { + return $hasName && $namedArgumentsVariants !== null + ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack) + : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack); + } + + /** + * Applies the intrinsic argument overrides (array_map/filter/walk/find, + * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope via + * the non-reprocessing readers, then type-selects the metadata acceptor over + * the arg types gathered so far. The overrides read sibling arg types - which + * closures-last ordering keeps in scope/$gatheredTypes before any closure. + * + * @param Node\Arg[] $args + * @param array $gatheredTypes + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function selectArgsMetadataAcceptor(NodeScopeResolver $nodeScopeResolver, array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor + { + $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides( + $args, + $parametersAcceptors, + $namedArgumentsVariants, + $scope, + static fn (Expr $e): Type => $nodeScopeResolver->readTypeOfMaybeStored($e, $scope), + static fn (Expr $e): Type => $nodeScopeResolver->readTypeOfMaybeStored($e, $scope->doNotTreatPhpDocTypesAsCertain()), + static fn (Type $t): Type => $scope->getIterableValueType($t), + static fn (Type $t): Type => $scope->getIterableKeyType($t), + ); + + return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); + } + + /** + * Arguments normalization (reordering, default-filling) can drop an original + * argument from the call processArgs() iterates - duplicate, unknown-named or + * extra arguments in an invalid call. The parameters check still asks their + * types to report the error, so process them too (their result is stored). + * A NoopNodeCallback keeps the dropped arguments out of rule processing, + * matching the behaviour when this guard is off. + */ + public function processDroppedArgs( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + CallLike $originalCall, + CallLike $normalizedCall, + MutatingScope $scope, + ExpressionResultStorage $storage, + ExpressionContext $context, + ): void + { + if ($originalCall === $normalizedCall) { + return; + } + + $keptValueIds = []; + foreach ($normalizedCall->getArgs() as $normalizedArg) { + $keptValueIds[spl_object_id($normalizedArg->value)] = true; + } + + foreach ($originalCall->getArgs() as $originalArg) { + if (isset($keptValueIds[spl_object_id($originalArg->value)])) { + continue; + } + + $nodeScopeResolver->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution()); + } + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool + { + $parameterCallableType = null; + if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) { + $parameterCallableType = TypeUtils::findCallableType($parameterType); + } + + if ($parameter instanceof ExtendedParameterReflection) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $parameterCallableType !== null; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $parameterCallableType !== null; + } + + return $callCallbackImmediately; + } + + /** + * A callback passed as an argument escapes the current scope and may be invoked, + * so its mutations have to invalidate the outer scope - unless the parameter is + * explicitly marked as later-invoked, in which case the callback only runs after + * the current function returns and its mutations are not visible here yet. + */ + private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool + { + if ($parameter instanceof ExtendedParameterReflection) { + return !$parameter->isImmediatelyInvokedCallable()->no(); + } + + return true; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + { + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) { + if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { + return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($calleeReflection instanceof MethodReflection) { + if ($callLike instanceof StaticCall) { + foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { + $staticCall = new StaticCall( + $callLike->class, + new Identifier('__construct'), + $callLike->getArgs(), + ); + foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); + } + } + } elseif ($callLike instanceof MethodCall) { + foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) { + if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { + return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } + } + + return null; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type + { + $paramOutTypes = []; + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) { + if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) { + if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) { + if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } + + if (count($paramOutTypes) === 1) { + return $paramOutTypes[0]; + } + + if (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); + } + + return null; + } + + /** + * @param FunctionReflection|MethodReflection|null $calleeReflection + */ + private function resolveClosureThisType( + ?CallLike $call, + $calleeReflection, + ParameterReflection $parameter, + MutatingScope $scope, + ): ?Type + { + if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + if ($parameter instanceof ExtendedParameterReflection) { + return $parameter->getClosureThisType(); + } + + return null; + } + + /** + * The parameter type an argument is observed against: the declared one with + * the template types the call already decided substituted - the receiver's + * class-level arguments, a template an earlier argument inferred - while a + * template still open (ErrorType in the resolved map, typically the one this + * very argument decides) stays a TemplateType, which the observer ignores. + * The resolved parameter type would carry such a template's bound instead. + */ + private function findOriginalParameterType(ParametersAcceptor $acceptor, ParameterReflection $parameter): ?Type + { + if (!$acceptor instanceof ResolvedFunctionVariant) { + return $parameter->getType(); + } + $originalParameters = $acceptor->getOriginalParametersAcceptor()->getParameters(); + foreach ($acceptor->getParameters() as $index => $resolvedParameter) { + if ($resolvedParameter !== $parameter) { + continue; + } + if (!isset($originalParameters[$index])) { + return null; + } + $originalType = $originalParameters[$index]->getType(); + if (!$originalType->hasTemplateOrLateResolvableType()) { + return $originalType; + } + $decided = []; + foreach ($acceptor->getResolvedTemplateTypeMap()->getTypes() as $name => $type) { + if ($type instanceof ErrorType) { + continue; + } + $decided[$name] = $type; + } + + return TemplateTypeHelper::resolveTemplateTypes( + $originalType, + new TemplateTypeMap($decided), + $acceptor->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + + return null; + } + + /** + * The result processArgs() captured for a call argument. Unlike the storage + * lookup this reads the argument's authoritative result: a closure/arrow + * function argument's captured result is the properly-typed one, not the + * placeholder its body walk stored. + * + * @param array $argResults + */ + private function readArgResult(array $argResults, Expr $argValue): ExpressionResult + { + $result = $argResults[spl_object_id($argValue)] ?? null; + if ($result === null) { + throw new ShouldNotHappenException(sprintf( + '%s on line %d has no captured ExpressionResult - it was not processed as an argument by processArgs().', + get_class($argValue), + $argValue->getStartLine(), + )); + } + + return $result; + } + +} diff --git a/src/Analyser/AttributesHandler.php b/src/Analyser/AttributesHandler.php new file mode 100644 index 00000000000..d5a491fde8a --- /dev/null +++ b/src/Analyser/AttributesHandler.php @@ -0,0 +1,71 @@ +attrs as $attr) { + $className = $scope->resolveName($attr->name); + if ($this->reflectionProvider->hasClass($className)) { + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( + $attr->args, + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + $expr = new New_($attr->name, $attr->args); + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $nodeScopeResolver->callNodeCallback($nodeCallback, $attr, $scope, $storage); + continue; + } + } + + foreach ($attr->args as $arg) { + $nodeScopeResolver->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $nodeScopeResolver->callNodeCallback($nodeCallback, $arg, $scope, $storage); + } + $nodeScopeResolver->callNodeCallback($nodeCallback, $attr, $scope, $storage); + } + $nodeScopeResolver->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); + } + } + +} diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php new file mode 100644 index 00000000000..e414b9a0f65 --- /dev/null +++ b/src/Analyser/ClosureProcessor.php @@ -0,0 +1,531 @@ +container->getByType(ParametersProcessor::class); + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processClosureNode( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\Closure $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ?Type $passedToType, + ?Type $nativePassedToType = null, + ): ProcessClosureResult + { + return $this->processClosureNodeInternal($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processClosureNodeInternal( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\Closure $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ?Type $passedToType, + ?Type $nativePassedToType = null, + ): ProcessClosureResult + { + $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + + $byRefUses = []; + + $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); + $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); + $callableParameters = $parameterTypes->parameters; + $nativeCallableParameters = $parameterTypes->nativeParameters; + + $useScope = $scope; + foreach ($expr->uses as $use) { + if ($use->byRef) { + $byRefUses[] = $use; + $useScope = $useScope->enterExpressionAssign($use->var); + + $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); + $inAssignRightSideExpr = $context->getInAssignRightSideExpr(); + if ( + $inAssignRightSideVariableName === $use->var->name + && $inAssignRightSideExpr !== null + ) { + // a call's type is carried by the context (see + // ExpressionContext::enterAssignRightSideCallArgs()); a closure + // right side resolves through the closure type resolver + $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); + if ($inAssignRightSideType instanceof ClosureType) { + $variableType = $inAssignRightSideType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); + } else { + $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); + } + } + $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); + if ($inAssignRightSideNativeType instanceof ClosureType) { + $variableNativeType = $inAssignRightSideNativeType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); + } else { + $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); + } + } + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); + } + } + $nodeScopeResolver->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context->withoutValueFlow()); + if (!$use->byRef) { + continue; + } + + $useScope = $useScope->exitExpressionAssign($use->var); + } + + if ($expr->returnType !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); + } + + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); + $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); + $closureType = $closureScope->getAnonymousFunctionReflection(); + if (!$closureType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); + + $executionEnds = []; + $gatheredReturnStatements = []; + $gatheredReturnStatementsAfterFinally = []; + $gatheredReturnStatementsWithScope = []; + $gatheredYieldStatements = []; + $gatheredYieldStatementsWithScope = []; + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsAfterFinally, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { + return; + } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if ($node instanceof ReturnAfterFinallyNode) { + $gatheredReturnStatementsAfterFinally[] = new ReturnStatement($scope, $node->getReturnNode()); + return; + } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + $gatheredYieldStatementsWithScope[] = [$node, $scope]; + } + if (!$node instanceof Return_) { + return; + } + + $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + $gatheredReturnStatementsWithScope[] = [$node, $scope]; + }; + + if (count($byRefUses) === 0) { + $nodeScopeResolver->pushNodeGatherer($closureStmtsGatherer); + try { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $publicStatementResult = $statementResult->toPublic(); + $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( + $expr, + $gatheredReturnStatements, + $gatheredReturnStatementsAfterFinally, + $gatheredYieldStatements, + $publicStatementResult, + $executionEnds, + array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), + ), $closureReturnStatementsNodeScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); + + return new ProcessClosureResult( + $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + ); + } + + $originalStorage = $storage; + + $count = 0; + $closureResultScope = null; + $replayBodyRecording = null; + $replayPassStorage = null; + $replayPassResult = null; + $replayEntryScope = null; + $bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($expr, $expr->stmts); + do { + $prevScope = $closureScope; + + $storage = $originalStorage->duplicate(); + $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); + // deep context, like the loop handlers' own convergence passes: inner + // loops walk single-pass here and only the final walk below (top-level) + // runs their full convergence - otherwise every closure-convergence + // pass would re-converge every inner loop from scratch + $intermediaryClosureScopeResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false)); + // the candidate to replace the final walk when this pass's entry + // turns out to be the fixpoint + if ($bodyRecording instanceof RecordingNodeCallback) { + $replayBodyRecording = $bodyRecording; + $replayPassStorage = $storage; + $replayPassResult = $intermediaryClosureScopeResult; + $replayEntryScope = $prevScope; + } + $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); + foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { + $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); + } + + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { + $closureResultScope = $intermediaryClosureScope; + break; + } + + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); + $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + + if ($closureScope->equals($prevScope)) { + break; + } + if ($count >= NodeScopeResolver::GENERALIZE_AFTER_ITERATION) { + $closureScope = $prevScope->generalizeWith($closureScope); + } + $count++; + } while ($count < NodeScopeResolver::LOOP_SCOPE_ITERATIONS); + + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + + $storage = $originalStorage; + $nodeScopeResolver->pushNodeGatherer($closureStmtsGatherer); + try { + if ( + $replayBodyRecording !== null && $replayPassStorage !== null + && $replayPassResult !== null && $replayEntryScope !== null + && $closureScope->equals($replayEntryScope) + ) { + // the final walk would repeat the recorded fixpoint pass exactly + // (same entry scope, deterministic walk) - adopt the pass's result + // and replay its emissions through the real callback instead. + // The pass's own entry scope takes over: the recorded pairs carry + // its anonymous-function reflection, which the gatherer's filter + // compares by identity (the state is equals-identical anyway). + $closureScope = $replayEntryScope; + $originalStorage->mergeResults($replayPassStorage); + $nodeScopeResolver->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope); + $statementResult = $replayPassResult; + } else { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + } + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $publicStatementResult = $statementResult->toPublic(); + $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( + $expr, + $gatheredReturnStatements, + $gatheredReturnStatementsAfterFinally, + $gatheredYieldStatements, + $publicStatementResult, + $executionEnds, + array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), + ), $closureReturnStatementsNodeScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); + + return new ProcessClosureResult( + $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + $closureResultScope, + $byRefUses, + ); + } + + /** + * The closure scope was entered with a shallow reflection (parameters + + * declared return, no body walk - see ClosureTypeResolver::getClosureType() + * with $shallow). Now that the single body walk has gathered the returns, + * build the refined ClosureType from them (no second walk) and swap it onto + * the scope the ClosureReturnStatementsNode fires with, so the return-type + * rules see the refined expected return (e.g. Bar&Foo, not just Foo). + * + * @param list $gatheredReturnStatementsWithScope + * @param list $gatheredYieldStatementsWithScope + * @param list $executionEnds + * @param InternalThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + */ + private function refineClosureNodeScope( + MutatingScope $closureScope, + MutatingScope $scope, + Expr\Closure $expr, + array $gatheredReturnStatementsWithScope, + array $gatheredYieldStatementsWithScope, + array $executionEnds, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + ExpressionResultStorage $storage, + ): MutatingScope + { + $refinedClosureType = $this->closureTypeResolver->buildClosureTypeForClosure( + $scope, + $expr, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + $throwPoints, + $impurePoints, + $invalidateExpressions, + false, + $storage, + ); + + return $closureScope->withAnonymousFunctionReflection($refinedClosureType); + } + + /** + * @param InvalidateExprNode[] $invalidatedExpressions + * @param string[] $uses + */ + public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope + { + if ($scope->isInClass()) { + $uses[] = 'this'; + } + + $finder = new NodeFinder(); + foreach ($invalidatedExpressions as $invalidateExpression) { + $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true)); + if ($result === null) { + continue; + } + + $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable; + $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters); + } + + return $scope; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processArrowFunctionNode( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\ArrowFunction $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ?Type $passedToType, + ?Type $nativePassedToType = null, + ?ExpressionContext $context = null, + ): ProcessArrowFunctionResult + { + $context ??= ExpressionContext::createTopLevel(); + $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + if ($expr->returnType !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); + } + + $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); + $callableParameters = $parameterTypes->parameters; + $nativeCallableParameters = $parameterTypes->nativeParameters; + $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); + if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) { + throw new ShouldNotHappenException(); + } + + // Gather the property-assign impure points and invalidate expressions the + // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()), + // on top of the regular rule node callback, so the single body walk here + // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if (!$node instanceof PropertyAssignNode) { + return; + } + + $arrowFunctionImpurePoints[] = new ImpurePoint( + $innerScope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + }; + + $nodeScopeResolver->pushNodeGatherer($arrowFunctionStmtsGatherer); + try { + $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $scope = $scope->addTemplateArgumentConstraints($exprResult->getScope()->getTemplateArgumentConstraints()); + $scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectReturnSend($arrowFunctionScope, $exprResult)); + + $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); + $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); + + // The arrow scope was entered with a shallow reflection (parameters + + // declared return, no body walk). Now that the single body walk above has + // run, build the refined arrow function type from the body expression's + // stored type (no second walk) and fire InArrowFunctionNode with it, so the + // node and the return-type rules see the refined expected return. + $refinedArrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction( + $scope, + $expr, + $arrowFunctionScope, + $closureTypeThrowPoints, + $closureTypeImpurePoints, + $invalidateExpressions, + false, + $storage, + ); + $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType); + $nodeScopeResolver->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage); + + return new ProcessArrowFunctionResult( + $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: $exprResult->isAlwaysTerminating(), + variableFlow: ArrowFunctionHandler::getVariableFlow($expr, $exprResult), + throwPoints: $exprResult->getThrowPoints(), + impurePoints: $exprResult->getImpurePoints(), + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $arrowFunctionScope, + $closureTypeThrowPoints, + $closureTypeImpurePoints, + $invalidateExpressions, + ); + } + +} diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index c5ecb82d682..75d4c5c6859 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Stmt; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -30,6 +31,7 @@ public function __construct( private ClosureTypeResolver $closureTypeResolver, private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private ClosureProcessor $closureProcessor, ) { } @@ -41,7 +43,7 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, $context->getPassedToType(), $context->getNativePassedToType(), $context); + $arrowFunctionResult = $this->closureProcessor->processArrowFunctionNode($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context->getPassedToType(), $context->getNativePassedToType(), $context); $result = $arrowFunctionResult->getExpressionResult(); // A plain typeCallback recursing through getClosureType() would re-walk diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 7e7759f1c17..abdd17e2fd2 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -36,6 +36,7 @@ use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; +use PHPStan\Analyser\Generics\TemplateArgumentConstraints; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -46,13 +47,16 @@ use PHPStan\Analyser\PropertyHookThrowPointsResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VarAnnotationProcessor; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; +use PHPStan\Analyser\VirtualAssignNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; @@ -132,6 +136,7 @@ public function __construct( private StaticPropertyFetchHandler $staticPropertyFetchHandler, private MethodThrowPointHelper $methodThrowPointHelper, private PropertyHookThrowPointsResolver $propertyHookThrowPointsResolver, + private StatementsHandler $statementsHandler, ) { } @@ -252,7 +257,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $varChangedScope = false; $scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt, $varChangedScope); if (!$varChangedScope) { - $scope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); + $scope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, null, $nodeCallback); } else { // the @var tag is a declared type the assigned value flows into $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); @@ -545,6 +550,59 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi }; } + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processVirtualAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, ?ExpressionResult $assignedExprResult = null): ExpressionResult + { + // work off an available result for the assigned expr: passed by the + // caller, or fabricated from a type-carrying virtual node - threaded + // straight into applyWrite() so its reads compose instead of falling + // back to on-demand pricing of the type, the truthy/falsey narrowing, + // and the synthetic sentinel comparisons + if ( + $assignedExprResult === null + && ($assignedExpr instanceof TypeExpr || $assignedExpr instanceof NativeTypeExpr) + && $storage->findExpressionResult($assignedExpr) === null + ) { + $assignedExprResult = $this->virtualExprResultHelper->createTypeExprResult($scope, $assignedExpr); + } + + $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback); + $target = $this->prepareTarget( + $nodeScopeResolver, + $scope, + $storage, + $stmt, + $var, + $assignedExpr, + $virtualAssignNodeCallback, + ExpressionContext::createDeep(), + AssignTargetWalkMode::virtualAssign(), + ); + + return $this->applyWrite( + $nodeScopeResolver, + $target, + $this->expressionResultFactory->create( + $target->getScope(), + beforeScope: $target->getScope(), + expr: $assignedExpr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $assignedExprResult, + $stmt, + $storage, + $virtualAssignNodeCallback, + ExpressionContext::createDeep(), + ); + } + /** * The pre-value half of an assignment: walks the target's sub-expressions * (root, dimensions, receiver, dynamic name) in PHP's evaluation order and @@ -1435,7 +1493,8 @@ public function applyWrite( && !$setVarType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no() ) { - $scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectOffsetSetUsage( + $scope = $scope->addTemplateArgumentConstraints($this->collectOffsetSetUsage( + $nodeScopeResolver, $scope, $setVarType, $offsetTypes[count($offsetTypes) - 1][0], @@ -1756,6 +1815,32 @@ public function applyWrite( ); } + /** + * A write through ArrayAccess (`$storage[$key] = $value`) reaches the + * object's template arguments exactly like the offsetSet() call it stands + * for, so observe both the key and the written value against that method's + * parameters resolved on the receiver. + */ + private function collectOffsetSetUsage(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Type $receiverType, ?Type $keyType, Type $valueType): TemplateArgumentConstraints + { + $constraints = TemplateArgumentConstraints::createEmpty(); + $frame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($frame === null || !$receiverType->hasMethod('offsetSet')->yes()) { + return $constraints; + } + + $parameters = $receiverType->getMethod('offsetSet', $scope)->getOnlyVariant()->getParameters(); + if ($keyType !== null && isset($parameters[0])) { + $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[0]->getType(), $keyType)); + } + if (!isset($parameters[1])) { + return $constraints; + } + + $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[1]->getType(), $valueType)); + return $constraints; + } + private function createArrayDimFetchConditionalExpressionHolder( Expr\Variable $keyVar, Expr $arrayArg, diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index 116d6bc0d8b..ea60026b066 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Stmt; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -30,6 +31,7 @@ public function __construct( private ClosureTypeResolver $closureTypeResolver, private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private ClosureProcessor $closureProcessor, ) { } @@ -41,7 +43,7 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $processClosureResult = $nodeScopeResolver->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, $context->getPassedToType(), $context->getNativePassedToType()); + $processClosureResult = $this->closureProcessor->processClosureNode($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context, $context->getPassedToType(), $context->getNativePassedToType()); // A plain typeCallback recursing through getClosureType() would re-walk // the body each getType() ask before the cache populates and hang; diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 78ad61113b9..f5bf2db635e 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -13,7 +13,9 @@ use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -103,6 +105,9 @@ public function __construct( private DynamicReturnTypeStoragePrimer $storagePrimer, private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private ClosureTypeResolver $closureTypeResolver, + private ArgumentsHandler $argumentsHandler, + private ClosureProcessor $closureProcessor, + private AssignHandler $assignHandler, ) { } @@ -140,7 +145,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // their turn. The closure is then walked on the post-argument scope. $shallowVariants = $this->closureTypeResolver->getDeclaredClosureType($scope, $expr->name)->getCallableParametersAcceptors($scope); $shallowAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $shallowVariants, null); - $argumentsWalkedAhead = $nodeScopeResolver->processArgs($stmt, null, null, $shallowVariants, null, ArgumentsNormalizer::reorderFuncArguments($shallowAcceptor, $expr) ?? $expr, $scope, $storage, $nodeCallback, $context); + $argumentsWalkedAhead = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, null, null, $shallowVariants, null, ArgumentsNormalizer::reorderFuncArguments($shallowAcceptor, $expr) ?? $expr, $scope, $storage, $nodeCallback, $context); $scope = $argumentsWalkedAhead->getScope(); } if ($expr->name instanceof Expr) { @@ -174,7 +179,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $callableThrowPoints); $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); - $scope = $nodeScopeResolver->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); + $scope = $this->closureProcessor->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); @@ -224,7 +229,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $propertyAttributes = $normalizedExpr->getAttributes(); $propertyAttributes['inCloneWith'] = true; if (count($clonePropertyKeyTypeScalars) === 1) { - $nodeScopeResolver->processVirtualAssign( + $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -235,7 +241,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } - $nodeScopeResolver->processVirtualAssign( + $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -320,7 +327,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // the arguments are processed; the call resolves from the walked // closure's acceptor, which is the sole variant of its type ? $argumentsWalkedAhead->withResolvedParametersAcceptor($parametersAcceptor) - : $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + : $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); } finally { if ($argsGatherer !== null) { $nodeScopeResolver->popNodeGatherer(); @@ -332,7 +339,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($argumentsWalkedAhead === null) { $scope = $argsResult->getScope(); } - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php index cefe53f9fa8..2410e505bc2 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php @@ -2,24 +2,36 @@ namespace PHPStan\Analyser\ExprHandler\Helper; +use Closure; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\ClosureParameterTypes; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Parser\ArrayMapArgVisitor; -use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; -use PHPStan\Reflection\PassedByReference; -use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; -/** Resolve the same contextual parameters for body analysis and closure type inference. */ +/** + * Resolves closure parameters for the body walk: the contextual parameters + * (ContextualClosureParameterResolver), refined from the arguments of a call + * that invokes the closure in place. Pricing those arguments needs the closure + * types of nested closures, which is why this part is separate from what + * ClosureTypeResolver itself builds parameters through. + */ #[AutowiredService] final class ClosureParameterResolver { - public function __construct(private NodeScopeResolver $nodeScopeResolver) + public function __construct( + private NodeScopeResolver $nodeScopeResolver, + private ClosureTypeResolver $closureTypeResolver, + private ContextualClosureParameterResolver $contextualClosureParameterResolver, + ) { } @@ -33,31 +45,78 @@ public function resolve( ?Type $nativePassedToType, ): ClosureParameterTypes { - $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); - $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); - $intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs; - if ($intrinsicArgs === null) { - return new ClosureParameterTypes( - $this->nodeScopeResolver->createCallableParameters($scope, $expr, $callArgs, $passedToType), - $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, $callArgs, $nativePassedToType), - ); + if ($callArgs === null || $this->contextualClosureParameterResolver->hasIntrinsicArgs($expr)) { + return $this->contextualClosureParameterResolver->resolve($scope, $expr, $storage, $passedToType, $nativePassedToType); } - $parameters = []; - $nativeParameters = []; - foreach ($intrinsicArgs as $arg) { - $result = $storage !== null ? $storage->findExpressionResult($arg->value) : null; - $type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope); - $nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain()); - if ($arrayMapArgs !== null) { - $type = $type->getIterableValueType(); - $nativeType = $nativeType->getIterableValueType(); + return new ClosureParameterTypes( + $this->createCallArgsParameters($scope, $expr, $callArgs, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)), + $this->createCallArgsParameters($scope, $expr, $callArgs, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())), + ); + } + + /** + * Resolves the type of an expression a callable parameter is derived from - + * either the closure/arrow function whose acceptors describe the parameters, + * or a call argument refining them. A closure/arrow function is resolved + * directly through ClosureTypeResolver (as Scope::getType() would), not by + * processing it on demand: createCallArgsParameters() runs while that very + * closure is being processed, so on-demand processing would re-enter + * ClosureProcessor::processClosureNodeInternal() endlessly. + */ + public function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type + { + if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { + return $this->closureTypeResolver->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); + } + + return $this->nodeScopeResolver->readTypeOfMaybeStored($expr, $scope); + } + + /** + * @param Node\Arg[] $args + * @param Closure(MutatingScope, Expr): Type $typeGetter + * @return ParameterReflection[]|null + */ + private function createCallArgsParameters(MutatingScope $scope, Expr $closureExpr, array $args, Closure $typeGetter): ?array + { + $closureType = $typeGetter($scope, $closureExpr); + if ($closureType->isCallable()->no()) { + return null; + } + + $callableParameters = null; + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; + } + + if ($callableParameter->isVariadic()) { + $argTypes = []; + $argNumber = count($args); + for ($j = $index; $j < $argNumber; $j++) { + $argTypes[] = $typeGetter($scope, $args[$j]->value); + } + $type = TypeCombinator::union(...$argTypes); + } else { + $type = $typeGetter($scope, $args[$index]->value); + } + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); } - $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } - return new ClosureParameterTypes($parameters, $nativeParameters); + return $callableParameters; } } diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index d62b0d070b0..ec9cc7924f8 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -80,7 +80,7 @@ final class ClosureTypeResolver implements PerFileAnalysisResettable public function __construct( private NodeScopeResolver $nodeScopeResolver, private InitializerExprTypeResolver $initializerExprTypeResolver, - private ClosureParameterResolver $closureParameterResolver, + private ContextualClosureParameterResolver $contextualClosureParameterResolver, ) { } @@ -129,7 +129,7 @@ public function getClosureType( // ENTRY (enterAnonymousFunction()/enterArrowFunction()) so entering a // closure/arrow scope never re-walks the body - the refined return type is // built afterwards from the single body walk's gathered returns and carried - // on the node/rule scope (see NodeScopeResolver::processClosureNodeInternal() + // on the node/rule scope (see ClosureProcessor::processClosureNodeInternal() // and processArrowFunctionNode()). if ($shallow) { return new ClosureType( @@ -286,7 +286,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu /** * Constructs a closure type from data the engine already gathered while - * walking the body once (see NodeScopeResolver::processClosureNode()), + * walking the body once (see ClosureProcessor::processClosureNode()), * without a second walk. The return/yield expression types are read from * their stored results. * @@ -341,7 +341,7 @@ public function buildClosureTypeForClosure( /** * Constructs an arrow function type from data the engine already gathered - * while walking the body once (see NodeScopeResolver:: + * while walking the body once (see ClosureProcessor:: * processArrowFunctionNode()), without a second walk. The return/yield * expression types are read from their stored results on $arrowScope. * @@ -801,7 +801,7 @@ private function buildParametersAndAcceptors( $nativePassedToType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType(); } } - $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, null, $passedToType, $nativePassedToType); + $parameterTypes = $this->contextualClosureParameterResolver->resolve($scope, $expr, $storage, $passedToType, $nativePassedToType); return [$parameters, $isVariadic, $parameterTypes->parameters, $parameterTypes->nativeParameters]; } diff --git a/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php new file mode 100644 index 00000000000..344f86872f5 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php @@ -0,0 +1,135 @@ +getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) !== null + || $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME) !== null; + } + + public function resolve( + MutatingScope $scope, + Node\Expr\Closure|Node\Expr\ArrowFunction $expr, + ?ExpressionResultStorage $storage, + ?Type $passedToType, + ?Type $nativePassedToType, + ): ClosureParameterTypes + { + $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); + $intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs; + if ($intrinsicArgs === null) { + return new ClosureParameterTypes( + $this->createPassedToTypeParameters($scope, $passedToType), + $this->createPassedToTypeParameters($scope, $nativePassedToType), + ); + } + + $parameters = []; + $nativeParameters = []; + foreach ($intrinsicArgs as $arg) { + $result = $storage !== null ? $storage->findExpressionResult($arg->value) : null; + $type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope); + $nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain()); + if ($arrayMapArgs !== null) { + $type = $type->getIterableValueType(); + $nativeType = $nativeType->getIterableValueType(); + } + $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + + return new ClosureParameterTypes($parameters, $nativeParameters); + } + + /** + * @return ParameterReflection[]|null + */ + private function createPassedToTypeParameters(MutatingScope $scope, ?Type $passedToType): ?array + { + if ($passedToType === null || $passedToType->isCallable()->no()) { + return null; + } + + if ($passedToType instanceof UnionType) { + $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); + + if ($passedToType->isCallable()->no()) { + return null; + } + } + + $callableParameters = null; + $acceptors = $passedToType->getCallableParametersAcceptors($scope); + foreach ($acceptors as $acceptor) { + $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + + if ($callableParameters === null) { + $callableParameters = $acceptorParameters; + continue; + } + + $newParameters = []; + $parameterCount = max(count($callableParameters), count($acceptorParameters)); + for ($i = 0; $i < $parameterCount; $i++) { + if (!array_key_exists($i, $acceptorParameters)) { + $newParameters[] = $callableParameters[$i]->toOptional(); + continue; + } + + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $acceptorParameters[$i]->toOptional(); + continue; + } + + $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); + } + + $callableParameters = $newParameters; + } + + return $callableParameters; + } + +} diff --git a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php index 255400602cd..5e4f91d4e79 100644 --- a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php +++ b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -64,6 +65,7 @@ public function __construct( private OutputBufferHelper $outputBufferHelper, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private AssignHandler $assignHandler, ) { } @@ -82,7 +84,8 @@ public function applyArrayWalkResult(NodeScopeResolver $nodeScopeResolver, Stmt $newArrayType = $arrayWalkOriginalArrayType->mapValueType(static fn (Type $type): Type => $arrayWalkValueType); $newArrayNativeType = $arrayWalkOriginalArrayNativeType->mapValueType(static fn (Type $type): Type => $arrayWalkValueNativeType); - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -169,7 +172,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt $arrayArgNativeType = $arrayArgResult->getTypeOnScope($scope, true); $isArrayPop = $functionReflection->getName() === 'array_pop'; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -189,7 +193,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -215,7 +220,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -255,7 +261,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt $replacementNativeType = new ConstantArrayType([], []); } - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -275,7 +282,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -292,7 +300,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -385,7 +394,7 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt /** * Invalidates the arguments a callee could write through, mirroring what - * NodeScopeResolver::processArgs() does for a callee with side effects. A + * ArgumentsHandler::processArgs() does for a callee with side effects. A * closure has no FunctionReflection, so processArgs() skips it. * * @param callable(Node $node, Scope $scope): void $nodeCallback diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 718bbeeba81..ab75278cd1f 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\CalledMethodProcessor; use PHPStan\Analyser\ExpressionContext; @@ -75,6 +76,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, + private ArgumentsHandler $argumentsHandler, ) { } @@ -156,7 +158,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null && $context->getInAssignRightSideExpr() === $expr) { $context = $context->enterAssignRightSideCallArgs($parametersAcceptor); } - $argsResult = $nodeScopeResolver->processArgs( + $argsResult = $this->argumentsHandler->processArgs( + $nodeScopeResolver, $stmt, $methodReflection, $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, @@ -170,7 +173,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { // created after the args were processed - the pure-unless-callable- diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index efa729771f4..0d25d3b6469 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -97,6 +98,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private Container $container, + private ArgumentsHandler $argumentsHandler, ) { } @@ -236,10 +238,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $variants = $constructorReflection !== null ? $constructorReflection->getVariants() : []; $namedArgumentsVariants = $constructorReflection !== null ? $constructorReflection->getNamedArgumentsVariants() : null; $scopeBeforeArgs = $scope; - $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $argsResult = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $hasYield || $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 6c97d45b54c..1bbf61f00bf 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -34,6 +34,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, private IncDecTypeHelper $incDecTypeHelper, + private AssignHandler $assignHandler, ) { } @@ -70,7 +71,8 @@ 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( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 83368839776..17a385712d2 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -34,6 +34,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, private IncDecTypeHelper $incDecTypeHelper, + private AssignHandler $assignHandler, ) { } @@ -70,7 +71,8 @@ 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( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index c646ea565cc..2346942353e 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -32,6 +32,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private IncDecTypeHelper $incDecTypeHelper, private DefaultNarrowingHelper $defaultNarrowingHelper, + private AssignHandler $assignHandler, ) { } @@ -72,7 +73,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index ba7b208166d..ec72ef2f4cc 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -32,6 +32,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private IncDecTypeHelper $incDecTypeHelper, private DefaultNarrowingHelper $defaultNarrowingHelper, + private AssignHandler $assignHandler, ) { } @@ -72,7 +73,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index d8816c219fc..9a453b2ea95 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -78,6 +79,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, + private ArgumentsHandler $argumentsHandler, ) { } @@ -250,10 +252,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null && $context->getInAssignRightSideExpr() === $expr) { $context = $context->enterAssignRightSideCallArgs($parametersAcceptor); } - $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScopeFactory); + $argsResult = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScopeFactory); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { // created after the args were processed - the pure-unless-callable- diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 483d58981da..d0eb57ef6aa 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1458,7 +1458,7 @@ protected function findSettledStoredResult(Expr $node): ?ExpressionResult return null; } - return $this->container->getByType(NodeScopeResolver::class)->findSettledExpressionResult($storage, $node); + return $storage->findExpressionResult($node); } public function getCurrentExpressionResultStorage(): ?ExpressionResultStorage diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2220437ee3f..3e6a28511b0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3,14 +3,9 @@ namespace PHPStan\Analyser; use Closure; -use PhpParser\Comment\Doc; use PhpParser\Node; -use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\AssignRef; -use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\List_; use PhpParser\Node\Expr\MethodCall; @@ -18,123 +13,46 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; -use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Echo_; use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\Goto_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Return_; 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; -use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; use PHPStan\Analyser\Generics\TemplateArgumentConstraints; use PHPStan\Analyser\Generics\TemplateArgumentFrame; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\Generics\TemplateArgumentStats; use PHPStan\DependencyInjection\AutowiredExtensions; -use PHPStan\DependencyInjection\AutowiredParameter; 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; -use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionCallExpressionNode; -use PHPStan\Node\InArrowFunctionNode; -use PHPStan\Node\InClosureNode; use PHPStan\Node\InstantiationCallableNode; -use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodCallExpressionNode; -use PHPStan\Node\PropertyAssignNode; -use PHPStan\Node\PropertyHookStatementNode; -use PHPStan\Node\ReturnAfterFinallyNode; -use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\StaticMethodCallExpressionNode; -use PHPStan\Node\UnreachableStatementNode; -use PHPStan\Node\VarTagChangedExpressionTypeNode; -use PHPStan\Parser\ArrowFunctionArgVisitor; -use PHPStan\Parser\ClosureArgVisitor; -use PHPStan\Parser\GotoLabelVisitor; -use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; -use PHPStan\PhpDoc\Tag\VarTag; -use PHPStan\Reflection\Callables\SimpleImpurePoint; -use PHPStan\Reflection\Callables\SimpleThrowPoint; -use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ExtendedParameterReflection; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Reflection\ResolvedFunctionVariant; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; -use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\Generic\TemplateTypeVariance; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\ResourceType; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use function array_filter; -use function array_key_exists; -use function array_keys; -use function array_last; -use function array_map; use function array_merge; use function array_pop; -use function array_slice; -use function array_values; -use function count; use function get_class; use function getenv; -use function implode; -use function in_array; use function is_array; -use function is_int; use function is_string; -use function max; use function spl_object_id; use function sprintf; -use function usort; #[AutowiredService] class NodeScopeResolver @@ -176,9 +94,6 @@ class NodeScopeResolver /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */ public static bool $guardNewWorld = false; - /** PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG=1 prints every second-pass re-walk/replay decision. */ - private static bool $debugTemplateArguments = false; - /** * spl_object_id => true of every Expr in the file's parsed AST. Populated * only when the PHPSTAN_GUARD_NW diagnostic is enabled, so the guards can @@ -199,62 +114,19 @@ class NodeScopeResolver public static array $guardProcessedExprIds = []; /** - * @param ExtensionsCollection $functionParameterOutTypeExtensions - * @param ExtensionsCollection $methodParameterOutTypeExtensions - * @param ExtensionsCollection $staticMethodParameterOutTypeExtensions - * @param ExtensionsCollection $readWritePropertiesExtensions - * @param ExtensionsCollection $functionParameterClosureThisExtensions - * @param ExtensionsCollection $methodParameterClosureThisExtensions - * @param ExtensionsCollection $staticMethodParameterClosureThisExtensions - * @param ExtensionsCollection $functionParameterClosureTypeExtensions - * @param ExtensionsCollection $methodParameterClosureTypeExtensions - * @param ExtensionsCollection $staticMethodParameterClosureTypeExtensions * @param ExtensionsCollection $perFileAnalysisResettables */ public function __construct( private readonly Container $container, 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)] - private readonly ExtensionsCollection $methodParameterOutTypeExtensions, - #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterOutTypeExtensions, - private readonly FileTypeMapper $fileTypeMapper, - #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] - private readonly ExtensionsCollection $readWritePropertiesExtensions, - #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $functionParameterClosureThisExtensions, - #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $methodParameterClosureThisExtensions, - #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterClosureThisExtensions, - #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $functionParameterClosureTypeExtensions, - #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $methodParameterClosureTypeExtensions, - #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterClosureTypeExtensions, #[AutowiredExtensions(of: PerFileAnalysisResettable::class)] private readonly ExtensionsCollection $perFileAnalysisResettables, - #[AutowiredParameter] - private readonly bool $polluteScopeWithLoopInitialAssignments, - #[AutowiredParameter] - private readonly bool $polluteScopeWithAlwaysIterableForeach, - #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] - private readonly bool $implicitThrows, - #[AutowiredParameter] - private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, - #[AutowiredParameter(ref: '%featureToggles.unresolvedTemplateArguments%')] - private readonly bool $unresolvedTemplateArguments, + private readonly StatementsHandler $statementsHandler, ) { self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; - self::$debugTemplateArguments = getenv('PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG') === '1'; TemplateArgumentStats::enableFromEnvironment(); } @@ -323,126 +195,15 @@ public function processNodes( $scope->pushExpressionResultStorage($expressionResultStorage); // a fresh walk an extension starts mid-analysis must not feed the // interrupted walk's gatherer frames (see processStmtNodes()) - $gatherers = $this->nodeGatherers; - $this->nodeGatherers = []; + $gatherers = $this->suspendNodeGatherers(); try { - $this->processNodesWithStorage($nodes, $scope, $expressionResultStorage, $nodeCallback); + $this->statementsHandler->processNodesWithStorage($this, $nodes, $scope, $expressionResultStorage, $nodeCallback); } finally { - $this->nodeGatherers = $gatherers; + $this->restoreNodeGatherers($gatherers); $scope->popExpressionResultStorage(); } } - /** - * @param Node[] $nodes - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processNodesWithStorage( - array $nodes, - MutatingScope $scope, - ExpressionResultStorage $expressionResultStorage, - callable $nodeCallback, - ): void - { - $alreadyTerminated = false; - $exitPoints = []; - - $stmts = []; - $stmtToNodeIndex = []; - foreach ($nodes as $i => $node) { - if (!($node instanceof Node\Stmt)) { - continue; - } - - $stmtToNodeIndex[count($stmts)] = $i; - $stmts[] = $node; - } - - $dummyParent = new Node\Stmt\Nop(); - foreach ($stmts as $si => $node) { - if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { - continue; - } - - $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); - if ($nestedLabelNames !== null) { - $scope = $this->resolveBackwardGotoScope( - $dummyParent, - [$node], - $scope, - $expressionResultStorage, - StatementContext::createDeep(), - static fn (string $name): bool => isset($nestedLabelNames[$name]), - false, - ); - } - - $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); - $scope = $statementResult->getScope(); - - if ($node instanceof Node\Stmt\Label) { - $labelName = $node->name->toString(); - - [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( - $labelName, - $scope, - $alreadyTerminated, - $exitPoints, - ); - - if ($alreadyTerminated) { - continue; - } - - if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) { - $scope = $this->resolveBackwardGotoScope( - $dummyParent, - array_slice($stmts, $si + 1), - $scope, - $expressionResultStorage, - StatementContext::createDeep(), - static fn (string $name): bool => $name === $labelName, - true, - ); - } - } - - $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); - - if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { - continue; - } - - $alreadyTerminated = true; - $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true); - $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback); - } - } - - /** The stored result an outside asker may consume. */ - public function findSettledExpressionResult(ExpressionResultStorage $storage, Expr $expr): ?ExpressionResult - { - return $storage->findExpressionResult($expr); - } - - /** An effect-free result carrying eagerly known types, positioned at the given scope. */ - protected function createEagerExpressionResult(MutatingScope $scope, Expr $expr, Type $type, Type $nativeType): ExpressionResult - { - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - type: $type, - nativeType: $nativeType, - ); - } - public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { if (self::$guardNewWorld) { @@ -452,10 +213,6 @@ public function storeExpressionResult(ExpressionResultStorage $storage, Expr $ex $storage->storeExpressionResult($expr, $expressionResult); } - /** - * @param Node\Stmt[] $bodyStmts - * @param Closure(string): bool $gotoNameMatcher - */ /** * Narrows a scope by a (often synthetic) control-flow condition the new-world * way: resolve its narrowing through the scope's on-demand dispatcher and apply @@ -468,130 +225,6 @@ public function narrowScopeWithCondition(MutatingScope $scope, Expr $expr, TypeS return $scope->applySpecifiedTypes($specifiedTypes); } - /** - * @param Node\Stmt[] $bodyStmts - * @param Closure(string): bool $gotoNameMatcher - */ - private function resolveBackwardGotoScope( - Node $parentNode, - array $bodyStmts, - MutatingScope $scope, - ExpressionResultStorage $storage, - StatementContext $context, - Closure $gotoNameMatcher, - bool $mergeBodyScopeEachIteration, - ): MutatingScope - { - $bodyScope = $scope; - $count = 0; - $prevEntryScope = null; - do { - $prevScope = $bodyScope; - if ($mergeBodyScopeEachIteration) { - $bodyScope = $bodyScope->mergeWith($scope); - } - if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) { - // walking is deterministic in the entry scope - an unchanged entry - // reproduces the previous pass's exit, so the verification walk is skipped - $bodyScope = $prevScope; - break; - } - $prevEntryScope = $bodyScope; - $tempStorage = $storage->duplicate(); - $bodyScopeResult = $this->processStmtNodesInternal( - $parentNode, - $bodyStmts, - $bodyScope, - $tempStorage, - new NoopNodeCallback(), - $context->withoutTemplateArgumentResolution(), - ); - - $gotoScope = null; - foreach ($bodyScopeResult->getExitPoints() as $ep) { - $epStmt = $ep->getStatement(); - if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) { - continue; - } - - $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope()); - } - - if ($gotoScope !== null) { - $bodyScope = $scope->mergeWith($gotoScope); - } - - if ($bodyScope->equals($prevScope)) { - break; - } - - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while ($count < self::LOOP_SCOPE_ITERATIONS); - - return $bodyScope; - } - - /** - * @param InternalStatementExitPoint[] $exitPoints - * @return array{MutatingScope, bool, list} - */ - private function mergeForwardGotoExitPoints( - string $labelName, - MutatingScope $scope, - bool $alreadyTerminated, - array $exitPoints, - ): array - { - $newExitPoints = []; - foreach ($exitPoints as $exitPoint) { - $exitStmt = $exitPoint->getStatement(); - if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) { - if ($alreadyTerminated) { - $scope = $exitPoint->getScope(); - $alreadyTerminated = false; - } else { - $scope = $scope->mergeWith($exitPoint->getScope()); - } - } else { - $newExitPoints[] = $exitPoint; - } - } - - return [$scope, $alreadyTerminated, $newExitPoints]; - } - - /** - * @param Node\Stmt[] $nextStmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void - { - if ($nextStmts === []) { - return; - } - - $unreachableStatement = null; - $nextStatements = []; - - foreach ($nextStmts as $key => $nextStmt) { - if ($key === 0) { - $unreachableStatement = $nextStmt; - continue; - } - - $nextStatements[] = $nextStmt; - } - - if (!$unreachableStatement instanceof Node\Stmt) { - return; - } - - $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); - } - /** * @api * @param Node\Stmt[] $stmts @@ -615,8 +248,7 @@ public function processStmtNodes( // a fresh walk an extension starts mid-analysis must not feed the // interrupted walk's gatherer frames - they describe the body walk // that was interrupted, not the nested one - $gatherers = $this->nodeGatherers; - $this->nodeGatherers = []; + $gatherers = $this->suspendNodeGatherers(); try { return $this->processStmtNodesInternal( $parentNode, @@ -627,7 +259,7 @@ public function processStmtNodes( $context, )->toPublic(); } finally { - $this->nodeGatherers = $gatherers; + $this->restoreNodeGatherers($gatherers); $scope->popExpressionResultStorage(); } } @@ -656,7 +288,7 @@ public function processStmtNodesInternal( $scope->pushExpressionResultStorage($storage); } try { - return $this->doProcessStmtNodes($parentNode, $stmts, $scope, $storage, $nodeCallback, $context); + return $this->statementsHandler->doProcessStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context); } finally { if ($pushStorage) { $scope->popExpressionResultStorage(); @@ -665,3197 +297,736 @@ public function processStmtNodesInternal( } /** - * @param Node\Stmt[] $stmts * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function doProcessStmtNodes( - Node $parentNode, - array $stmts, + public function processStmtNode( + Node\Stmt $stmt, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, StatementContext $context, ): InternalStatementResult { - $stmtCount = count($stmts); - $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ - || $parentNode instanceof Node\Stmt\ClassMethod - || $parentNode instanceof PropertyHookStatementNode - || $parentNode instanceof Expr\Closure; - + $overridingThrowPoints = null; if ( - $shouldCheckLastStatement - && $stmtCount > 0 - && $this->unresolvedTemplateArguments - && $context->shouldResolveTemplateArguments() + !$stmt instanceof Static_ + && !$stmt instanceof Node\Stmt\Global_ + && !$stmt instanceof Node\Stmt\Property + && !$stmt instanceof Node\Stmt\ClassConst + && !$stmt instanceof Node\Stmt\Const_ + && !$stmt instanceof Node\Stmt\ClassLike + && !$stmt instanceof Node\Stmt\Function_ + && !$stmt instanceof Node\Stmt\ClassMethod ) { - return $this->processBodyStmtNodesTwoPass($parentNode, $stmts, $scope, $storage, $nodeCallback, $context); + if (!$stmt instanceof Foreach_) { + $scope = $this->statementsHandler->processStmtVarAnnotation($this, $scope, $storage, $stmt, null, $nodeCallback); + } + $overridingThrowPoints = $this->statementsHandler->getOverridingThrowPoints($stmt, $scope); + } + + if ($stmt instanceof Node\Stmt\ClassMethod) { + // a trait method the using class overrides is not analysed here at all - + // decided before the node callback is emitted + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + if ( + $scope->isInTrait() + && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString()) + ) { + $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); + if ($methodReflection instanceof NativeMethodReflection) { + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); + } + if ($methodReflection instanceof PhpMethodReflection) { + $declaringTrait = $methodReflection->getDeclaringTrait(); + if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); + } + } + } } - $state = new StatementListWalkState($scope); - foreach ($stmts as $i => $stmt) { - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, $shouldCheckLastStatement); + // Statements whose work is processing their expressions emit their node + // callback AFTER that processing, inside their branches below, with the + // entry scope - a synchronously invoked rule (the plain resolver, + // PHP < 8.1) then finds the expressions' results in the storage instead + // of re-walking them on demand, mirroring processExprNodeInternal(). + $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_ + || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_ + || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst + || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_ + || $stmt instanceof Node\Stmt\Do_; + if (!$deferredStmtCallback) { + $this->callNodeCallback($nodeCallback, $stmt, $scope, $storage); } - $statementResult = $state->toResult(); - if ($stmtCount === 0 && $shouldCheckLastStatement) { - $returnTypeNode = $parentNode->getReturnType(); - if ($parentNode instanceof Expr\Closure) { - $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + $stmtHandler = StmtHandlerRegistry::resolve($stmt, $this->container); + if ($stmtHandler !== null) { + $stmtResult = $stmtHandler->processStmt($this, $stmt, $scope, $storage, $nodeCallback, $context); + if ($overridingThrowPoints !== null) { + // the overriding throw points use the scope before the statement, + // so the variable flow throws before the statement does its work + $overridingThrowFlows = []; + foreach ($overridingThrowPoints as $overridingThrowPoint) { + $overridingThrowFlows[] = VariableFlow::throwing($overridingThrowPoint->getType(), true, $overridingThrowPoint->canContainAnyThrowable()); + } + + return new InternalStatementResult( + $stmtResult->getScope(), + hasYield: $stmtResult->hasYield(), + isAlwaysTerminating: $stmtResult->isAlwaysTerminating(), + exitPoints: $stmtResult->getExitPoints(), + throwPoints: $overridingThrowPoints, + impurePoints: $stmtResult->getImpurePoints(), + endStatements: $stmtResult->getEndStatements(), + variableFlow: VariableFlow::sequence(...$overridingThrowFlows, ...[$stmtResult->getVariableFlow()]), + ); } - // the body is empty - the statement above is a synthetic wrapper around - // the closure, not an expression statement that was processed - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $parentNode, - $statementResult->toPublic(), - $returnTypeNode !== null, - ), $scope, $storage); + + return $stmtResult; } - return $statementResult; + // statements with no analysis of their own (e.g. HaltCompiler) + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []); } - /** - * One statement of a statement list, advancing $state past it. - * - * @param Node\Stmt[] $stmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processStatementStep( - Node $parentNode, - array $stmts, - int $i, - Node\Stmt $stmt, - StatementListWalkState $state, - ExpressionResultStorage $storage, - callable $nodeCallback, - StatementContext $context, - bool $shouldCheckLastStatement, - ): void + public function isAnalysedFile(string $fileName): bool { - if ($state->alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) { - return; - } + return isset($this->analysedFiles[$fileName]); + } - $isLast = $i === count($stmts) - 1; + /** Whether an on-demand walk answers already processed real nodes from their stored results (see processExprOnDemand()). */ + public function isReturningStoredExpressionResults(): bool + { + return $this->returnStoredExpressionResults; + } - $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); - if ($nestedLabelNames !== null && $context->isTopLevel()) { - $state->scope = $this->resolveBackwardGotoScope( - $parentNode, - [$stmt], - $state->scope, - $storage, - $context->enterDeep(), - static fn (string $name): bool => isset($nestedLabelNames[$name]), - false, - ); - } + /** Whether the walk consumes stored results unconditionally (see processExprNodeConsumingStored()). */ + public function isConsumingStoredExpressionResults(): bool + { + return $this->consumeStoredExpressionResults; + } - $statementResult = $this->processStmtNode( - $stmt, - $state->scope, - $storage, - $nodeCallback, - $context, - ); - $state->variableFlows[$i] = $statementResult->getVariableFlow(); - $state->scope = $statementResult->getScope(); - $state->hasYield = $state->hasYield || $statementResult->hasYield(); - - if ($stmt instanceof Node\Stmt\Label) { - $labelName = $stmt->name->toString(); - - [$state->scope, $state->alreadyTerminated, $state->exitPoints] = $this->mergeForwardGotoExitPoints( - $labelName, - $state->scope, - $state->alreadyTerminated, - $state->exitPoints, - ); + public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope + { + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); + } - if ($state->alreadyTerminated) { - return; - } + public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope + { + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); + } - if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) { - $state->scope = $this->resolveBackwardGotoScope( - $parentNode, - array_slice($stmts, $i + 1), - $state->scope, - $storage, - $context->enterDeep(), - static fn (string $name): bool => $name === $labelName, - true, - ); - } + /** + * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback + */ + private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope + { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { + $scope = $callback($scope, $expr); } - if ($shouldCheckLastStatement && $isLast) { - $hasDeclaredReturnType = ($parentNode instanceof Node\FunctionLike || $parentNode instanceof PropertyHookStatementNode) - && $parentNode->getReturnType() !== null; - $endStatements = $statementResult->getEndStatements(); - if (count($endStatements) > 0) { - foreach ($endStatements as $endStatement) { - $endStatementResult = $endStatement->getResult(); - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $endStatement->getStatement(), - (new InternalStatementResult( - $endStatementResult->getScope(), - $state->hasYield, - $endStatementResult->isAlwaysTerminating(), - $endStatementResult->getExitPoints(), - $endStatementResult->getThrowPoints(), - $endStatementResult->getImpurePoints(), - ))->toPublic(), - $hasDeclaredReturnType, - $this->readEndStatementExprResult($endStatement->getStatement(), $storage), - ), $endStatementResult->getScope(), $storage); + if ($expr instanceof ArrayDimFetch) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { + $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); + } elseif ($expr instanceof List_) { + foreach ($expr->items as $item) { + if ($item === null) { + continue; } - } else { - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $stmt, - (new InternalStatementResult( - $state->scope, - $state->hasYield, - $statementResult->isAlwaysTerminating(), - $statementResult->getExitPoints(), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - ))->toPublic(), - $hasDeclaredReturnType, - $this->readEndStatementExprResult($stmt, $storage), - ), $state->scope, $storage); - } - } - $state->exitPoints = array_merge($state->exitPoints, $statementResult->getExitPoints()); - $state->throwPoints = array_merge($state->throwPoints, $statementResult->getThrowPoints()); - $state->impurePoints = array_merge($state->impurePoints, $statementResult->getImpurePoints()); - - if ($state->alreadyTerminated || !$statementResult->isAlwaysTerminating()) { - return; + $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); + } } - $state->alreadyTerminated = true; - $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - $this->processUnreachableStatement($nextStmts, $state->scope, $storage, $nodeCallback); + return $scope; } /** - * A function-like body under the unresolvedTemplateArguments toggle is - * walked in two passes. The observation pass walks every statement - * recording rule-facing emissions and threading immutable constraints - * through the scopes returned by expressions and statements. A body that - * created no unresolved template argument simply replays the recording. - * Otherwise TemplateArgumentResolver builds a new resolved frame for the - * second pass, which re-walks only the statements the resolutions can - * influence. Recorded scopes retain their original collection context. - * - * The outer gatherer frames (the method's return statements, execution - * ends, impure points) are suspended during the observation pass and fed - * by the replay and the re-walk, so each emission reaches them once. - * - * @param Node\Stmt[] $stmts - * @param callable(Node $node, Scope $scope): void $nodeCallback + * Processes an expression outside the normal AST traversal - e.g. a synthetic + * node a rule or extension asks about. Real AST nodes contained in it return + * their already-stored results instead of being processed again. New results + * are stored into the given storage - pass a duplicate to keep them isolated. */ - private function processBodyStmtNodesTwoPass( - Node $parentNode, - array $stmts, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - StatementContext $context, - ): InternalStatementResult + /** + * Processes an expression whose already-walked subtrees must be CONSUMED + * from their stored results instead of re-walked: the nullsafe handlers + * process the receiver once (real callbacks) and then walk the plain twin, + * whose receiver subtree answers from storage, re-anchored to the twin's + * (ensured) scope. + * + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processExprNodeConsumingStored(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $statementStartTokenPositions = []; - foreach ($stmts as $stmt) { - $statementStartTokenPositions[] = $stmt->getStartTokenPos(); - } - $parentFrame = $scope->getCurrentTemplateArgumentFrame(); - $parentConstraints = $scope->getTemplateArgumentConstraints(); - $frame = new TemplateArgumentFrame($parentFrame); - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('bodiesWalked'); - TemplateArgumentStats::increment('statementsTotal', count($stmts)); - } - $scope = $scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); - $recording = new RecordingNodeCallback(); - $state = new StatementListWalkState($scope); - /** @var list $entries the state and recording offset before each statement, plus the final ones */ - $entries = []; - $observationContext = $context->withoutTemplateArgumentResolution(); - $suspendedGatherers = $this->nodeGatherers; - $this->nodeGatherers = []; + $previous = $this->consumeStoredExpressionResults; + $this->consumeStoredExpressionResults = true; try { - foreach ($stmts as $i => $stmt) { - $entries[$i] = [clone $state, $recording->count()]; - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $recording, $observationContext, true); - } + return $this->processExprNode($stmt, $expr, $scope, $storage, $nodeCallback, $context); } finally { - $this->nodeGatherers = $suspendedGatherers; + $this->consumeStoredExpressionResults = $previous; } - $frame = $this->templateArgumentResolver->resolve($state->scope->getTemplateArgumentConstraints() ?? TemplateArgumentConstraints::createEmpty(), $parentFrame, $statementStartTokenPositions); - $stmtCount = count($stmts); - $entries[$stmtCount] = [clone $state, $recording->count()]; - - $firstSiteStatementIndex = $frame->firstSiteStatementIndex(); - if ($firstSiteStatementIndex === null) { - $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); + } - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); + public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult + { + // A node no handler supports - a virtual node (BooleanOrNode, ...) a + // rule asked the type of - degrades to mixed, mirroring + // MutatingScope::resolveType()'s fallback. The main walk's unhandled + // throw stays: real source nodes must have a handler. + if ( + ExprHandlerRegistry::resolve($expr, $this->container) === null + && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) + ) { + $mixed = new MixedType(); + return $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + type: $mixed, + nativeType: $mixed, + ); } - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('bodiesWithSites'); - TemplateArgumentStats::increment('statementsReplayed', $firstSiteStatementIndex); - } - // the second pass: the statements before the first site stand as - // recorded; from there on a statement is re-walked only when it - // created a site or mentions a variable whose tracked state the - // resolutions changed - the rest replay their recording and carry - // their recorded effect onto the re-walked scope - $this->replayRecordingRange($recording, 0, $entries[$firstSiteStatementIndex][1], $nodeCallback, $storage, $scope); - $hasLabels = false; - foreach ($stmts as $stmt) { - if (!$stmt instanceof Node\Stmt\Label && $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE) === null) { - continue; - } - $hasLabels = true; - break; + // save/restore, never reset: on-demand walks nest (a typeCallback + // evaluated mid-walk prices another synthetic node) and a hard reset + // would turn stored-result consumption off for the rest of the outer + // walk - re-processing every remaining subtree and bypassing the + // closure-argument consume guards in ArgumentsHandler::processArgs() + $previous = $this->returnStoredExpressionResults; + $this->returnStoredExpressionResults = true; + $scope->pushExpressionResultStorage($storage); + try { + return $this->processExprNode( + new Node\Stmt\Expression($expr), + $expr, + $scope, + $storage, + new NoopNodeCallback(), + ExpressionContext::createTopLevel(resolveTemplateArguments: false), + ); + } finally { + $scope->popExpressionResultStorage(); + $this->returnStoredExpressionResults = $previous; } - $state = clone $entries[$firstSiteStatementIndex][0]; - $state->scope = $state->scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); - for ($i = $firstSiteStatementIndex; $i < $stmtCount; $i++) { - [$recordedEntry, $offset] = $entries[$i]; - [$recordedExit, $nextOffset] = $entries[$i + 1]; - $differingRoots = $state->alreadyTerminated === $recordedEntry->alreadyTerminated - ? $state->scope->getDifferingVariableRoots($recordedEntry->scope) - : null; - if ($differingRoots === [] && !$frame->hasSiteAtOrAfter($i)) { - // converged with the observation pass: the rest of its recording stands - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('earlyExits'); - TemplateArgumentStats::increment('statementsReplayed', $stmtCount - $i); - } - $this->replayRecordingRange($recording, $offset, $recording->count(), $nodeCallback, $storage, $scope); - $this->appendRecordedStatementResults($state, $recordedEntry, $entries[$stmtCount][0]); - $state->scope = $entries[$stmtCount][0]->scope; - - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); - } - - $stmt = $stmts[$i]; - $reWalk = $differingRoots === null - || $hasLabels - || $frame->ownsSiteInStatement($i) - || $this->statementMentionsAnyVariable($stmt, $differingRoots); - if (self::$debugTemplateArguments) { - echo sprintf( - "[template-arguments] %s:%d statement %d: %s (differing: %s)\n", - $scope->getFile(), - $stmt->getStartLine(), - $i, - $reWalk ? 're-walk' : 'replay', - $differingRoots === null ? 'non-variable key' : implode(', ', $differingRoots), - ); - } - if ($reWalk) { - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('statementsReWalked'); - } - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, true); - continue; - } + } - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('statementsReplayed'); - } - $this->replayRecordingRange($recording, $offset, $nextOffset, $nodeCallback, $storage, $scope); - $this->appendRecordedStatementResults($state, $recordedEntry, $recordedExit); - $state->scope = $state->scope->withRecordedStatementDelta($recordedEntry->scope, $recordedExit->scope); + /** + * The stored ExpressionResult of a node processExprNode() already processed + * into the given storage - the caller asserts the processing order by + * holding the very storage it processed the node into (a scope-based lookup + * would miss loop-convergence storages, which are never scope-visible). + * Throws when the node has no stored result. + */ + public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): ExpressionResult + { + $result = $storage->findExpressionResult($expr); + if ($result === null) { + throw new ShouldNotHappenException(sprintf( + '%s on line %d has no stored ExpressionResult - it was not processed by processExprNode().', + get_class($expr), + $expr->getStartLine(), + )); } - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); + return $result; } /** - * The parameter type an argument is observed against: the declared one with - * the template types the call already decided substituted - the receiver's - * class-level arguments, a template an earlier argument inferred - while a - * template still open (ErrorType in the resolved map, typically the one this - * very argument decides) stays a TemplateType, which the observer ignores. - * The resolved parameter type would carry such a template's bound instead. + * The type, on the given scope, of a node that may or may not have a stored + * ExpressionResult. Every call site of this method is UNDECIDED about whether + * the node was already analysed - each should eventually either consume the + * node's ExpressionResult where it was processed or be a synthetic node + * (processSyntheticOnDemand()). */ - private function findOriginalParameterType(ParametersAcceptor $acceptor, ParameterReflection $parameter): ?Type + public function readTypeOfMaybeStored(Expr $expr, MutatingScope $scope): Type { - if (!$acceptor instanceof ResolvedFunctionVariant) { - return $parameter->getType(); - } - $originalParameters = $acceptor->getOriginalParametersAcceptor()->getParameters(); - foreach ($acceptor->getParameters() as $index => $resolvedParameter) { - if ($resolvedParameter !== $parameter) { - continue; - } - if (!isset($originalParameters[$index])) { - return null; - } - $originalType = $originalParameters[$index]->getType(); - if (!$originalType->hasTemplateOrLateResolvableType()) { - return $originalType; - } - $decided = []; - foreach ($acceptor->getResolvedTemplateTypeMap()->getTypes() as $name => $type) { - if ($type instanceof ErrorType) { - continue; - } - $decided[$name] = $type; - } - - return TemplateTypeHelper::resolveTemplateTypes( - $originalType, - new TemplateTypeMap($decided), - $acceptor->getCallSiteVarianceMap(), - TemplateTypeVariance::createContravariant(), - ); + $storage = $scope->getCurrentExpressionResultStorage(); + $result = $storage !== null ? $storage->findExpressionResult($expr) : null; + if ($result !== null) { + return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted); } - return null; + return $this->readScopeStateOrSyntheticType($expr, $scope); } - /** Carries what the recorded walk from $from to $to added onto $state. */ - private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void + /** + * The type the scope itself knows for the expression, without any node + * processing: a string-named variable read is scope state (mirrors + * VariableHandler's typeCallback), and a type tracked for the whole + * expression answers directly - an on-demand walk would return that very + * holder anyway (the fresh result's beforeScope is the asking scope), + * after paying the walk. Null when the scope has no answer; the caller + * decides whether that means a synthetic walk (processSyntheticOnDemand()) + * or an invariant violation. + */ + public function findScopeStateType(Expr $expr, MutatingScope $scope): ?Type { - foreach ($to->variableFlows as $index => $flow) { - if (array_key_exists($index, $from->variableFlows)) { - continue; + if ($expr instanceof Expr\Variable && is_string($expr->name)) { + if ($scope->hasVariableType($expr->name)->no()) { + return new ErrorType(); } - $state->variableFlows[$index] = $flow; + return $scope->getVariableType($expr->name); } - $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))); - $state->throwPoints = array_merge($state->throwPoints, array_slice($to->throwPoints, count($from->throwPoints))); - $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); + // a literal is position-independent: the scope prices it without a walk, + // so an argument the walk reaches only later (an IIFE's or a pipe's + // operand) never has to be processed ahead of its turn + if ($expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_) { + return $scope->getStateType($expr); } - $flows = []; - foreach (array_keys($names) as $name) { - $flows[] = VariableFlow::mention($name); + // A variable whose name is an expression ($$name) never reaches the read + // above, and the scope tracks it like any other expression - so it belongs + // here rather than falling through to a walk. + if ( + !$expr instanceof Expr\Closure + && !$expr instanceof Expr\ArrowFunction + && $scope->hasExpressionType($expr)->yes() + ) { + return TypeUtils::resolveLateResolvableTypes($scope->getTrackedExpressionType($expr)); } - return VariableFlow::sequence(...$flows); + + return null; } - private const MENTIONED_VARIABLES_ATTRIBUTE = 'templateArgumentMentionedVariables'; + /** + * The type, on the given scope, of a node the caller knows has no stored + * ExpressionResult in its walk: scope state (variable read / tracked + * holder) answers without a walk, anything else is priced as a synthetic + * node. + */ + public function readScopeStateOrSyntheticType(Expr $expr, MutatingScope $scope): Type + { + return $this->findScopeStateType($expr, $scope) ?? $this->processSyntheticOnDemand($expr, $scope)->getTypeOnScope($scope, $scope->nativeTypesPromoted); + } /** - * @param list $variableNames + * The type the scope knows for an expression the caller has already pinned as + * tracked there (hasExpressionType() yes, or a string-named variable). Unlike + * readScopeStateOrSyntheticType() this never falls back to a synthetic walk - + * the caller decided that the scope answers. */ - private function statementMentionsAnyVariable(Node\Stmt $stmt, array $variableNames): bool + public function requireScopeStateType(Expr $expr, MutatingScope $scope): Type { - /** @var array{array, bool}|null $mentions */ - $mentions = $stmt->getAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE); - if ($mentions === null) { - $names = []; - $mentionsEverything = false; - $this->collectMentionedVariables($stmt, $names, $mentionsEverything); - $mentions = [$names, $mentionsEverything]; - $stmt->setAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE, $mentions); - } - [$names, $mentionsEverything] = $mentions; - if ($mentionsEverything) { - return true; - } - foreach ($variableNames as $variableName) { - if (isset($names[$variableName])) { - return true; - } + $type = $this->findScopeStateType($expr, $scope); + if ($type === null) { + throw new ShouldNotHappenException(sprintf( + '%s on line %d is not tracked on the scope it was pinned as tracked on.', + get_class($expr), + $expr->getStartLine(), + )); } - return false; + return $type; } /** - * Every variable the statement can read or write - syntactically, so - * reads and writes alike - with `$this`; a closure body lives in its own - * scope, so only its use() clause (and its bound `$this`) count, an arrow - * function captures implicitly and is traversed. Dynamic access - * (`$$name`, compact(), extract(), get_defined_vars(), eval, include) - * mentions everything. - * - * @param array $names + * Fires the PHPSTAN_GUARD_NW diagnostic when a real (non-synthetic) AST node + * reaches an on-demand pricing path without having been processed and stored + * by processExprNode() first. Mirrors the guard in MutatingScope::getType(): + * such a node should be answered from its stored ExpressionResult, never + * re-priced as if it were synthetic. Dormant unless PHPSTAN_GUARD_NW=1. */ - private function collectMentionedVariables(Node $node, array &$names, bool &$mentionsEverything): void + private function guardAgainstUnprocessedRealNode(Expr $expr, string $caller): void { - if ($node instanceof Expr\Variable) { - if (!is_string($node->name)) { - $mentionsEverything = true; - return; - } - $names[$node->name] = true; + if ( + !self::$guardNewWorld + || !isset(self::$guardRealExprIds[spl_object_id($expr)]) + || isset(self::$guardProcessedExprIds[spl_object_id($expr)]) + ) { return; } - if ($node instanceof Expr\Closure) { - if (!$node->static) { - $names['this'] = true; - } - foreach ($node->uses as $use) { - if (!is_string($use->var->name)) { - $mentionsEverything = true; - continue; - } - $names[$use->var->name] = true; - } - return; - } - if ($node instanceof Expr\Eval_ || $node instanceof Expr\Include_) { - $mentionsEverything = true; - } elseif ( - $node instanceof Expr\FuncCall - && $node->name instanceof Name - && in_array($node->name->toLowerString(), ['compact', 'extract', 'get_defined_vars'], true) - ) { - $mentionsEverything = true; - } + throw new ShouldNotHappenException(sprintf( + '%s() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.', + $caller, + get_class($expr), + $expr->getStartLine(), + )); + } - foreach ($node->getSubNodeNames() as $subNodeName) { - $subNode = $node->$subNodeName; - if ($subNode instanceof Node) { - $this->collectMentionedVariables($subNode, $names, $mentionsEverything); - } elseif (is_array($subNode)) { - foreach ($subNode as $item) { - if (!$item instanceof Node) { - continue; - } - $this->collectMentionedVariables($item, $names, $mentionsEverything); - } - } - } + /** + * Processes a synthetic node (one an ExprHandler built itself) on a duplicate + * of the storage of the analysis currently in progress, mirroring + * MutatingScope::resolveTypeOfNewWorldHandlerNode(): the duplicate isolates + * the synthetic node's own stored result from the live storage while its real + * subnodes still resolve from the fallback. + */ + public function processSyntheticOnDemand(Expr $expr, MutatingScope $scope): ExpressionResult + { + $this->guardAgainstUnprocessedRealNode($expr, __FUNCTION__); + $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); + + return $this->processExprOnDemand($expr, $scope, $current->duplicate()); } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - public function processStmtNode( + public function processExprNode( Node\Stmt $stmt, + Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, - StatementContext $context, - ): InternalStatementResult + ExpressionContext $context, + ): ExpressionResult { - $overridingThrowPoints = null; - if ( - !$stmt instanceof Static_ - && !$stmt instanceof Node\Stmt\Global_ - && !$stmt instanceof Node\Stmt\Property - && !$stmt instanceof Node\Stmt\ClassConst - && !$stmt instanceof Node\Stmt\Const_ - && !$stmt instanceof Node\Stmt\ClassLike - && !$stmt instanceof Node\Stmt\Function_ - && !$stmt instanceof Node\Stmt\ClassMethod - ) { - if (!$stmt instanceof Foreach_) { - $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); - } - $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); - } - - if ($stmt instanceof Node\Stmt\ClassMethod) { - // a trait method the using class overrides is not analysed here at all - - // decided before the node callback is emitted - if (!$scope->isInClass()) { - throw new ShouldNotHappenException(); - } - if ( - $scope->isInTrait() - && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString()) - ) { - $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); - if ($methodReflection instanceof NativeMethodReflection) { - return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); - } - if ($methodReflection instanceof PhpMethodReflection) { - $declaringTrait = $methodReflection->getDeclaringTrait(); - if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { - return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); - } - } - } - } - - // Statements whose work is processing their expressions emit their node - // callback AFTER that processing, inside their branches below, with the - // entry scope - a synchronously invoked rule (the plain resolver, - // PHP < 8.1) then finds the expressions' results in the storage instead - // of re-walking them on demand, mirroring processExprNodeInternal(). - $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_ - || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_ - || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst - || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_ - || $stmt instanceof Node\Stmt\Do_; - if (!$deferredStmtCallback) { - $this->callNodeCallback($nodeCallback, $stmt, $scope, $storage); - } - - $stmtHandler = StmtHandlerRegistry::resolve($stmt, $this->container); - if ($stmtHandler !== null) { - $stmtResult = $stmtHandler->processStmt($this, $stmt, $scope, $storage, $nodeCallback, $context); - if ($overridingThrowPoints !== null) { - // the overriding throw points use the scope before the statement, - // so the variable flow throws before the statement does its work - $overridingThrowFlows = []; - foreach ($overridingThrowPoints as $overridingThrowPoint) { - $overridingThrowFlows[] = VariableFlow::throwing($overridingThrowPoint->getType(), true, $overridingThrowPoint->canContainAnyThrowable()); - } - - return new InternalStatementResult( - $stmtResult->getScope(), - hasYield: $stmtResult->hasYield(), - isAlwaysTerminating: $stmtResult->isAlwaysTerminating(), - exitPoints: $stmtResult->getExitPoints(), - throwPoints: $overridingThrowPoints, - impurePoints: $stmtResult->getImpurePoints(), - endStatements: $stmtResult->getEndStatements(), - variableFlow: VariableFlow::sequence(...$overridingThrowFlows, ...[$stmtResult->getVariableFlow()]), - ); - } - - return $stmtResult; - } - - // statements with no analysis of their own (e.g. HaltCompiler) - return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []); - } - - /** - * @return InternalThrowPoint[]|null - */ - private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array - { - foreach ($statement->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $function = $scope->getFunction(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - - $throwsTag = $resolvedPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwsType = $throwsTag->getType(); - if ($throwsType->isVoid()->yes()) { - return []; - } - - return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; - } - } - - return null; - } - - public function isAnalysedFile(string $fileName): bool - { - return isset($this->analysedFiles[$fileName]); - } - - public function shouldPolluteScopeWithLoopInitialAssignments(): bool - { - return $this->polluteScopeWithLoopInitialAssignments; - } - - public function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - - public function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - - /** @return ExtensionsCollection */ - public function getReadWritePropertiesExtensions(): ExtensionsCollection - { - return $this->readWritePropertiesExtensions; - } - - public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope - { - return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); - } - - public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope - { - return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); - } - - /** - * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback - */ - private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope - { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $callback($scope, $expr); - } - - if ($expr instanceof ArrayDimFetch) { - $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); - } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { - $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); - } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { - $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); - } elseif ($expr instanceof List_) { - foreach ($expr->items as $item) { - if ($item === null) { - continue; - } - - $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); - } - } - - return $scope; - } - - /** - * Processes an expression outside the normal AST traversal - e.g. a synthetic - * node a rule or extension asks about. Real AST nodes contained in it return - * their already-stored results instead of being processed again. New results - * are stored into the given storage - pass a duplicate to keep them isolated. - */ - /** - * Processes an expression whose already-walked subtrees must be CONSUMED - * from their stored results instead of re-walked: the nullsafe handlers - * process the receiver once (real callbacks) and then walk the plain twin, - * whose receiver subtree answers from storage, re-anchored to the twin's - * (ensured) scope. - * - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processExprNodeConsumingStored(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - $previous = $this->consumeStoredExpressionResults; - $this->consumeStoredExpressionResults = true; - try { - return $this->processExprNode($stmt, $expr, $scope, $storage, $nodeCallback, $context); - } finally { - $this->consumeStoredExpressionResults = $previous; - } - } - - public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult - { - // A node no handler supports - a virtual node (BooleanOrNode, ...) a - // rule asked the type of - degrades to mixed, mirroring - // MutatingScope::resolveType()'s fallback. The main walk's unhandled - // throw stays: real source nodes must have a handler. - if ( - ExprHandlerRegistry::resolve($expr, $this->container) === null - && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) - ) { - return $this->createEagerExpressionResult($scope, $expr, new MixedType(), new MixedType()); - } - - // save/restore, never reset: on-demand walks nest (a typeCallback - // evaluated mid-walk prices another synthetic node) and a hard reset - // would turn stored-result consumption off for the rest of the outer - // walk - re-processing every remaining subtree and bypassing the - // closure-argument consume guards in processArgs() - $previous = $this->returnStoredExpressionResults; - $this->returnStoredExpressionResults = true; - $scope->pushExpressionResultStorage($storage); - try { - return $this->processExprNode( - new Node\Stmt\Expression($expr), - $expr, - $scope, - $storage, - new NoopNodeCallback(), - ExpressionContext::createTopLevel(resolveTemplateArguments: false), - ); - } finally { - $scope->popExpressionResultStorage(); - $this->returnStoredExpressionResults = $previous; - } - } - - /** - * Processes an expression for an independent analysis pass - a reflection- - * level lazy computation that builds its own scope from scratch (e.g. - * private property type inference from constructor assignments). Such a - * pass legitimately prices real AST nodes outside the file's main walk, - * on a fresh storage of its own. - */ - public function processIndependentPassExpr(Expr $expr, MutatingScope $scope): ExpressionResult - { - return $this->processExprOnDemand($expr, $scope, new ExpressionResultStorage()); - } - - /** - * The stored ExpressionResult of a node processExprNode() already processed - * into the given storage - the caller asserts the processing order by - * holding the very storage it processed the node into (a scope-based lookup - * would miss loop-convergence storages, which are never scope-visible). - * Throws when the node has no stored result. - */ - public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): ExpressionResult - { - $result = $storage->findExpressionResult($expr); - if ($result === null) { - throw new ShouldNotHappenException(sprintf( - '%s on line %d has no stored ExpressionResult - it was not processed by processExprNode().', - get_class($expr), - $expr->getStartLine(), - )); - } - - return $result; - } - - /** - * The result of the expression an ending statement evaluated, for - * ExecutionEndNode. Null when the statement is not an expression statement, - * and when its expression was not processed into this storage - an end - * statement is collected from wherever execution ended, which can be a - * nested walk whose results never reach the frame the end node is built in. - */ - private function readEndStatementExprResult(Node\Stmt $stmt, ExpressionResultStorage $storage): ?ExpressionResult - { - if (!$stmt instanceof Node\Stmt\Expression) { - return null; - } - - return $storage->findExpressionResult($stmt->expr); - } - - /** - * The result processArgs() captured for a call argument. Unlike the storage - * lookup this reads the argument's authoritative result: a closure/arrow - * function argument's captured result is the properly-typed one, not the - * placeholder its body walk stored. - * - * @param array $argResults - */ - private function readArgResult(array $argResults, Expr $argValue): ExpressionResult - { - $result = $argResults[spl_object_id($argValue)] ?? null; - if ($result === null) { - throw new ShouldNotHappenException(sprintf( - '%s on line %d has no captured ExpressionResult - it was not processed as an argument by processArgs().', - get_class($argValue), - $argValue->getStartLine(), - )); - } - - return $result; - } - - /** - * The type, on the given scope, of a node that may or may not have a stored - * ExpressionResult. Every call site of this method is UNDECIDED about whether - * the node was already analysed - each should eventually either consume the - * node's ExpressionResult where it was processed or be a synthetic node - * (processSyntheticOnDemand()). - */ - public function readTypeOfMaybeStored(Expr $expr, MutatingScope $scope): Type - { - $storage = $scope->getCurrentExpressionResultStorage(); - $result = $storage !== null ? $storage->findExpressionResult($expr) : null; - if ($result !== null) { - return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted); - } - - return $this->readScopeStateOrSyntheticType($expr, $scope); - } - - /** - * The type the scope itself knows for the expression, without any node - * processing: a string-named variable read is scope state (mirrors - * VariableHandler's typeCallback), and a type tracked for the whole - * expression answers directly - an on-demand walk would return that very - * holder anyway (the fresh result's beforeScope is the asking scope), - * after paying the walk. Null when the scope has no answer; the caller - * decides whether that means a synthetic walk (processSyntheticOnDemand()) - * or an invariant violation. - */ - public function findScopeStateType(Expr $expr, MutatingScope $scope): ?Type - { - if ($expr instanceof Expr\Variable && is_string($expr->name)) { - if ($scope->hasVariableType($expr->name)->no()) { - return new ErrorType(); - } - - return $scope->getVariableType($expr->name); - } - - // a literal is position-independent: the scope prices it without a walk, - // so an argument the walk reaches only later (an IIFE's or a pipe's - // operand) never has to be processed ahead of its turn - if ($expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_) { - return $scope->getStateType($expr); - } - - // A variable whose name is an expression ($$name) never reaches the read - // above, and the scope tracks it like any other expression - so it belongs - // here rather than falling through to a walk. - if ( - !$expr instanceof Expr\Closure - && !$expr instanceof Expr\ArrowFunction - && $scope->hasExpressionType($expr)->yes() - ) { - return TypeUtils::resolveLateResolvableTypes($scope->getTrackedExpressionType($expr)); - } - - return null; - } - - /** - * The type, on the given scope, of a node the caller knows has no stored - * ExpressionResult in its walk: scope state (variable read / tracked - * holder) answers without a walk, anything else is priced as a synthetic - * node. - */ - public function readScopeStateOrSyntheticType(Expr $expr, MutatingScope $scope): Type - { - return $this->findScopeStateType($expr, $scope) ?? $this->processSyntheticOnDemand($expr, $scope)->getTypeOnScope($scope, $scope->nativeTypesPromoted); - } - - /** - * The type the scope knows for an expression the caller has already pinned as - * tracked there (hasExpressionType() yes, or a string-named variable). Unlike - * readScopeStateOrSyntheticType() this never falls back to a synthetic walk - - * the caller decided that the scope answers. - */ - public function requireScopeStateType(Expr $expr, MutatingScope $scope): Type - { - $type = $this->findScopeStateType($expr, $scope); - if ($type === null) { - throw new ShouldNotHappenException(sprintf( - '%s on line %d is not tracked on the scope it was pinned as tracked on.', - get_class($expr), - $expr->getStartLine(), - )); - } - - return $type; - } - - /** - * Fires the PHPSTAN_GUARD_NW diagnostic when a real (non-synthetic) AST node - * reaches an on-demand pricing path without having been processed and stored - * by processExprNode() first. Mirrors the guard in MutatingScope::getType(): - * such a node should be answered from its stored ExpressionResult, never - * re-priced as if it were synthetic. Dormant unless PHPSTAN_GUARD_NW=1. - */ - private function guardAgainstUnprocessedRealNode(Expr $expr, string $caller): void - { - if ( - !self::$guardNewWorld - || !isset(self::$guardRealExprIds[spl_object_id($expr)]) - || isset(self::$guardProcessedExprIds[spl_object_id($expr)]) - ) { - return; - } - - throw new ShouldNotHappenException(sprintf( - '%s() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.', - $caller, - get_class($expr), - $expr->getStartLine(), - )); - } - - /** - * Processes a synthetic node (one an ExprHandler built itself) on a duplicate - * of the storage of the analysis currently in progress, mirroring - * MutatingScope::resolveTypeOfNewWorldHandlerNode(): the duplicate isolates - * the synthetic node's own stored result from the live storage while its real - * subnodes still resolve from the fallback. - */ - public function processSyntheticOnDemand(Expr $expr, MutatingScope $scope): ExpressionResult - { - $this->guardAgainstUnprocessedRealNode($expr, __FUNCTION__); - $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); - - return $this->processExprOnDemand($expr, $scope, $current->duplicate()); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processExprNode( - Node\Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { - $storedResult = $storage->findExpressionResult($expr); - // a stored result only answers when the current scope agrees with its - // evaluation position on the variables the expression reads - a - // counterfactual walk (an extension re-binding a variable and pricing - // a real subtree, e.g. array_filter's per-element callback evaluation) - // re-processes the node on its own scope instead. In CONSUME mode the - // divergence is intentional (an ensured-non-null device) and the - // stored result is consumed unconditionally, re-anchored below. - if ($storedResult !== null && ($this->consumeStoredExpressionResults || $storedResult->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted))) { - // a foreign-position answer must not thread its original walk - // scopes into THIS walk - re-anchor it to the asking position so - // subsequent operands keep evaluating on the asking scope - if ($storedResult->getBeforeScope() === $scope) { - return $storedResult; - } - - $reanchored = $storedResult->atAskPosition($scope); - if ($this->consumeStoredExpressionResults) { - // the re-anchored view IS this walk's result for the node - // (the nullsafe twin's receiver at the ensured position) - - // store it so later asks (rules' storage reads) see the same - // result the twin walk itself consumed, exactly like the - // receiver walked inside the twin used to be stored - $this->storeExpressionResult($storage, $expr, $reanchored); - } - - return $reanchored; - } - } - - return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processExprNodeInternal( - Node\Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { - if ($expr instanceof FuncCall) { - $newExpr = new FunctionCallableNode($expr->name, $expr); - } elseif ($expr instanceof MethodCall) { - $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); - } elseif ($expr instanceof StaticCall) { - $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); - } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { - $newExpr = new InstantiationCallableNode($expr->class, $expr); - } else { - throw new ShouldNotHappenException(); - } - - $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); - $expressionResult = $this->expressionResultFactory->create( - $newExprResult->getScope(), - beforeScope: $scope, - expr: $expr, - hasYield: $newExprResult->hasYield(), - 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()), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $expr, $expressionResult); - return $expressionResult; - } - - $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container); - if ($exprHandler !== null) { - $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); - // a chain link an enclosing isset/empty/?? could not device ahead - // of its walk (an untracked call) is deviced now, from the type - // the walk produced - $expressionResult = $this->getNonNullabilityHelper()->applyPendingEnsure($expr, $expressionResult); - $this->storeExpressionResult($storage, $expr, $expressionResult); - // Force potential producers before collecting the body's constraints. - // Type reads only construct markers; they never register sites as a side effect. - $frame = $scope->getCurrentTemplateArgumentFrame(); - if ( - $frame !== null && $frame->isObserving() - && $expr instanceof Expr\CallLike - ) { - $constraints = $this->templateArgumentObserver->collectSites($expressionResult->getType()); - $expressionResult = $expressionResult->withScope($expressionResult->getScope()->addTemplateArgumentConstraints($constraints)); - $this->storeExpressionResult($storage, $expr, $expressionResult); - } - // The node's own callback fires AFTER its result is stored, with the - // scope captured before processing. Rules observe the same (scope, - // answer) pair as at a pre-order emission - previously a pre-order - // rule parks on its first ask and resumes at this store anyway - but - // a synchronously invoked rule (the plain resolver, PHP < 8.1) now - // finds the node's and its subtree's results in the storage instead - // of re-walking them on demand. - $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); - // the call is now processed and stored; emit a virtual node so - // impossible-check rules read its specified types from the result - // instead of asking the scope before the call node is processed - if ($expr instanceof FuncCall) { - $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); - } elseif ($expr instanceof MethodCall) { - $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); - } elseif ($expr instanceof StaticCall) { - $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); - } - return $expressionResult; - } - - throw new ShouldNotHappenException(sprintf('Unhandled expr: %s', get_class($expr))); - } - - /** - * @return string[] - */ - public function getAssignedVariables(Expr $expr): array - { - if ($expr instanceof Expr\Variable) { - if (is_string($expr->name)) { - return [$expr->name]; - } - - return []; - } - - if ($expr instanceof Expr\List_) { - $names = []; - foreach ($expr->items as $item) { - if ($item === null) { - continue; - } - - $names = array_merge($names, $this->getAssignedVariables($item->value)); - } - - return $names; - } - - if ($expr instanceof ArrayDimFetch) { - return $this->getAssignedVariables($expr->var); - } - - return []; - } - - private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody'; - - /** - * Whether a recorded convergence pass over the loop body can replace the - * final walk. A pass runs at deep statement context, the final walk at top - * level - constructs that analyse differently between the two (nested - * loop/label fixpoints run only at top level, statement-level classes are - * skipped at deep context) disqualify the body. Closure bodies process - * context-independently and are not traversed. - * - * @param Node\Stmt[] $bodyStmts - */ - public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool - { - $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE); - if ($cached !== null) { - return $cached; - } - - $replayable = true; - foreach ($bodyStmts as $bodyStmt) { - if ($this->hasContextSensitiveConstruct($bodyStmt)) { - $replayable = false; - break; - } - } - $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable); - - return $replayable; - } - - private function hasContextSensitiveConstruct(Node $node): bool - { - if ($node instanceof Expr\Closure) { - return false; - } - if ( - $node instanceof Node\Stmt\While_ - || $node instanceof Node\Stmt\Do_ - || $node instanceof Node\Stmt\For_ - || $node instanceof Foreach_ - || $node instanceof Node\Stmt\Label - || $node instanceof Node\Stmt\ClassLike - ) { - return true; - } - - foreach ($node->getSubNodeNames() as $subNodeName) { - $subNode = $node->$subNodeName; - if ($subNode instanceof Node) { - if ($this->hasContextSensitiveConstruct($subNode)) { - return true; - } - } elseif (is_array($subNode)) { - foreach ($subNode as $item) { - if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) { - return true; - } - } - } - } - - return false; - } - - /** - * Opens an engine-feeding gatherer frame for the duration of a body walk. - * The caller closes it in a finally block via popNodeGatherer(). - * - * @param callable(Node, Scope): void $gatherer - */ - public function pushNodeGatherer(callable $gatherer): void - { - $this->nodeGatherers[] = $gatherer; - } - - public function popNodeGatherer(): void - { - array_pop($this->nodeGatherers); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void - { - $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); - } - - /** - * Replays the recorded pairs [$from, $to) the way callNodeCallback() would - * have emitted them: gatherer frames observe them exactly like live ones - * (with the raw walk scope), a real callback gets the callback scope - and - * a recording callback records them again (a loop's fixpoint replay running - * inside a body's observation pass). - * - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function replayRecordingRange(RecordingNodeCallback $recording, int $from, int $to, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void - { - $pairs = $recording->getPairs(); - $scope->pushExpressionResultStorage($storage); - try { - for ($i = $from; $i < $to; $i++) { - [$node, $pairScope] = $pairs[$i]; - if (!$pairScope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - $this->callNodeCallback($nodeCallback, $node, $pairScope, $storage); - } - } finally { - $scope->popExpressionResultStorage(); - } - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function callNodeCallbackWithExpression( - callable $nodeCallback, - Node $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - ExpressionContext $context, - ): void - { - if ($context->isDeep()) { - $scope = $scope->exitFirstLevelStatements(); - } - $this->callNodeCallback($nodeCallback, $expr, $scope, $storage); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function callNodeCallback( - callable $nodeCallback, - Node $node, - MutatingScope $scope, - ExpressionResultStorage $storage, - ): void - { - // Engine-feeding gatherer frames observe the node at the emission - // position - their arrays are read as soon as the enclosing body walk - // returns. Gatherers are engine code and never ask about types - - // handing them the raw scope skips a NodeCallbackScope construction per - // emission; the scopes they capture (return statements, impure points) - // answer later asks through the storage hub like any MutatingScope. - foreach ($this->nodeGatherers as $gatherer) { - $gatherer($node, $scope); - } - - if ($nodeCallback instanceof NoopNodeCallback) { - return; - } - - if ($nodeCallback instanceof RecordingNodeCallback) { - // recording never asks about types - the pairs are wrapped and - // bound to the storage at replay time instead - $nodeCallback($node, $scope); - return; - } - - // post-order emission means the node's own result and every subnode - // result are already stored when the callback fires - NodeCallbackScope - // answers every ask synchronously from the storage - $nodeCallback($node, $scope->toNodeCallbackScope()); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processClosureNode( - Node\Stmt $stmt, - Expr\Closure $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ): ProcessClosureResult - { - return $this->processClosureNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processClosureNodeInternal( - Node\Stmt $stmt, - Expr\Closure $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ): ProcessClosureResult - { - foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } - - $byRefUses = []; - - $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); - $callableParameters = $parameterTypes->parameters; - $nativeCallableParameters = $parameterTypes->nativeParameters; - - $useScope = $scope; - foreach ($expr->uses as $use) { - if ($use->byRef) { - $byRefUses[] = $use; - $useScope = $useScope->enterExpressionAssign($use->var); - - $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); - $inAssignRightSideExpr = $context->getInAssignRightSideExpr(); - if ( - $inAssignRightSideVariableName === $use->var->name - && $inAssignRightSideExpr !== null - ) { - // a call's type is carried by the context (see - // ExpressionContext::enterAssignRightSideCallArgs()); a closure - // right side resolves through the closure type resolver - $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); - if ($inAssignRightSideType instanceof ClosureType) { - $variableType = $inAssignRightSideType; - } else { - $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); - if ($alreadyHasVariableType->no()) { - $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); - } else { - $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); - } - } - $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); - if ($inAssignRightSideNativeType instanceof ClosureType) { - $variableNativeType = $inAssignRightSideNativeType; - } else { - $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); - if ($alreadyHasVariableType->no()) { - $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); - } else { - $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); - } - } - $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); - } - } - $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context->withoutValueFlow()); - if (!$use->byRef) { - continue; - } - - $useScope = $useScope->exitExpressionAssign($use->var); - } - - if ($expr->returnType !== null) { - $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); - } - - $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); - $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); - $closureType = $closureScope->getAnonymousFunctionReflection(); - if (!$closureType instanceof ClosureType) { - throw new ShouldNotHappenException(); - } - - $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); - - $executionEnds = []; - $gatheredReturnStatements = []; - $gatheredReturnStatementsAfterFinally = []; - $gatheredReturnStatementsWithScope = []; - $gatheredYieldStatements = []; - $gatheredYieldStatementsWithScope = []; - $closureImpurePoints = []; - $invalidateExpressions = []; - $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsAfterFinally, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } - if ($node instanceof PropertyAssignNode) { - $closureImpurePoints[] = new ImpurePoint( - $scope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - return; - } - if ($node instanceof ExecutionEndNode) { - $executionEnds[] = $node; - return; - } - if ($node instanceof ReturnAfterFinallyNode) { - $gatheredReturnStatementsAfterFinally[] = new ReturnStatement($scope, $node->getReturnNode()); - return; - } - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } - if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { - $gatheredYieldStatements[] = $node; - $gatheredYieldStatementsWithScope[] = [$node, $scope]; - } - if (!$node instanceof Return_) { - return; - } - - $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - $gatheredReturnStatementsWithScope[] = [$node, $scope]; - }; - - if (count($byRefUses) === 0) { - $this->pushNodeGatherer($closureStmtsGatherer); - try { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); - } finally { - $this->popNodeGatherer(); - } - $publicStatementResult = $statementResult->toPublic(); - $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); - $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( - $expr, - $gatheredReturnStatements, - $gatheredReturnStatementsAfterFinally, - $gatheredYieldStatements, - $publicStatementResult, - $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()), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - $invalidateExpressions, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - array_merge($closureImpurePoints, $statementResult->getImpurePoints()), - ); - } - - $originalStorage = $storage; - - $count = 0; - $closureResultScope = null; - $replayBodyRecording = null; - $replayPassStorage = null; - $replayPassResult = null; - $replayEntryScope = null; - $bodyIsReplayable = $this->isReplayableConvergenceBody($expr, $expr->stmts); - do { - $prevScope = $closureScope; - - $storage = $originalStorage->duplicate(); - $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); - // deep context, like the loop handlers' own convergence passes: inner - // loops walk single-pass here and only the final walk below (top-level) - // runs their full convergence - otherwise every closure-convergence - // pass would re-converge every inner loop from scratch - $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false)); - // the candidate to replace the final walk when this pass's entry - // turns out to be the fixpoint - if ($bodyRecording instanceof RecordingNodeCallback) { - $replayBodyRecording = $bodyRecording; - $replayPassStorage = $storage; - $replayPassResult = $intermediaryClosureScopeResult; - $replayEntryScope = $prevScope; - } - $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); - foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { - $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); - } - - if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { - $closureResultScope = $intermediaryClosureScope; - break; - } - - $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); - $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); - - if ($closureScope->equals($prevScope)) { - break; - } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $closureScope = $prevScope->generalizeWith($closureScope); - } - $count++; - } while ($count < self::LOOP_SCOPE_ITERATIONS); - - if ($closureResultScope === null) { - $closureResultScope = $closureScope; - } - - $storage = $originalStorage; - $this->pushNodeGatherer($closureStmtsGatherer); - try { - if ( - $replayBodyRecording !== null && $replayPassStorage !== null - && $replayPassResult !== null && $replayEntryScope !== null - && $closureScope->equals($replayEntryScope) - ) { - // the final walk would repeat the recorded fixpoint pass exactly - // (same entry scope, deterministic walk) - adopt the pass's result - // and replay its emissions through the real callback instead. - // The pass's own entry scope takes over: the recorded pairs carry - // its anonymous-function reflection, which the gatherer's filter - // compares by identity (the state is equals-identical anyway). - $closureScope = $replayEntryScope; - $originalStorage->mergeResults($replayPassStorage); - $this->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope); - $statementResult = $replayPassResult; - } else { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); - } - } finally { - $this->popNodeGatherer(); - } - $publicStatementResult = $statementResult->toPublic(); - $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); - $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( - $expr, - $gatheredReturnStatements, - $gatheredReturnStatementsAfterFinally, - $gatheredYieldStatements, - $publicStatementResult, - $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()), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - $invalidateExpressions, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - array_merge($closureImpurePoints, $statementResult->getImpurePoints()), - $closureResultScope, - $byRefUses, - ); - } - - /** - * The closure scope was entered with a shallow reflection (parameters + - * declared return, no body walk - see ClosureTypeResolver::getClosureType() - * with $shallow). Now that the single body walk has gathered the returns, - * build the refined ClosureType from them (no second walk) and swap it onto - * the scope the ClosureReturnStatementsNode fires with, so the return-type - * rules see the refined expected return (e.g. Bar&Foo, not just Foo). - * - * @param list $gatheredReturnStatementsWithScope - * @param list $gatheredYieldStatementsWithScope - * @param list $executionEnds - * @param InternalThrowPoint[] $throwPoints - * @param ImpurePoint[] $impurePoints - * @param InvalidateExprNode[] $invalidateExpressions - */ - private function refineClosureNodeScope( - MutatingScope $closureScope, - MutatingScope $scope, - Expr\Closure $expr, - array $gatheredReturnStatementsWithScope, - array $gatheredYieldStatementsWithScope, - array $executionEnds, - array $throwPoints, - array $impurePoints, - array $invalidateExpressions, - ExpressionResultStorage $storage, - ): MutatingScope - { - $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure( - $scope, - $expr, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - $throwPoints, - $impurePoints, - $invalidateExpressions, - false, - $storage, - ); - - return $closureScope->withAnonymousFunctionReflection($refinedClosureType); - } - - /** - * @param InvalidateExprNode[] $invalidatedExpressions - * @param string[] $uses - */ - public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope - { - if ($scope->isInClass()) { - $uses[] = 'this'; - } - - $finder = new NodeFinder(); - foreach ($invalidatedExpressions as $invalidateExpression) { - $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true)); - if ($result === null) { - continue; - } - - $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable; - $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters); - } - - return $scope; - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processArrowFunctionNode( - Node\Stmt $stmt, - Expr\ArrowFunction $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ?ExpressionContext $context = null, - ): ProcessArrowFunctionResult - { - $context ??= ExpressionContext::createTopLevel(); - foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } - if ($expr->returnType !== null) { - $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); - } - - $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); - $callableParameters = $parameterTypes->parameters; - $nativeCallableParameters = $parameterTypes->nativeParameters; - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); - if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) { - throw new ShouldNotHappenException(); - } - - // Gather the property-assign impure points and invalidate expressions the - // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()), - // on top of the regular rule node callback, so the single body walk here - // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). - $arrowFunctionImpurePoints = []; - $invalidateExpressions = []; - $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { - if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { - return; - } - - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } - - if (!$node instanceof PropertyAssignNode) { - return; - } - - $arrowFunctionImpurePoints[] = new ImpurePoint( - $innerScope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - }; - - $this->pushNodeGatherer($arrowFunctionStmtsGatherer); - try { - $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); - } finally { - $this->popNodeGatherer(); - } - $scope = $scope->addTemplateArgumentConstraints($exprResult->getScope()->getTemplateArgumentConstraints()); - $scope = $scope->addTemplateArgumentConstraints($this->collectReturnSend($arrowFunctionScope, $exprResult)); - - $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); - $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); - - // The arrow scope was entered with a shallow reflection (parameters + - // declared return, no body walk). Now that the single body walk above has - // run, build the refined arrow function type from the body expression's - // stored type (no second walk) and fire InArrowFunctionNode with it, so the - // node and the return-type rules see the refined expected return. - $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction( - $scope, - $expr, - $arrowFunctionScope, - $closureTypeThrowPoints, - $closureTypeImpurePoints, - $invalidateExpressions, - false, - $storage, - ); - $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType); - $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage); - - return new ProcessArrowFunctionResult( - $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - variableFlow: ArrowFunctionHandler::getVariableFlow($expr, $exprResult), - throwPoints: $exprResult->getThrowPoints(), - impurePoints: $exprResult->getImpurePoints(), - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $arrowFunctionScope, - $closureTypeThrowPoints, - $closureTypeImpurePoints, - $invalidateExpressions, - ); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - public function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - public function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())); - } - - /** - * Resolves the type of an expression a callable parameter is derived from - - * either the closure/arrow function whose acceptors describe the parameters, - * or a call argument refining them. A closure/arrow function is resolved - * directly through ClosureTypeResolver (as Scope::getType() would), not by - * processing it on demand: createCallableParameters() runs while that very - * closure is being processed, so on-demand processing would re-enter - * processClosureNodeInternal() endlessly. - */ - private function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type - { - if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { - return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); - } - - return $this->readTypeOfMaybeStored($expr, $scope); - } - - /** - * @param Node\Arg[]|null $args - * @param Closure(MutatingScope, Expr): Type $typeGetter - * @return ParameterReflection[]|null - */ - private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array - { - $callableParameters = null; - if ($args !== null) { - $closureType = $typeGetter($scope, $closureExpr); - - if ($closureType->isCallable()->no()) { - return null; - } - - $acceptors = $closureType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - - foreach ($callableParameters as $index => $callableParameter) { - if (!isset($args[$index])) { - continue; - } - - if ($callableParameter->isVariadic()) { - $argTypes = []; - $argNumber = count($args); - for ($j = $index; $j < $argNumber; $j++) { - $argTypes[] = $typeGetter($scope, $args[$j]->value); - } - $type = TypeCombinator::union(...$argTypes); - } else { - $type = $typeGetter($scope, $args[$index]->value); - } - $callableParameters[$index] = new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $type, - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ); - } - } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); - - if ($passedToType->isCallable()->no()) { - return null; - } - } - - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - foreach ($acceptors as $acceptor) { - $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $callableParameter->getType(), - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ), $acceptor->getParameters()); - - if ($callableParameters === null) { - $callableParameters = $acceptorParameters; - continue; - } - - $newParameters = []; - $parameterCount = max(count($callableParameters), count($acceptorParameters)); - for ($i = 0; $i < $parameterCount; $i++) { - if (!array_key_exists($i, $acceptorParameters)) { - $newParameters[] = $callableParameters[$i]->toOptional(); - continue; - } - - if (!array_key_exists($i, $callableParameters)) { - $newParameters[] = $acceptorParameters[$i]->toOptional(); - continue; - } - - $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); - } - - $callableParameters = $newParameters; - } - } - - return $callableParameters; - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processParamNode( - Node\Stmt $stmt, - Node\Param $param, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ): void - { - $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback); - $this->callNodeCallback($nodeCallback, $param, $scope, $storage); - if ($param->type !== null) { - $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage); - } - if ($param->default === null) { - return; - } - - $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - } - - /** - * @param AttributeGroup[] $attrGroups - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processAttributeGroups( - Node\Stmt $stmt, - array $attrGroups, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ): void - { - foreach ($attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - $className = $scope->resolveName($attr->name); - if ($this->reflectionProvider->hasClass($className)) { - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasConstructor()) { - $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( - $attr->args, - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); - $expr = new New_($attr->name, $attr->args); - $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); - continue; - } + if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { + $storedResult = $storage->findExpressionResult($expr); + // a stored result only answers when the current scope agrees with its + // evaluation position on the variables the expression reads - a + // counterfactual walk (an extension re-binding a variable and pricing + // a real subtree, e.g. array_filter's per-element callback evaluation) + // re-processes the node on its own scope instead. In CONSUME mode the + // divergence is intentional (an ensured-non-null device) and the + // stored result is consumed unconditionally, re-anchored below. + if ($storedResult !== null && ($this->consumeStoredExpressionResults || $storedResult->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted))) { + // a foreign-position answer must not thread its original walk + // scopes into THIS walk - re-anchor it to the asking position so + // subsequent operands keep evaluating on the asking scope + if ($storedResult->getBeforeScope() === $scope) { + return $storedResult; } - foreach ($attr->args as $arg) { - $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $this->callNodeCallback($nodeCallback, $arg, $scope, $storage); + $reanchored = $storedResult->atAskPosition($scope); + if ($this->consumeStoredExpressionResults) { + // the re-anchored view IS this walk's result for the node + // (the nullsafe twin's receiver at the ensured position) - + // store it so later asks (rules' storage reads) see the same + // result the twin walk itself consumed, exactly like the + // receiver walked inside the twin used to be stored + $this->storeExpressionResult($storage, $expr, $reanchored); } - $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); - } - $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); - } - } - /** - * @param FunctionReflection|MethodReflection|null $calleeReflection - */ - private function resolveClosureThisType( - ?CallLike $call, - $calleeReflection, - ParameterReflection $parameter, - MutatingScope $scope, - ): ?Type - { - if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } - } - } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } - } - } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isMethodSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } + return $reanchored; } } - if ($parameter instanceof ExtendedParameterReflection) { - return $parameter->getClosureThisType(); - } - - return null; + return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context); } /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants * @param callable(Node $node, Scope $scope): void $nodeCallback - * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory */ - public function processArgs( + private function processExprNodeInternal( Node\Stmt $stmt, - $calleeReflection, - ?ExtendedMethodReflection $nakedMethodReflection, - array $parametersAcceptors, - ?array $namedArgumentsVariants, - CallLike $callLike, + Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, - ?callable $closureBindScopeFactory = null, - ): ArgsResult + ): ExpressionResult { - $args = $callLike->getArgs(); - - // Evolving-scope arg types: gathered as each argument is processed on the - // scope that evolves arg-to-arg. They select the FINAL resolved acceptor - // (the call's return type, by-ref OUT types), which type-resolves generics - // from the actual argument types. - $gatheredTypes = []; - $gatheredUnpack = false; - $gatheredHasName = false; - $gatheredArgTypeByIndex = []; - - // Metadata acceptor base - NO forward read. The per-argument resolution below picks the - // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is - // keyed off the argument count, e.g. sscanf - and the count is known structurally) and - // resolves generic parameter types from the args gathered so far; the call's return type - // comes from the post-loop resolved acceptor. - $metadataAcceptor = $parametersAcceptors[0] ?? null; - - // Whether selecting an acceptor is type-driven at all: multiple variants to - // choose between, templates or conditionals to resolve from the arg types, - // or named-argument variants. When it is not, the gathered arg types can - // never influence the selected acceptor, so the faithful-return gather walk - // of a closure/arrow argument (gatherClosureArgType()) would be pure waste - - // its signature-only shallow type keeps the count/name bookkeeping correct. - $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1 - || $namedArgumentsVariants !== null - || ($metadataAcceptor !== null && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($metadataAcceptor)); - - // Both predicates are hoisted out of the per-argument loop - they traverse - // the acceptor's parameter types. - $hasTemplateParameterType = $metadataAcceptor !== null - && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor); - $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType; - - $hasYield = false; - $throwPoints = []; - $impurePoints = []; - $isAlwaysTerminating = false; - /** @var list $deferredInvalidateExpressions */ - $deferredInvalidateExpressions = []; - /** @var ProcessClosureResult[] $deferredByRefClosureResults */ - $deferredByRefClosureResults = []; - - $processingOrder = array_keys($args); - usort($processingOrder, static function (int $a, int $b) use ($args): int { - $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; - $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; - $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction; - $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction; - if ($aIsClosure !== $bIsClosure) { - // closures sort after non-closures so every sibling feeding an - // intrinsic override / generic callable(T) is in scope first - return $aIsClosure ? 1 : -1; - } - - $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - if ($aOriginal === null && $bOriginal === null) { - return $a <=> $b; - } - if ($aOriginal === null) { - return 1; - } - if ($bOriginal === null) { - return -1; - } - - return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); - }); - - $argResults = []; - $byRefArguments = []; - $countStableMetadataAcceptor = null; - foreach ($processingOrder as $i) { - $arg = $args[$i]; - - if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { - // Gather the closure/arrow type for the FINAL resolved acceptor on - // the evolving scope, BEFORE the body is processed with a possibly - // generic-resolved parameter injected, so the inferred return type - // stays faithful to the closure's own declaration and its own - // contribution (a TValue from its return) participates in the final - // resolution (see gatherClosureArgType()). - $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection - ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) - : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); - $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); - } - - $argMetadataAcceptor = $metadataAcceptor; - if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { - if ($this->argConsumesResolvedParameterType($arg->value)) { - // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the - // full argument count with mixed. Closures sort last and by-ref out-params follow the - // args that pin them, so determining siblings are already processed; the mixed pad keeps - // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), - // while processed siblings resolve a generic callable(T) parameter. No forward read. - $paddedTypes = []; - $paddedUnpack = false; - $paddedHasName = false; - foreach ($args as $j => $paddedArg) { - $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); - } - $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); - } else { - // Only a closure/arrow function consumes the generic-RESOLVED - // parameter type: its body is inferred from the resolved - // callable(T) - directly, or through the in-function-call stack - // when nested anywhere inside the argument. Every other argument - // reads variant-stable facts off its parameter (by-ref flag, - // callable bookkeeping), so one all-mixed count-stable selection - // serves them all instead of a full template inference per argument. - if ($countStableMetadataAcceptor === null) { - $paddedTypes = []; - $paddedUnpack = false; - $paddedHasName = false; - foreach ($args as $j => $paddedArg) { - $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); - } - $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); - } - $argMetadataAcceptor = $countStableMetadataAcceptor; - } - } - $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; - - $assignByReference = false; - $parameter = null; - $parameterType = null; - $parameterNativeType = null; - if ($parameters !== null) { - $matchedParameter = null; - if ($arg->name !== null) { - foreach ($parameters as $p) { - if ($p->getName() === $arg->name->toString()) { - $matchedParameter = $p; - break; - } - } - } elseif (isset($parameters[$i])) { - $matchedParameter = $parameters[$i]; - } - - if ($matchedParameter !== null) { - $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); - $parameterType = $matchedParameter->getType(); - - if ($matchedParameter instanceof ExtendedParameterReflection) { - $parameterNativeType = $matchedParameter->getNativeType(); - } - $parameter = $matchedParameter; - } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { - $lastParameter = array_last($parameters); - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); - $parameterType = $lastParameter->getType(); - - if ($lastParameter instanceof ExtendedParameterReflection) { - $parameterNativeType = $lastParameter->getNativeType(); - } - $parameter = $lastParameter; - } - } - - if ($parameter !== null && !$parameter->passedByReference()->no()) { - $byRefArguments[spl_object_id($arg->value)] = true; - } - $lookForUnset = false; - if ($assignByReference) { - $isBuiltin = false; - if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { - $isBuiltin = true; - } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { - $isBuiltin = true; - } - if ( - $isBuiltin - || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) - ) { - $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); - $lookForUnset = true; - } - } - - $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - if ($calleeReflection !== null) { - $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction; - $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes); - } - - $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); - - $originalScope = $scope; - $scopeToPass = $scope; - if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) { - $scopeToPass = $closureBindScopeFactory($scope); - } - - if ($arg->value instanceof Expr\Closure) { - - $storedClosureArgResult = null; - if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { - // an on-demand re-walk of the enclosing call must not re-run the - // closure's whole by-ref convergence: consume the main walk's - // stored result, or (when the body release already dropped it) - // price the closure through getClosureType's per-node cache - - // a single body walk on miss, none on repeat asks - $storedClosureArgResult = $storage->findExpressionResult($arg->value); - // consume mode alone (the nullsafe call's plain-twin walk) is the - // FIRST and only walk of these arguments - nothing stored them, so - // a miss means "walk it", not "price it silently": pricing skips - // the body walk and never fires the closure's node callbacks - if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureArgResult = $this->expressionResultFactory->create( - $scopeToPass, - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); - } - } - if ($storedClosureArgResult !== null) { - $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; - } else { - $restoreThisScope = null; - if ( - $closureBindScopeFactory === null - && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static - ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } - } - - if ($parameter !== null) { - $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); - - if ($overwritingParameterType !== null) { - $parameterType = $overwritingParameterType; - - // resolve the native flavour through the same extension on the - // natively-promoted scope, so the closure parameters keep - // their native precision too - $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); - if ($overwritingParameterNativeType !== null) { - $parameterNativeType = $overwritingParameterNativeType; - } - } - } - - $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); - $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); - } - - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureResult = $this->expressionResultFactory->create( - $closureResult->getScope(), - $scopeToPass, - $arg->value, - variableFlow: ClosureHandler::getVariableFlow($arg->value), - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->buildClosureTypeForClosure( - $scopeToPass, - $arg->value, - $closureResult->getGatheredReturnStatements(), - $closureResult->getGatheredYieldStatements(), - $closureResult->getExecutionEnds(), - $closureResult->getThrowPoints(), - $closureResult->getClosureTypeImpurePoints(), - $closureResult->getInvalidateExpressions(), - false, - $storage, - ), - // the native flavour reads the stored native types off the same - // single body walk - no second walk on the promoted scope - nativeType: $closureTypeResolver->buildClosureTypeForClosure( - $scopeToPass, - $arg->value, - $closureResult->getGatheredReturnStatements(), - $closureResult->getGatheredYieldStatements(), - $closureResult->getExecutionEnds(), - $closureResult->getThrowPoints(), - $closureResult->getClosureTypeImpurePoints(), - $closureResult->getInvalidateExpressions(), - true, - $storage, - ), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureResult); - // the closure node's own callback fires after its result is - // stored, mirroring processExprNodeInternal() - callback-side - // getType() answers from the stored result - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); - // the arg result must be the properly-typed stored result - - // ArgsResult readers price array_push() & co. from it - $argResults[spl_object_id($arg->value)] = $storedClosureResult; - - $uses = []; - foreach ($arg->value->uses as $use) { - if (!is_string($use->var->name)) { - continue; - } - - $uses[] = $use->var->name; - } - - $scope = $closureResult->getScope(); - $deferredByRefClosureResults[] = $closureResult; - // Prefer the invalidate expressions collected on the ClosureType - - // they also cover writes the closure's own body walk observed, - // unlike $closureResult->getInvalidateExpressions(). - $closureExprType = $storedClosureResult->getType(); - $invalidateExpressions = $closureExprType instanceof ClosureType - ? $closureExprType->getInvalidateExpressions() - : $closureResult->getInvalidateExpressions(); - if ($restoreThisScope !== null) { - $nodeFinder = new NodeFinder(); - $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; - foreach ($invalidateExpressions as $j => $invalidateExprNode) { - $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); - if ($foundThis === null) { - continue; - } - - unset($invalidateExpressions[$j]); - } - $invalidateExpressions = array_values($invalidateExpressions); - $scope = $scope->restoreThis($restoreThisScope); - } - - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses]; - } - } - } elseif ($arg->value instanceof Expr\ArrowFunction) { - - $storedClosureArgResult = null; - if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { - // see the Closure branch above - consume or price via the cache - $storedClosureArgResult = $storage->findExpressionResult($arg->value); - // consume mode alone (the nullsafe call's plain-twin walk) is the - // first and only walk of the argument - a miss means walk it - if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureArgResult = $this->expressionResultFactory->create( - $scopeToPass, - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); - } - } - if ($storedClosureArgResult !== null) { - $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; - } else { - if ( - $closureBindScopeFactory === null - && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static - ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } - } - - if ($parameter !== null) { - $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); - - if ($overwritingParameterType !== null) { - $parameterType = $overwritingParameterType; - - // resolve the native flavour through the same extension on the - // natively-promoted scope, so the closure parameters keep - // their native precision too - $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); - if ($overwritingParameterNativeType !== null) { - $parameterNativeType = $overwritingParameterNativeType; - } - } - } - - $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); - $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionExprResult->getThrowPoints())); - $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); - } - $arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); - // both flavours are built from the single body walk (see - // ArrowFunctionHandler); the built type also answers the - // invalidate-expressions read below without re-walking the - // still-unstored node through Scope::getType() - $arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( - $scopeToPass, - $arg->value, - $arrowFunctionScope, - $arrowFunctionResult->getClosureTypeThrowPoints(), - $arrowFunctionResult->getClosureTypeImpurePoints(), - $arrowFunctionResult->getInvalidateExpressions(), - false, - $storage, - ); - $storedArrowResult = $this->expressionResultFactory->create( - $arrowFunctionExprResult->getScope(), - variableFlow: $arrowFunctionExprResult->getVariableFlow(), - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: $arrowFunctionExprResult->hasYield(), - isAlwaysTerminating: $arrowFunctionExprResult->isAlwaysTerminating(), - throwPoints: $arrowFunctionExprResult->getThrowPoints(), - impurePoints: $arrowFunctionExprResult->getImpurePoints(), - type: $arrowFunctionType, - nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( - $scopeToPass, - $arg->value, - $arrowFunctionScope, - $arrowFunctionResult->getClosureTypeThrowPoints(), - $arrowFunctionResult->getClosureTypeImpurePoints(), - $arrowFunctionResult->getInvalidateExpressions(), - true, - $storage, - ), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedArrowResult); - // the arrow function node's own callback fires after its result - // is stored, mirroring processExprNodeInternal() - callback-side - // getType() answers from the stored result - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); - // the arg result must be the properly-typed stored result, not - // the body walk's placeholder (whose typeCallback answers mixed) - - // ArgsResult readers price array_push() & co. from it - $argResults[spl_object_id($arg->value)] = $storedArrowResult; - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()]; - } - } - $scope = $scope->addTemplateArgumentConstraints($argResults[spl_object_id($arg->value)]->getScope()->getTemplateArgumentConstraints()); + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + if ($expr instanceof FuncCall) { + $newExpr = new FunctionCallableNode($expr->name, $expr); + } elseif ($expr instanceof MethodCall) { + $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); + } elseif ($expr instanceof StaticCall) { + $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); + } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { + $newExpr = new InstantiationCallableNode($expr->class, $expr); } else { - $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; - if ($enterExpressionAssignForByRef) { - $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); - } - $argContext = $context->enterDeep(); - if (!$arg->unpack && $arg->value instanceof Expr\Array_) { - $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); - } - $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $argContext); - $argResults[spl_object_id($arg->value)] = $exprResult; - $exprType = $exprResult->getType(); - $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); - $scope = $exprResult->getScope(); - if ($enterExpressionAssignForByRef) { - $scope = $scope->exitExpressionAssign($arg->value); - } - $hasYield = $hasYield || $exprResult->hasYield(); - - if ($exprType->isCallable()->yes()) { - $acceptors = $exprType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()]; - } - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); - if (!$this->implicitThrows) { - $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); - } - $throwPoints = array_merge($throwPoints, $callableThrowPoints); - $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); - } - } - } - - $gatheredArgTypeByIndex[$i] = $exprResult->getType(); - $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); - $templateArgumentFrame = $this->observingTemplateArgumentFrame($scope); - if ($templateArgumentFrame !== null && $parameter !== null) { - // the metadata acceptor is resolved against the arguments gathered - // before this one, so a template this argument itself decides is - // still its bound there - observe the declared parameter type, - // where such a template is uninformative and the receiver's - // class-level arguments are already in place - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectArgument( - $this->findOriginalParameterType($argMetadataAcceptor, $parameter) ?? $parameter->getType(), - $gatheredArgTypeByIndex[$i], - ($calleeReflection instanceof FunctionReflection || $calleeReflection instanceof ExtendedMethodReflection) && $calleeReflection->isPure()->yes(), - )); - } - } - - if ($assignByReference && $lookForUnset) { - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); - } - - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); - } - - if ($i !== 0 || $closureBindScopeFactory === null) { - continue; + throw new ShouldNotHappenException(); } - $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); - } - - foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { - $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); - } - - foreach ($deferredByRefClosureResults as $deferredClosureResult) { - $scope = $deferredClosureResult->applyByRefUseScope($scope); - } - - // Type-driven resolved acceptor: the arg types gathered on the evolving - // scope select (and generic-resolve) the acceptor that drives the call's - // return type. Intrinsic overrides are applied on the final scope, - // mirroring the original selectFromArgs(). - // When the selection is not type-driven, the single acceptor IS the - // resolved acceptor - the fast path selectFromArgs() used to take. - $resolvedAcceptor = null; - if ($parametersAcceptors !== []) { - $resolvedAcceptor = $typeDrivenAcceptorSelection - ? $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope) - : $metadataAcceptor; - } - - if ($resolvedAcceptor !== null && $this->observingTemplateArgumentFrame($scope) !== null) { - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectCall( - $callLike, - $resolvedAcceptor, - $gatheredTypes, - $callLike instanceof New_ && $calleeReflection instanceof MethodReflection ? $calleeReflection->getDeclaringClass()->getTemplateTypeMap() : null, - )); - } - - // The by-ref OUT writeback reads the metadata acceptor: it is selected from - // the full argument count (stable variant). When that single acceptor still - // carries templates (fast path), its OUT types need generic-resolving from the - // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly - // that (same variant, resolved); otherwise the metadata acceptor is already resolved. - $writebackAcceptor = $metadataAcceptor; - if ( - $metadataAcceptor !== null - && $argMetadataIsTypeDriven - ) { - $writebackAcceptor = $resolvedAcceptor; + $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); + $expressionResult = $this->expressionResultFactory->create( + $newExprResult->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $newExprResult->hasYield(), + 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()), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $this->storeExpressionResult($storage, $expr, $expressionResult); + return $expressionResult; } - $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null; - if ($writebackParameters !== null) { - foreach ($args as $i => $arg) { - $assignByReference = false; - $currentParameter = null; - if (isset($writebackParameters[$i])) { - $currentParameter = $writebackParameters[$i]; - } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) { - $currentParameter = array_last($writebackParameters); - } - - if ($currentParameter !== null) { - $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); - } - if ($assignByReference) { - $argValue = $arg->value; - if (!$argValue instanceof Variable || $argValue->name !== 'this') { - $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); - if ($paramOutType !== null) { - $byRefType = $paramOutType; - } elseif ( - $currentParameter instanceof ExtendedParameterReflection - && $currentParameter->getOutType() !== null - ) { - $byRefType = $currentParameter->getOutType(); - } elseif ( - $calleeReflection instanceof MethodReflection - && !$calleeReflection->getDeclaringClass()->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } elseif ( - $calleeReflection instanceof FunctionReflection - && !$calleeReflection->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } else { - $byRefType = new MixedType(); - } - - $scope = $this->processVirtualAssign( - $scope, - $storage, - $stmt, - $argValue, - new TypeExpr($byRefType), - $nodeCallback, - )->getScope(); - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); - } - } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { - $argType = $this->readArgResult($argResults, $arg->value)->getTypeOnScope($scope, false); - if (!$argType->isObject()->no()) { - $nakedReturnType = null; - if ($nakedMethodReflection !== null) { - $nakedParametersAcceptor = $this->selectArgsAcceptor( - $gatheredTypes, - $nakedMethodReflection->getVariants(), - $nakedMethodReflection->getNamedArgumentsVariants(), - $gatheredHasName, - $gatheredUnpack, - ); - $nakedReturnType = $nakedParametersAcceptor->getReturnType(); - } - if ( - $nakedReturnType === null - || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() - || $nakedMethodReflection->isPure()->no() - ) { - $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); - $scope = $scope->invalidateExpression($arg->value, true); - } - } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { - $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); - $scope = $scope->invalidateExpression($arg->value, true); - } - } + $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container); + if ($exprHandler !== null) { + $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); + // a chain link an enclosing isset/empty/?? could not device ahead + // of its walk (an untracked call) is deviced now, from the type + // the walk produced + $expressionResult = $this->getNonNullabilityHelper()->applyPendingEnsure($expr, $expressionResult); + $this->storeExpressionResult($storage, $expr, $expressionResult); + // Force potential producers before collecting the body's constraints. + // Type reads only construct markers; they never register sites as a side effect. + $frame = $scope->getCurrentTemplateArgumentFrame(); + if ( + $frame !== null && $frame->isObserving() + && $expr instanceof Expr\CallLike + ) { + $constraints = $this->templateArgumentObserver->collectSites($expressionResult->getType()); + $expressionResult = $expressionResult->withScope($expressionResult->getScope()->addTemplateArgumentConstraints($constraints)); + $this->storeExpressionResult($storage, $expr, $expressionResult); + } + // The node's own callback fires AFTER its result is stored, with the + // scope captured before processing. Rules observe the same (scope, + // answer) pair as at a pre-order emission - previously a pre-order + // rule parks on its first ask and resumes at this store anyway - but + // a synchronously invoked rule (the plain resolver, PHP < 8.1) now + // finds the node's and its subtree's results in the storage instead + // of re-walking them on demand. + $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); + // the call is now processed and stored; emit a virtual node so + // impossible-check rules read its specified types from the result + // instead of asking the scope before the call node is processed + if ($expr instanceof FuncCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); + } elseif ($expr instanceof MethodCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); + } elseif ($expr instanceof StaticCall) { + $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); } + return $expressionResult; } - // not storing this, it's scope after processing all args - return new ArgsResult( - $this->expressionResultFactory->create( - $scope, - $scope, - $callLike, - $hasYield, - $isAlwaysTerminating, - $throwPoints, - $impurePoints, - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $resolvedAcceptor, - $argResults, - $byRefArguments, - ); + throw new ShouldNotHappenException(sprintf('Unhandled expr: %s', get_class($expr))); } /** - * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs(): - * indexes the gathered arg type by name (sets $hasName) vs position, and - * expands unpacked constant arrays / falls back to the iterable value type - * (sets $unpack), so selectFromTypes() picks the matching variant. - * - * @param array $types + * @return string[] */ - private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void + public function getAssignedVariables(Expr $expr): array { - if ($originalArg->name !== null) { - $index = $originalArg->name->toString(); - $hasName = true; - } else { - $index = $i; - } - - if ($originalArg->unpack) { - $unpack = true; - $constantArrays = $type->getConstantArrays(); - if (count($constantArrays) > 0) { - foreach ($constantArrays as $constantArray) { - $values = $constantArray->getValueTypes(); - foreach ($constantArray->getKeyTypes() as $j => $keyType) { - $valueType = $values[$j]; - $valueIndex = $keyType->getValue(); - if (is_string($valueIndex)) { - $hasName = true; - } else { - $valueIndex = $i + $j; - } - - $types[$valueIndex] = isset($types[$valueIndex]) - ? TypeCombinator::union($types[$valueIndex], $valueType) - : $valueType; - } - } - } else { - $types[$index] = $type->getIterableValueType(); + if ($expr instanceof Expr\Variable) { + if (is_string($expr->name)) { + return [$expr->name]; } - } else { - $types[$index] = $type; - } - } - - /** - * Whether processing this argument consumes the generic-RESOLVED parameter - * type: a closure/arrow function does - its parameters and body scope are - * typed from the resolved callable(T) - whether it IS the argument or is - * nested anywhere inside it (the enclosing parameter is pushed on the - * in-function-call stack and the nested closure types itself from there). - * Every other argument only reads variant-stable facts off its parameter. - */ - private function argConsumesResolvedParameterType(Expr $value): bool - { - if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) { - return true; - } - // cached on the node - args are re-processed across convergence passes - $cached = $value->getAttribute('phpstanArgContainsClosure'); - if ($cached !== null) { - return $cached; + return []; } - $contains = (new NodeFinder())->findFirst( - [$value], - static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction, - ) !== null; - $value->setAttribute('phpstanArgContainsClosure', $contains); - - return $contains; - } - - /** - * Resolves the type of a closure/arrow function argument for the generic - * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure - * type is read with the RAW (un-generic-resolved) acceptor parameter pushed - * onto the in-function-call stack, so its body sees the template parameter - * (effectively mixed for an untyped param) rather than a parameter already - * resolved from sibling args. That keeps the inferred return type (the U in - * callable(T): U) faithful to the closure's own declaration. - * - * @param ParametersAcceptor[] $parametersAcceptors - */ - private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type - { - $rawParameter = null; - if (count($parametersAcceptors) === 1) { - $rawParameters = $parametersAcceptors[0]->getParameters(); - if (isset($rawParameters[$i])) { - $rawParameter = $rawParameters[$i]; - } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) { - $rawParameter = array_last($rawParameters); + if ($expr instanceof Expr\List_) { + $names = []; + foreach ($expr->items as $item) { + if ($item === null) { + continue; + } + + $names = array_merge($names, $this->getAssignedVariables($item->value)); } + + return $names; } - if ($rawParameter !== null) { - $scope = $scope->pushInFunctionCall(null, $rawParameter, false); + if ($expr instanceof ArrayDimFetch) { + return $this->getAssignedVariables($expr->var); } - return $this->resolveCallableTypeForScope($closureExpr, $scope); + return []; } - /** - * @param array $types - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor - { - return $hasName && $namedArgumentsVariants !== null - ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack) - : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack); - } + private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody'; /** - * Applies the intrinsic argument overrides (array_map/filter/walk/find, - * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope via - * the non-reprocessing readers, then type-selects the metadata acceptor over - * the arg types gathered so far. The overrides read sibling arg types - which - * closures-last ordering keeps in scope/$gatheredTypes before any closure. + * Whether a recorded convergence pass over the loop body can replace the + * final walk. A pass runs at deep statement context, the final walk at top + * level - constructs that analyse differently between the two (nested + * loop/label fixpoints run only at top level, statement-level classes are + * skipped at deep context) disqualify the body. Closure bodies process + * context-independently and are not traversed. * - * @param Node\Arg[] $args - * @param array $gatheredTypes - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function selectArgsMetadataAcceptor(array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor - { - $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides( - $args, - $parametersAcceptors, - $namedArgumentsVariants, - $scope, - fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope), - fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope->doNotTreatPhpDocTypesAsCertain()), - static fn (Type $t): Type => $scope->getIterableValueType($t), - static fn (Type $t): Type => $scope->getIterableKeyType($t), - ); - - return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); - } - - /** - * Arguments normalization (reordering, default-filling) can drop an original - * argument from the call processArgs() iterates - duplicate, unknown-named or - * extra arguments in an invalid call. The parameters check still asks their - * types to report the error, so process them too (their result is stored). - * A NoopNodeCallback keeps the dropped arguments out of rule processing, - * matching the behaviour when this guard is off. + * @param Node\Stmt[] $bodyStmts */ - public function processDroppedArgs( - Node\Stmt $stmt, - CallLike $originalCall, - CallLike $normalizedCall, - MutatingScope $scope, - ExpressionResultStorage $storage, - ExpressionContext $context, - ): void + public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool { - if ($originalCall === $normalizedCall) { - return; - } - - $keptValueIds = []; - foreach ($normalizedCall->getArgs() as $normalizedArg) { - $keptValueIds[spl_object_id($normalizedArg->value)] = true; + $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE); + if ($cached !== null) { + return $cached; } - foreach ($originalCall->getArgs() as $originalArg) { - if (isset($keptValueIds[spl_object_id($originalArg->value)])) { - continue; + $replayable = true; + foreach ($bodyStmts as $bodyStmt) { + if ($this->hasContextSensitiveConstruct($bodyStmt)) { + $replayable = false; + break; } - - $this->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution()); } + $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable); + + return $replayable; } - /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - */ - private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool + private function hasContextSensitiveConstruct(Node $node): bool { - $parameterCallableType = null; - if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) { - $parameterCallableType = TypeUtils::findCallableType($parameterType); + if ($node instanceof Expr\Closure) { + return false; + } + if ( + $node instanceof Node\Stmt\While_ + || $node instanceof Node\Stmt\Do_ + || $node instanceof Node\Stmt\For_ + || $node instanceof Foreach_ + || $node instanceof Node\Stmt\Label + || $node instanceof Node\Stmt\ClassLike + ) { + return true; } - if ($parameter instanceof ExtendedParameterReflection) { - $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); - if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $parameterCallableType !== null; - } else { - $callCallbackImmediately = $parameterCallImmediately->yes(); + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + if ($this->hasContextSensitiveConstruct($subNode)) { + return true; + } + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) { + return true; + } + } } - } else { - $callCallbackImmediately = $parameterCallableType !== null; } - return $callCallbackImmediately; + return false; } /** - * A callback passed as an argument escapes the current scope and may be invoked, - * so its mutations have to invalidate the outer scope - unless the parameter is - * explicitly marked as later-invoked, in which case the callback only runs after - * the current function returns and its mutations are not visible here yet. + * Opens an engine-feeding gatherer frame for the duration of a body walk. + * The caller closes it in a finally block via popNodeGatherer(). + * + * @param callable(Node, Scope): void $gatherer */ - private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool + public function pushNodeGatherer(callable $gatherer): void { - if ($parameter instanceof ExtendedParameterReflection) { - return !$parameter->isImmediatelyInvokedCallable()->no(); - } + $this->nodeGatherers[] = $gatherer; + } - return true; + public function popNodeGatherer(): void + { + array_pop($this->nodeGatherers); } /** - * @param MethodReflection|FunctionReflection|null $calleeReflection + * Detaches all gatherer frames for a walk that must not feed them. The + * caller reattaches them in a finally block via restoreNodeGatherers(). + * + * @return list */ - private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + public function suspendNodeGatherers(): array { - if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) { - if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { - return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } elseif ($calleeReflection instanceof MethodReflection) { - if ($callLike instanceof StaticCall) { - foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { - if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { - return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { - $staticCall = new StaticCall( - $callLike->class, - new Identifier('__construct'), - $callLike->getArgs(), - ); - foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { - if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { - return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); - } - } - } elseif ($callLike instanceof MethodCall) { - foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) { - if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { - return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } - } + $gatherers = $this->nodeGatherers; + $this->nodeGatherers = []; - return null; + return $gatherers; } /** - * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param list $gatherers */ - private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type + public function restoreNodeGatherers(array $gatherers): void { - $paramOutTypes = []; - if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) { - if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) { - if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) { - if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } - - if (count($paramOutTypes) === 1) { - return $paramOutTypes[0]; - } - - if (count($paramOutTypes) > 1) { - return TypeCombinator::union(...$paramOutTypes); - } - - return null; + $this->nodeGatherers = $gatherers; } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, ?ExpressionResult $assignedExprResult = null): ExpressionResult + public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void { - // work off an available result for the assigned expr: passed by the - // caller, or fabricated from a type-carrying virtual node - threaded - // straight into applyWrite() so its reads compose instead of falling - // back to on-demand pricing of the type, the truthy/falsey narrowing, - // and the synthetic sentinel comparisons - if ( - $assignedExprResult === null - && ($assignedExpr instanceof TypeExpr || $assignedExpr instanceof NativeTypeExpr) - && $storage->findExpressionResult($assignedExpr) === null - ) { - $assignedExprResult = $this->container->getByType(VirtualExprResultHelper::class)->createTypeExprResult($scope, $assignedExpr); - } - - $assignHandler = $this->container->getByType(AssignHandler::class); - $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback); - $target = $assignHandler->prepareTarget( - $this, - $scope, - $storage, - $stmt, - $var, - $assignedExpr, - $virtualAssignNodeCallback, - ExpressionContext::createDeep(), - AssignTargetWalkMode::virtualAssign(), - ); - - return $assignHandler->applyWrite( - $this, - $target, - $this->expressionResultFactory->create( - $target->getScope(), - beforeScope: $target->getScope(), - expr: $assignedExpr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $assignedExprResult, - $stmt, - $storage, - $virtualAssignNodeCallback, - ExpressionContext::createDeep(), - ); + $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); } /** + * Replays the recorded pairs [$from, $to) the way callNodeCallback() would + * have emitted them: gatherer frames observe them exactly like live ones + * (with the raw walk scope), a real callback gets the callback scope - and + * a recording callback records them again (a loop's fixpoint replay running + * inside a body's observation pass). + * * @param callable(Node $node, Scope $scope): void $nodeCallback */ - public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope + public function replayRecordingRange(RecordingNodeCallback $recording, int $from, int $to, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void { - $function = $scope->getFunction(); - $variableLessTags = []; - - foreach ($stmt->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - - $assignedVariable = null; - if ( - $stmt instanceof Node\Stmt\Expression - && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) - && $stmt->expr->var instanceof Variable - && is_string($stmt->expr->var->name) - ) { - $assignedVariable = $stmt->expr->var->name; - } - - foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { - if (is_int($name)) { - $variableLessTags[] = $varTag; - continue; - } - - if ($name === $assignedVariable) { - continue; - } - - $certainty = $scope->hasVariableType($name); - if ($certainty->no()) { - continue; - } - - if ($scope->isInClass() && $scope->getFunction() === null) { - continue; - } - - if ($scope->canAnyVariableExist()) { - $certainty = TrinaryLogic::createYes(); - } - - $variableNode = new Variable($name, $stmt->getAttributes()); - $originalType = $scope->getVariableType($name); - if (!$originalType->equals($varTag->getType())) { - $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); - } - $templateArgumentFrame = $this->observingTemplateArgumentFrame($scope); - if ($templateArgumentFrame !== null) { - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($varTag->getType(), $originalType)); + $pairs = $recording->getPairs(); + $scope->pushExpressionResultStorage($storage); + try { + for ($i = $from; $i < $to; $i++) { + [$node, $pairScope] = $pairs[$i]; + if (!$pairScope instanceof MutatingScope) { + throw new ShouldNotHappenException(); } - - $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $scope = $scope->assignVariable( - $name, - $varTag->getType(), - // a plain variable read is scope state - $nativeScope->hasVariableType($name)->no() ? new ErrorType() : $nativeScope->getVariableType($name), - $certainty, - ); + $this->callNodeCallback($nodeCallback, $node, $pairScope, $storage); } + } finally { + $scope->popExpressionResultStorage(); } - - if (count($variableLessTags) === 1 && $defaultExpr !== null) { - // only the scope effect here: the changed-type node is emitted by the - // statement handler AFTER it walked the expression (emitVarTagChangedNode), - // so the rule prices the expression from its stored result rather than - // asking the scope about a node not processed yet - $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType(), new MixedType()); - } - - return $scope; } /** - * The single variable-less @var tag on the statement's doc comment, or null - * when there is none or more than one - the shape processStmtVarAnnotation() - * applies to $defaultExpr and emitVarTagChangedNode() reports on. + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag + public function callNodeCallbackWithExpression( + callable $nodeCallback, + Node $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + ExpressionContext $context, + ): void { - $function = $scope->getFunction(); - $variableLessTags = []; - foreach ($stmt->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { - if (!is_int($name)) { - continue; - } - - $variableLessTags[] = $varTag; - } + if ($context->isDeep()) { + $scope = $scope->exitFirstLevelStatements(); } - - return count($variableLessTags) === 1 ? $variableLessTags[0] : null; + $this->callNodeCallback($nodeCallback, $expr, $scope, $storage); } /** - * Emits the node the @var-changed-type rule listens to, for a statement with - * a single variable-less @var tag over $defaultExpr - called by the handler - * once the expression has been walked (see processStmtVarAnnotation()). - * * @param callable(Node $node, Scope $scope): void $nodeCallback */ - public function emitVarTagChangedNode(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): TemplateArgumentConstraints + public function callNodeCallback( + callable $nodeCallback, + Node $node, + MutatingScope $scope, + ExpressionResultStorage $storage, + ): void { - $varTag = $this->findSingleVariableLessVarTag($scope, $stmt); - if ($varTag === null) { - return TemplateArgumentConstraints::createEmpty(); + // Engine-feeding gatherer frames observe the node at the emission + // position - their arrays are read as soon as the enclosing body walk + // returns. Gatherers are engine code and never ask about types - + // handing them the raw scope skips a NodeCallbackScope construction per + // emission; the scopes they capture (return statements, impure points) + // answer later asks through the storage hub like any MutatingScope. + foreach ($this->nodeGatherers as $gatherer) { + $gatherer($node, $scope); } - $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); - $defaultExprResult = $storage->findExpressionResult($defaultExpr); - if ($defaultExprResult === null || $this->observingTemplateArgumentFrame($defaultExprResult->getScope()) === null) { - return TemplateArgumentConstraints::createEmpty(); + if ($nodeCallback instanceof NoopNodeCallback) { + return; + } + + if ($nodeCallback instanceof RecordingNodeCallback) { + // recording never asks about types - the pairs are wrapped and + // bound to the storage at replay time instead + $nodeCallback($node, $scope); + return; } - return $this->templateArgumentObserver->collectSend($varTag->getType(), $defaultExprResult->getType()); + + // post-order emission means the node's own result and every subnode + // result are already stored when the callback fires - NodeCallbackScope + // answers every ask synchronously from the storage + $nodeCallback($node, $scope->toNodeCallbackScope()); } /** @@ -3896,61 +1067,4 @@ public function collectReturnSend(MutatingScope $scope, ExpressionResult $return return $this->templateArgumentObserver->collectSend($declaredReturnType, $returnedResult->getType()); } - /** - * A write through ArrayAccess (`$storage[$key] = $value`) reaches the - * object's template arguments exactly like the offsetSet() call it stands - * for, so observe both the key and the written value against that method's - * parameters resolved on the receiver. - */ - public function collectOffsetSetUsage(MutatingScope $scope, Type $receiverType, ?Type $keyType, Type $valueType): TemplateArgumentConstraints - { - $constraints = TemplateArgumentConstraints::createEmpty(); - $frame = $this->observingTemplateArgumentFrame($scope); - if ($frame === null || !$receiverType->hasMethod('offsetSet')->yes()) { - return $constraints; - } - - $parameters = $receiverType->getMethod('offsetSet', $scope)->getOnlyVariant()->getParameters(); - if ($keyType !== null && isset($parameters[0])) { - $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[0]->getType(), $keyType)); - } - if (!isset($parameters[1])) { - return $constraints; - } - - $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[1]->getType(), $valueType)); - return $constraints; - } - - /** - * @param array $nodes - * @return list - */ - private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array - { - $stmts = []; - $isPassedUnreachableStatement = false; - foreach ($nodes as $node) { - if ($node instanceof Node\Stmt\Label) { - break; - } - if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { - continue; - } - if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { - $stmts[] = $node; - continue; - } - if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { - continue; - } - if (!$node instanceof Node\Stmt) { - continue; - } - $stmts[] = $node; - $isPassedUnreachableStatement = true; - } - return $stmts; - } - } diff --git a/src/Analyser/ParametersProcessor.php b/src/Analyser/ParametersProcessor.php new file mode 100644 index 00000000000..9d0abdd370c --- /dev/null +++ b/src/Analyser/ParametersProcessor.php @@ -0,0 +1,45 @@ +attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $param->attrGroups, $scope, $storage, $nodeCallback); + $nodeScopeResolver->callNodeCallback($nodeCallback, $param, $scope, $storage); + if ($param->type !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $param->type, $scope, $storage); + } + if ($param->default === null) { + continue; + } + + $nodeScopeResolver->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + } + } + +} diff --git a/src/Analyser/PropertyHooksProcessor.php b/src/Analyser/PropertyHooksProcessor.php index 5b9afdd6a5b..0bbbc636d7f 100644 --- a/src/Analyser/PropertyHooksProcessor.php +++ b/src/Analyser/PropertyHooksProcessor.php @@ -30,6 +30,8 @@ final class PropertyHooksProcessor public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, + private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -58,13 +60,11 @@ public function processPropertyHooks( foreach ($hooks as $hook) { $nodeScopeResolver->callNodeCallback($nodeCallback, $hook, $scope, $storage); - $nodeScopeResolver->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->phpDocsResolver->getPhpDocs($scope, $hook); - foreach ($hook->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $hook->params, $scope, $storage, $nodeCallback); [$isDeprecated, $deprecatedDescription] = $this->deprecatedAttributeResolver->getDeprecatedAttribute($scope, $hook); diff --git a/src/Analyser/StatementsHandler.php b/src/Analyser/StatementsHandler.php new file mode 100644 index 00000000000..c306b69bc72 --- /dev/null +++ b/src/Analyser/StatementsHandler.php @@ -0,0 +1,945 @@ +debugTemplateArguments = getenv('PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG') === '1'; + } + + /** + * @param Node[] $nodes + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processNodesWithStorage( + NodeScopeResolver $nodeScopeResolver, + array $nodes, + MutatingScope $scope, + ExpressionResultStorage $expressionResultStorage, + callable $nodeCallback, + ): void + { + $alreadyTerminated = false; + $exitPoints = []; + + $stmts = []; + $stmtToNodeIndex = []; + foreach ($nodes as $i => $node) { + if (!($node instanceof Node\Stmt)) { + continue; + } + + $stmtToNodeIndex[count($stmts)] = $i; + $stmts[] = $node; + } + + $dummyParent = new Node\Stmt\Nop(); + foreach ($stmts as $si => $node) { + if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { + continue; + } + + $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); + if ($nestedLabelNames !== null) { + $scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $dummyParent, + [$node], + $scope, + $expressionResultStorage, + StatementContext::createDeep(), + static fn (string $name): bool => isset($nestedLabelNames[$name]), + false, + ); + } + + $statementResult = $nodeScopeResolver->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); + $scope = $statementResult->getScope(); + + if ($node instanceof Node\Stmt\Label) { + $labelName = $node->name->toString(); + + [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( + $labelName, + $scope, + $alreadyTerminated, + $exitPoints, + ); + + if ($alreadyTerminated) { + continue; + } + + if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) { + $scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $dummyParent, + array_slice($stmts, $si + 1), + $scope, + $expressionResultStorage, + StatementContext::createDeep(), + static fn (string $name): bool => $name === $labelName, + true, + ); + } + } + + $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); + + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { + continue; + } + + $alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true); + $this->processUnreachableStatement($nodeScopeResolver, $nextStmts, $scope, $expressionResultStorage, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $bodyStmts + * @param Closure(string): bool $gotoNameMatcher + */ + private function resolveBackwardGotoScope( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $bodyStmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + StatementContext $context, + Closure $gotoNameMatcher, + bool $mergeBodyScopeEachIteration, + ): MutatingScope + { + $bodyScope = $scope; + $count = 0; + $prevEntryScope = null; + do { + $prevScope = $bodyScope; + if ($mergeBodyScopeEachIteration) { + $bodyScope = $bodyScope->mergeWith($scope); + } + if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) { + // walking is deterministic in the entry scope - an unchanged entry + // reproduces the previous pass's exit, so the verification walk is skipped + $bodyScope = $prevScope; + break; + } + $prevEntryScope = $bodyScope; + $tempStorage = $storage->duplicate(); + $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal( + $parentNode, + $bodyStmts, + $bodyScope, + $tempStorage, + new NoopNodeCallback(), + $context->withoutTemplateArgumentResolution(), + ); + + $gotoScope = null; + foreach ($bodyScopeResult->getExitPoints() as $ep) { + $epStmt = $ep->getStatement(); + if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) { + continue; + } + + $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope()); + } + + if ($gotoScope !== null) { + $bodyScope = $scope->mergeWith($gotoScope); + } + + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= NodeScopeResolver::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < NodeScopeResolver::LOOP_SCOPE_ITERATIONS); + + return $bodyScope; + } + + /** + * @param InternalStatementExitPoint[] $exitPoints + * @return array{MutatingScope, bool, list} + */ + private function mergeForwardGotoExitPoints( + string $labelName, + MutatingScope $scope, + bool $alreadyTerminated, + array $exitPoints, + ): array + { + $newExitPoints = []; + foreach ($exitPoints as $exitPoint) { + $exitStmt = $exitPoint->getStatement(); + if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) { + if ($alreadyTerminated) { + $scope = $exitPoint->getScope(); + $alreadyTerminated = false; + } else { + $scope = $scope->mergeWith($exitPoint->getScope()); + } + } else { + $newExitPoints[] = $exitPoint; + } + } + + return [$scope, $alreadyTerminated, $newExitPoints]; + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(NodeScopeResolver $nodeScopeResolver, array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } + + $unreachableStatement = null; + $nextStatements = []; + + foreach ($nextStmts as $key => $nextStmt) { + if ($key === 0) { + $unreachableStatement = $nextStmt; + continue; + } + + $nextStatements[] = $nextStmt; + } + + if (!$unreachableStatement instanceof Node\Stmt) { + return; + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); + } + + /** + * @param array $nodes + * @return list + */ + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array + { + $stmts = []; + $isPassedUnreachableStatement = false; + foreach ($nodes as $node) { + if ($node instanceof Node\Stmt\Label) { + break; + } + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + continue; + } + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } + if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { + continue; + } + if (!$node instanceof Node\Stmt) { + continue; + } + $stmts[] = $node; + $isPassedUnreachableStatement = true; + } + return $stmts; + } + + /** + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function doProcessStmtNodes( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + ): InternalStatementResult + { + $stmtCount = count($stmts); + $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ + || $parentNode instanceof Node\Stmt\ClassMethod + || $parentNode instanceof PropertyHookStatementNode + || $parentNode instanceof Expr\Closure; + + if ( + $shouldCheckLastStatement + && $stmtCount > 0 + && $this->unresolvedTemplateArguments + && $context->shouldResolveTemplateArguments() + ) { + return $this->processBodyStmtNodesTwoPass($nodeScopeResolver, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context); + } + + $state = new StatementListWalkState($scope); + foreach ($stmts as $i => $stmt) { + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, $shouldCheckLastStatement); + } + + $statementResult = $state->toResult(); + if ($stmtCount === 0 && $shouldCheckLastStatement) { + $returnTypeNode = $parentNode->getReturnType(); + if ($parentNode instanceof Expr\Closure) { + $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + } + // the body is empty - the statement above is a synthetic wrapper around + // the closure, not an expression statement that was processed + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $parentNode, + $statementResult->toPublic(), + $returnTypeNode !== null, + ), $scope, $storage); + } + + return $statementResult; + } + + /** + * One statement of a statement list, advancing $state past it. + * + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processStatementStep( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + int $i, + Node\Stmt $stmt, + StatementListWalkState $state, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + bool $shouldCheckLastStatement, + ): void + { + if ($state->alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) { + return; + } + + $isLast = $i === count($stmts) - 1; + + $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); + if ($nestedLabelNames !== null && $context->isTopLevel()) { + $state->scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $parentNode, + [$stmt], + $state->scope, + $storage, + $context->enterDeep(), + static fn (string $name): bool => isset($nestedLabelNames[$name]), + false, + ); + } + + $statementResult = $nodeScopeResolver->processStmtNode( + $stmt, + $state->scope, + $storage, + $nodeCallback, + $context, + ); + $state->variableFlows[$i] = $statementResult->getVariableFlow(); + $state->scope = $statementResult->getScope(); + $state->hasYield = $state->hasYield || $statementResult->hasYield(); + + if ($stmt instanceof Node\Stmt\Label) { + $labelName = $stmt->name->toString(); + + [$state->scope, $state->alreadyTerminated, $state->exitPoints] = $this->mergeForwardGotoExitPoints( + $labelName, + $state->scope, + $state->alreadyTerminated, + $state->exitPoints, + ); + + if ($state->alreadyTerminated) { + return; + } + + if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) { + $state->scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $parentNode, + array_slice($stmts, $i + 1), + $state->scope, + $storage, + $context->enterDeep(), + static fn (string $name): bool => $name === $labelName, + true, + ); + } + } + + if ($shouldCheckLastStatement && $isLast) { + $hasDeclaredReturnType = ($parentNode instanceof Node\FunctionLike || $parentNode instanceof PropertyHookStatementNode) + && $parentNode->getReturnType() !== null; + $endStatements = $statementResult->getEndStatements(); + if (count($endStatements) > 0) { + foreach ($endStatements as $endStatement) { + $endStatementResult = $endStatement->getResult(); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $endStatement->getStatement(), + (new InternalStatementResult( + $endStatementResult->getScope(), + $state->hasYield, + $endStatementResult->isAlwaysTerminating(), + $endStatementResult->getExitPoints(), + $endStatementResult->getThrowPoints(), + $endStatementResult->getImpurePoints(), + ))->toPublic(), + $hasDeclaredReturnType, + $this->readEndStatementExprResult($endStatement->getStatement(), $storage), + ), $endStatementResult->getScope(), $storage); + } + } else { + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $stmt, + (new InternalStatementResult( + $state->scope, + $state->hasYield, + $statementResult->isAlwaysTerminating(), + $statementResult->getExitPoints(), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + ))->toPublic(), + $hasDeclaredReturnType, + $this->readEndStatementExprResult($stmt, $storage), + ), $state->scope, $storage); + } + } + + $state->exitPoints = array_merge($state->exitPoints, $statementResult->getExitPoints()); + $state->throwPoints = array_merge($state->throwPoints, $statementResult->getThrowPoints()); + $state->impurePoints = array_merge($state->impurePoints, $statementResult->getImpurePoints()); + + if ($state->alreadyTerminated || !$statementResult->isAlwaysTerminating()) { + return; + } + + $state->alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + $this->processUnreachableStatement($nodeScopeResolver, $nextStmts, $state->scope, $storage, $nodeCallback); + } + + /** + * The result of the expression an ending statement evaluated, for + * ExecutionEndNode. Null when the statement is not an expression statement, + * and when its expression was not processed into this storage - an end + * statement is collected from wherever execution ended, which can be a + * nested walk whose results never reach the frame the end node is built in. + */ + private function readEndStatementExprResult(Node\Stmt $stmt, ExpressionResultStorage $storage): ?ExpressionResult + { + if (!$stmt instanceof Node\Stmt\Expression) { + return null; + } + + return $storage->findExpressionResult($stmt->expr); + } + + /** + * A function-like body under the unresolvedTemplateArguments toggle is + * walked in two passes. The observation pass walks every statement + * recording rule-facing emissions and threading immutable constraints + * through the scopes returned by expressions and statements. A body that + * created no unresolved template argument simply replays the recording. + * Otherwise TemplateArgumentResolver builds a new resolved frame for the + * second pass, which re-walks only the statements the resolutions can + * influence. Recorded scopes retain their original collection context. + * + * The outer gatherer frames (the method's return statements, execution + * ends, impure points) are suspended during the observation pass and fed + * by the replay and the re-walk, so each emission reaches them once. + * + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processBodyStmtNodesTwoPass( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + ): InternalStatementResult + { + $statementStartTokenPositions = []; + foreach ($stmts as $stmt) { + $statementStartTokenPositions[] = $stmt->getStartTokenPos(); + } + $parentFrame = $scope->getCurrentTemplateArgumentFrame(); + $parentConstraints = $scope->getTemplateArgumentConstraints(); + $frame = new TemplateArgumentFrame($parentFrame); + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('bodiesWalked'); + TemplateArgumentStats::increment('statementsTotal', count($stmts)); + } + $scope = $scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); + $recording = new RecordingNodeCallback(); + $state = new StatementListWalkState($scope); + /** @var list $entries the state and recording offset before each statement, plus the final ones */ + $entries = []; + $observationContext = $context->withoutTemplateArgumentResolution(); + $suspendedGatherers = $nodeScopeResolver->suspendNodeGatherers(); + try { + foreach ($stmts as $i => $stmt) { + $entries[$i] = [clone $state, $recording->count()]; + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $recording, $observationContext, true); + } + } finally { + $nodeScopeResolver->restoreNodeGatherers($suspendedGatherers); + } + $frame = $this->templateArgumentResolver->resolve($state->scope->getTemplateArgumentConstraints() ?? TemplateArgumentConstraints::createEmpty(), $parentFrame, $statementStartTokenPositions); + $stmtCount = count($stmts); + $entries[$stmtCount] = [clone $state, $recording->count()]; + + $firstSiteStatementIndex = $frame->firstSiteStatementIndex(); + if ($firstSiteStatementIndex === null) { + $nodeScopeResolver->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('bodiesWithSites'); + TemplateArgumentStats::increment('statementsReplayed', $firstSiteStatementIndex); + } + // the second pass: the statements before the first site stand as + // recorded; from there on a statement is re-walked only when it + // created a site or mentions a variable whose tracked state the + // resolutions changed - the rest replay their recording and carry + // their recorded effect onto the re-walked scope + $nodeScopeResolver->replayRecordingRange($recording, 0, $entries[$firstSiteStatementIndex][1], $nodeCallback, $storage, $scope); + $hasLabels = false; + foreach ($stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\Label && $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE) === null) { + continue; + } + $hasLabels = true; + break; + } + $state = clone $entries[$firstSiteStatementIndex][0]; + $state->scope = $state->scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); + for ($i = $firstSiteStatementIndex; $i < $stmtCount; $i++) { + [$recordedEntry, $offset] = $entries[$i]; + [$recordedExit, $nextOffset] = $entries[$i + 1]; + $differingRoots = $state->alreadyTerminated === $recordedEntry->alreadyTerminated + ? $state->scope->getDifferingVariableRoots($recordedEntry->scope) + : null; + if ($differingRoots === [] && !$frame->hasSiteAtOrAfter($i)) { + // converged with the observation pass: the rest of its recording stands + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('earlyExits'); + TemplateArgumentStats::increment('statementsReplayed', $stmtCount - $i); + } + $nodeScopeResolver->replayRecordingRange($recording, $offset, $recording->count(), $nodeCallback, $storage, $scope); + $this->appendRecordedStatementResults($state, $recordedEntry, $entries[$stmtCount][0]); + $state->scope = $entries[$stmtCount][0]->scope; + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + $stmt = $stmts[$i]; + $reWalk = $differingRoots === null + || $hasLabels + || $frame->ownsSiteInStatement($i) + || $this->statementMentionsAnyVariable($stmt, $differingRoots); + if ($this->debugTemplateArguments) { + echo sprintf( + "[template-arguments] %s:%d statement %d: %s (differing: %s)\n", + $scope->getFile(), + $stmt->getStartLine(), + $i, + $reWalk ? 're-walk' : 'replay', + $differingRoots === null ? 'non-variable key' : implode(', ', $differingRoots), + ); + } + if ($reWalk) { + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('statementsReWalked'); + } + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, true); + continue; + } + + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('statementsReplayed'); + } + $nodeScopeResolver->replayRecordingRange($recording, $offset, $nextOffset, $nodeCallback, $storage, $scope); + $this->appendRecordedStatementResults($state, $recordedEntry, $recordedExit); + $state->scope = $state->scope->withRecordedStatementDelta($recordedEntry->scope, $recordedExit->scope); + } + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + /** 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))); + $state->throwPoints = array_merge($state->throwPoints, array_slice($to->throwPoints, count($from->throwPoints))); + $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); + } + + /** + * @param list $variableNames + */ + private function statementMentionsAnyVariable(Node\Stmt $stmt, array $variableNames): bool + { + /** @var array{array, bool}|null $mentions */ + $mentions = $stmt->getAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE); + if ($mentions === null) { + $names = []; + $mentionsEverything = false; + $this->collectMentionedVariables($stmt, $names, $mentionsEverything); + $mentions = [$names, $mentionsEverything]; + $stmt->setAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE, $mentions); + } + [$names, $mentionsEverything] = $mentions; + if ($mentionsEverything) { + return true; + } + foreach ($variableNames as $variableName) { + if (isset($names[$variableName])) { + return true; + } + } + + return false; + } + + /** + * Every variable the statement can read or write - syntactically, so + * reads and writes alike - with `$this`; a closure body lives in its own + * scope, so only its use() clause (and its bound `$this`) count, an arrow + * function captures implicitly and is traversed. Dynamic access + * (`$$name`, compact(), extract(), get_defined_vars(), eval, include) + * mentions everything. + * + * @param array $names + */ + private function collectMentionedVariables(Node $node, array &$names, bool &$mentionsEverything): void + { + if ($node instanceof Expr\Variable) { + if (!is_string($node->name)) { + $mentionsEverything = true; + return; + } + $names[$node->name] = true; + return; + } + if ($node instanceof Expr\Closure) { + if (!$node->static) { + $names['this'] = true; + } + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + $mentionsEverything = true; + continue; + } + $names[$use->var->name] = true; + } + + return; + } + if ($node instanceof Expr\Eval_ || $node instanceof Expr\Include_) { + $mentionsEverything = true; + } elseif ( + $node instanceof Expr\FuncCall + && $node->name instanceof Name + && in_array($node->name->toLowerString(), ['compact', 'extract', 'get_defined_vars'], true) + ) { + $mentionsEverything = true; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $this->collectMentionedVariables($subNode, $names, $mentionsEverything); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if (!$item instanceof Node) { + continue; + } + $this->collectMentionedVariables($item, $names, $mentionsEverything); + } + } + } + } + + /** + * @return InternalThrowPoint[]|null + */ + public function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array + { + foreach ($statement->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $function = $scope->getFunction(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + + $throwsTag = $resolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwsType = $throwsTag->getType(); + if ($throwsType->isVoid()->yes()) { + return []; + } + + return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; + } + } + + return null; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope + { + $function = $scope->getFunction(); + $variableLessTags = []; + + foreach ($stmt->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + + $assignedVariable = null; + if ( + $stmt instanceof Node\Stmt\Expression + && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) + && $stmt->expr->var instanceof Variable + && is_string($stmt->expr->var->name) + ) { + $assignedVariable = $stmt->expr->var->name; + } + + foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { + if (is_int($name)) { + $variableLessTags[] = $varTag; + continue; + } + + if ($name === $assignedVariable) { + continue; + } + + $certainty = $scope->hasVariableType($name); + if ($certainty->no()) { + continue; + } + + if ($scope->isInClass() && $scope->getFunction() === null) { + continue; + } + + if ($scope->canAnyVariableExist()) { + $certainty = TrinaryLogic::createYes(); + } + + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); + } + $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($templateArgumentFrame !== null) { + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($varTag->getType(), $originalType)); + } + + $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + // a plain variable read is scope state + $nativeScope->hasVariableType($name)->no() ? new ErrorType() : $nativeScope->getVariableType($name), + $certainty, + ); + } + } + + if (count($variableLessTags) === 1 && $defaultExpr !== null) { + // only the scope effect here: the changed-type node is emitted by the + // statement handler AFTER it walked the expression (emitVarTagChangedNode), + // so the rule prices the expression from its stored result rather than + // asking the scope about a node not processed yet + $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType(), new MixedType()); + } + + return $scope; + } + + /** + * The single variable-less @var tag on the statement's doc comment, or null + * when there is none or more than one - the shape processStmtVarAnnotation() + * applies to $defaultExpr and emitVarTagChangedNode() reports on. + */ + private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag + { + $function = $scope->getFunction(); + $variableLessTags = []; + foreach ($stmt->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { + if (!is_int($name)) { + continue; + } + + $variableLessTags[] = $varTag; + } + } + + return count($variableLessTags) === 1 ? $variableLessTags[0] : null; + } + + /** + * Emits the node the @var-changed-type rule listens to, for a statement with + * a single variable-less @var tag over $defaultExpr - called by the handler + * once the expression has been walked (see processStmtVarAnnotation()). + * + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function emitVarTagChangedNode(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): TemplateArgumentConstraints + { + $varTag = $this->findSingleVariableLessVarTag($scope, $stmt); + if ($varTag === null) { + return TemplateArgumentConstraints::createEmpty(); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); + $defaultExprResult = $storage->findExpressionResult($defaultExpr); + if ($defaultExprResult === null || $nodeScopeResolver->observingTemplateArgumentFrame($defaultExprResult->getScope()) === null) { + return TemplateArgumentConstraints::createEmpty(); + } + return $this->templateArgumentObserver->collectSend($varTag->getType(), $defaultExprResult->getType()); + } + +} diff --git a/src/Analyser/StmtHandler/ClassConstHandler.php b/src/Analyser/StmtHandler/ClassConstHandler.php index 1db93fad6ea..9a00b230f4b 100644 --- a/src/Analyser/StmtHandler/ClassConstHandler.php +++ b/src/Analyser/StmtHandler/ClassConstHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassConst; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -24,6 +25,12 @@ final class ClassConstHandler implements StmtHandler { + public function __construct( + private AttributesHandler $attributesHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof ClassConst; @@ -40,7 +47,7 @@ public function processStmt( { $entryScope = $scope; $impurePoints = []; - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); foreach ($stmt->consts as $const) { $constResult = $nodeScopeResolver->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); // the constant's callback fires after its value was processed, so diff --git a/src/Analyser/StmtHandler/ClassLikeHandler.php b/src/Analyser/StmtHandler/ClassLikeHandler.php index 1cc07a26e10..8b7f9dc0d6c 100644 --- a/src/Analyser/StmtHandler/ClassLikeHandler.php +++ b/src/Analyser/StmtHandler/ClassLikeHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\Trait_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\CalledMethodProcessor; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -21,7 +22,9 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\File\FileReader; use PHPStan\Node\ClassConstantsNode; use PHPStan\Node\ClassMethodsNode; @@ -31,6 +34,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use function sprintf; use function usort; @@ -45,11 +49,17 @@ final class ClassLikeHandler implements StmtHandler { + /** + * @param ExtensionsCollection $readWritePropertiesExtensions + */ public function __construct( private Reflector $reflector, private ClassReflectionFactory $classReflectionFactory, private CalledMethodProcessor $calledMethodProcessor, private ReflectionProvider $reflectionProvider, + private AttributesHandler $attributesHandler, + #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] + private ExtensionsCollection $readWritePropertiesExtensions, ) { } @@ -104,7 +114,7 @@ public function processStmt( // the class attributes are processed before the InClassNode emission, so // rules firing on it (ClassAttributesRule) read the attribute arguments // from the storage - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer); $nodeScopeResolver->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); $classLikeStatements = $stmt->stmts; @@ -127,7 +137,7 @@ public function processStmt( // Class members have their own inference context, including when the class // declaration is visited during an enclosing body's observation pass. $nodeScopeResolver->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, StatementContext::createTopLevel()); - $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $nodeScopeResolver->getReadWritePropertiesExtensions(), $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensions, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage); $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage); $classReflection->evictPrivateSymbols(); diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index 346397d47fc..a7bb190c5e8 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -8,12 +8,14 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Return_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\DeprecatedAttributeResolver; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\ParametersProcessor; use PHPStan\Analyser\PhpDocsResolver; use PHPStan\Analyser\PropertyHooksProcessor; use PHPStan\Analyser\Scope; @@ -48,6 +50,8 @@ public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, private PropertyHooksProcessor $propertyHooksProcessor, + private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -66,12 +70,10 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); - foreach ($stmt->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); if ($stmt->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index ae63fdc64e1..b273f7153b1 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\DoWhileLoopConditionNode; use function array_merge; @@ -28,6 +29,13 @@ final class DoWhileHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Do_; @@ -137,7 +145,7 @@ public function processStmt( $alwaysIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); } diff --git a/src/Analyser/StmtHandler/EnumCaseHandler.php b/src/Analyser/StmtHandler/EnumCaseHandler.php index bbaf1d1beee..c48cfe5061a 100644 --- a/src/Analyser/StmtHandler/EnumCaseHandler.php +++ b/src/Analyser/StmtHandler/EnumCaseHandler.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\EnumCase; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -20,6 +21,12 @@ final class EnumCaseHandler implements StmtHandler { + public function __construct( + private AttributesHandler $attributesHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof EnumCase; @@ -34,7 +41,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $impurePoints = []; if ($stmt->expr !== null) { $exprResult = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); diff --git a/src/Analyser/StmtHandler/ExpressionHandler.php b/src/Analyser/StmtHandler/ExpressionHandler.php index 9386dbb2506..77748ff0a57 100644 --- a/src/Analyser/StmtHandler/ExpressionHandler.php +++ b/src/Analyser/StmtHandler/ExpressionHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -31,6 +32,12 @@ final class ExpressionHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Expression; @@ -48,7 +55,7 @@ public function processStmt( $preAnnotationScope = $scope; $stmtScope = $scope; if ($stmt->expr instanceof Expr\Throw_) { - $stmtScope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback); + $stmtScope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback); $scope = $stmtScope; } $hasAssign = false; @@ -68,7 +75,7 @@ public function processStmt( $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); if ($stmt->expr instanceof Expr\Throw_) { // the @var-changed-type node fires now that the thrown expression is stored - $result = $result->withScope($result->getScope()->addTemplateArgumentConstraints($nodeScopeResolver->emitVarTagChangedNode($preAnnotationScope, $storage, $stmt, $stmt->expr->expr, $nodeCallback))); + $result = $result->withScope($result->getScope()->addTemplateArgumentConstraints($this->statementsHandler->emitVarTagChangedNode($nodeScopeResolver, $preAnnotationScope, $storage, $stmt, $stmt->expr->expr, $nodeCallback))); } } finally { $nodeScopeResolver->popNodeGatherer(); diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 445afa016b1..3191663e97f 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -25,6 +25,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\TrinaryLogic; use function array_last; @@ -41,6 +42,15 @@ final class ForHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $polluteScopeWithLoopInitialAssignments, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof For_; @@ -166,7 +176,7 @@ public function processStmt( // only the last condition expression is relevant whether the loop continues // see https://www.php.net/manual/en/control-structures.for.php if ($condExpr === $lastCondExpr) { - $condTruthiness = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } @@ -285,20 +295,20 @@ public function processStmt( } if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating() || ($backEdgeDead && count($breakExitPoints) === 0)) { - if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if ($this->polluteScopeWithLoopInitialAssignments) { $finalScope = $initScope; } else { $finalScope = $scope; } } elseif ($isIterableAtLeastOnce->maybe()) { - if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if ($this->polluteScopeWithLoopInitialAssignments) { $finalScope = $finalScope->mergeWith($initScope); } else { $finalScope = $finalScope->mergeWith($scope); } } else { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $finalScope = $finalScope->mergeWith($scope); } } diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 9d8921de38d..057ad69e12f 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -23,6 +23,7 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\IdenticalNarrowingHelper; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\InternalThrowPoint; @@ -83,6 +84,9 @@ public function __construct( #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, private VarAnnotationProcessor $varAnnotationProcessor, + private AssignHandler $assignHandler, + #[AutowiredParameter] + private bool $polluteScopeWithAlwaysIterableForeach, ) { } @@ -149,7 +153,7 @@ public function processStmt( // walked synthetic would delegate to); the walk is the composition's // miss seam $nonEmptyIterateeScope = $scope; - if ($nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach()) { + if ($this->polluteScopeWithAlwaysIterableForeach) { $identicalNarrowingHelper = $this->container->getByType(IdenticalNarrowingHelper::class); $emptyArrayType = new ConstantArrayType([], []); $nonEmptyTypes = $identicalNarrowingHelper->specifyIdenticalAgainstType( @@ -458,7 +462,7 @@ static function () use ($condResult, $emptyArrayType): Type { ), TypeSpecifierContext::createTruthy())); } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; - } elseif (!$nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach()) { + } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables } @@ -482,7 +486,7 @@ static function () use ($condResult, $emptyArrayType): Type { 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); + $loopFlow = VariableFlow::loop($traversableThrowPoint !== null ? VariableFlow::throwing($traversableThrowPoint->getType(), true) : null, VariableFlow::sequence($bindingFlow, $finalScopeResult->getVariableFlow()), null, $isIterableAtLeastOnce->yes() && $this->polluteScopeWithAlwaysIterableForeach, true); $bindingWrites = VariableFlowBuilder::writes($bindingFlow); $bindings = []; foreach ($bindingWrites as $write) { @@ -531,7 +535,8 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $vars[] = $keyVarName; } } else { - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -550,7 +555,8 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $iterateeType, $nativeIterateeType, $stmt->keyVar->name); $vars[] = $stmt->keyVar->name; } elseif ($stmt->keyVar !== null) { - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 600c3aa8f38..bb95212ae43 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -7,12 +7,14 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Return_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\DeprecatedAttributeResolver; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\ParametersProcessor; use PHPStan\Analyser\PhpDocsResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; @@ -39,6 +41,8 @@ final class FunctionHandler implements StmtHandler public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, + private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -57,12 +61,10 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); - foreach ($stmt->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); if ($stmt->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); diff --git a/src/Analyser/StmtHandler/IfHandler.php b/src/Analyser/StmtHandler/IfHandler.php index 59526da6eac..6595d804528 100644 --- a/src/Analyser/StmtHandler/IfHandler.php +++ b/src/Analyser/StmtHandler/IfHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use function array_merge; use function array_reverse; @@ -25,6 +26,13 @@ final class IfHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof If_; @@ -44,7 +52,7 @@ public function processStmt( $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(); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); $exitPoints = []; $throwPoints = $condResult->getThrowPoints(); @@ -79,7 +87,7 @@ public function processStmt( foreach ($stmt->elseifs as $elseif) { $condResult = $nodeScopeResolver->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $nodeScopeResolver->callNodeCallback($nodeCallback, $elseif, $scope, $storage); - $elseIfConditionType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $branchScopeStatementResult = $nodeScopeResolver->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); diff --git a/src/Analyser/StmtHandler/PropertyHandler.php b/src/Analyser/StmtHandler/PropertyHandler.php index 7489cf17ca4..f98095a34c9 100644 --- a/src/Analyser/StmtHandler/PropertyHandler.php +++ b/src/Analyser/StmtHandler/PropertyHandler.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Property; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -29,6 +30,7 @@ final class PropertyHandler implements StmtHandler public function __construct( private PhpDocsResolver $phpDocsResolver, private PropertyHooksProcessor $propertyHooksProcessor, + private AttributesHandler $attributesHandler, ) { } @@ -47,7 +49,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null; diff --git a/src/Analyser/StmtHandler/ReturnHandler.php b/src/Analyser/StmtHandler/ReturnHandler.php index b8eefe161e3..a91be8272c4 100644 --- a/src/Analyser/StmtHandler/ReturnHandler.php +++ b/src/Analyser/StmtHandler/ReturnHandler.php @@ -12,6 +12,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; @@ -24,6 +25,12 @@ final class ReturnHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Return_; @@ -38,14 +45,14 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $stmtScope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback); + $stmtScope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr, $nodeCallback); if ($stmt->expr !== null) { $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $stmtScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); // the @var-changed-type node fires now that the expression is stored // on the scope BEFORE the @var tag re-typed the expression, so the rule // compares the tag against the expression's walked type - $varConstraints = $nodeScopeResolver->emitVarTagChangedNode($scope, $storage, $stmt, $stmt->expr, $nodeCallback); + $varConstraints = $this->statementsHandler->emitVarTagChangedNode($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr, $nodeCallback); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $scope = $result->getScope()->addTemplateArgumentConstraints($varConstraints)->addTemplateArgumentConstraints($nodeScopeResolver->collectReturnSend($stmtScope, $result)); diff --git a/src/Analyser/StmtHandler/TraitUseHandler.php b/src/Analyser/StmtHandler/TraitUseHandler.php index efb57a59973..44e22a2bfab 100644 --- a/src/Analyser/StmtHandler/TraitUseHandler.php +++ b/src/Analyser/StmtHandler/TraitUseHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\TraitUse; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; @@ -38,6 +39,7 @@ public function __construct( private FileHelper $fileHelper, #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, + private AttributesHandler $attributesHandler, ) { } @@ -176,7 +178,7 @@ private function processNodesForTraitUse(NodeScopeResolver $nodeScopeResolver, $ // attribute args are not processed as part of the trait statements // but rules like TraitAttributesRule ask about their types - $nodeScopeResolver->processAttributeGroups($node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); $nodeScopeResolver->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage); $nodeScopeResolver->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel()); diff --git a/src/Analyser/StmtHandler/TryCatchHandler.php b/src/Analyser/StmtHandler/TryCatchHandler.php index 5f3426d23b8..aedccaccac8 100644 --- a/src/Analyser/StmtHandler/TryCatchHandler.php +++ b/src/Analyser/StmtHandler/TryCatchHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; @@ -43,6 +44,12 @@ final class TryCatchHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof TryCatch; @@ -194,7 +201,7 @@ public function processStmt( } if (count($matchingThrowPoints) === 0) { - $catchFlows[] = [$originalCatchType, $nodeScopeResolver->getVariableMentionFlow($catchNode)]; + $catchFlows[] = [$originalCatchType, $this->statementsHandler->getVariableMentionFlow($catchNode)]; continue; } diff --git a/src/Analyser/StmtHandler/UnsetHandler.php b/src/Analyser/StmtHandler/UnsetHandler.php index 2c798f90687..7874a82009b 100644 --- a/src/Analyser/StmtHandler/UnsetHandler.php +++ b/src/Analyser/StmtHandler/UnsetHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\Unset_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; use PHPStan\Analyser\ImpurePoint; @@ -48,7 +49,10 @@ public function supports(Stmt $stmt): bool return $stmt instanceof Unset_; } - public function __construct(private Container $container) + public function __construct( + private Container $container, + private AssignHandler $assignHandler, + ) { } @@ -109,7 +113,8 @@ public function processStmt( }; $clonedVar = $buildExistingChain($var->var); $unsetOffsetExpr = new UnsetOffsetExpr($var->var, $var->dim); - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index ba044123c0d..44bc28b83b1 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BreaklessWhileLoopNode; use function array_merge; @@ -29,6 +30,15 @@ final class WhileHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $polluteScopeWithLoopInitialAssignments, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof While_; @@ -51,10 +61,10 @@ public function processStmt( $scope->pushExpressionResultStorage($storage); try { $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false)); - $beforeCondBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $scope = $condScope->mergeWith($scope); } @@ -173,7 +183,7 @@ public function processStmt( $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $bodyCondResult->getType() : $bodyCondResult->getNativeType())->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyCondResult->getType() : $bodyCondResult->getNativeType())->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } @@ -200,7 +210,7 @@ public function processStmt( $finalScope = null; } if (!$isIterableAtLeastOnce) { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $condScope = $condScope->mergeWith($scope); } $finalScope = $finalScope === null ? $condScope : $finalScope->mergeWith($condScope); diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 441874ecf5b..80874d54ae0 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Namespace_; +use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Analyser\PhpDocsResolver; @@ -1239,7 +1240,7 @@ private function inferAndCachePropertyTypes( // an independent lazy pass on its own scope - never read through // Scope::getType(), which is reserved for the file's main walk - $propertyType = $this->nodeScopeResolver->processIndependentPassExpr($expr->expr, $methodScope)->getType(); + $propertyType = $this->nodeScopeResolver->processExprOnDemand($expr->expr, $methodScope, new ExpressionResultStorage())->getType(); if ($propertyType instanceof ErrorType || $propertyType instanceof NeverType) { continue; } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 3888ef15c45..82f16583c30 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -9,19 +9,18 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\IgnoreErrorExtension; use PHPStan\Analyser\InternalError; use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\PerFileAnalysisResettable; use PHPStan\Analyser\RuleErrorTransformer; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\PackageDependencyResolver; -use PHPStan\DependencyInjection\DirectExtensionsCollection; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Fixable\Patcher; @@ -29,16 +28,7 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; +use PHPStan\ShouldNotHappenException; use function array_map; use function array_merge; use function count; @@ -71,6 +61,10 @@ protected function getCollectors(): array } /** + * No longer configures the analysis - register the extensions as services + * in a config file returned from getAdditionalConfigFiles() instead. + * Returning any extension fails the test. + * * @return ReadWritePropertiesExtension[] */ protected function getReadWritePropertiesExtensions(): array @@ -85,33 +79,21 @@ protected function getTypeSpecifier(): TypeSpecifier protected function createNodeScopeResolver(): NodeScopeResolver { - $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); - $reflectionProvider = $this->createReflectionProvider(); + if ( + $this->getReadWritePropertiesExtensions() !== [] + || $this->shouldPolluteScopeWithLoopInitialAssignments() !== self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments') + || $this->shouldPolluteScopeWithAlwaysIterableForeach() !== self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach') + ) { + throw new ShouldNotHappenException('getReadWritePropertiesExtensions(), shouldPolluteScopeWithLoopInitialAssignments() and shouldPolluteScopeWithAlwaysIterableForeach() no longer configure the analysis. Register the extensions as services and set the parameters in a config file returned from getAdditionalConfigFiles() instead.'); + } return new NodeScopeResolver( self::getContainer(), 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), - self::getContainer()->getByType(FileTypeMapper::class), - $readWritePropertiesExtensions !== [] ? new DirectExtensionsCollection($readWritePropertiesExtensions) : self::getContainer()->getExtensionsCollection(ReadWritePropertiesExtension::class), - self::getContainer()->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(MethodParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - self::getContainer()->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::getContainer()->getExtensionsCollection(PerFileAnalysisResettable::class), - $this->shouldPolluteScopeWithLoopInitialAssignments(), - $this->shouldPolluteScopeWithAlwaysIterableForeach(), - self::getContainer()->getParameter('exceptions')['implicitThrows'], - $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), - self::getContainer()->getParameter('featureToggles')['unresolvedTemplateArguments'], + self::getContainer()->getByType(StatementsHandler::class), ); } @@ -330,14 +312,24 @@ private function gatherAnalyserErrorsWithDelayedErrors(array $files): array ]; } + /** + * No longer configures the analysis - set the polluteScopeWithLoopInitialAssignments + * parameter in a config file returned from getAdditionalConfigFiles() instead. + * Returning a different value than the parameter fails the test. + */ protected function shouldPolluteScopeWithLoopInitialAssignments(): bool { - return true; + return self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'); } + /** + * No longer configures the analysis - set the polluteScopeWithAlwaysIterableForeach + * parameter in a config file returned from getAdditionalConfigFiles() instead. + * Returning a different value than the parameter fails the test. + */ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool { - return true; + return self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'); } protected function shouldFailOnPhpErrors(): bool diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 77203859e35..071bb4bb190 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -8,31 +8,20 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\PerFileAnalysisResettable; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\File\FileHelper; use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use Symfony\Component\Finder\Finder; @@ -61,32 +50,14 @@ abstract class TypeInferenceTestCase extends PHPStanTestCase protected static function createNodeScopeResolver(): NodeScopeResolver { $container = self::getContainer(); - $reflectionProvider = self::createReflectionProvider(); return new NodeScopeResolver( $container, $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), - $container->getByType(FileTypeMapper::class), - $container->getExtensionsCollection(ReadWritePropertiesExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), - $container->getParameter('polluteScopeWithLoopInitialAssignments'), - $container->getParameter('polluteScopeWithAlwaysIterableForeach'), - $container->getParameter('exceptions')['implicitThrows'], - $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), - $container->getParameter('featureToggles')['unresolvedTemplateArguments'], + $container->getByType(StatementsHandler::class), ); } diff --git a/src/Type/Generic/UnresolvedTemplateArgumentType.php b/src/Type/Generic/UnresolvedTemplateArgumentType.php index 517741be4fb..f394e9de2b2 100644 --- a/src/Type/Generic/UnresolvedTemplateArgumentType.php +++ b/src/Type/Generic/UnresolvedTemplateArgumentType.php @@ -33,7 +33,7 @@ * generic call, before the surrounding function body decided what it should be. * * Exists only during the observation pass of a function body (see - * NodeScopeResolver::processBodyStmtNodesTwoPass()): every send of the object + * StatementsHandler::processBodyStmtNodesTwoPass()): every send of the object * to a declared type (property, parameter, return, @var) and every method call * on it is recorded against the site (the creating node) and the template name, * and the second pass substitutes the resolved type. Rules never see it. diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index a9fe9e9b25c..adb02c6e81b 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -7,7 +7,6 @@ use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\Ignore\IgnoreLexer; use PHPStan\Collectors\Registry as CollectorRegistry; @@ -21,18 +20,8 @@ use PHPStan\Parser\RichParser; use PHPStan\Rules\AlwaysFailRule; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; use PHPUnit\Framework\Attributes\DataProvider; use function array_map; use function array_merge; @@ -862,27 +851,10 @@ private function createAnalyser(): Analyser $nodeScopeResolver = new NodeScopeResolver( $container, $container->getByType(TemplateArgumentObserver::class), - $container->getByType(TemplateArgumentResolver::class), - $reflectionProvider, $fileHelper, - $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), - $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), - $fileTypeMapper, - $container->getExtensionsCollection(ReadWritePropertiesExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), - false, - true, - true, - $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), - $container->getParameter('featureToggles')['unresolvedTemplateArguments'], + $container->getByType(StatementsHandler::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index e701bf91f4e..81d5ef2aee3 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -11,8 +11,6 @@ use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; -use function sprintf; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -20,8 +18,6 @@ class ImpossibleInstanceOfRuleTest extends RuleTestCase { - private bool $treatPhpDocTypesAsCertain; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -43,7 +39,7 @@ protected function getRule(): Rule $ruleLevelHelper, new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), - treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, + treatPhpDocTypesAsCertain: true, reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, treatPhpDocTypesAsCertainTip: true, ), @@ -51,14 +47,8 @@ protected function getRule(): Rule ]); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - public function testInstanceof(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/impossible-instanceof.php'], @@ -213,32 +203,8 @@ public function testInstanceof(): void ); } - public function testDoNotReportTypesFromPhpDocs(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ - [ - 'Instanceof between stdClass and stdClass will always evaluate to true.', - 12, - ], - [ - 'Instanceof between stdClass and Exception will always evaluate to false.', - 15, - ], - [ - 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', - 27, - ], - [ - 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', - 30, - ], - ]); - } - public function testReportTypesFromPhpDocs(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -271,26 +237,17 @@ public function testReportTypesFromPhpDocs(): void public function testBug3096(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3096.php'], []); } public function testBug6213(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6213.php'], []); } - public function testBug5333(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-5333.php'], []); - } - #[RequiresPhp('>= 8.0.0')] public function testBug8042(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8042.php'], [ [ 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', @@ -308,13 +265,11 @@ public function testBug8042(): void #[RequiresPhp('>= 8.1.0')] public function testBug7721(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7721.php'], []); } public function testUnreachableIfBranches(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -336,30 +291,8 @@ public function testUnreachableIfBranches(): void ]); } - public function testIfBranchesDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 16, - ], - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 26, - 'Remove remaining cases below this one and this error will disappear too.', - ], - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 36, - 'Remove remaining cases below this one and this error will disappear too.', - ], - ]); - } - public function testIfBranchesReportPhpDoc(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ [ @@ -396,7 +329,6 @@ public function testIfBranchesReportPhpDoc(): void public function testUnreachableTernaryElse(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -409,28 +341,8 @@ public function testUnreachableTernaryElse(): void ]); } - public function testTernaryElseDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 16, - ], - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 17, - ], - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 20, - ], - ]); - } - public function testTernaryElseReportPhpDoc(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ [ @@ -453,12 +365,6 @@ public function testTernaryElseReportPhpDoc(): void ]); } - public function testBug4689(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4689.php'], []); - } - public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ @@ -490,7 +396,6 @@ public static function dataReportAlwaysTrueInLastCondition(): iterable #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-instanceof-report-always-true-last-condition.php'], $expectedErrors); } @@ -498,7 +403,6 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast #[RequiresPhp('>= 8.1.0')] public function testBug10201(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10201.php'], [ [ 'Instanceof between string and Bug10201\Hello will always evaluate to false.', @@ -509,8 +413,6 @@ public function testBug10201(): void public function testBug3632(): void { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/bug-3632.php'], [ [ @@ -531,8 +433,6 @@ public function testBug3632(): void public function testBug10036(): void { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-10036.php'], [ [ 'Instanceof between stdClass and string|null results in an error.', @@ -551,20 +451,17 @@ public function testBug10036(): void public function testBug10353(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10353.php'], []); } public function testBug12267(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12267.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testNewIsAlwaysFinalClass(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-new-is-always-final.php'], [ [ 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', @@ -597,17 +494,6 @@ public function testNewIsAlwaysFinalClass(): void ]); } - public function testBug13469(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13469.php'], [ - [ - sprintf('Instanceof between Bug13469\Foo and Stringable will always evaluate to %s.', PHP_VERSION_ID >= 80000 ? 'true' : 'false'), - 23, - ], - ]); - } - public static function dataBug13975(): iterable { yield [__DIR__ . '/data/bug-13975-a.php']; @@ -617,13 +503,11 @@ public static function dataBug13975(): iterable #[DataProvider('dataBug13975')] public function testBug13975(string $file): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([$file], []); } public function testPossiblyImpureTip(): void { - $this->treatPhpDocTypesAsCertain = true; $learnMore = ' Learn more: https://phpstan.org/blog/remembering-and-forgetting-returned-values'; $this->analyse([__DIR__ . '/data/possibly-impure-instanceof-tip.php'], [ // maybe-impure: tip expected @@ -643,7 +527,6 @@ public function testPossiblyImpureTip(): void public function testInTrait(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/impossible-instanceof-in-trait.php'], [ [ @@ -659,10 +542,4 @@ public function testInTrait(): void ]); } - public function testBug5271(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5271.php'], []); - } - } diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..2ee8db20e0d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,149 @@ + + */ +class ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $ruleLevelHelper = new RuleLevelHelper( + self::createReflectionProvider(), + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); + + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleInstanceOfRule( + $ruleLevelHelper, + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + treatPhpDocTypesAsCertain: false, + reportAlwaysTrueInLastCondition: false, + treatPhpDocTypesAsCertainTip: true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testDoNotReportTypesFromPhpDocs(): void + { + $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 12, + ], + [ + 'Instanceof between stdClass and Exception will always evaluate to false.', + 15, + ], + [ + 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', + 27, + ], + [ + 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', + 30, + ], + ]); + } + + public function testBug5333(): void + { + $this->analyse([__DIR__ . '/data/bug-5333.php'], []); + } + + public function testIfBranchesDoNotReportPhpDoc(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testTernaryElseDoNotReportPhpDoc(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testBug4689(): void + { + $this->analyse([__DIR__ . '/data/bug-4689.php'], []); + } + + public function testBug13469(): void + { + $this->analyse([__DIR__ . '/data/bug-13469.php'], [ + [ + sprintf('Instanceof between Bug13469\Foo and Stringable will always evaluate to %s.', PHP_VERSION_ID >= 80000 ? 'true' : 'false'), + 23, + ], + ]); + } + + public function testBug5271(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5271.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index 837668739b0..348fc70f524 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -237,21 +237,6 @@ public function testRuleLogicalOr(): void ]); } - public function testDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/boolean-or-not-phpdoc.php'], [ - [ - 'Left side of || is always true.', - 24, - ], - [ - 'Right side of || is always true.', - 30, - ], - ]); - } - public function testReportPhpDoc(): void { $this->treatPhpDocTypesAsCertain = true; @@ -288,9 +273,6 @@ public static function dataTreatPhpDocTypesAsCertainRegression(): array [ true, ], - [ - false, - ], ]; } @@ -441,13 +423,6 @@ public function testBug14473(): void $this->analyse([__DIR__ . '/data/bug-14473.php'], []); } - public function testBug15169(): void - { - $this->treatPhpDocTypesAsCertain = false; - - $this->analyse([__DIR__ . '/../DeadCode/data/bug-15169.php'], []); - } - public function testBug15169TreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..b78733e32bc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,128 @@ + + */ +class BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanOrConstantConditionRule( + new ConstantConditionRuleHelper( + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new FunctionCallConstantConditionRule(), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testDoNotReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/boolean-or-not-phpdoc.php'], [ + [ + 'Left side of || is always true.', + 24, + ], + [ + 'Right side of || is always true.', + 30, + ], + ]); + } + + public static function dataTreatPhpDocTypesAsCertainRegression(): array + { + return [ + [ + false, + ], + ]; + } + + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] + public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/boolean-or-treat-phpdoc-types-regression.php'], []); + } + + public function testBug15169(): void + { + $this->treatPhpDocTypesAsCertain = false; + + $this->analyse([__DIR__ . '/../DeadCode/data/bug-15169.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 7dff486519c..dc0b44131f7 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -7,10 +7,7 @@ use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; -use function array_filter; use function array_map; -use function array_values; -use function count; /** * @extends RuleTestCase @@ -18,8 +15,6 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - private bool $treatPhpDocTypesAsCertain; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -29,12 +24,12 @@ protected function getRule(): Rule new ImpossibleCheckTypeFunctionCallRule( new ImpossibleCheckTypeHelper( self::createReflectionProvider(), - $this->treatPhpDocTypesAsCertain, + true, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), - $this->treatPhpDocTypesAsCertain, + true, $this->reportAlwaysTrueInLastCondition, true, ), @@ -42,15 +37,9 @@ protected function getRule(): Rule ]); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - #[RequiresPhp('>= 8.0.0')] public function testImpossibleCheckTypeFunctionCall(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse( [__DIR__ . '/data/check-type-function-call.php'], [ @@ -290,14 +279,12 @@ public function testImpossibleCheckTypeFunctionCall(): void public function testBug7898(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7898.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testStructExists(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/check-type-function-call-struct-exists.php'], [ ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 23], ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 25], @@ -313,20 +300,8 @@ public function testStructExists(): void ]); } - public function testDoNotReportTypesFromPhpDocs(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ - [ - 'Call to function is_int() with int will always evaluate to true.', - 16, - ], - ]); - } - public function testReportTypesFromPhpDocs(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ 'Call to function is_int() with int will always evaluate to true.', @@ -352,44 +327,27 @@ public function testReportTypesFromPhpDocs(): void public function testBug2550(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2550.php'], []); } public function testBug3994(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3994.php'], []); } public function testBug1613(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-1613.php'], []); } public function testBug2714(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2714.php'], []); } - public function testBug4657(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4657.php'], []); - } - - public function testBug4999(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4999.php'], []); - } - #[RequiresPhp('>= 8.1.0')] public function testArrayIsList(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/array-is-list.php'], [ [ 'Call to function array_is_list() with array will always evaluate to false.', @@ -409,13 +367,11 @@ public function testArrayIsList(): void public function testBug3766(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3766.php'], []); } public function testBug6305(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6305.php'], [ [ 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', @@ -430,13 +386,11 @@ public function testBug6305(): void public function testBug6305b(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6305b.php'], []); } public function testBug13713(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13713.php'], [ [ "Call to function is_subclass_of() with arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.", @@ -452,19 +406,16 @@ public function testBug13713(): void public function testBug6698(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6698.php'], []); } public function testBug5369(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5369.php'], []); } public function testBugInArrayDateFormat(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/in-array-date-format.php'], [ [ 'Call to function in_array() with arguments \'a\', non-empty-array and true will always evaluate to true.', @@ -490,62 +441,52 @@ public function testBugInArrayDateFormat(): void public function testBug5496(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5496.php'], []); } public function testBug3892(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3892.php'], []); } public function testBug3314(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3314.php'], []); } public function testBug2870(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2870.php'], []); } public function testBug5354(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5354.php'], []); } public function testSlevomatCsInArrayBug(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); } public function testNonEmptySpecifiedString(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []); } public function testBug2755(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2755.php'], []); } public function testBug7079(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7079.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testConditionalTypesInference(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/conditional-types-inference.php'], [ [ 'Call to function testIsInt() with string will always evaluate to false.', @@ -572,56 +513,47 @@ public function testConditionalTypesInference(): void public function testBug6697(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6697.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug6443(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6443.php'], []); } public function testBug7684(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7684.php'], []); } public function testBug7224(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7224.php'], []); } public function testBug4708(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4708.php'], []); } public function testBug3821(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3821.php'], []); } public function testBug6599(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6599.php'], []); } public function testBug7914(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7914.php'], []); } public function testDocblockAssertEquality(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/docblock-assert-equality.php'], [ [ 'Call to function isAnInteger() with int will always evaluate to true.', @@ -632,56 +564,36 @@ public function testDocblockAssertEquality(): void public function testBug8076(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8076.php'], []); } public function testBug8562(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8562.php'], []); } - public function testBug6938(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-6938.php'], []); - } - public function testBug8727(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8727.php'], []); } public function testBug8474(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8474.php'], []); } - public function testBug5695(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-5695.php'], []); - } - public function testBug8752(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); } public function testDiscussion9134(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/discussion-9134.php'], []); } public function testImpossibleMethodExistOnGenericClassString(): void { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [ [ @@ -740,14 +652,12 @@ public static function dataReportAlwaysTrueInLastCondition(): iterable #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-function-report-always-true-last-condition.php'], $expectedErrors); } public function testObjectShapes(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/property-exists-object-shapes.php'], [ [ 'Call to function property_exists() with object{foo: int, bar?: string} and \'baz\' will always evaluate to false.', @@ -920,7 +830,6 @@ private static function getLooseComparisonAgainsEnumsIssues(): array #[RequiresPhp('>= 8.1.0')] public function testLooseComparisonAgainstEnums(): void { - $this->treatPhpDocTypesAsCertain = true; $issues = array_map( static function (array $i): array { if (($i[2] ?? null) === 'BUG') { @@ -936,7 +845,6 @@ static function (array $i): array { public function testNonStrictInArray(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662.php'], []); } @@ -945,7 +853,6 @@ public function testNonStrictInArrayEnums(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662-enums.php'], [ [ "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", @@ -968,18 +875,8 @@ public function testNonStrictInArrayEnums(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testLooseComparisonAgainstEnumsNoPhpdoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $issues = self::getLooseComparisonAgainsEnumsIssues(); - $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); - $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); - } - public function testBug4890b(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4890b.php'], []); } @@ -987,7 +884,6 @@ public function testBug10502(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10502.php'], [ [ "Call to function is_callable() with array{ArrayObject<0, 0>, 'count'} will always evaluate to true.", @@ -1003,32 +899,27 @@ public function testBug10502(): void public function testAlwaysTruePregMatch(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/always-true-preg-match.php'], []); } public function testBug3979(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3979.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug8464(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8464.php'], []); } public function testBug8954(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8954.php'], []); } public function testBugPR3404(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-pr-3404.php'], [ [ 'Call to function is_a() with arguments BugPR3404\Location, \'BugPR3404\\\\Location\' and true will always evaluate to true.', @@ -1039,13 +930,11 @@ public function testBugPR3404(): void public function testBug13151(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13151.php'], []); } public function testBug8818(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8818.php'], []); } @@ -1053,7 +942,6 @@ public function testBug12755(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12755.php'], [ [ 'Call to function in_array() with arguments null, array{key1: bool|null, key2: null} and true will always evaluate to true.', @@ -1065,19 +953,16 @@ public function testBug12755(): void public function testBugStrictRule147(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-strict-147.php'], []); } public function testBugStrictRule143(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-strict-143.php'], []); } public function testBug12412(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12412.php'], []); } @@ -1085,7 +970,6 @@ public function testBug14873(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14873.php'], [ [ 'Call to function in_array() with arguments \'a\'|\'b\'|\'c\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.', @@ -1102,7 +986,6 @@ public function testBug14873(): void public function testBug2730(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2730.php'], [ [ 'Call to function is_object() with int will always evaluate to false.', @@ -1114,20 +997,17 @@ public function testBug2730(): void #[RequiresPhp('>= 8.2.0')] public function testBug13291(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13291.php'], []); } public function testBug6788(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6788.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug13268(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13268.php'], []); } @@ -1136,7 +1016,6 @@ public function testBug12087(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12087.php'], [ [ 'Call to function is_null() with null will always evaluate to true.', @@ -1151,7 +1030,6 @@ public function testBug12087c(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12087c.php'], [ [ 'Call to function is_null() with null will always evaluate to true.', @@ -1171,64 +1049,54 @@ public function testBug12087c(): void public function testBug10394(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10394.php'], []); } public function testBug12930(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12930.php'], []); } public function testBug13628(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13628.php'], []); } public function testBug13023(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13023.php'], []); } public function testBug9095(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9095.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug7599(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7599.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug13474(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13474.php'], []); } public function testBug13687(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13687.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug12798(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12798.php'], []); } public function testBug4570(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4570.php'], []); } @@ -1236,7 +1104,6 @@ public function testBug9666(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9666.php'], [ [ 'Call to function is_bool() with bool will always evaluate to true.', @@ -1249,31 +1116,26 @@ public function testBug9666(): void #[RequiresPhp('>= 8.0.0')] public function testBug9445(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9445.php'], []); } public function testBug7773(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7773.php'], []); } public function testPr4375(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/pr-4375.php'], []); } public function testBug14177(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14177.php'], []); } public function testBug13566(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13566.php'], [ [ 'Call to function is_numeric() with 123 will always evaluate to true.', @@ -1286,22 +1148,13 @@ public function testBug13566(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug14429(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-14429.php'], []); - } - public function testBug13705(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13705.php'], []); } public function testBug13799(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13799.php'], [ [ 'Call to function in_array() with arguments \'c\', list<\'a\'|\'b\'> and true will always evaluate to false.', @@ -1313,7 +1166,6 @@ public function testBug13799(): void public function testInTrait(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-function-call-in-trait.php'], [ [ 'Call to function is_string() with int will always evaluate to false.', @@ -1324,38 +1176,27 @@ public function testInTrait(): void public function testBug12063(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12063.php'], []); } - public function testBug11014(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11014.php'], []); - } - public function testBug8217(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8217.php'], []); } public function testBug8980(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8980.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug14908(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14908.php'], []); } public function testBug6211(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6211.php'], [ [ 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..74a451a4dc2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,264 @@ + + */ +class ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + false, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + false, + false, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testDoNotReportTypesFromPhpDocs(): void + { + $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 16, + ], + ]); + } + + public function testBug4657(): void + { + $this->analyse([__DIR__ . '/data/bug-4657.php'], []); + } + + public function testBug4999(): void + { + $this->analyse([__DIR__ . '/data/bug-4999.php'], []); + } + + public function testBug6938(): void + { + $this->analyse([__DIR__ . '/data/bug-6938.php'], []); + } + + public function testBug5695(): void + { + $this->analyse([__DIR__ . '/data/bug-5695.php'], []); + } + + /** @return list */ + private static function getLooseComparisonAgainsEnumsIssues(): array + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + return [ + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooUnitEnum and array{\'A\'} will always evaluate to false.', + 21, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 24, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooBackedEnum and array{\'A\'} will always evaluate to false.', + 27, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum, array{\'A\'} and false will always evaluate to false.', + 30, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 33, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooUnitEnum} will always evaluate to false.', + 39, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 42, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooBackedEnum} will always evaluate to false.', + 45, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum} and false will always evaluate to false.', + 48, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 51, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', + 57, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 60, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', + 63, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', + 66, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 69, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', + 75, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 78, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 81, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', + 84, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 87, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', + 93, + ], + [ + 'Call to function in_array() with null and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 96, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array will always evaluate to false.', + 125, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and false will always evaluate to false.', + 128, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and true will always evaluate to false.', + 131, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 143, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and false will always evaluate to false.', + 146, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and true will always evaluate to false.', + 149, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::B and non-empty-array will always evaluate to false.', + 159, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::A and non-empty-array will always evaluate to true.', + 162, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and false will always evaluate to true.', + 165, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and true will always evaluate to true.', + 168, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and false will always evaluate to false.', + 171, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and true will always evaluate to false.', + 174, + 'BUG', + //$tipText, + ], + ]; + } + + #[RequiresPhp('>= 8.1.0')] + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void + { + $issues = self::getLooseComparisonAgainsEnumsIssues(); + $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug14429(): void + { + $this->analyse([__DIR__ . '/data/bug-14429.php'], []); + } + + public function testBug11014(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11014.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 6011cadf288..dcbc8672de2 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -16,8 +16,6 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; - private bool $polluteScopeWithAlwaysIterableForeach = true; - protected function getRule(): Rule { // @phpstan-ignore argument.type @@ -32,11 +30,6 @@ protected function getRule(): Rule ]); } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - public function testBug8277(): void { $this->analyse([__DIR__ . '/data/bug-8277.php'], []); @@ -288,12 +281,6 @@ public function testBug3387(): void $this->analyse([__DIR__ . '/data/bug-3387.php'], []); } - public function testBug13984(): void - { - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/data/bug-13984.php'], []); - } - public function testBug13874(): void { $this->analyse([__DIR__ . '/data/bug-13874.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..035af5cacba --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php @@ -0,0 +1,45 @@ + + */ +class NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new NumberComparisonOperatorsConstantConditionRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + true, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testBug13984(): void + { + $this->analyse([__DIR__ . '/data/bug-13984.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index c59730e3b94..debc27ee4e3 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -21,8 +21,6 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; - private bool $polluteScopeWithAlwaysIterableForeach = true; - protected function getRule(): Rule { // @phpstan-ignore argument.type @@ -44,11 +42,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - public function testStrictComparison(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; @@ -1235,12 +1228,6 @@ public function testBug13421(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []); } - public function testBug14446(): void - { - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); - } - public function testBug13444(): void { $this->analyse([__DIR__ . '/data/bug-13444.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..10d86a63661 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php @@ -0,0 +1,48 @@ + + */ +class StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + true, + false, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testBug14446(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 4e04595e917..e2085ce0830 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -77,9 +77,6 @@ public static function dataBugWithoutGitHubIssue1(): array [ true, ], - [ - false, - ], ]; } @@ -224,23 +221,6 @@ public function testBug11179(): void $this->analyse([__DIR__ . '/data/bug-11179.php'], []); } - public function testBug11992(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-11992.php'], []); - } - - public function testBug7531(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-7531.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 22, - ], - ]); - } - public function testMultipleUnreachable(): void { $this->treatPhpDocTypesAsCertain = true; @@ -252,127 +232,6 @@ public function testMultipleUnreachable(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug11909(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-11909.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 10, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232a(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 10, - ], - [ - 'Unreachable statement - code above always terminates.', - 17, - ], - [ - 'Unreachable statement - code above always terminates.', - 23, - ], - [ - 'Unreachable statement - code above always terminates.', - 32, - ], - [ - 'Unreachable statement - code above always terminates.', - 38, - ], - [ - 'Unreachable statement - code above always terminates.', - 44, - ], - [ - 'Unreachable statement - code above always terminates.', - 52, - ], - [ - 'Unreachable statement - code above always terminates.', - 61, - ], - [ - 'Unreachable statement - code above always terminates.', - 70, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232b(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 19, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232c(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 12, - ], - [ - 'Unreachable statement - code above always terminates.', - 20, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232d(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 11, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13288(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13288.php'], []); - } - - public function testBug13311(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13311.php'], []); - } - - public function testBug13307(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13307.php'], []); - } - - public function testBug13331(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13331.php'], []); - } - #[RequiresPhp('>= 8.2.0')] public function testBug14328(): void { @@ -434,12 +293,6 @@ public function testBug11731(): void ]); } - public function testBug15169(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169.php'], []); - } - public function testBug15169TreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; @@ -471,12 +324,6 @@ public function testBug15169TreatPhpDocTypesAsCertain(): void ]); } - public function testBug15169Analogous(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169b.php'], []); - } - public function testBug15169AnalogousTreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; @@ -516,13 +363,6 @@ public function testBug15169AnalogousTreatPhpDocTypesAsCertain(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug15169FirstClassCallables(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169c.php'], []); - } - #[RequiresPhp('>= 8.1.0')] public function testBug15169FirstClassCallablesTreatPhpDocTypesAsCertain(): void { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..ce32dee53a2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,212 @@ + + */ +class UnreachableStatementRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new UnreachableStatementRule(); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public static function dataBugWithoutGitHubIssue1(): array + { + return [ + [ + false, + ], + ]; + } + + #[DataProvider('dataBugWithoutGitHubIssue1')] + public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-without-issue-1.php'], []); + } + + public function testBug11992(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11992.php'], []); + } + + public function testBug7531(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7531.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 22, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug11909(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11909.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232a(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + [ + 'Unreachable statement - code above always terminates.', + 17, + ], + [ + 'Unreachable statement - code above always terminates.', + 23, + ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], + [ + 'Unreachable statement - code above always terminates.', + 38, + ], + [ + 'Unreachable statement - code above always terminates.', + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 52, + ], + [ + 'Unreachable statement - code above always terminates.', + 61, + ], + [ + 'Unreachable statement - code above always terminates.', + 70, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232b(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232c(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 12, + ], + [ + 'Unreachable statement - code above always terminates.', + 20, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232d(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 11, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13288(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13288.php'], []); + } + + public function testBug13311(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13311.php'], []); + } + + public function testBug13307(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13307.php'], []); + } + + public function testBug13331(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13331.php'], []); + } + + public function testBug15169(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169.php'], []); + } + + public function testBug15169Analogous(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169b.php'], []); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug15169FirstClassCallables(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169c.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php index 0df8d42932a..439b1afb2dc 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -13,13 +13,6 @@ class UnusedVariableRuleTest extends RuleTestCase { - private bool $polluteScopeWithAlwaysIterableForeach = true; - - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - protected function getRule(): Rule { return new UnusedVariableRule(self::getContainer()->getByType(ExprPrinter::class)); @@ -264,12 +257,6 @@ public function testResultFlow(): void ]); } - 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'], []); diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..044e38339ce --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php @@ -0,0 +1,36 @@ + + */ +class UnusedVariableRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UnusedVariableRule(self::getContainer()->getByType(ExprPrinter::class)); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testForeachWithoutPollution(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-foreach-pollution.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 7d296db1152..6b028fdade8 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -4,11 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use function in_array; +use function array_merge; /** * @extends RuleTestCase @@ -28,34 +27,14 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array + public static function getAdditionalConfigFiles(): array { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - private function isEntityId(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssignPhpDoc\\Entity' - && in_array($propertyName, ['id'], true); - } - - }, - ]; + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/missing-readonly-by-phpdoc-property-assign-rule.neon', + ], + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 6e9a1b98c9a..1e4af3d4f4a 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -4,12 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use function in_array; -use function strpos; +use function array_merge; /** * @extends RuleTestCase @@ -33,53 +31,14 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array + public static function getAdditionalConfigFiles(): array { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - private function isEntityId(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssign\\Entity' - && in_array($propertyName, ['id'], true); - } - - }, - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isInitialized($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->isPublic() && - strpos($property->getDocComment() ?? '', '@init') !== false; - } - - }, - ]; + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/missing-readonly-property-assign-rule.neon', + ], + ); } #[RequiresPhp('>= 8.1.0')] diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index b89b1e75959..c0f6a1a9262 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -4,12 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; use function array_merge; -use function strpos; /** * @extends RuleTestCase @@ -33,51 +31,6 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array - { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; - } - - }, - - // bug-9619 - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isInitialized($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->isPublic() && - strpos($property->getDocComment() ?? '', '@inject') !== false; - } - - }, - ]; - } - public static function getAdditionalConfigFiles(): array { return array_merge( diff --git a/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php new file mode 100644 index 00000000000..116e81159e8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php @@ -0,0 +1,32 @@ +isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssign\\Entity' + && in_array($propertyName, ['id'], true); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php new file mode 100644 index 00000000000..30bc41b0ca9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php @@ -0,0 +1,27 @@ +isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@init') !== false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php new file mode 100644 index 00000000000..3271beb0a2d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php @@ -0,0 +1,25 @@ +getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php new file mode 100644 index 00000000000..2d05c78539e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php @@ -0,0 +1,27 @@ +isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@inject') !== false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php new file mode 100644 index 00000000000..8f5bb0d1324 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php @@ -0,0 +1,32 @@ +isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssignPhpDoc\\Entity' + && in_array($propertyName, ['id'], true); + } + +} diff --git a/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon b/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon new file mode 100644 index 00000000000..7e3c57da61e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\Properties\PhpDocEntityIdReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon b/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon new file mode 100644 index 00000000000..a41724bd7d7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon @@ -0,0 +1,9 @@ +services: + - + class: PHPStan\Rules\Properties\EntityIdReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension + - + class: PHPStan\Rules\Properties\InitAnnotationReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon index 6362843ec84..552e3fdda76 100644 --- a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon +++ b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon @@ -3,3 +3,11 @@ services: class: PHPStan\Rules\Properties\TestInitializedProperty tags: - phpstan.additionalConstructorsExtension + - + class: PHPStan\Rules\Properties\InitedPropertyReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension + - + class: PHPStan\Rules\Properties\InjectAnnotationReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 97095f70866..c01fabdb376 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -17,10 +17,6 @@ class DefinedVariableRuleTest extends RuleTestCase private bool $checkMaybeUndefinedVariables; - private bool $polluteScopeWithLoopInitialAssignments; - - private bool $polluteScopeWithAlwaysIterableForeach; - protected function getRule(): Rule { return new DefinedVariableRule( @@ -29,313 +25,9 @@ protected function getRule(): Rule ); } - protected function shouldPolluteScopeWithLoopInitialAssignments(): bool - { - return $this->polluteScopeWithLoopInitialAssignments; - } - - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - - public function testDefinedVariables(): void - { - require_once __DIR__ . '/data/defined-variables-definition.php'; - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables.php'], [ - [ - 'Undefined variable: $definedLater', - 5, - ], - [ - 'Variable $definedInIfOnly might not be defined.', - 10, - ], - [ - 'Variable $definedInCases might not be defined.', - 21, - ], - [ - 'Undefined variable: $fooParameterBeforeDeclaration', - 29, - ], - [ - 'Undefined variable: $parseStrParameter', - 34, - ], - [ - 'Undefined variable: $parseStrParameter', - 36, - ], - [ - 'Undefined variable: $foo', - 39, - ], - [ - 'Undefined variable: $willBeUnset', - 44, - ], - [ - 'Undefined variable: $mustAlreadyExistWhenDividing', - 50, - ], - [ - 'Undefined variable: $arrayDoesNotExist', - 57, - ], - [ - 'Undefined variable: $undefinedVariable', - 59, - ], - [ - 'Undefined variable: $this', - 96, - ], - [ - 'Undefined variable: $this', - 99, - ], - [ - 'Undefined variable: $variableInEmpty', - 145, - ], - [ - 'Undefined variable: $negatedVariableInEmpty', - 152, - ], - [ - 'Undefined variable: $variableInEmpty', - 155, - ], - [ - 'Undefined variable: $negatedVariableInEmpty', - 156, - ], - [ - 'Undefined variable: $variableInIsset', - 159, - ], - [ - 'Undefined variable: $anotherVariableInIsset', - 159, - ], - [ - 'Undefined variable: $variableInIsset', - 161, - ], - [ - 'Undefined variable: $anotherVariableInIsset', - 161, - ], - [ - 'Undefined variable: $http_response_header', - 185, - ], - [ - 'Undefined variable: $http_response_header', - 191, - ], - [ - 'Undefined variable: $assignedInKey', - 203, - ], - [ - 'Undefined variable: $assignedInKey', - 204, - ], - [ - 'Variable $forI might not be defined.', - 250, - ], - [ - 'Variable $forJ might not be defined.', - 251, - ], - [ - 'Variable $variableAvailableInAllCatches might not be defined.', - 266, - ], - [ - 'Variable $variableDefinedOnlyInOneCatch might not be defined.', - 267, - ], - [ - 'Undefined variable: $variableInBitwiseAndAssign', - 277, - ], - [ - 'Variable $mightBeUndefinedInDoWhile might not be defined.', - 282, - ], - [ - 'Undefined variable: $variableInSecondCase', - 290, - ], - [ - 'Variable $variableInSecondCase might not be defined.', - 293, - ], - [ - 'Undefined variable: $variableAssignedInSecondCase', - 300, - ], - [ - 'Variable $variableInFallthroughCase might not be defined.', - 302, - ], - [ - 'Variable $variableFromDefaultFirst might not be defined.', - 312, - ], - [ - 'Undefined variable: $undefinedVariableInForeach', - 315, - ], - [ - 'Variable $anotherForLoopVariable might not be defined.', - 328, - ], - [ - 'Variable $maybeDefinedInTernary might not be defined.', - 351, - ], - [ - 'Variable $anotherMaybeDefinedInTernary might not be defined.', - 354, - ], - [ - 'Variable $whileVariableUsedAndThenDefined might not be defined.', - 356, - ], - [ - 'Variable $forVariableUsedAndThenDefined might not be defined.', - 360, - ], - [ - 'Variable $unknownVariablePassedToReset might not be defined.', - 368, - ], - [ - 'Variable $unknownVariablePassedToReset might not be defined.', - 369, - ], - [ - 'Variable $variableInAssign might not be defined.', - 384, - ], - [ - 'Variable $undefinedArrayIndex might not be defined.', - 409, - ], - [ - 'Variable $anotherUndefinedArrayIndex might not be defined.', - 409, - ], - [ - 'Variable $str might not be defined.', - 423, - ], - [ - 'Variable $str might not be defined.', - 428, - ], - ]); - } - - public function testDefinedVariablesInClosures(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ - [ - 'Undefined variable: $this', - 14, - ], - ]); - } - - public function testDefinedVariablesInShortArrayDestructuringSyntax(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ - [ - 'Undefined variable: $f', - 11, - ], - [ - 'Undefined variable: $f', - 14, - ], - [ - 'Undefined variable: $var3', - 32, - ], - ]); - } - - public function testCliArgumentsVariablesNotRegistered(): void - { - $this->cliArgumentsVariablesRegistered = false; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ - [ - 'Variable $argc might not be defined.', - 3, - ], - [ - 'Undefined variable: $argc', - 5, - ], - ]); - } - - public function testCliArgumentsVariablesRegistered(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ - [ - 'Undefined variable: $argc', - 5, - ], - ]); - } - public static function dataLoopInitialAssignments(): array { return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - [ - 'Variable $i might not be defined.', - 7, - ], - [ - 'Variable $whileVar might not be defined.', - 13, - ], - ], - ], [ true, false, @@ -358,632 +50,53 @@ public function testLoopInitialAssignments( bool $checkMaybeUndefinedVariables, array $expectedErrors, ): void - { - $this->cliArgumentsVariablesRegistered = false; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); - } - - public function testDefineVariablesInClass(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); - } - - public function testDeadBranches(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/dead-branches.php'], [ - [ - 'Undefined variable: $test', - 21, - ], - [ - 'Undefined variable: $test', - 33, - ], - [ - 'Undefined variable: $test', - 55, - ], - [ - 'Undefined variable: $test', - 66, - ], - [ - 'Undefined variable: $test', - 94, - ], - ]); - } - - public function testForeach(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/foreach.php'], [ - [ - 'Variable $val might not be defined.', - 9, - ], - [ - 'Variable $test might not be defined.', - 10, - ], - [ - 'Undefined variable: $val', - 46, - ], - [ - 'Undefined variable: $test', - 47, - ], - [ - 'Variable $val might not be defined.', - 62, - ], - [ - 'Variable $test might not be defined.', - 63, - ], - [ - 'Undefined variable: $val', - 200, - ], - [ - 'Undefined variable: $test', - 201, - ], - [ - 'Undefined variable: $val', - 216, - ], - [ - 'Undefined variable: $test', - 217, - ], - [ - 'Variable $val might not be defined.', - 246, - ], - [ - 'Variable $test might not be defined.', - 247, - ], - ]); - } - - public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array - { - return [ - [ - true, - [ - [ - 'Undefined variable: $key', - 8, - ], - [ - 'Undefined variable: $val', - 9, - ], - [ - 'Undefined variable: $test', - 10, - ], - [ - 'Variable $test might not be defined.', - 34, - ], - [ - 'Variable $key might not be defined.', - 47, - ], - [ - 'Variable $test might not be defined.', - 48, - ], - [ - 'Variable $key might not be defined.', - 61, - ], - [ - 'Variable $test might not be defined.', - 62, - ], - ], - ], - [ - false, - [ - [ - 'Undefined variable: $key', - 8, - ], - [ - 'Undefined variable: $val', - 9, - ], - [ - 'Undefined variable: $test', - 10, - ], - [ - 'Variable $key might not be defined.', - 19, - ], - [ - 'Variable $val might not be defined.', - 20, - ], - [ - 'Variable $test might not be defined.', - 21, - ], - [ - 'Variable $key might not be defined.', - 32, - ], - [ - 'Variable $val might not be defined.', - 33, - ], - [ - 'Variable $test might not be defined.', - 34, - ], - [ - 'Variable $key might not be defined.', - 47, - ], - [ - 'Variable $test might not be defined.', - 48, - ], - [ - 'Variable $key might not be defined.', - 61, - ], - [ - 'Variable $test might not be defined.', - 62, - ], - [ - 'Variable $key might not be defined.', - 75, - ], - [ - 'Variable $test might not be defined.', - 76, - ], - ], - ], - ]; - } - - /** - * @param list $errors - */ - #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] - public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; - $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); - } - - public function testBooleanOperatorsTruthyFalsey(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ - [ - 'Variable $matches might not be defined.', - 9, - ], - [ - 'Variable $matches might not be defined.', - 15, - ], - ]); - } - - public function testArrowFunctions(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ - [ - 'Undefined variable: $a', - 10, - ], - [ - 'Undefined variable: $this', - 19, - ], - ]); - } - - public function testCoalesceAssign(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ - [ - 'Undefined variable: $b', - 16, - ], - ]); - } - - public function testBug2748(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-2748.php'], [ - [ - 'Undefined variable: $foo', - 10, - ], - [ - 'Undefined variable: $foo', - 15, - ], - ]); - } - - public function testGlobalVariables(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/global-variables.php'], []); - } - - public function testRootScopeMaybeDefined(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = false; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); - } - - public function testRootScopeMaybeDefinedCheck(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ - [ - 'Variable $maybe might not be defined.', - 3, - ], - [ - 'Variable $this might not be defined.', - 5, - ], - ]); - } - - public function testFormerThisVariableRule(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/this.php'], [ - [ - 'Undefined variable: $this', - 16, - ], - [ - 'Undefined variable: $this', - 20, - ], - [ - 'Undefined variable: $this', - 26, - ], - [ - 'Undefined variable: $this', - 38, - ], - ]); - } - - public function testClosureUse(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ - [ - 'Variable $bar might not be defined.', - 5, - ], - [ - 'Variable $wrongErrorHandler might not be defined.', - 22, - ], - [ - 'Variable $onlyInIf might not be defined.', - 23, - ], - [ - 'Variable $forI might not be defined.', - 24, - ], - [ - 'Variable $forJ might not be defined.', - 25, - ], - [ - 'Variable $anotherVariableFromForCond might not be defined.', - 26, - ], - ]); - } - - public function testNullsafeIsset(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug7291(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-7291.php'], [ - [ - 'Variable $a might not be defined.', - 23, - ], - [ - 'Variable $b might not be defined.', - 32, - ], - [ - 'Variable $c might not be defined.', - 41, - ], - [ - 'Variable $d might not be defined.', - 50, - ], - ]); - } - - public function testBug1306(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1306.php'], []); - } - - public function testBug3515(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3515.php'], [ - [ - 'Undefined variable: $anArray', - 19, - ], - [ - 'Undefined variable: $anArray', - 20, - ], - ]); - } - - public function testBug4412(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4412.php'], [ - [ - 'Undefined variable: $a', - 17, - ], - ]); - } - - public function testBug3283(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3283.php'], []); - } - - public function testFirstClassCallables(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ - [ - 'Undefined variable: $foo', - 10, - ], - [ - 'Undefined variable: $foo', - 11, - ], - [ - 'Undefined variable: $foo', - 29, - ], - [ - 'Undefined variable: $foo', - 30, - ], - [ - 'Undefined variable: $foo', - 48, - ], - [ - 'Undefined variable: $foo', - 49, - ], - ]); - } - - public function testBug6112(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-6112.php'], []); - } - - public function testBug3601(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3601.php'], []); - } - - public function testBug1016(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1016.php'], []); - } - - public function testBug1016b(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug8142(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8142.php'], []); - } - - public function testBug5401(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; + $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); } - public function testBug8212(): void + public function testBug3601(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8212.php'], []); + $this->analyse([__DIR__ . '/data/bug-3601.php'], []); } - public function testBug4173(): void + public function testBug1016(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4173.php'], []); + $this->analyse([__DIR__ . '/data/bug-1016.php'], []); } - public function testBug5805(): void + public function testBug1016b(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); } - public function testBug8467c(): void + #[RequiresPhp('>= 8.0.0')] + public function testBug8142(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ - [ - 'Variable $v might not be defined.', - 16, - ], - [ - 'Variable $v might not be defined.', - 18, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-8142.php'], []); } - public function testBug393(): void + public function testBug8212(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-393.php'], []); + $this->analyse([__DIR__ . '/data/bug-8212.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug9474(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9474.php'], []); } @@ -991,36 +104,28 @@ public function testBug9474(): void public function testEnum(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-enum.php'], []); } public function testBug5326(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5326.php'], []); } public function testBug5266(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5266.php'], []); } public function testIsStringNarrowsCertainty(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/isstring-certainty.php'], [ [ 'Variable $a might not be defined.', @@ -1036,9 +141,7 @@ public function testIsStringNarrowsCertainty(): void public function testBug12364(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-12364.php'], [ [ 'Variable $z might not be defined.', @@ -1054,9 +157,7 @@ public function testBug12364(): void public function testDiscussion10252(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); } @@ -1064,9 +165,7 @@ public function testDiscussion10252(): void public function testBug10418(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10418.php'], []); } @@ -1074,9 +173,7 @@ public function testBug10418(): void public function testPassByReferenceIntoNotNullable(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ [ 'Undefined variable: $three', @@ -1088,18 +185,14 @@ public function testPassByReferenceIntoNotNullable(): void public function testBug10228(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10228.php'], []); } public function testBug9426(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9426.php'], []); } @@ -1107,9 +200,7 @@ public function testBug9426(): void public function testPropertyHooks(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/property-hooks.php'], [ [ 'Undefined variable: $val', @@ -1133,9 +224,7 @@ public function testPropertyHooks(): void public function testDynamicAccess(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/dynamic-access.php'], [ [ 'Undefined variable: $bar', @@ -1187,9 +276,7 @@ public function testDynamicAccess(): void public function testBug8719(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-8719.php'], []); } @@ -1198,9 +285,7 @@ public function testBug8719(): void public function testBug13353(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13353.php'], []); } @@ -1208,9 +293,7 @@ public function testBug13353(): void public function testBug13694(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13694.php'], []); } @@ -1219,9 +302,7 @@ public function testBug13694(): void public function testBug5191(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5191.php'], [ [ @@ -1235,9 +316,7 @@ public function testBug5191(): void public function testBug10909(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10909.php'], []); } @@ -1246,9 +325,7 @@ public function testBug10909(): void public function testBug13981(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13981.php'], [ [ @@ -1266,9 +343,7 @@ public function testBug13981(): void public function testBug7705(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-7705.php'], []); } @@ -1277,40 +352,16 @@ public function testBug7705(): void public function testBug12944(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-12944.php'], []); } - public function testBug9023(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-9023.php'], []); - } - - public function testBug11984(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-11984.php'], []); - } - #[DataProvider('dataBug11545')] public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $errors = []; if (!$polluteScopeWithLoopInitialAssignments) { @@ -1326,294 +377,22 @@ public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void /** @return iterable */ public static function dataBug11545(): iterable { - yield [false]; yield [true]; } - public function testBug10245(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-10245.php'], []); - } - - public function testBug5919(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-5919.php'], []); - } - - public function testBug8430(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-8430.php'], []); - } - - public function testBug8430b(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-8430b.php'], []); - } - - public function testBug5477(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-5477.php'], []); - } - - public function testBug10657(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-10657.php'], []); - } - - public function testBug6830(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-6830.php'], []); - } - - public function testBug14019(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []); - } - - public function testBug9349(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-9349.php'], [ - [ - 'Variable $sql might not be defined.', - 19, - ], - [ - 'Variable $sql might not be defined.', - 78, - ], - ]); - } - - public function testBug14318(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14318.php'], []); - } - - public function testBug14323(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14323.php'], [ - [ - 'Variable $command might not be defined.', - 24, - ], - [ - 'Variable $command might not be defined.', - 50, - ], - [ - 'Variable $command might not be defined.', - 119, - ], - ]); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug14274(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14274.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug12373(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-12373.php'], []); - } - public function testBug13920(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13920.php'], []); } - public function testBug12992(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-12992.php'], []); - } - - public function testBug14227(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-14227.php'], []); - } - - public function testBug14117(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14117.php'], [ - [ - 'Variable $value might not be defined.', - 49, - ], - [ - 'Undefined variable: $value', - 65, - ], - [ - 'Undefined variable: $value', - 81, - ], - ]); - } - - public function testBug12597(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-12597.php'], []); - } - - public function testBug11218(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-11218.php'], []); - } - - public function testBug4352(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4352.php'], []); - } - - public function testBug6688(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-6688.php'], [ - [ - 'Variable $a might not be defined.', - 5, - ], - ]); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug10729(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-10729.php'], [ - [ - 'Variable $format might not be defined.', - 12, - ], - [ - 'Undefined variable: $format', - 25, - ], - [ - 'Variable $format might not be defined.', - 31, - ], - [ - 'Variable $value might not be defined.', - 32, - ], - [ - 'Variable $format might not be defined.', - 38, - ], - ]); - } - #[RequiresPhp('>= 8.0.0')] public function testBug6833(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6833.php'], [ [ 'Variable $file might not be defined.', @@ -1666,114 +445,14 @@ public function testBug6833(): void public function testBug9392(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9392.php'], []); } - public function testBug10090(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10090.php'], [ - [ - 'Undefined variable: $shortcut_id', - 18, - ], - [ - 'Undefined variable: $shortcut_id', - 19, - ], - [ - 'Variable $x might not be defined.', - 35, - ], - [ - 'Variable $x might not be defined.', - 36, - ], - [ - 'Variable $x might not be defined.', - 80, - ], - [ - 'Variable $x might not be defined.', - 81, - ], - [ - 'Undefined variable: $x', - 96, - ], - ]); - } - - public function testBug2032(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-2032.php'], [ - [ - 'Undefined variable: $undefined', - 6, - ], - [ - 'Undefined variable: $undefined', - 9, - ], - [ - 'Undefined variable: $undefined', - 15, - ], - ]); - } - - public function testBug13833(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-13833.php'], []); - } - - public function testBug9685(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-9685.php'], []); - } - - public function testBug7706(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-7706.php'], []); - } - - public function testBug8360(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8360.php'], []); - } - public function testBug14418(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-14418.php'], []); } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php new file mode 100644 index 00000000000..205bb1c0cfc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php @@ -0,0 +1,129 @@ + + */ +class DefinedVariableRuleWithoutLoopAndForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DefinedVariableRule( + true, + true, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-loop-pollution.neon', + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + { + return [ + [ + false, + [ + [ + 'Undefined variable: $key', + 8, + ], + [ + 'Undefined variable: $val', + 9, + ], + [ + 'Undefined variable: $test', + 10, + ], + [ + 'Variable $key might not be defined.', + 19, + ], + [ + 'Variable $val might not be defined.', + 20, + ], + [ + 'Variable $test might not be defined.', + 21, + ], + [ + 'Variable $key might not be defined.', + 32, + ], + [ + 'Variable $val might not be defined.', + 33, + ], + [ + 'Variable $test might not be defined.', + 34, + ], + [ + 'Variable $key might not be defined.', + 47, + ], + [ + 'Variable $test might not be defined.', + 48, + ], + [ + 'Variable $key might not be defined.', + 61, + ], + [ + 'Variable $test might not be defined.', + 62, + ], + [ + 'Variable $key might not be defined.', + 75, + ], + [ + 'Variable $test might not be defined.', + 76, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] + public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void + { + $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); + } + + public function testBug8467c(): void + { + $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ + [ + 'Variable $v might not be defined.', + 16, + ], + [ + 'Variable $v might not be defined.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php new file mode 100644 index 00000000000..a8c90ab2f9f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php @@ -0,0 +1,1124 @@ + + */ +class DefinedVariableRuleWithoutLoopPollutionTest extends RuleTestCase +{ + + private bool $cliArgumentsVariablesRegistered; + + private bool $checkMaybeUndefinedVariables; + + protected function getRule(): Rule + { + return new DefinedVariableRule( + $this->cliArgumentsVariablesRegistered, + $this->checkMaybeUndefinedVariables, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-loop-pollution.neon', + ], + ); + } + + public function testDefinedVariables(): void + { + require_once __DIR__ . '/data/defined-variables-definition.php'; + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables.php'], [ + [ + 'Undefined variable: $definedLater', + 5, + ], + [ + 'Variable $definedInIfOnly might not be defined.', + 10, + ], + [ + 'Variable $definedInCases might not be defined.', + 21, + ], + [ + 'Undefined variable: $fooParameterBeforeDeclaration', + 29, + ], + [ + 'Undefined variable: $parseStrParameter', + 34, + ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], + [ + 'Undefined variable: $foo', + 39, + ], + [ + 'Undefined variable: $willBeUnset', + 44, + ], + [ + 'Undefined variable: $mustAlreadyExistWhenDividing', + 50, + ], + [ + 'Undefined variable: $arrayDoesNotExist', + 57, + ], + [ + 'Undefined variable: $undefinedVariable', + 59, + ], + [ + 'Undefined variable: $this', + 96, + ], + [ + 'Undefined variable: $this', + 99, + ], + [ + 'Undefined variable: $variableInEmpty', + 145, + ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 152, + ], + [ + 'Undefined variable: $variableInEmpty', + 155, + ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 156, + ], + [ + 'Undefined variable: $variableInIsset', + 159, + ], + [ + 'Undefined variable: $anotherVariableInIsset', + 159, + ], + [ + 'Undefined variable: $variableInIsset', + 161, + ], + [ + 'Undefined variable: $anotherVariableInIsset', + 161, + ], + [ + 'Undefined variable: $http_response_header', + 185, + ], + [ + 'Undefined variable: $http_response_header', + 191, + ], + [ + 'Undefined variable: $assignedInKey', + 203, + ], + [ + 'Undefined variable: $assignedInKey', + 204, + ], + [ + 'Variable $forI might not be defined.', + 250, + ], + [ + 'Variable $forJ might not be defined.', + 251, + ], + [ + 'Variable $variableAvailableInAllCatches might not be defined.', + 266, + ], + [ + 'Variable $variableDefinedOnlyInOneCatch might not be defined.', + 267, + ], + [ + 'Undefined variable: $variableInBitwiseAndAssign', + 277, + ], + [ + 'Variable $mightBeUndefinedInDoWhile might not be defined.', + 282, + ], + [ + 'Undefined variable: $variableInSecondCase', + 290, + ], + [ + 'Variable $variableInSecondCase might not be defined.', + 293, + ], + [ + 'Undefined variable: $variableAssignedInSecondCase', + 300, + ], + [ + 'Variable $variableInFallthroughCase might not be defined.', + 302, + ], + [ + 'Variable $variableFromDefaultFirst might not be defined.', + 312, + ], + [ + 'Undefined variable: $undefinedVariableInForeach', + 315, + ], + [ + 'Variable $anotherForLoopVariable might not be defined.', + 328, + ], + [ + 'Variable $maybeDefinedInTernary might not be defined.', + 351, + ], + [ + 'Variable $anotherMaybeDefinedInTernary might not be defined.', + 354, + ], + [ + 'Variable $whileVariableUsedAndThenDefined might not be defined.', + 356, + ], + [ + 'Variable $forVariableUsedAndThenDefined might not be defined.', + 360, + ], + [ + 'Variable $unknownVariablePassedToReset might not be defined.', + 368, + ], + [ + 'Variable $unknownVariablePassedToReset might not be defined.', + 369, + ], + [ + 'Variable $variableInAssign might not be defined.', + 384, + ], + [ + 'Variable $undefinedArrayIndex might not be defined.', + 409, + ], + [ + 'Variable $anotherUndefinedArrayIndex might not be defined.', + 409, + ], + [ + 'Variable $str might not be defined.', + 423, + ], + [ + 'Variable $str might not be defined.', + 428, + ], + ]); + } + + public function testDefinedVariablesInClosures(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ + [ + 'Undefined variable: $this', + 14, + ], + ]); + } + + public function testDefinedVariablesInShortArrayDestructuringSyntax(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ + [ + 'Undefined variable: $f', + 11, + ], + [ + 'Undefined variable: $f', + 14, + ], + [ + 'Undefined variable: $var3', + 32, + ], + ]); + } + + public function testCliArgumentsVariablesNotRegistered(): void + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ + [ + 'Variable $argc might not be defined.', + 3, + ], + [ + 'Undefined variable: $argc', + 5, + ], + ]); + } + + public function testCliArgumentsVariablesRegistered(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ + [ + 'Undefined variable: $argc', + 5, + ], + ]); + } + + public static function dataLoopInitialAssignments(): array + { + return [ + [ + false, + false, + [], + ], + [ + false, + true, + [ + [ + 'Variable $i might not be defined.', + 7, + ], + [ + 'Variable $whileVar might not be defined.', + 13, + ], + ], + ], + ]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataLoopInitialAssignments')] + public function testLoopInitialAssignments( + bool $polluteScopeWithLoopInitialAssignments, + bool $checkMaybeUndefinedVariables, + array $expectedErrors, + ): void + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; + $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); + } + + public function testDefineVariablesInClass(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); + } + + public function testDeadBranches(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/dead-branches.php'], [ + [ + 'Undefined variable: $test', + 21, + ], + [ + 'Undefined variable: $test', + 33, + ], + [ + 'Undefined variable: $test', + 55, + ], + [ + 'Undefined variable: $test', + 66, + ], + [ + 'Undefined variable: $test', + 94, + ], + ]); + } + + public function testForeach(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/foreach.php'], [ + [ + 'Variable $val might not be defined.', + 9, + ], + [ + 'Variable $test might not be defined.', + 10, + ], + [ + 'Undefined variable: $val', + 46, + ], + [ + 'Undefined variable: $test', + 47, + ], + [ + 'Variable $val might not be defined.', + 62, + ], + [ + 'Variable $test might not be defined.', + 63, + ], + [ + 'Undefined variable: $val', + 200, + ], + [ + 'Undefined variable: $test', + 201, + ], + [ + 'Undefined variable: $val', + 216, + ], + [ + 'Undefined variable: $test', + 217, + ], + [ + 'Variable $val might not be defined.', + 246, + ], + [ + 'Variable $test might not be defined.', + 247, + ], + ]); + } + + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + { + return [ + [ + true, + [ + [ + 'Undefined variable: $key', + 8, + ], + [ + 'Undefined variable: $val', + 9, + ], + [ + 'Undefined variable: $test', + 10, + ], + [ + 'Variable $test might not be defined.', + 34, + ], + [ + 'Variable $key might not be defined.', + 47, + ], + [ + 'Variable $test might not be defined.', + 48, + ], + [ + 'Variable $key might not be defined.', + 61, + ], + [ + 'Variable $test might not be defined.', + 62, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] + public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); + } + + public function testBooleanOperatorsTruthyFalsey(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ + [ + 'Variable $matches might not be defined.', + 9, + ], + [ + 'Variable $matches might not be defined.', + 15, + ], + ]); + } + + public function testArrowFunctions(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ + [ + 'Undefined variable: $a', + 10, + ], + [ + 'Undefined variable: $this', + 19, + ], + ]); + } + + public function testCoalesceAssign(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ + [ + 'Undefined variable: $b', + 16, + ], + ]); + } + + public function testBug2748(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-2748.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 15, + ], + ]); + } + + public function testGlobalVariables(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/global-variables.php'], []); + } + + public function testRootScopeMaybeDefined(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = false; + $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); + } + + public function testRootScopeMaybeDefinedCheck(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ + [ + 'Variable $maybe might not be defined.', + 3, + ], + [ + 'Variable $this might not be defined.', + 5, + ], + ]); + } + + public function testFormerThisVariableRule(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/this.php'], [ + [ + 'Undefined variable: $this', + 16, + ], + [ + 'Undefined variable: $this', + 20, + ], + [ + 'Undefined variable: $this', + 26, + ], + [ + 'Undefined variable: $this', + 38, + ], + ]); + } + + public function testClosureUse(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ + [ + 'Variable $bar might not be defined.', + 5, + ], + [ + 'Variable $wrongErrorHandler might not be defined.', + 22, + ], + [ + 'Variable $onlyInIf might not be defined.', + 23, + ], + [ + 'Variable $forI might not be defined.', + 24, + ], + [ + 'Variable $forJ might not be defined.', + 25, + ], + [ + 'Variable $anotherVariableFromForCond might not be defined.', + 26, + ], + ]); + } + + public function testNullsafeIsset(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug7291(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-7291.php'], [ + [ + 'Variable $a might not be defined.', + 23, + ], + [ + 'Variable $b might not be defined.', + 32, + ], + [ + 'Variable $c might not be defined.', + 41, + ], + [ + 'Variable $d might not be defined.', + 50, + ], + ]); + } + + public function testBug1306(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-1306.php'], []); + } + + public function testBug3515(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-3515.php'], [ + [ + 'Undefined variable: $anArray', + 19, + ], + [ + 'Undefined variable: $anArray', + 20, + ], + ]); + } + + public function testBug4412(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4412.php'], [ + [ + 'Undefined variable: $a', + 17, + ], + ]); + } + + public function testBug3283(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-3283.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 11, + ], + [ + 'Undefined variable: $foo', + 29, + ], + [ + 'Undefined variable: $foo', + 30, + ], + [ + 'Undefined variable: $foo', + 48, + ], + [ + 'Undefined variable: $foo', + 49, + ], + ]); + } + + public function testBug6112(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-6112.php'], []); + } + + public function testBug5401(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + } + + public function testBug4173(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4173.php'], []); + } + + public function testBug5805(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + } + + public function testBug393(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testBug9023(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-9023.php'], []); + } + + public function testBug11984(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-11984.php'], []); + } + + #[DataProvider('dataBug11545')] + public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $errors = []; + if (!$polluteScopeWithLoopInitialAssignments) { + $errors[] = [ + 'Variable $result might not be defined.', + 24, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-11545.php'], $errors); + } + + /** @return iterable */ + public static function dataBug11545(): iterable + { + yield [false]; + } + + public function testBug10245(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-10245.php'], []); + } + + public function testBug5919(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-5919.php'], []); + } + + public function testBug8430(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-8430.php'], []); + } + + public function testBug8430b(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-8430b.php'], []); + } + + public function testBug5477(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-5477.php'], []); + } + + public function testBug10657(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-10657.php'], []); + } + + public function testBug6830(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-6830.php'], []); + } + + public function testBug14019(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []); + } + + public function testBug9349(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-9349.php'], [ + [ + 'Variable $sql might not be defined.', + 19, + ], + [ + 'Variable $sql might not be defined.', + 78, + ], + ]); + } + + public function testBug14318(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14318.php'], []); + } + + public function testBug14323(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14323.php'], [ + [ + 'Variable $command might not be defined.', + 24, + ], + [ + 'Variable $command might not be defined.', + 50, + ], + [ + 'Variable $command might not be defined.', + 119, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug14274(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14274.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug12373(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-12373.php'], []); + } + + public function testBug12992(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-12992.php'], []); + } + + public function testBug14227(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-14227.php'], []); + } + + public function testBug14117(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14117.php'], [ + [ + 'Variable $value might not be defined.', + 49, + ], + [ + 'Undefined variable: $value', + 65, + ], + [ + 'Undefined variable: $value', + 81, + ], + ]); + } + + public function testBug12597(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-12597.php'], []); + } + + public function testBug11218(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-11218.php'], []); + } + + public function testBug4352(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4352.php'], []); + } + + public function testBug6688(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-6688.php'], [ + [ + 'Variable $a might not be defined.', + 5, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug10729(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-10729.php'], [ + [ + 'Variable $format might not be defined.', + 12, + ], + [ + 'Undefined variable: $format', + 25, + ], + [ + 'Variable $format might not be defined.', + 31, + ], + [ + 'Variable $value might not be defined.', + 32, + ], + [ + 'Variable $format might not be defined.', + 38, + ], + ]); + } + + public function testBug10090(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10090.php'], [ + [ + 'Undefined variable: $shortcut_id', + 18, + ], + [ + 'Undefined variable: $shortcut_id', + 19, + ], + [ + 'Variable $x might not be defined.', + 35, + ], + [ + 'Variable $x might not be defined.', + 36, + ], + [ + 'Variable $x might not be defined.', + 80, + ], + [ + 'Variable $x might not be defined.', + 81, + ], + [ + 'Undefined variable: $x', + 96, + ], + ]); + } + + public function testBug2032(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-2032.php'], [ + [ + 'Undefined variable: $undefined', + 6, + ], + [ + 'Undefined variable: $undefined', + 9, + ], + [ + 'Undefined variable: $undefined', + 15, + ], + ]); + } + + public function testBug13833(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-13833.php'], []); + } + + public function testBug9685(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-9685.php'], []); + } + + public function testBug7706(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-7706.php'], []); + } + + public function testBug8360(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-8360.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/uncertain-phpdoc-types.neon b/tests/PHPStan/Rules/uncertain-phpdoc-types.neon new file mode 100644 index 00000000000..c551b84f1f6 --- /dev/null +++ b/tests/PHPStan/Rules/uncertain-phpdoc-types.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Rules/without-foreach-pollution.neon b/tests/PHPStan/Rules/without-foreach-pollution.neon new file mode 100644 index 00000000000..3ee516d3be6 --- /dev/null +++ b/tests/PHPStan/Rules/without-foreach-pollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Rules/without-loop-pollution.neon b/tests/PHPStan/Rules/without-loop-pollution.neon new file mode 100644 index 00000000000..47811f500ec --- /dev/null +++ b/tests/PHPStan/Rules/without-loop-pollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithLoopInitialAssignments: false