diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2c3e0d16c3c..c805a89d7a3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -36,12 +36,6 @@ parameters: count: 2 path: src/Analyser/ExprHandler/BinaryOpHandler.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Analyser/ExprHandler/BooleanNotHandler.php - - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType @@ -69,7 +63,7 @@ parameters: - rawMessage: Casting to string something that's already string. identifier: cast.useless - count: 3 + count: 5 path: src/Analyser/MutatingScope.php - diff --git a/src/Analyser/ArgsResult.php b/src/Analyser/ArgsResult.php new file mode 100644 index 00000000000..217cccecf5d --- /dev/null +++ b/src/Analyser/ArgsResult.php @@ -0,0 +1,61 @@ +expressionResult->getScope(); + } + + public function hasYield(): bool + { + return $this->expressionResult->hasYield(); + } + + public function isAlwaysTerminating(): bool + { + return $this->expressionResult->isAlwaysTerminating(); + } + + /** + * @return InternalThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->expressionResult->getThrowPoints(); + } + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->expressionResult->getImpurePoints(); + } + + public function getResolvedParametersAcceptor(): ?ParametersAcceptor + { + return $this->resolvedParametersAcceptor; + } + +} diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 533d37c5f92..02afcab90e5 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -19,6 +19,8 @@ final class DirectInternalScopeFactory implements InternalScopeFactory { + private ExpressionResultStorageStack $expressionResultStorageStack; + /** * @param int|array{min: int, max: int}|null $configPhpVersion * @param callable(Node $node, Scope $scope): void|null $nodeCallback @@ -38,8 +40,10 @@ public function __construct( private $nodeCallback, private ConstantResolver $constantResolver, private bool $fiber = false, + ?ExpressionResultStorageStack $expressionResultStorageStack = null, ) { + $this->expressionResultStorageStack = $expressionResultStorageStack ?? new ExpressionResultStorageStack(); } public function create( @@ -77,6 +81,7 @@ public function create( $this->propertyReflectionFinder, $this->parser, $this->constantResolver, + $this->expressionResultStorageStack, $context, $this->phpVersion, $this->attributeReflectionFactory, @@ -102,25 +107,15 @@ public function create( public function toFiberFactory(): InternalScopeFactory { - return new self( - $this->container, - $this->reflectionProvider, - $this->initializerExprTypeResolver, - $this->expressionTypeResolverExtensionRegistryProvider, - $this->exprPrinter, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->phpVersion, - $this->attributeReflectionFactory, - $this->configPhpVersion, - $this->nodeCallback, - $this->constantResolver, - true, - ); + return $this->withFlavor(true); } public function toMutatingFactory(): InternalScopeFactory + { + return $this->withFlavor(false); + } + + private function withFlavor(bool $fiber): self { return new self( $this->container, @@ -136,7 +131,8 @@ public function toMutatingFactory(): InternalScopeFactory $this->configPhpVersion, $this->nodeCallback, $this->constantResolver, - false, + $fiber, + $this->expressionResultStorageStack, ); } diff --git a/src/Analyser/ExprHandler.php b/src/Analyser/ExprHandler.php index 98b8728ca40..7e93df1a6cf 100644 --- a/src/Analyser/ExprHandler.php +++ b/src/Analyser/ExprHandler.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; -use PHPStan\Type\Type; /** * @template T of Expr @@ -32,19 +31,4 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult; - /** - * @param T $expr - */ - public function resolveType(MutatingScope $scope, Expr $expr): Type; - - /** - * @param T $expr - */ - public function specifyTypes( - TypeSpecifier $typeSpecifier, - Scope $scope, - Expr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes; - } diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 1c389f9fb9a..9ee5588a8e8 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -11,21 +11,22 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_merge; /** @@ -35,59 +36,37 @@ final class ArrayDimFetchHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof ArrayDimFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - if ($expr->dim === null) { - return new NeverType(); - } - - $offsetAccessibleType = $scope->getType($expr->var); - if ( - !$offsetAccessibleType->isArray()->yes() - && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() - ) { - return NullsafeShortCircuitingHelper::getType( - $scope, - $expr->var, - $scope->getType( - new MethodCall( - $expr->var, - new Identifier('offsetGet'), - [ - new Arg($expr->dim), - ], - ), - ), - ); - } - - $offsetType = $scope->getType($expr->dim); - return NullsafeShortCircuitingHelper::getType( - $scope, - $expr->var, - $offsetAccessibleType->getOffsetValueType($offsetType), - ); + return $expr instanceof ArrayDimFetch; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; if ($expr->dim === null) { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $varResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: $varResult->containsNullsafe(), + // `$arr[]` only appears as an assignment target; reading it is a NeverType + typeCallback: static fn (): Type => new NeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } @@ -97,7 +76,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($dimResult->getImpurePoints(), $varResult->getImpurePoints()); $scope = $varResult->getScope(); - $varType = $scope->getType($expr->var); + $varType = $varResult->getTypeForScope($scope); + $offsetGetResult = null; if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( $stmt, @@ -107,22 +87,45 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex new NoopNodeCallback(), $context, )->getThrowPoints()); + // process the offsetGet here (storage is available, so the result is + // captured - not the storage - avoiding a reference cycle) so the + // typeCallback reads its result instead of Scope::getType(). Gated by + // the same maybe-ArrayAccess condition, so plain arrays never reach it. + $offsetGetResult = $nodeScopeResolver->processExprOnDemand( + new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)]), + $scope, + $storage, + ); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $dimResult->hasYield() || $varResult->hasYield(), isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } + containsNullsafe: $varResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::offset($varResult, $dimResult), + typeCallback: static function (MutatingScope $s) use ($varResult, $dimResult, $offsetGetResult): Type { + $offsetAccessibleType = $varResult->getTypeForScope($s); + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($offsetAccessibleType) + ? TypeCombinator::addNull($type) + : $type; - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + if ( + $offsetGetResult !== null + && !$offsetAccessibleType->isArray()->yes() + && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() + ) { + return $shortCircuit($offsetGetResult->getTypeForScope($s)); + } + + return $shortCircuit($offsetAccessibleType->getOffsetValueType($dimResult->getTypeForScope($s))); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index f64a168e848..a0550ffad21 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -10,23 +10,23 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\CallableType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; use function array_merge; use function count; +use function spl_object_id; /** * @implements ExprHandler @@ -37,6 +37,7 @@ final class ArrayHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -46,33 +47,11 @@ public function supports(Expr $expr): bool return $expr instanceof Array_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $type = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - - if ( - count($expr->items) === 2 - && isset($expr->items[0], $expr->items[1]) - && $type->isCallable()->maybe() - ) { - $isCallableCall = new FuncCall( - new FullyQualified('is_callable'), - [new Arg($expr)], - ); - if ( - $scope->hasExpressionType($isCallableCall)->yes() - && $scope->getType($isCallableCall)->isTrue()->yes() - ) { - $type = TypeCombinator::intersect($type, new CallableType()); - } - } - - return $type; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $itemNodes = []; + $itemResults = []; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -82,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage); if ($arrayItem->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep()); + $itemResults[spl_object_id($arrayItem->key)] = $keyResult; $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); @@ -90,6 +70,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep()); + $itemResults[spl_object_id($arrayItem->value)] = $valueResult; $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); @@ -98,18 +79,52 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $s) use ($expr, $itemResults): Type { + // each item type was captured at its own evaluation point in the + // sequence - resolving all items on any single scope (the old world) + // cannot handle items with side effects like [$b = 1, $b + 1, $b++] + $type = $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $inner) use ($itemResults, $s): Type { + $id = spl_object_id($inner); + if (array_key_exists($id, $itemResults)) { + return $s->nativeTypesPromoted + ? $itemResults[$id]->getNativeType() + : $itemResults[$id]->getType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + throw new ShouldNotHappenException(); + }); + + if ( + count($expr->items) === 2 + && isset($expr->items[0], $expr->items[1]) + && $type->isCallable()->maybe() + ) { + $isCallableCall = new FuncCall( + new FullyQualified('is_callable'), + [new Arg($expr)], + ); + if ( + $s->hasExpressionType($isCallableCall)->yes() + // read the narrowed type from expressionTypes directly (the + // synthetic is_callable() call was never processed as a child), + // mirroring ConstFetchHandler's narrowed-constant lookup + && $s->expressionTypes[$s->getNodeKey($isCallableCall)]->getType()->isTrue()->yes() + ) { + $type = TypeCombinator::intersect($type, new CallableType()); + } + } + + return $type; + }, + ); } } diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 0cdfddf675d..da01eb8e39f 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -7,27 +7,29 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ArrowFunctionHandler implements ExprHandler +final class ArrowFunctionHandler implements TypeResolvingExprHandler { public function __construct( private ClosureTypeResolver $closureTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,8 +43,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $result = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); - return new ExpressionResult( + return $this->expressionResultFactory->create( $result->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 540119391e7..726ea56810a 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -25,9 +25,11 @@ use PHPStan\Analyser\ConditionalExpressionHolder; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -39,9 +41,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; @@ -51,6 +51,7 @@ use PHPStan\Node\VariableAssignNode; use PHPStan\Node\VirtualNode; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -72,6 +73,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use TypeError; use function array_key_last; use function array_merge; @@ -95,6 +97,9 @@ public function __construct( private PhpVersion $phpVersion, private ExprPrinter $exprPrinter, private MatchHandler $matchHandler, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + private PropertyReflectionFinder $propertyReflectionFinder, ) { } @@ -104,191 +109,10 @@ public function supports(Expr $expr): bool return $expr instanceof Assign || $expr instanceof AssignRef; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->expr); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$expr instanceof Assign) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - if ($context->null()) { - $specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); - $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var)); - } else { - $specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); - } - - // infer $arr[$key] after $key = array_key_first/last($arr) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) - && count($expr->expr->getArgs()) >= 1 - ) { - $arrayArg = $expr->expr->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - - if ($arrayType->isArray()->yes()) { - if ($context->true()) { - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope), - ); - $isNonEmpty = true; - } else { - $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes(); - } - - if ($isNonEmpty) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { - $keyType = $scope->getType($expr->expr); - $nonNullKeyType = TypeCombinator::removeNull($keyType); - if (!$nonNullKeyType instanceof NeverType) { - $specifiedTypes = $specifiedTypes->unionWith( - $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()), - ); - } - } - } - } - - // infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && count($expr->expr->getArgs()) >= 2 - ) { - $funcName = $expr->expr->name->toLowerString(); - $arrayArg = null; - $sentinelType = null; - $isStrictArraySearch = false; - - if ($funcName === 'array_search') { - $arrayArg = $expr->expr->getArgs()[1]->value; - $sentinelType = new ConstantBooleanType(false); - $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes(); - } elseif ($funcName === 'array_find_key') { - $arrayArg = $expr->expr->getArgs()[0]->value; - $sentinelType = new NullType(); - } - - if ($arrayArg !== null) { - $arrayType = $scope->getType($arrayArg); - - if ($arrayType->isArray()->yes()) { - if ($context->true()) { - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope), - ); - - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - if ($isStrictArraySearch) { - $needleType = $scope->getType($expr->expr->getArgs()[0]->value); - $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); - } else { - $dimFetchType = $arrayType->getIterableValueType(); - } - - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $dimFetchType, TypeSpecifierContext::createTrue(), $scope), - ); - } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { - $keyType = $scope->getType($expr->expr); - $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType); - if (!$narrowedKeyType instanceof NeverType) { - if ($isStrictArraySearch) { - $needleType = $scope->getType($expr->expr->getArgs()[0]->value); - $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); - } else { - $dimFetchType = $arrayType->getIterableValueType(); - } - $specifiedTypes = $specifiedTypes->unionWith( - $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType), - ); - } - } - } - } - } - - if ($context->null()) { - // infer $arr[$key] after $key = array_rand($arr) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) - && count($expr->expr->getArgs()) >= 1 - ) { - $numArg = null; - $args = $expr->expr->getArgs(); - $arrayArg = $args[0]->value; - if (count($args) > 1) { - $numArg = $args[1]->value; - } - $one = new ConstantIntegerType(1); - $arrayType = $scope->getType($arrayArg); - - if ( - $arrayType->isArray()->yes() - && $arrayType->isIterableAtLeastOnce()->yes() - && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes()) - ) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - return $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } - } - - // infer $list[$count] after $count = count($list) - 1 - if ( - $expr->expr instanceof Expr\BinaryOp\Minus - && $expr->expr->left instanceof FuncCall - && $expr->expr->left->name instanceof Name - && !$expr->expr->left->isFirstClassCallable() - && $expr->expr->right instanceof Node\Scalar\Int_ - && $expr->expr->right->value === 1 - && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) - && count($expr->expr->left->getArgs()) >= 1 - ) { - $arrayArg = $expr->expr->left->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - if ( - $arrayType->isList()->yes() - && $arrayType->isIterableAtLeastOnce()->yes() - ) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - return $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } - } - - return $specifiedTypes; - } - - return $specifiedTypes; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; + $assignedExprResult = null; $result = $this->processAssignVar( $nodeScopeResolver, $scope, @@ -298,7 +122,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->expr, $nodeCallback, $context, - static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver, &$assignedExprResult): ExpressionResult { + $beforeScope = $scope; $impurePoints = []; if ($expr instanceof AssignRef) { $referencedExpr = $expr->expr; @@ -326,8 +151,8 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex ); } - $nodeScopeResolver->storeBeforeScope($storage, $expr, $scope); $result = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); + $assignedExprResult = $result; $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -338,7 +163,16 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr->expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: static fn ($scope) => $result->getTypeForScope($scope), + ); }, true, ); @@ -353,8 +187,8 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex ) { $varName = $expr->var->name; $refName = $expr->expr->name; - $type = $scope->getType($expr->var); - $nativeType = $scope->getNativeType($expr->var); + $type = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $scope); + $nativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($expr->var, $scope); // When $varName is assigned, update $refName $scope = $scope->assignExpression( @@ -380,17 +214,218 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: static fn (MutatingScope $s): Type => $assignedExprResult !== null ? $assignedExprResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s), + specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($nodeScopeResolver, $expr, $assignedExprResult) : null, + createTypesCallback: $expr instanceof Assign ? $this->createCreateTypesCallback($expr, $assignedExprResult) : null, ); } + /** + * A type constraint on an assignment constrains the assigned variable + * and the assigned expression - what TypeSpecifier::create() recovered + * by unwrapping assign chains. Nested assignments compose through the + * assigned expression's own result. + * + * @return Closure(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes + */ + private function createCreateTypesCallback(Assign $expr, ?ExpressionResult $assignedExprResult): Closure + { + return function (MutatingScope $s, Type $type, TypeSpecifierContext $context) use ($expr, $assignedExprResult): SpecifiedTypes { + $types = $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->var, null, $type, $context); + + return $types->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->expr, $assignedExprResult, $type, $context), + ); + }; + } + + /** + * New-world copy of the non-null contexts of specifyTypes(): the assigned + * variable narrows by the boolean outcome, plus the $arr[$key] inference + * after $key = array_key_first/array_key_last/array_search/array_find_key. + * The null-context inferences stay in specifyTypes() - result-based asks + * are always truthy or falsey. + * + * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes + */ + private function createSpecifyTypesCallback(NodeScopeResolver $nodeScopeResolver, Assign $expr, ?ExpressionResult $assignedExprResult): Closure + { + return function (MutatingScope $s, TypeSpecifierContext $context) use ($nodeScopeResolver, $expr, $assignedExprResult): SpecifiedTypes { + if ($context->null()) { + $specifiedTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s->exitFirstLevelStatements(), $expr->expr, $assignedExprResult, $context)->setRootExpr($expr); + $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var)); + } else { + $specifiedTypes = $this->defaultNarrowingHelper->specifyDefaultTypes($expr->var, $context)->setRootExpr($expr); + } + + // infer $arr[$key] after $key = array_key_first/last($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); + + if ($arrayType->isArray()->yes()) { + if ($context->true()) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $arrayArg, null, new NonEmptyArrayType(), TypeSpecifierContext::createTrue()), + ); + $isNonEmpty = true; + } else { + $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes(); + } + + if ($isNonEmpty) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s); + $nonNullKeyType = TypeCombinator::removeNull($keyType); + if (!$nonNullKeyType instanceof NeverType) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()), + ); + } + } + } + } + + // infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && count($expr->expr->getArgs()) >= 2 + ) { + $funcName = $expr->expr->name->toLowerString(); + $arrayArg = null; + $sentinelType = null; + $isStrictArraySearch = false; + + if ($funcName === 'array_search') { + $arrayArg = $expr->expr->getArgs()[1]->value; + $sentinelType = new ConstantBooleanType(false); + $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[2]->value, $s)->isTrue()->yes(); + } elseif ($funcName === 'array_find_key') { + $arrayArg = $expr->expr->getArgs()[0]->value; + $sentinelType = new NullType(); + } + + if ($arrayArg !== null) { + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); + + if ($arrayType->isArray()->yes()) { + if ($context->true()) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $arrayArg, null, new NonEmptyArrayType(), TypeSpecifierContext::createTrue()), + ); + + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + if ($isStrictArraySearch) { + $needleType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[0]->value, $s); + $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); + } else { + $dimFetchType = $arrayType->getIterableValueType(); + } + + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $dimFetchType, TypeSpecifierContext::createTrue()), + ); + } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s); + $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType); + if (!$narrowedKeyType instanceof NeverType) { + if ($isStrictArraySearch) { + $needleType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[0]->value, $s); + $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); + } else { + $dimFetchType = $arrayType->getIterableValueType(); + } + $specifiedTypes = $specifiedTypes->unionWith( + $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType), + ); + } + } + } + } + } + + if ($context->null()) { + // infer $arr[$key] after $key = array_rand($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $numArg = null; + $args = $expr->expr->getArgs(); + $arrayArg = $args[0]->value; + if (count($args) > 1) { + $numArg = $args[1]->value; + } + $one = new ConstantIntegerType(1); + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); + + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + && ($numArg === null || $one->isSuperTypeOf($nodeScopeResolver->readStoredOrPriceOnDemand($numArg, $s))->yes()) + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } + } + + // infer $list[$count] after $count = count($list) - 1 + if ( + $expr->expr instanceof Expr\BinaryOp\Minus + && $expr->expr->left instanceof FuncCall + && $expr->expr->left->name instanceof Name + && !$expr->expr->left->isFirstClassCallable() + && $expr->expr->right instanceof Node\Scalar\Int_ + && $expr->expr->right->value === 1 + && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($expr->expr->left->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->left->getArgs()[0]->value; + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } + } + } + + return $specifiedTypes; + }; + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback @@ -408,6 +443,7 @@ public function processAssignVar( bool $enterExpressionAssign, ): ExpressionResult { + $beforeScope = $scope; $nodeScopeResolver->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; $throwPoints = []; @@ -415,7 +451,6 @@ public function processAssignVar( $isAlwaysTerminating = false; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -428,7 +463,7 @@ public function processAssignVar( $impurePoints[] = new ImpurePoint($scopeBeforeAssignEval, $var, 'superglobal', 'assign to superglobal variable', true); } $assignedExpr = $this->unwrapAssign($assignedExpr); - $type = $scopeBeforeAssignEval->getType($assignedExpr); + $type = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scopeBeforeAssignEval); $conditionalExpressions = []; if ($assignedExpr instanceof Ternary) { @@ -441,37 +476,37 @@ public function processAssignVar( $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes); - $truthyType = $truthyScope->getType($if); - $falseyType = $falsyScope->getType($assignedExpr->else); + $truthyType = $nodeScopeResolver->readStoredOrPriceOnDemand($if, $truthyScope); + $falseyType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr->else, $falsyScope); if ( $truthyType->isSuperTypeOf($falseyType)->no() && $falseyType->isSuperTypeOf($truthyType)->no() ) { - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } } if ($assignedExpr instanceof Match_) { $conditionalExpressions = $this->mergeConditionalExpressions( $conditionalExpressions, - $this->processMatchForConditionalExpressionsAfterAssign($scopeBeforeAssignEval, $var->name, $assignedExpr), + $this->processMatchForConditionalExpressionsAfterAssign($nodeScopeResolver, $scopeBeforeAssignEval, $var->name, $assignedExpr), ); } $truthyType = TypeCombinator::removeFalsey($type); if ($truthyType !== $type) { $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } foreach ([null, false, 0, 0.0, '', '0', []] as $falseyScalar) { @@ -500,23 +535,23 @@ public function processAssignVar( $notIdenticalConditionExpr = new Expr\BinaryOp\NotIdentical($assignedExpr, $astNode); $notIdenticalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $notIdenticalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); $identicalConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, $astNode); $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $identicalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($var->name, $type, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions((string) $exprString, $holders); } if ($assignedExpr instanceof Expr\Array_) { - $scope = $this->processArrayByRefItems($scope, $var->name, $assignedExpr, new Variable($var->name)); + $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $var->name, $assignedExpr, new Variable($var->name)); } } else { $nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); @@ -533,7 +568,7 @@ public function processAssignVar( while ($var instanceof ArrayDimFetch) { $varForSetOffsetValue = $var->var; if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($nodeScopeResolver, $varForSetOffsetValue, $scope)); } if ( @@ -561,12 +596,12 @@ public function processAssignVar( if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $impurePoints = $result->getImpurePoints(); - $isAlwaysTerminating = $result->isAlwaysTerminating(); - $scope = $result->getScope(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); + $hasYield = $varResult->hasYield(); + $throwPoints = $varResult->getThrowPoints(); + $impurePoints = $varResult->getImpurePoints(); + $isAlwaysTerminating = $varResult->isAlwaysTerminating(); + $scope = $varResult->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); } @@ -587,16 +622,34 @@ public function processAssignVar( if ($dimExpr === null) { $offsetTypes[] = [null, $dimFetch]; $offsetNativeTypes[] = [null, $dimFetch]; - $nodeScopeResolver->storeBeforeScope($storage, $dimFetch, $scope); + $nodeScopeResolver->storeExpressionResult($storage, $dimFetch, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $dimFetch, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: static fn (): Type => new NeverType(), + )); } else { - $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; - $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch]; + $offsetTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope), $dimFetch]; + $offsetNativeTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemandNative($dimExpr, $scope), $dimFetch]; if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); } - $nodeScopeResolver->storeBeforeScope($storage, $dimFetch, $scope); + $nodeScopeResolver->storeExpressionResult($storage, $dimFetch, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $dimFetch, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch->var, $s)->getOffsetValueType($nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $s)), + )); $result = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -608,6 +661,15 @@ public function processAssignVar( } } + // SKIPPED (single-pass inside-out invariant): these two reads must stay as + // Scope::getType()/getNativeType(). This is NOT a scope-state side effect + // (assignExpression cannot reproduce it): getType() returns its cached + // resolvedTypes value, computed during loop convergence when a `$x[...] ??= + // []` left side was still maybe-set, so the coalesced value keeps its + // optional array{} branch. The side-effect-free helpers re-price on the + // converged (definitely-set) scope, where CoalesceHandler drops the array{} + // branch (issetCheck === true) - which regresses bug-13623. The optionality + // lives in the loop history the converged scope no longer carries. $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); $scopeBeforeAssignEval = $scope; @@ -620,8 +682,8 @@ public function processAssignVar( $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); - $varType = $scope->getType($var); - $varNativeType = $scope->getNativeType($var); + $varType = $varResult->getTypeForScope($scope); + $varNativeType = $varResult->getNativeTypeForScope($scope); // 4. compose types $isImplicitArrayCreation = $this->isImplicitArrayCreation($dimFetchStack, $scope); @@ -632,10 +694,10 @@ public function processAssignVar( $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); } else { $rewritten = false; foreach ($offsetTypes as $i => [$offsetType]) { @@ -654,7 +716,7 @@ public function processAssignVar( continue; } - [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); $rewritten = true; break; } @@ -672,7 +734,7 @@ public function processAssignVar( if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { - $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + $scope = $scope->assignInitializedProperty($nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope), $var->name->toString()); } } $scope = $scope->assignExpression( @@ -687,7 +749,7 @@ public function processAssignVar( } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { - $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + $scope = $scope->assignInitializedProperty($nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope), $var->name->toString()); } } } @@ -702,7 +764,7 @@ public function processAssignVar( $scope = $scope->assignExpression($expr, $type, $nativeType); } - $setVarType = $scope->getType($originalVar->var); + $setVarType = $nodeScopeResolver->readStoredOrPriceOnDemand($originalVar->var, $scope); if ( !$setVarType instanceof ErrorType && !$setVarType->isArray()->yes() @@ -718,7 +780,6 @@ public function processAssignVar( )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $objectResult = $nodeScopeResolver->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); @@ -750,10 +811,10 @@ public function processAssignVar( $throwPoints[] = InternalThrowPoint::createImplicit($scope, $var); } - $propertyHolderType = $scope->getType($var->var); + $propertyHolderType = $objectResult->getTypeForScope($scope); if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { @@ -770,16 +831,16 @@ public function processAssignVar( } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } else { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } $declaringClass = $propertyReflection->getDeclaringClass(); @@ -814,9 +875,9 @@ public function processAssignVar( } } else { // fallback - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( @@ -831,12 +892,11 @@ public function processAssignVar( } } elseif ($var instanceof Expr\StaticPropertyFetch) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { - $nodeScopeResolver->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); - $propertyHolderType = $scope->getType($var->class); + $classResult = $nodeScopeResolver->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); + $propertyHolderType = $classResult->getTypeForScope($scope); } $propertyName = null; @@ -861,7 +921,7 @@ public function processAssignVar( if ($propertyName !== null) { $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName); - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { @@ -878,26 +938,25 @@ public function processAssignVar( } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } else { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } } else { // fallback - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } elseif ($var instanceof List_) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -929,16 +988,17 @@ public function processAssignVar( } else { $dimExpr = $arrayItem->key; } + $getOffsetValueTypeExpr = new TypeExpr($nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope)->getOffsetValueType($nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope))); $result = $this->processAssignVar( $nodeScopeResolver, $scope, $storage, $stmt, $arrayItem->value, - new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), + $getOffsetValueTypeExpr, $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $getOffsetValueTypeExpr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -953,7 +1013,7 @@ public function processAssignVar( while ($var instanceof ExistingArrayDimFetch) { $varForSetOffsetValue = $var->getVar(); if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($nodeScopeResolver, $varForSetOffsetValue, $scope)); } $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( $varForSetOffsetValue, @@ -964,18 +1024,24 @@ public function processAssignVar( $var = $var->getVar(); } + // the chain is usually a clone of AST nodes already processed elsewhere + // (see Unset_ handling) - process it with a noop callback so that + // results for its nodes are stored without invoking rules twice + $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $offsetTypes = []; $offsetNativeTypes = []; foreach (array_reverse($dimFetchStack) as $dimFetch) { $dimExpr = $dimFetch->getDim(); - $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; - $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch]; + $dimResult = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $offsetTypes[] = [$dimResult->getTypeForScope($scope), $dimFetch]; + $offsetNativeTypes[] = [$dimResult->getNativeTypeForScope($scope), $dimFetch]; } - $valueToWrite = $scope->getType($assignedExpr); - $nativeValueToWrite = $scope->getNativeType($assignedExpr); - $varType = $scope->getType($var); - $varNativeType = $scope->getNativeType($var); + $valueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); + $nativeValueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope); + $varType = $varResult->getTypeForScope($scope); + $varNativeType = $varResult->getNativeTypeForScope($scope); $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; @@ -1030,7 +1096,7 @@ public function processAssignVar( } // stored where processAssignVar is called - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $beforeScope, $var, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } private function createArrayDimFetchConditionalExpressionHolder( @@ -1068,7 +1134,7 @@ private function unwrapAssign(Expr $expr): Expr * @param ImpurePoint[] $rhsImpurePoints * @return array */ - private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array + private function processSureTypesForConditionalExpressionsAfterAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array { foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) { if (!$this->isExprSafeToProjectThroughVariable($expr, $variableName, $rhsImpurePoints, $assignedExpr)) { @@ -1083,7 +1149,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco $variableType, $innerExpr, $this->exprPrinter->printExpr($innerExpr), - $scope->getType($innerExpr), + $nodeScopeResolver->readStoredOrPriceOnDemand($innerExpr, $scope), TrinaryLogic::createMaybe(), ); continue; @@ -1097,7 +1163,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco $variableType, $expr, $exprString, - TypeCombinator::intersect($scope->getType($expr), $exprType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $exprType), TrinaryLogic::createYes(), ); } @@ -1110,7 +1176,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco * @param ImpurePoint[] $rhsImpurePoints * @return array */ - private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array + private function processSureNotTypesForConditionalExpressionsAfterAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array { foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) { if (!$this->isExprSafeToProjectThroughVariable($expr, $variableName, $rhsImpurePoints, $assignedExpr)) { @@ -1139,7 +1205,7 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ $variableType, $expr, $exprString, - TypeCombinator::remove($scope->getType($expr), $exprType), + TypeCombinator::remove($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $exprType), TrinaryLogic::createYes(), ); } @@ -1205,12 +1271,13 @@ private function mergeConditionalExpressions(array $conditionalExpressions, arra * @return array */ private function processMatchForConditionalExpressionsAfterAssign( + NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, Match_ $expr, ): array { - $armScopesAndTypes = $this->matchHandler->getArmScopesAndTypes($scope, $expr); + $armScopesAndTypes = $this->matchHandler->getArmScopesAndTypes($nodeScopeResolver, $scope, $expr); if (count($armScopesAndTypes) < 2) { return []; } @@ -1339,12 +1406,12 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr return $scope->hasVariableType($varNode->name)->negate(); } - private function processArrayByRefItems(MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope + private function processArrayByRefItems(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope { $implicitIndex = 0; foreach ($arrayExpr->items as $arrayItem) { if ($arrayItem->key !== null) { - $keyType = $scope->getType($arrayItem->key)->toArrayKey(); + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayItem->key, $scope)->toArrayKey(); if ($implicitIndex !== null) { $keyValues = $keyType->getConstantScalarValues(); @@ -1370,7 +1437,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam if ($arrayItem->value instanceof Expr\Array_) { $dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr); - $scope = $this->processArrayByRefItems($scope, $rootVarName, $arrayItem->value, $dimFetchExpr); + $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $rootVarName, $arrayItem->value, $dimFetchExpr); } if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) { @@ -1379,8 +1446,8 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam $refVarName = $arrayItem->value->name; $dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr); - $refType = $scope->getType(new Variable($refVarName)); - $refNativeType = $scope->getNativeType(new Variable($refVarName)); + $refType = $nodeScopeResolver->readStoredOrPriceOnDemand(new Variable($refVarName), $scope); + $refNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative(new Variable($refVarName), $scope); // When $rootVarName's array key changes, update $refVarName $scope = $scope->assignExpression( @@ -1408,7 +1475,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam * * @return array{Type, list} */ - private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array + private function produceArrayDimFetchAssignValueToWrite(NodeScopeResolver $nodeScopeResolver, array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, MutatingScope $scope): array { $originalValueToWrite = $valueToWrite; @@ -1427,14 +1494,14 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $has = $offsetValueType->hasOffsetValueType($offsetType); if ($has->yes()) { if ($scope->hasExpressionType($dimFetch)->yes()) { - $offsetValueType = $scope->getType($dimFetch); + $offsetValueType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch, $scope); } else { $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } } elseif ($has->maybe()) { if ($scope->hasExpressionType($dimFetch)->yes()) { $generalizeOnWrite = false; - $offsetValueType = $scope->getType($dimFetch); + $offsetValueType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch, $scope); } else { $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); } @@ -1512,7 +1579,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); } - if ($arrayDimFetch !== null && $offsetValueType->isList()->yes() && $this->shouldKeepList($arrayDimFetch, $scope, $offsetValueType)) { + if ($arrayDimFetch !== null && $offsetValueType->isList()->yes() && $this->shouldKeepList($nodeScopeResolver, $arrayDimFetch, $scope, $offsetValueType)) { $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); } @@ -1535,7 +1602,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } elseif (isset($computedContainerValues[$key])) { $additionalValueType = $computedContainerValues[$key]; } else { - $offsetType = $scope->getType($dimFetch->dim); + $offsetType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch->dim, $scope); $additionalValueType = $valueToWrite->getOffsetValueType($offsetType); } @@ -1545,7 +1612,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar return [$valueToWrite, $additionalExpressions]; } - private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $offsetValueType): bool + private function shouldKeepList(NodeScopeResolver $nodeScopeResolver, ArrayDimFetch $arrayDimFetch, MutatingScope $scope, Type $offsetValueType): bool { if ($arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) { if ( // keep list for $list[$index + 1] assignments @@ -1571,7 +1638,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type && in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true) && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($nodeScopeResolver->readStoredOrPriceOnDemand($arrayDimFetch->dim, $scope))->yes() && $offsetValueType->isIterableAtLeastOnce()->yes() ) { return true; @@ -1605,4 +1672,20 @@ private function isSameVariable(Expr $a, Expr $b): bool return false; } + /** + * Returns the property's readable (declared) type, filtered down to the union + * members that are not disjoint from the currently narrowed property type. + */ + private function getOriginalPropertyType(NodeScopeResolver $nodeScopeResolver, PropertyFetch|StaticPropertyFetch $propertyFetch, MutatingScope $scope): Type + { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); + $originalPropertyType = $propertyReflection !== null ? $propertyReflection->getReadableType() : new ErrorType(); + if ($originalPropertyType instanceof UnionType) { + $currentPropertyType = $nodeScopeResolver->readStoredOrPriceOnDemand($propertyFetch, $scope); + $originalPropertyType = $originalPropertyType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); + } + + return $originalPropertyType; + } + } diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 31af0b9c77f..76f9ac93b2e 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -11,15 +11,15 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -42,6 +42,8 @@ public function __construct( private AssignHandler $assignHandler, private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -53,6 +55,90 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; + + $typeCallback = function (MutatingScope $s) use ($expr, $nodeScopeResolver): Type { + // $expr->var and $expr->expr were processed during this handler's + // processExpr (the var as the assignment target, the value expr by the + // inner closure below), so their ExpressionResults are stored - read + // them instead of re-walking via Scope::getType(). + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $s); + + if ($expr instanceof Expr\AssignOp\Coalesce) { + // the coalesce is synthetic - price it on demand against the + // current storage, mirroring resolveTypeOfNewWorldHandlerNode(). + $coalesce = new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes()); + + return $nodeScopeResolver->priceSyntheticOnDemand($coalesce, $s); + } + + if ($expr instanceof Expr\AssignOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Div) { + return $this->initializerExprTypeResolver->getDivType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Mod) { + return $this->initializerExprTypeResolver->getModType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($expr->var, $expr->expr, $getType); + } + + throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + + // processAssignVar asks getType($expr) for the value to assign; store this + // result first so it resolves from the typeCallback above rather than + // re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + $assignResult = $this->assignHandler->processAssignVar( $nodeScopeResolver, $scope, @@ -62,7 +148,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr, $nodeCallback, $context, - static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { $originalScope = $scope; if ($expr instanceof Expr\AssignOp\Coalesce) { $scope = $scope->filterByFalseyValue( @@ -72,10 +158,11 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); if ($expr instanceof Expr\AssignOp\Coalesce) { - $nodeScopeResolver->storeBeforeScope($storage, $expr, $originalScope); - $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes(); - return new ExpressionResult( + $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $originalScope)->isNull()->yes(); + return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), + $originalScope, + $expr->expr, $exprResult->hasYield(), $isAlwaysTerminating, $exprResult->getThrowPoints(), @@ -87,97 +174,32 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex }, $expr instanceof Expr\AssignOp\Coalesce, ); - if (!$expr instanceof Expr\AssignOp\Coalesce) { - $nodeScopeResolver->storeBeforeScope($storage, $expr, $scope); - } $scope = $assignResult->getScope(); $throwPoints = $assignResult->getThrowPoints(); $impurePoints = $assignResult->getImpurePoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && - !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + !$nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $scope)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); } if ($expr instanceof Expr\AssignOp\Concat) { - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $getType = static fn (Expr $expr): Type => $scope->getType($expr); - - if ($expr instanceof Expr\AssignOp\Coalesce) { - return $scope->getType(new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes())); - } - - if ($expr instanceof Expr\AssignOp\Concat) { - return $this->initializerExprTypeResolver->getConcatType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseAnd) { - return $this->initializerExprTypeResolver->getBitwiseAndType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseOr) { - return $this->initializerExprTypeResolver->getBitwiseOrType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseXor) { - return $this->initializerExprTypeResolver->getBitwiseXorType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Div) { - return $this->initializerExprTypeResolver->getDivType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Mod) { - return $this->initializerExprTypeResolver->getModType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Plus) { - return $this->initializerExprTypeResolver->getPlusType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Minus) { - return $this->initializerExprTypeResolver->getMinusType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Mul) { - return $this->initializerExprTypeResolver->getMulType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Pow) { - return $this->initializerExprTypeResolver->getPowType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\ShiftLeft) { - return $this->initializerExprTypeResolver->getShiftLeftType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\ShiftRight) { - return $this->initializerExprTypeResolver->getShiftRightType($expr->var, $expr->expr, $getType); - } - - throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 0f604c86441..e48728343e1 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -14,15 +14,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\EqualityTypeSpecifyingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\RicherScopeGetTypeHelper; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -66,6 +67,9 @@ public function __construct( private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExprPrinter $exprPrinter, private EqualityTypeSpecifyingHelper $equalityTypeSpecifyingHelper, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -83,492 +87,561 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()); $impurePoints = array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && - !$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + // the right operand was just processed on $leftResult's scope; read its + // result instead of re-walking via Scope::getType(). + !$rightResult->getTypeForScope($leftResult->getScope())->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } if ($expr instanceof BinaryOp\Concat) { - $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->left, $scope); - $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->right, $leftResult->getScope()); + $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->left, $scope, $leftResult); + $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->right, $leftResult->getScope(), $rightResult); $throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints()); } $scope = $rightResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } + typeCallback: function (MutatingScope $scope) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): Type { + // the operands were processed during processExpr; read their already + // computed results instead of re-walking via Scope::getType(). + // Synthetic nodes the resolver builds (e.g. getDivType's Mod) are + // priced on demand by the same helper. + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $nodeScopeResolver): Type { + if ($e === $expr->left) { + return $leftResult->getTypeForScope($scope); + } + if ($e === $expr->right) { + return $rightResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $getType = static fn (Expr $expr): Type => $scope->getType($expr); + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; - if ($expr instanceof BinaryOp\Smaller) { - return $scope->getType($expr->left)->isSmallerThan($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\Smaller) { + return $getType($expr->left)->isSmallerThan($getType($expr->right), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\SmallerOrEqual) { - return $scope->getType($expr->left)->isSmallerThanOrEqual($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\SmallerOrEqual) { + return $getType($expr->left)->isSmallerThanOrEqual($getType($expr->right), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\Greater) { - return $scope->getType($expr->right)->isSmallerThan($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\Greater) { + return $getType($expr->right)->isSmallerThan($getType($expr->left), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $scope->getType($expr->right)->isSmallerThanOrEqual($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $getType($expr->right)->isSmallerThanOrEqual($getType($expr->left), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\Equal) { - if ( - $expr->left instanceof Variable - && is_string($expr->left->name) - && $expr->right instanceof Variable - && is_string($expr->right->name) - && $expr->left->name === $expr->right->name - ) { - return new ConstantBooleanType(true); - } + if ($expr instanceof BinaryOp\Equal) { + return $this->resolveEqualType($nodeScopeResolver, $scope, $expr); + } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + if ($expr instanceof BinaryOp\NotEqual) { + // negation of the Equal result - direct computation avoids + // synthesizing a BooleanNot node (which would route through + // on-demand re-processing once BooleanNot is migrated) + $equalType = $this->resolveEqualType($nodeScopeResolver, $scope, new BinaryOp\Equal($expr->left, $expr->right))->toBoolean(); + if ($equalType->isTrue()->yes()) { + return new ConstantBooleanType(false); + } + if ($equalType->isFalse()->yes()) { + return new ConstantBooleanType(true); + } - return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; - } + return new BooleanType(); + } - if ($expr instanceof BinaryOp\NotEqual) { - return $scope->getType(new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right))); - } + if ($expr instanceof BinaryOp\Identical) { + return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr, $nodeScopeResolver)->type; + } - if ($expr instanceof BinaryOp\Identical) { - return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr)->type; - } + if ($expr instanceof BinaryOp\NotIdentical) { + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr, $nodeScopeResolver)->type; + } - if ($expr instanceof BinaryOp\NotIdentical) { - return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr)->type; - } + if ($expr instanceof BinaryOp\LogicalXor) { + $leftBooleanType = $getType($expr->left)->toBoolean(); + $rightBooleanType = $getType($expr->right)->toBoolean(); - if ($expr instanceof BinaryOp\LogicalXor) { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - $rightBooleanType = $scope->getType($expr->right)->toBoolean(); - - if ( - $leftBooleanType instanceof ConstantBooleanType - && $rightBooleanType instanceof ConstantBooleanType - ) { - return new ConstantBooleanType( - $leftBooleanType->getValue() xor $rightBooleanType->getValue(), - ); - } + if ( + $leftBooleanType instanceof ConstantBooleanType + && $rightBooleanType instanceof ConstantBooleanType + ) { + return new ConstantBooleanType( + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), + ); + } - return new BooleanType(); - } + return new BooleanType(); + } - if ($expr instanceof BinaryOp\Spaceship) { - return $this->initializerExprTypeResolver->getSpaceshipType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Spaceship) { + return $this->initializerExprTypeResolver->getSpaceshipType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Concat) { - return $this->initializerExprTypeResolver->getConcatType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseAnd) { - return $this->initializerExprTypeResolver->getBitwiseAndType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseOr) { - return $this->initializerExprTypeResolver->getBitwiseOrType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseXor) { - return $this->initializerExprTypeResolver->getBitwiseXorType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Div) { - return $this->initializerExprTypeResolver->getDivType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Div) { + return $this->initializerExprTypeResolver->getDivType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Mod) { - return $this->initializerExprTypeResolver->getModType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Mod) { + return $this->initializerExprTypeResolver->getModType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Plus) { - return $this->initializerExprTypeResolver->getPlusType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Minus) { - return $this->initializerExprTypeResolver->getMinusType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Mul) { - return $this->initializerExprTypeResolver->getMulType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Pow) { - return $this->initializerExprTypeResolver->getPowType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\ShiftLeft) { - return $this->initializerExprTypeResolver->getShiftLeftType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\ShiftRight) { - return $this->initializerExprTypeResolver->getShiftRightType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($expr->left, $expr->right, $getType); + } - throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); - } + throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); + }, + specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { + if ($expr instanceof BinaryOp\Identical) { + return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($nodeScopeResolver, $expr, $scope, $context); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr instanceof BinaryOp\Identical) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($expr, $scope, $context); - } + if ($expr instanceof BinaryOp\NotIdentical) { + // negating the context is exactly what a BooleanNot around the + // Identical would do - direct computation avoids synthesizing a + // BooleanNot node (on-demand re-processing once it is migrated). + // A null context never negates (BooleanNot defaults on it too). + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - if ($expr instanceof BinaryOp\NotIdentical) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot(new BinaryOp\Identical($expr->left, $expr->right)), - $context, - )->setRootExpr($expr); - } + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new BinaryOp\Identical($expr->left, $expr->right), + $context->negate(), + )->setRootExpr($expr); + } - if ($expr instanceof BinaryOp\Equal) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($expr, $scope, $context); - } + if ($expr instanceof BinaryOp\Equal) { + return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($nodeScopeResolver, $expr, $scope, $context); + } - if ($expr instanceof BinaryOp\NotEqual) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof BinaryOp\NotEqual) { + // see NotIdentical above + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { - if ( - $expr->left instanceof Expr\FuncCall - && $expr->left->name instanceof Name - && !$expr->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - && count($expr->left->getArgs()) >= 1 - && ( - !$expr->right instanceof Expr\FuncCall - || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - ) - ) { - $inverseOperator = $expr instanceof BinaryOp\Smaller - ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) - : new BinaryOp\Smaller($expr->right, $expr->left); - - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot($inverseOperator), - $context, - )->setRootExpr($expr); - } + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new BinaryOp\Equal($expr->left, $expr->right), + $context->negate(), + )->setRootExpr($expr); + } - $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); - $result = (new SpecifiedTypes([], []))->setRootExpr($expr); - - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && count($expr->right->getArgs()) >= 1 - && $leftType->isInteger()->yes() - ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - - $sizeType = null; - if ($leftType instanceof ConstantIntegerType) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { + if ( + $expr->left instanceof Expr\FuncCall + && $expr->left->name instanceof Name + && !$expr->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + && count($expr->left->getArgs()) >= 1 + && ( + !$expr->right instanceof Expr\FuncCall + || !$expr->right->name instanceof Name + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + ) + ) { + $inverseOperator = $expr instanceof BinaryOp\Smaller + ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) + : new BinaryOp\Smaller($expr->right, $expr->left); + + // negating the context is exactly what a BooleanNot around the + // inverse operator would do - direct computation avoids + // synthesizing a BooleanNot node. A null context never negates + // (BooleanNot defaults on it too). + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + $inverseOperator, + $context->negate(), + )->setRootExpr($expr); } - } elseif ($leftType instanceof IntegerRangeType) { - if ($context->falsey() && $leftType->getMax() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); + + $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + // the operands were processed during processExpr; read their + // already computed results instead of re-walking via + // Scope::getType(). Their subexpressions (e.g. count() arguments) + // were also processed and are read from the stored result. + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $nodeScopeResolver): Type { + if ($e === $expr->left) { + return $leftResult->getTypeForScope($scope); + } + if ($e === $expr->right) { + return $rightResult->getTypeForScope($scope); } - } elseif ($context->truthy() && $leftType->getMin() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); + + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; + $leftType = $getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); + + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) + && count($expr->right->getArgs()) >= 1 + && $leftType->isInteger()->yes() + ) { + $argType = $getType($expr->right->getArgs()[0]->value); + + $sizeType = null; + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + if ($context->falsey() && $leftType->getMax() !== null) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); + } + } elseif ($context->truthy() && $leftType->getMin() !== null) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); + } + } } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); + $sizeType = $leftType; } - } - } else { - $sizeType = $leftType; - } - if ($sizeType !== null) { - $specifiedTypes = $typeSpecifier->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); - if ($specifiedTypes !== null) { - $result = $result->unionWith($specifiedTypes); - } - } + if ($sizeType !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); + } + } + + if ( + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); + $countables[] = $innerType; + } + + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $countables[] = $innerType; + } + } + + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - if ($context->truthy() && $argType->isArray()->maybe()) { - $countables = []; - if ($argType instanceof UnionType) { - $countableInterface = new ObjectType(Countable::class); - foreach ($argType->getTypes() as $innerType) { - if ($innerType->isArray()->yes()) { - $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); - $countables[] = $innerType; + return $this->typeSpecifier->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); } + } - if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { - continue; + if ($argType->isArray()->yes()) { + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); } - $countables[] = $innerType; + $result = $result->unionWith( + $this->typeSpecifier->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), + ); } } - if (count($countables) > 0) { - $countableType = TypeCombinator::union(...$countables); - - return $typeSpecifier->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + // infer $list[$index] after $index < count($list) + if ( + $context->true() + && !$orEqual + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $argType->isList()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $arrayArg = $expr->right->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->typeSpecifier->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } } - if ($argType->isArray()->yes()) { - $newType = new NonEmptyArrayType(); - if ($context->true() && $argType->isList()->yes()) { - $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + // infer $list[$index] after $zeroOrMore < count($list) - N + // infer $list[$index] after $zeroOrMore <= count($list) - N + if ( + $context->true() + && $expr->right instanceof BinaryOp\Minus + && $expr->right->left instanceof Expr\FuncCall + && $expr->right->left->name instanceof Name + && !$expr->right->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) + && count($expr->right->left->getArgs()) >= 1 + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $leftType->isInteger()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $countArgType = $getType($expr->right->left->getArgs()[0]->value); + $subtractedType = $getType($expr->right->right); + if ( + $countArgType->isList()->yes() + && $this->typeSpecifier->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() + ) { + $arrayArg = $expr->right->left->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->typeSpecifier->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } + } - $result = $result->unionWith( - $typeSpecifier->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), - ); + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && count($expr->right->getArgs()) >= 3 + && ( + IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() + || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) + ) + ) { + // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match + $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); + + return $this->typeSpecifier->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr); } - } - // infer $list[$index] after $index < count($list) - if ( - $context->true() - && !$orEqual - // constant offsets are handled via HasOffsetType/HasOffsetValueType - && !$leftType instanceof ConstantIntegerType - && $argType->isList()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - $arrayArg = $expr->right->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); - $result = $result->unionWith( - $typeSpecifier->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), - ); - } - } + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && count($expr->right->getArgs()) === 1 + && $leftType->isInteger()->yes() + ) { + if ( + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + $argType = $getType($expr->right->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); + + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } - // infer $list[$index] after $zeroOrMore < count($list) - N - // infer $list[$index] after $zeroOrMore <= count($list) - N - if ( - $context->true() - && $expr->right instanceof BinaryOp\Minus - && $expr->right->left instanceof Expr\FuncCall - && $expr->right->left->name instanceof Name - && !$expr->right->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) - && count($expr->right->left->getArgs()) >= 1 - // constant offsets are handled via HasOffsetType/HasOffsetValueType - && !$leftType instanceof ConstantIntegerType - && $leftType->isInteger()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); - $subtractedType = $scope->getType($expr->right->right); - if ( - $countArgType->isList()->yes() - && $typeSpecifier->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() - && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() - ) { - $arrayArg = $expr->right->left->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); - $result = $result->unionWith( - $typeSpecifier->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), - ); - } - } + $result = $result->unionWith($this->typeSpecifier->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + } + } + } - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) - && count($expr->right->getArgs()) >= 3 - && ( - IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() - || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) - ) - ) { - // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match - $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); - - return $typeSpecifier->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr); - } + if ($leftType instanceof ConstantIntegerType) { + if ($expr->right instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), + $context, + )); + } + } - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) - && count($expr->right->getArgs()) === 1 - && $leftType->isInteger()->yes() - ) { - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $accessory = new AccessoryNonEmptyStringType(); - - if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { - $accessory = new AccessoryNonFalsyStringType(); + $rightType = $getType($expr->right); + if ($rightType instanceof ConstantIntegerType) { + if ($expr->left instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), + $context, + )); } + } - $result = $result->unionWith($typeSpecifier->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + if ($context->true()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->left, + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->right, + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + } elseif ($context->false()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->left, + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->right, + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } } - } - } - if ($leftType instanceof ConstantIntegerType) { - if ($expr->right instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), - $context, - )); + return $result; } - } - $rightType = $scope->getType($expr->right); - if ($rightType instanceof ConstantIntegerType) { - if ($expr->left instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), - $context, - )); + if ($expr instanceof BinaryOp\Greater) { + return $this->typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); } - } - if ($context->true()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->left, - $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->right, - $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - } elseif ($context->false()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->left, - $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->right, - $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $this->typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); } - } - return $result; - } + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + }, + ); + } - if ($expr instanceof BinaryOp\Greater) { - return $typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); + /** + * The boolean result of a `==` comparison, including the same-variable + * special case. Shared by the Equal and NotEqual type callbacks. + */ + private function resolveEqualType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, BinaryOp\Equal $expr): Type + { + if ( + $expr->left instanceof Variable + && is_string($expr->left->name) + && $expr->right instanceof Variable + && is_string($expr->right->name) + && $expr->left->name === $expr->right->name + ) { + return new ConstantBooleanType(true); } - if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); - } + // the operands were processed during processExpr; read their stored + // results instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope); - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; } private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index de49fb09887..95564a87bf3 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -7,16 +7,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; /** @@ -28,6 +28,8 @@ final class BitwiseNotHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -41,23 +43,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getBitwiseNotType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getBitwiseNotType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 383b03c7d66..b06378b06c6 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -4,32 +4,26 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; -use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; -use PhpParser\Node\Expr\BinaryOp\LogicalOr; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanAndNode; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function array_merge; -use function array_reverse; use function is_string; /** @@ -39,11 +33,10 @@ final class BooleanAndHandler implements ExprHandler { - private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - public function __construct( - private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -53,183 +46,6 @@ public function supports(Expr $expr): bool return $expr instanceof BooleanAnd || $expr instanceof LogicalAnd; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - if ($leftBooleanType->isFalse()->yes()) { - return new ConstantBooleanType(false); - } - - if (self::getBooleanExpressionDepth($expr->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $leftResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->left), $expr->left, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - $rightBooleanType = $leftResult->getTruthyScope()->getType($expr->right)->toBoolean(); - } else { - $rightBooleanType = $scope->filterByTruthyValue($expr->left)->getType($expr->right)->toBoolean(); - } - - if ($rightBooleanType->isFalse()->yes()) { - return new ConstantBooleanType(false); - } - - if ( - $leftBooleanType->isTrue()->yes() - && $rightBooleanType->isTrue()->yes() - ) { - return new ConstantBooleanType(true); - } - - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - // For deep BooleanAnd chains in truthy context, flatten and - // process all arms at once to avoid O(N²) recursive - // filterByTruthyValue calls. - if ( - $context->true() - && self::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH - ) { - return $this->specifyTypesForFlattenedBooleanAnd($typeSpecifier, $scope, $expr, $context); - } - - $leftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); - $rightScope = $scope->filterByTruthyValue($expr->left); - $rightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - if ($context->true()) { - $types = $leftTypes->unionWith($rightTypes); - } else { - $leftNormalized = $leftTypes->normalize($scope); - $rightNormalized = $rightTypes->normalize($rightScope); - $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); - } - if ($context->false()) { - // Consequent (holder) narrowings projected by each holder: these must be - // the genuine falsey narrowing of the arm. When that is empty, the arm - // has no sound falsey narrowing and must not contribute a consequent. - $leftHolderTypes = $leftTypes; - $rightHolderTypes = $rightTypes; - // In a mixed truthy-and-false context, re-derive empty holders from the falsey narrowing. - if ($context->truthy()) { - if ($leftHolderTypes->getSureTypes() === [] && $leftHolderTypes->getSureNotTypes() === []) { - $leftHolderTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createFalsey())->setRootExpr($expr); - } - if ($rightHolderTypes->getSureTypes() === [] && $rightHolderTypes->getSureNotTypes() === []) { - $rightHolderTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createFalsey())->setRootExpr($expr); - } - } - // Condition (antecedent) narrowings: when an arm has no falsey narrowing - // (e.g. isset() on an array dim fetch), derive the condition from the truthy - // narrowing by swapping sure/sureNot types. This swap is only sound for the - // antecedent — processBooleanConditionalTypes inverts it back to the truthy - // narrowing. It must NOT feed the consequent: inverting a comparison's truthy - // narrowing (e.g. `$a === $b` narrowing `$a` to `$b`'s broad type) would - // over-narrow the consequent (see regression for `$x === $nonConstantString`). - $leftCondTypes = $leftHolderTypes; - $rightCondTypes = $rightHolderTypes; - if ($leftCondTypes->getSureTypes() === [] && $leftCondTypes->getSureNotTypes() === []) { - $truthyLeftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createTruthy()); - if ($this->allExpressionsTrackable($truthyLeftTypes)) { - $leftCondTypes = new SpecifiedTypes($truthyLeftTypes->getSureNotTypes(), $truthyLeftTypes->getSureTypes()); - } - } - if ($rightCondTypes->getSureTypes() === [] && $rightCondTypes->getSureNotTypes() === []) { - $truthyRightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createTruthy()); - if ($this->allExpressionsTrackable($truthyRightTypes)) { - $rightCondTypes = new SpecifiedTypes($truthyRightTypes->getSureNotTypes(), $truthyRightTypes->getSureTypes()); - } - } - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, false, true, $scope, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, true, true, $scope, $expr->left), - ]))->setRootExpr($expr); - } - - return $types; - } - - public static function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int - { - while ( - $expr instanceof BooleanOr - || $expr instanceof LogicalOr - || $expr instanceof BooleanAnd - || $expr instanceof LogicalAnd - ) { - return self::getBooleanExpressionDepth($expr->left, $depth + 1); - } - - return $depth; - } - - /** - * Flatten a deep BooleanAnd chain into leaf expressions and process them - * without recursive filterByTruthyValue calls. - * - * @param BooleanAnd|LogicalAnd $expr - */ - private function specifyTypesForFlattenedBooleanAnd( - TypeSpecifier $typeSpecifier, - MutatingScope $scope, - Expr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $arms = []; - $current = $expr; - while ($current instanceof BooleanAnd || $current instanceof LogicalAnd) { - $arms[] = $current->right; - $current = $current->left; - } - $arms[] = $current; - $arms = array_reverse($arms); - - // Truthy: all arms are true → union all SpecifiedTypes. - // Collect per-expression types first, then build unions once - // to avoid O(N²) from incremental growth. - /** @var array}> $sureTypesPerExpr */ - $sureTypesPerExpr = []; - /** @var array}> $sureNotTypesPerExpr */ - $sureNotTypesPerExpr = []; - - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) { - $sureTypesPerExpr[$exprString][0] = $exprNode; - $sureTypesPerExpr[$exprString][1][] = $type; - } - foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) { - $sureNotTypesPerExpr[$exprString][0] = $exprNode; - $sureNotTypesPerExpr[$exprString][1][] = $type; - } - } - - $sureTypes = []; - foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - $sureNotTypes = []; - foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - - return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr); - } - private function allExpressionsTrackable(SpecifiedTypes $types): bool { foreach ($types->getSureTypes() as [$expr]) { @@ -262,7 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $leftTruthyScope = $leftResult->getTruthyScope(); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftTruthyScope, $storage, $nodeCallback, $context); - $rightExprType = $rightResult->getScope()->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($rightResult->getScope()); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getFalseyScope(); } else { @@ -271,14 +87,105 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftTruthyScope), $scope, $storage, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $leftMergedWithRightScope, + beforeScope: $scope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), truthyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr->right), falseyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), + typeCallback: static function (MutatingScope $s) use ($leftResult, $rightResult, $leftTruthyScope): Type { + $leftBooleanType = $leftResult->getTypeForScope($s)->toBoolean(); + if ($leftBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + // the right side was processed on the left-truthy scope including + // the left's side effects (assignments, by-ref writes) - that + // captured scope is the evaluation point, no re-walk and no + // depth cap needed + $rightBooleanType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $leftTruthyScope->doNotTreatPhpDocTypesAsCertain() : $leftTruthyScope)->toBoolean(); + if ($rightBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + if ( + $leftBooleanType->isTrue()->yes() + && $rightBooleanType->isTrue()->yes() + ) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { + $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); + $rightScope = $s->filterByTruthyValue($expr->left); + $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); + if ($context->true()) { + $types = $leftTypes->unionWith($rightTypes); + } else { + $leftNormalized = $leftTypes->normalize($s, $nodeScopeResolver); + $rightNormalized = $rightTypes->normalize($rightScope, $nodeScopeResolver); + $types = $leftNormalized->intersectWith($rightNormalized); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); + } + if ($context->false()) { + // Consequent (holder) narrowings projected by each holder: these must be + // the genuine falsey narrowing of the arm. When that is empty, the arm + // has no sound falsey narrowing and must not contribute a consequent. + $leftHolderTypes = $leftTypes; + $rightHolderTypes = $rightTypes; + // In a mixed truthy-and-false context, re-derive empty holders from the falsey narrowing. + if ($context->truthy()) { + if ($leftHolderTypes->getSureTypes() === [] && $leftHolderTypes->getSureNotTypes() === []) { + $leftHolderTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, TypeSpecifierContext::createFalsey())->setRootExpr($expr); + } + if ($rightHolderTypes->getSureTypes() === [] && $rightHolderTypes->getSureNotTypes() === []) { + $rightHolderTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, TypeSpecifierContext::createFalsey())->setRootExpr($expr); + } + } + // Condition (antecedent) narrowings: when an arm has no falsey narrowing + // (e.g. isset() on an array dim fetch), derive the condition from the truthy + // narrowing by swapping sure/sureNot types. This swap is only sound for the + // antecedent — processBooleanConditionalTypes inverts it back to the truthy + // narrowing. It must NOT feed the consequent: inverting a comparison's truthy + // narrowing (e.g. `$a === $b` narrowing `$a` to `$b`'s broad type) would + // over-narrow the consequent (see regression for `$x === $nonConstantString`). + $leftCondTypes = $leftHolderTypes; + $rightCondTypes = $rightHolderTypes; + if ($leftCondTypes->getSureTypes() === [] && $leftCondTypes->getSureNotTypes() === []) { + $truthyLeftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, TypeSpecifierContext::createTruthy()); + if ($this->allExpressionsTrackable($truthyLeftTypes)) { + $leftCondTypes = new SpecifiedTypes($truthyLeftTypes->getSureNotTypes(), $truthyLeftTypes->getSureTypes()); + } + } + if ($rightCondTypes->getSureTypes() === [] && $rightCondTypes->getSureNotTypes() === []) { + $truthyRightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, TypeSpecifierContext::createTruthy()); + if ($this->allExpressionsTrackable($truthyRightTypes)) { + $rightCondTypes = new SpecifiedTypes($truthyRightTypes->getSureNotTypes(), $truthyRightTypes->getSureTypes()); + } + } + $result = new SpecifiedTypes( + $types->getSureTypes(), + $types->getSureNotTypes(), + ); + if ($types->shouldOverwrite()) { + $result = $result->setAlwaysOverwriteTypes(); + } + return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightCondTypes, $leftHolderTypes, false, true, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightCondTypes, $leftHolderTypes, true, true, $s, $expr->left), + ]))->setRootExpr($expr); + } + + return $types; + }, ); } diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 6bd2831fb27..09521ee26c9 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -7,11 +7,12 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -27,6 +28,14 @@ final class BooleanNotHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof BooleanNot; @@ -34,37 +43,37 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } + typeCallback: static function (MutatingScope $s) use ($exprResult): Type { + $exprBooleanType = $exprResult->getTypeForScope($s)->toBoolean(); + if ($exprBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(false); + } + if ($exprBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(true); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $exprBooleanType = $scope->getType($expr->expr)->toBoolean(); - if ($exprBooleanType instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$exprBooleanType->getValue()); - } + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - return $typeSpecifier->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); + return $this->typeSpecifier->specifyTypesInCondition($s, $expr->expr, $context->negate())->setRootExpr($expr); + }, + ); } } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index d439a2c8082..2e1fea4502f 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -8,19 +8,17 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanOrNode; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; @@ -28,8 +26,6 @@ use PHPStan\Type\TypeCombinator; use function array_key_first; use function array_merge; -use function array_reverse; -use function count; /** * @implements ExprHandler @@ -38,11 +34,10 @@ final class BooleanOrHandler implements ExprHandler { - private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - public function __construct( - private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -52,168 +47,6 @@ public function supports(Expr $expr): bool return $expr instanceof BooleanOr || $expr instanceof LogicalOr; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - if ($leftBooleanType->isTrue()->yes()) { - return new ConstantBooleanType(true); - } - - if (BooleanAndHandler::getBooleanExpressionDepth($expr->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $leftResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->left), $expr->left, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - $rightBooleanType = $leftResult->getFalseyScope()->getType($expr->right)->toBoolean(); - } else { - $rightBooleanType = $scope->filterByFalseyValue($expr->left)->getType($expr->right)->toBoolean(); - } - - if ($rightBooleanType->isTrue()->yes()) { - return new ConstantBooleanType(true); - } - - if ( - $leftBooleanType->isFalse()->yes() - && $rightBooleanType->isFalse()->yes() - ) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - // For deep BooleanOr chains, flatten and process all arms at once - // to avoid O(n^2) recursive filterByFalseyValue calls - if (BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - return $this->specifyTypesForFlattenedBooleanOr($typeSpecifier, $scope, $expr, $context); - } - - $leftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); - $rightScope = $scope->filterByFalseyValue($expr->left); - $rightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - - if ($context->true()) { - if ( - $scope->getType($expr->left)->toBoolean()->isFalse()->yes() - ) { - $types = $rightTypes->normalize($rightScope); - } elseif ( - $scope->getType($expr->left)->toBoolean()->isTrue()->yes() - || $scope->getType($expr->right)->toBoolean()->isFalse()->yes() - ) { - $types = $leftTypes->normalize($scope); - } else { - $leftNormalized = $leftTypes->normalize($scope); - $rightNormalized = $rightTypes->normalize($rightScope); - $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->augmentBooleanOrTruthyWithConditionalHolders($typeSpecifier, $scope, $rightScope, $expr, $types); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); - } - } else { - $types = $leftTypes->unionWith($rightTypes); - } - - if ($context->true()) { - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, false, false, $scope, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, true, false, $scope, $expr->left), - ]))->setRootExpr($expr); - } - - return $types; - } - - /** - * Flatten a deep BooleanOr chain into leaf expressions and process them - * without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n) - * for chains with many arms (e.g., 80+ === comparisons in ||). - */ - private function specifyTypesForFlattenedBooleanOr( - TypeSpecifier $typeSpecifier, - MutatingScope $scope, - BooleanOr|LogicalOr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - // Collect all leaf expressions from the chain - $arms = []; - $current = $expr; - while ($current instanceof BooleanOr || $current instanceof LogicalOr) { - $arms[] = $current->right; - $current = $current->left; - } - $arms[] = $current; // leftmost leaf - $arms = array_reverse($arms); - - if ($context->false() || $context->falsey()) { - // Falsey: all arms are false → union all SpecifiedTypes. - // Collect per-expression types first, then build unions once - // to avoid O(N²) from incremental TypeCombinator::union() growth. - /** @var array}> $sureTypesPerExpr */ - $sureTypesPerExpr = []; - /** @var array}> $sureNotTypesPerExpr */ - $sureNotTypesPerExpr = []; - - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) { - $sureTypesPerExpr[$exprString][0] = $exprNode; - $sureTypesPerExpr[$exprString][1][] = $type; - } - foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) { - $sureNotTypesPerExpr[$exprString][0] = $exprNode; - $sureNotTypesPerExpr[$exprString][1][] = $type; - } - } - - $sureTypes = []; - foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)]; - } - $sureNotTypes = []; - foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - - return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr); - } - - // Truthy: at least one arm is true → intersect all normalized SpecifiedTypes - $armSpecifiedTypes = []; - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - $armSpecifiedTypes[] = $armTypes->normalize($scope); - } - - $types = $armSpecifiedTypes[0]; - for ($i = 1; $i < count($armSpecifiedTypes); $i++) { - $types = $types->intersectWith($armSpecifiedTypes[$i]); - } - - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - - return $result->setRootExpr($expr); - } - /** * For `if ($a || $b)` truthy, expressions narrowed by stored conditional * holders (e.g. `$a = $obj instanceof ClassA;` records "when `$a` is @@ -232,7 +65,7 @@ private function specifyTypesForFlattenedBooleanOr( * skipped: in the OR-truthy scope the arm that didn't narrow could still be * the truthy one, so the sound result is the original (unnarrowed) type. */ - private function augmentBooleanOrTruthyWithConditionalHolders(TypeSpecifier $typeSpecifier, MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes + private function augmentBooleanOrTruthyWithConditionalHolders(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes { $leftTruthyScope = $scope->filterByTruthyValue($expr->left); $rightTruthyScope = $rightScope->filterByTruthyValue($expr->right); @@ -264,9 +97,9 @@ private function augmentBooleanOrTruthyWithConditionalHolders(TypeSpecifier $typ continue; } - $origType = $scope->getType($targetExpr); - $leftType = $leftTruthyScope->getType($targetExpr); - $rightType = $rightTruthyScope->getType($targetExpr); + $origType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $scope); + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $leftTruthyScope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $rightTruthyScope); $leftNarrowed = !$leftType->equals($origType) && $origType->isSuperTypeOf($leftType)->yes(); $rightNarrowed = !$rightType->equals($origType) && $origType->isSuperTypeOf($rightType)->yes(); @@ -281,7 +114,7 @@ private function augmentBooleanOrTruthyWithConditionalHolders(TypeSpecifier $typ } $types = $types->unionWith( - $typeSpecifier->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope), + $this->defaultNarrowingHelper->createSubjectTypes($scope, $targetExpr, null, $unionType, TypeSpecifierContext::createTrue()), ); } } @@ -294,7 +127,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $leftFalseyScope = $leftResult->getFalseyScope(); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftFalseyScope, $storage, $nodeCallback, $context); - $rightExprType = $rightResult->getScope()->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($rightResult->getScope()); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getTruthyScope(); } else { @@ -303,14 +136,84 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftFalseyScope), $scope, $storage, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $leftMergedWithRightScope, + beforeScope: $scope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), truthyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), falseyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr->right), + typeCallback: static function (MutatingScope $s) use ($leftResult, $rightResult, $leftFalseyScope): Type { + $leftBooleanType = $leftResult->getTypeForScope($s)->toBoolean(); + if ($leftBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + // the right side was processed on the left-falsey scope including + // the left's side effects (assignments, by-ref writes) - that + // captured scope is the evaluation point, no re-walk and no + // depth cap needed + $rightBooleanType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $leftFalseyScope->doNotTreatPhpDocTypesAsCertain() : $leftFalseyScope)->toBoolean(); + if ($rightBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + if ( + $leftBooleanType->isFalse()->yes() + && $rightBooleanType->isFalse()->yes() + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { + $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); + $rightScope = $s->filterByFalseyValue($expr->left); + $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); + + if ($context->true()) { + if ( + $leftResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() + ) { + $types = $rightTypes->normalize($rightScope, $nodeScopeResolver); + } elseif ( + $leftResult->getTypeForScope($s)->toBoolean()->isTrue()->yes() + || $rightResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() + ) { + $types = $leftTypes->normalize($s, $nodeScopeResolver); + } else { + $leftNormalized = $leftTypes->normalize($s, $nodeScopeResolver); + $rightNormalized = $rightTypes->normalize($rightScope, $nodeScopeResolver); + $types = $leftNormalized->intersectWith($rightNormalized); + $types = $this->augmentBooleanOrTruthyWithConditionalHolders($nodeScopeResolver, $s, $rightScope, $expr, $types); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); + } + } else { + $types = $leftTypes->unionWith($rightTypes); + } + + if ($context->true()) { + $result = new SpecifiedTypes( + $types->getSureTypes(), + $types->getSureNotTypes(), + ); + if ($types->shouldOverwrite()) { + $result = $result->setAlwaysOverwriteTypes(); + } + return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightTypes, $leftTypes, false, false, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightTypes, $leftTypes, true, false, $s, $expr->left), + ]))->setRootExpr($expr); + } + + return $types; + }, ); } diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index cbc3d70fcb0..ab4f122a54b 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -13,16 +13,17 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -35,6 +36,8 @@ final class CastHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -46,56 +49,62 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } + typeCallback: function (MutatingScope $s) use ($expr, $exprResult): Type { + if ($expr instanceof Cast\Unset_) { + return new NullType(); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr instanceof Cast\Unset_) { - return new NullType(); - } + return $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($s, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($s); + } - return $this->initializerExprTypeResolver->getCastType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } + throw new ShouldNotHappenException(); + }); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($expr instanceof Cast\Bool_) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))), + null, + $context, + )->setRootExpr($expr); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr instanceof Cast\Bool_) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))), - $context, - )->setRootExpr($expr); - } - - if ($expr instanceof Cast\Int_) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new Int_(0)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof Cast\Int_) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new Int_(0)), + null, + $context, + )->setRootExpr($expr); + } - if ($expr instanceof Cast\Double) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new Float_(0.0)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof Cast\Double) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new Float_(0.0)), + null, + $context, + )->setRootExpr($expr); + } - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + }, + ); } } diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 4fdfc9adfc6..785458350e4 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -9,17 +9,18 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; use function array_merge; @@ -33,6 +34,8 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -44,39 +47,39 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $impurePoints = $exprResult->getImpurePoints(); $throwPoints = $exprResult->getThrowPoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope, $exprResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getCastType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } + typeCallback: fn (MutatingScope $s): Type => $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($s, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($s); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new String_('')), - $context, - )->setRootExpr($expr); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new String_('')), + null, + $context, + )->setRootExpr($expr), + ); } } diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 25199ed14f7..2ce737b9db7 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -8,16 +8,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_merge; @@ -31,6 +31,8 @@ final class ClassConstFetchHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -40,27 +42,15 @@ public function supports(Expr $expr): bool return $expr instanceof ClassConstFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if (!$expr->name instanceof Identifier) { - return new MixedType(); - } - - return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( - $expr->class, - $expr->name->name, - $scope->isInClass() ? $scope->getClassReflection() : null, - static fn (Expr $e): Type => $scope->getType($e), - ); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); @@ -83,20 +73,36 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } + typeCallback: function (MutatingScope $scope) use ($expr, $classResult): Type { + if (!$expr->name instanceof Identifier) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( + $expr->class, + $expr->name->name, + $scope->isInClass() ? $scope->getClassReflection() : null, + // getClassConstFetchTypeByReflection only invokes this for $expr->class + // when it is an Expr, which is exactly when $classResult exists + static function (Expr $e) use ($classResult, $scope): Type { + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + + return $classResult->getTypeForScope($scope); + }, + ); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index 9d2f1bf6574..0492de78402 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -7,14 +7,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\Traverser\CloneTypeTraverser; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ObjectWithoutClassType; @@ -29,6 +28,13 @@ final class CloneHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Clone_; @@ -38,24 +44,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), + typeCallback: static function (MutatingScope $scope) use ($exprResult): Type { + $cloneType = TypeCombinator::intersect($exprResult->getTypeForScope($scope), new ObjectWithoutClassType()); + return TypeTraverser::map($cloneType, new CloneTypeTraverser()); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $cloneType = TypeCombinator::intersect($scope->getType($expr->expr), new ObjectWithoutClassType()); - return TypeTraverser::map($cloneType, new CloneTypeTraverser()); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index cc70889d379..65e801d6350 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -7,27 +7,29 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ClosureHandler implements ExprHandler +final class ClosureHandler implements TypeResolvingExprHandler { public function __construct( private ClosureTypeResolver $closureTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -40,10 +42,11 @@ 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, null); - $scope = $processClosureResult->applyByRefUseScope($processClosureResult->getScope()); - return new ExpressionResult( - $scope, + return $this->expressionResultFactory->create( + $processClosureResult->applyByRefUseScope($processClosureResult->getScope()), + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index eb566e0166d..8faff93160b 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -7,17 +7,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -34,6 +33,8 @@ final class CoalesceHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -43,100 +44,115 @@ public function supports(Expr $expr): bool return $expr instanceof Coalesce; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * A falsey coalesce means its left side was null (when it was surely set) - + * shared by the specifyTypesCallback and by processExpr() for the scope + * the right side evaluates under. + * + * @param Coalesce $expr + */ + private function getFalseySpecifiedTypes(MutatingScope $s, Expr $expr, ExpressionResult $condResult, TypeSpecifierContext $context): SpecifiedTypes { - $issetLeftExpr = new Expr\Isset_([$expr->left]); + $isset = $condResult->issetCheck($s, static fn () => true); - $result = $scope->issetCheck($expr->left, static function (Type $type): ?bool { - $isNull = $type->isNull(); - if ($isNull->maybe()) { - return null; - } - - return !$isNull->yes(); - }); - - if ($result !== null && $result !== false) { - return TypeCombinator::removeNull($scope->filterByTruthyValue($issetLeftExpr)->getType($expr->left)); - } - - $rightType = $scope->filterByFalseyValue($issetLeftExpr)->getType($expr->right); - - if ($result === null) { - return TypeCombinator::union( - TypeCombinator::removeNull($scope->filterByTruthyValue($issetLeftExpr)->getType($expr->left)), - $rightType, - ); - } - - return $rightType; - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if (!$context->true()) { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $isset = $scope->issetCheck($expr->left, static fn () => true); - - if ($isset !== true) { - return new SpecifiedTypes(); - } - - return $typeSpecifier->create( - $expr->left, - new NullType(), - $context->negate(), - $scope, - )->setRootExpr($expr); - } - - if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { - return $typeSpecifier->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - $scope, - )->setRootExpr($expr); + if ($isset !== true) { + return new SpecifiedTypes(); } - // The Coalesce condition matched but produced no narrowing; the legacy - // if/elseif chain fell through to its empty-SpecifiedTypes tail here, - // not to the truthy/falsey default. - return (new SpecifiedTypes([], []))->setRootExpr($expr); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, new NullType(), $context->negate())->setRootExpr($expr); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->left); + $beforeScope = $scope; + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $expr->left); $condScope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $condScope, $storage, $nodeCallback, $context->enterDeep()); $scope = $this->nonNullabilityHelper->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); - $rightScope = $scope->filterByFalseyValue($expr); + // the falsey narrowing of this very node - asking the scope about it + // mid-processing would take the on-demand path and recurse + $rightScope = $scope->applySpecifiedTypes($this->getFalseySpecifiedTypes($scope, $expr, $condResult, TypeSpecifierContext::createFalsey())); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep()); - $rightExprType = $scope->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($scope); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); } else { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $condResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: static function (MutatingScope $s) use ($expr, $condResult, $rightResult, $rightScope): Type { + $issetLeftExpr = new Expr\Isset_([$expr->left]); + + $result = $condResult->issetCheck($s, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + + if ($result !== null && $result !== false) { + return TypeCombinator::removeNull($condResult->getTypeForScope($s->filterByTruthyValue($issetLeftExpr))); + } + + // the right side was processed on the left-is-null scope - that + // captured scope is the evaluation point + $rightType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $rightScope->doNotTreatPhpDocTypesAsCertain() : $rightScope); + + if ($result === null) { + return TypeCombinator::union( + TypeCombinator::removeNull($condResult->getTypeForScope($s->filterByTruthyValue($issetLeftExpr))), + $rightType, + ); + } + + return $rightType; + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $condResult, $rightResult): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + if (!$context->true()) { + return $this->getFalseySpecifiedTypes($s, $expr, $condResult, $context); + } + + if ((new ConstantBooleanType(false))->isSuperTypeOf($rightResult->getTypeForScope($s)->toBoolean())->yes()) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, new NullType(), TypeSpecifierContext::createFalse())->setRootExpr($expr); + } + + // The Coalesce condition matched but produced no narrowing; the legacy + // if/elseif chain fell through to its empty-SpecifiedTypes tail here, + // not to the truthy/falsey default. + return (new SpecifiedTypes([], []))->setRootExpr($expr); + }, + // a type constraint on the coalesce constrains its left side when + // the type rules the right side in or out - what + // TypeSpecifier::create() recovered by unwrapping the coalesce + createTypesCallback: function (MutatingScope $s, Type $type, TypeSpecifierContext $context) use ($expr, $condResult, $rightResult): SpecifiedTypes { + if (!$context->null()) { + $rightType = $rightResult->getTypeForScope($s); + if ( + ($context->true() && $type->isSuperTypeOf($rightType)->no()) + || ($context->false() && $type->isSuperTypeOf($rightType)->yes()) + ) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, $type, $context); + } + } + + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr, null, $type, $context); + }, ); } diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index ba5ae2c71e1..02965694035 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -9,13 +9,12 @@ use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantBooleanType; @@ -33,6 +32,8 @@ final class ConstFetchHandler implements ExprHandler public function __construct( private ConstantResolver $constantResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -46,59 +47,53 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->name, $scope, $storage); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $constName = (string) $expr->name; - $loweredConstName = strtolower($constName); - if ($loweredConstName === 'true') { - return new ConstantBooleanType(true); - } elseif ($loweredConstName === 'false') { - return new ConstantBooleanType(false); - } elseif ($loweredConstName === 'null') { - return new NullType(); - } + typeCallback: function (MutatingScope $scope) use ($expr): Type { + $constName = (string) $expr->name; + $loweredConstName = strtolower($constName); + if ($loweredConstName === 'true') { + return new ConstantBooleanType(true); + } elseif ($loweredConstName === 'false') { + return new ConstantBooleanType(false); + } elseif ($loweredConstName === 'null') { + return new NullType(); + } - $namespacedName = null; - if (!$expr->name->isFullyQualified() && $scope->getNamespace() !== null) { - $namespacedName = new FullyQualified([$scope->getNamespace(), $expr->name->toString()]); - } - $globalName = new FullyQualified($expr->name->toString()); + $namespacedName = null; + if (!$expr->name->isFullyQualified() && $scope->getNamespace() !== null) { + $namespacedName = new FullyQualified([$scope->getNamespace(), $expr->name->toString()]); + } + $globalName = new FullyQualified($expr->name->toString()); - foreach ([$namespacedName, $globalName] as $name) { - if ($name === null) { - continue; - } - $constFetch = new ConstFetch($name); - if ($scope->hasExpressionType($constFetch)->yes()) { - return $this->constantResolver->resolveConstantType( - $name->toString(), - $scope->expressionTypes[$scope->getNodeKey($constFetch)]->getType(), - ); - } - } + foreach ([$namespacedName, $globalName] as $name) { + if ($name === null) { + continue; + } + $constFetch = new ConstFetch($name); + if ($scope->hasExpressionType($constFetch)->yes()) { + return $this->constantResolver->resolveConstantType( + $name->toString(), + $scope->expressionTypes[$scope->getNodeKey($constFetch)]->getType(), + ); + } + } - $constantType = $this->constantResolver->resolveConstant($expr->name, $scope); - if ($constantType !== null) { - return $constantType; - } + $constantType = $this->constantResolver->resolveConstant($expr->name, $scope); + if ($constantType !== null) { + return $constantType; + } - return new ErrorType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return new ErrorType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 185850e6e6f..4f31028040f 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -8,17 +8,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; @@ -32,6 +31,8 @@ final class EmptyHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, ) { } @@ -41,65 +42,43 @@ public function supports(Expr $expr): bool return $expr instanceof Empty_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $result = $scope->issetCheck($expr->expr, static function (Type $type): ?bool { - $isNull = $type->isNull(); - $isFalsey = $type->toBoolean()->isFalse(); - if ($isNull->maybe()) { - return null; - } - if ($isFalsey->maybe()) { - return null; - } - - if ($isNull->yes()) { - return $isFalsey->no(); - } - - return !$isFalsey->yes(); - }); - if ($result === null) { - return new BooleanType(); - } - - return new ConstantBooleanType(!$result); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $isset = $scope->issetCheck($expr->expr, static fn () => true); - if ($isset === false) { - return new SpecifiedTypes(); - } - - return $typeSpecifier->specifyTypesInCondition($scope, new BooleanOr( - new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), - new Expr\BooleanNot($expr->expr), - ), $context)->setRootExpr($expr); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->expr); + $beforeScope = $scope; + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $expr->expr); $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); $scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: static function (MutatingScope $s) use ($exprResult): Type { + $result = $exprResult->empty($s); + if ($result === null) { + return new BooleanType(); + } + + return new ConstantBooleanType(!$result); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $exprResult): SpecifiedTypes { + $isset = $exprResult->issetCheck($s, static fn () => true); + if ($isset === false) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->specifyTypesInCondition($s, new BooleanOr( + new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), + new Expr\BooleanNot($expr->expr), + ), $context)->setRootExpr($expr); + }, ); } diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 0e2e47fee9c..3c847d9fe0c 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -7,13 +7,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; @@ -25,6 +25,13 @@ final class ErrorSuppressHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ErrorSuppress; @@ -32,27 +39,20 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $exprResult->getTruthyScope(), - falseyScopeCallback: static fn (): MutatingScope => $exprResult->getFalseyScope(), + typeCallback: static fn (MutatingScope $s): Type => $exprResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->expr, $exprResult, $context)->setRootExpr($expr), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->expr); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); - } - } diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index c2e635c4880..8d31ed0aee9 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -7,15 +7,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; @@ -29,33 +28,35 @@ final class EvalHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof Eval_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - return new MixedType(); + return $expr instanceof Eval_; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'eval', 'eval', true)]), + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index 7734d8dea34..eff4504ddf3 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -7,14 +7,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; @@ -28,6 +27,13 @@ final class ExitHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Exit_; @@ -35,6 +41,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 { + $beforeScope = $scope; $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT); $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit'; $impurePoints = [ @@ -51,23 +58,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: true, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: static fn (MutatingScope $scope): Type => new NonAcceptingNeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new NonAcceptingNeverType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php deleted file mode 100644 index 266996eaeb2..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php +++ /dev/null @@ -1,81 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableFuncCallHandler implements ExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof FuncCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof Expr) { - $callableType = $scope->getType($expr->name); - if (!$callableType->isCallable()->yes()) { - return new ObjectType(Closure::class); - } - - return $this->initializerExprTypeResolver->createFirstClassCallable( - null, - $callableType->getCallableParametersAcceptors($scope), - $scope->nativeTypesPromoted, - ); - } - - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php deleted file mode 100644 index 1cafdd5b120..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableMethodCallHandler implements ExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof MethodCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if (!$expr->name instanceof Identifier) { - return new ObjectType(Closure::class); - } - - $varType = $scope->getType($expr->var); - $method = $scope->getMethodReflection($varType, $expr->name->toString()); - if ($method === null) { - return new ObjectType(Closure::class); - } - - return $this->initializerExprTypeResolver->createFirstClassCallable( - $method, - $method->getVariants(), - $scope->nativeTypesPromoted, - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php deleted file mode 100644 index e158a8cc7b8..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableNewHandler implements ExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof New_ && !$expr->class instanceof Class_ && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php deleted file mode 100644 index 4d3519cf944..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableStaticCallHandler implements ExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof StaticCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 21c8baa561f..48a2def446c 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -16,8 +16,10 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -95,6 +97,9 @@ public function __construct( private bool $implicitThrows, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -106,20 +111,23 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $functionReflection = null; + $nameResult = null; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); if (!$nameType->isCallable()->no()) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $nameType->getCallableParametersAcceptors($scope), - null, - ); + $variants = $nameType->getCallableParametersAcceptors($scope); + // A structural acceptor (names/positions/variadic) drives the per-arg + // metadata and the throw/impure points - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, null); } $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -145,12 +153,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $functionReflection->getVariants(), - $functionReflection->getNamedArgumentsVariants(), - ); + $variants = $functionReflection->getVariants(); + $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw points - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); @@ -273,8 +281,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $scopeBeforeArgs = $scope; - $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); + $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); @@ -296,6 +306,50 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex )->getScope(); } + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, on-demand / synthetic pricing, or special cases + // inside resolveReturnType), the acceptor is re-derived from the + // already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $nameResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw-point return type is computed: getFunctionThrowPoint() resolves the + // return type through dynamic return type extensions, one of which + // (TypeSpecifyingFunctionsDynamicReturnTypeExtension via + // ImpossibleCheckTypeHelper) narrows this very call. Without a stored result + // that narrowing would re-process this FuncCall on demand and recurse back + // into getFunctionThrowPoint(). The callbacks are scope-independent, so the + // preliminary result answers those asks correctly; the final result below + // overwrites it with the resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($normalizedExpr->name instanceof Expr) { $nameType = $scope->getType($normalizedExpr->name); if ( @@ -318,7 +372,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($functionReflection !== null) { - $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via readStoredOrPriceOnDemand, + // never re-running processArgs) - asking Scope::getType() for the + // FuncCall here would re-enter this handler on demand, as its result is + // not stored yet. + $returnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $nameResult, $resolvedParametersAcceptor); + // The early structural check above (line ~180) only sees the unresolved + // acceptor return type; a conditional-return never (e.g. + // `($x is Foo ? never : string)`) only resolves to never once the actual + // argument types are folded in by the type-driven resolved acceptor. Read + // it from that acceptor's return type, not resolveReturnType(), which + // folds in call_user_func()/dynamic-extension special cases that must not + // make the call itself always-terminating (e.g. + // `call_user_func(fn() => exit())`). + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $returnType, $normalizedExpr, $scope, $context); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; } @@ -570,20 +642,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->afterOpenSslCall($functionReflection->getName()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, + Type $returnType, FuncCall $normalizedFuncCall, MutatingScope $scope, ExpressionContext $context, @@ -604,7 +679,6 @@ private function getFunctionThrowPoint( $throwType = $functionReflection->getThrowType(); if ($throwType === null) { - $returnType = $scope->getType($normalizedFuncCall); if ($returnType instanceof NeverType && $returnType->isExplicit()) { $throwType = new ObjectType(Throwable::class); } @@ -632,8 +706,7 @@ private function getFunctionThrowPoint( || $requiredParameters > 0 || count($normalizedFuncCall->getArgs()) > 0 ) { - $functionReturnedType = $scope->getType($normalizedFuncCall); - if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedFuncCall); } } @@ -785,20 +858,45 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * a callable callee whose name was processed elsewhere), it falls back to a + * structural acceptor combined from the variants - generic resolution from the + * actual arg types lives in $preResolvedAcceptor, recomputed by on-demand / + * synthetic pricing that re-runs processArgs(). + * + * @param FuncCall $expr + */ + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // the operands/arguments were processed during processExpr; read their + // already computed results instead of re-walking via Scope::getType(). + // Synthetic nodes the resolver builds (e.g. Clone_, call_user_func's inner + // FuncCall) are priced on demand by the same helper. + $getType = static function (Expr $e) use ($expr, $nameResult, $scope, $nodeScopeResolver): Type { + if ($nameResult !== null && $e === $expr->name) { + return $nameResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; + if ($expr->name instanceof Expr) { - $calledOnType = $scope->getType($expr->name); + $calledOnType = $getType($expr->name); if ($calledOnType->isCallable()->no()) { return new ErrorType(); } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $calledOnType->getCallableParametersAcceptors($scope), - null, - ); + if ($preResolvedAcceptor !== null) { + $parametersAcceptor = $preResolvedAcceptor; + } else { + $variants = $calledOnType->getCallableParametersAcceptors($scope); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } $functionName = null; if ($expr->name instanceof String_) { @@ -839,7 +937,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type if ($result !== null) { [, $innerFuncCall] = $result; - return $scope->getType($innerFuncCall); + return $getType($innerFuncCall); } } @@ -848,22 +946,24 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type if ($result !== null) { [, $innerFuncCall] = $result; - return $scope->getType($innerFuncCall); + return $getType($innerFuncCall); } } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $functionReflection->getVariants(), - $functionReflection->getNamedArgumentsVariants(), - ); + if ($preResolvedAcceptor !== null) { + $parametersAcceptor = $preResolvedAcceptor; + } else { + $variants = $functionReflection->getVariants(); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr); if ($normalizedNode !== null) { if ($functionReflection->getName() === 'clone' && count($normalizedNode->getArgs()) > 0) { - $cloneType = $scope->getType(new Expr\Clone_($normalizedNode->getArgs()[0]->value)); + $cloneType = $getType(new Expr\Clone_($normalizedNode->getArgs()[0]->value)); if (count($normalizedNode->getArgs()) === 2) { - $propertiesType = $scope->getType($normalizedNode->getArgs()[1]->value); + $propertiesType = $getType($normalizedNode->getArgs()[1]->value); if ($propertiesType->isConstantArray()->yes()) { $constantArrays = $propertiesType->getConstantArrays(); if (count($constantArrays) === 1) { @@ -893,22 +993,26 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * FunctionTypeSpecifyingExtensions, conditional-return-type and @phpstan-assert + * narrowing are invoked on the already-processed argument results. The acceptor + * is $resolvedParametersAcceptor (type-driven, generics resolved by processArgs) + * rather than re-selected from the args on the asking scope. The subject's own + * default narrowing comes from DefaultNarrowingHelper instead of + * TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would re-enter this + * expression through TypeSpecifier::create(). + * + * @param FuncCall $expr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, FuncCall $normalizedExpr, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if ($expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; - } - foreach ($typeSpecifier->getFunctionTypeSpecifyingExtensions() as $extension) { + foreach ($this->typeSpecifier->getFunctionTypeSpecifyingExtensions() as $extension) { if (!$extension->isFunctionSupported($functionReflection, $normalizedExpr, $context)) { continue; } @@ -916,56 +1020,62 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e return $extension->specifyTypes($functionReflection, $normalizedExpr, $scope, $context); } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $functionReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); } - $specifiedTypes = $this->specifyTypesFromCallableCall($typeSpecifier, $context, $expr, $scope); + $specifiedTypes = $this->specifyTypesFromCallableCall($nodeScopeResolver, $context, $expr, $nameResult, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); } - private function specifyTypesFromCallableCall(TypeSpecifier $typeSpecifier, TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes + private function specifyTypesFromCallableCall(NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context, FuncCall $call, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, MutatingScope $scope): ?SpecifiedTypes { if (!$call->name instanceof Expr) { return null; } - $calleeType = $scope->getType($call->name); + $calleeType = $nameResult !== null + ? $nameResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($call->name, $scope); $assertions = null; $parametersAcceptor = null; if ($calleeType->isCallable()->yes()) { - $variants = $calleeType->getCallableParametersAcceptors($scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants); + if ($resolvedParametersAcceptor !== null) { + $parametersAcceptor = $resolvedParametersAcceptor; + } else { + $variants = $calleeType->getCallableParametersAcceptors($scope); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } if ($parametersAcceptor instanceof CallableParametersAcceptor) { $assertions = $parametersAcceptor->getAsserts(); } @@ -982,7 +1092,65 @@ private function specifyTypesFromCallableCall(TypeSpecifier $typeSpecifier, Type TemplateTypeVariance::createInvariant(), )); - return $typeSpecifier->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope); + return $this->typeSpecifier->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a function with side + * effects (or an unknown / impure callee whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s FuncCall handling inside-out, without re-entering + * this expression through create(). + * + */ + private function defaultFuncCallNarrowing(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isFuncCallNarrowable($nodeScopeResolver, $scope, $expr, $nameResult)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + private function isFuncCallNarrowable(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult): bool + { + if ($expr->name instanceof Name) { + if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) { + // backwards compatibility with previous behaviour + return false; + } + + $hasSideEffects = $this->reflectionProvider->getFunction($expr->name, $scope)->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); + } + + $nameType = $nameResult !== null + ? $nameResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); + if (!$nameType->isCallable()->yes()) { + return true; + } + + $isPure = null; + foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) { + $variantIsPure = $variant->isPure(); + $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure); + } + + if ($isPure === null) { + return true; + } + + if ($isPure->no()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $isPure->yes(); } private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type diff --git a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php index e8c1008c183..6c1bcfb85f7 100644 --- a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php +++ b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php @@ -10,7 +10,7 @@ use PHPStan\Analyser\ConditionalExpressionHolder; use PHPStan\Analyser\ExpressionTypeHolder; use PHPStan\Analyser\MutatingScope; -use PHPStan\Analyser\Scope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -37,6 +37,7 @@ public function __construct( } public function augmentDisjunctionTypes( + NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $rightScope, SpecifiedTypes $leftNormalized, @@ -88,9 +89,11 @@ public function augmentDisjunctionTypes( continue; } - $originalType = $scope->getType($targetExpr); - $leftType = $leftFilteredScope->getType($targetExpr); - $rightType = $rightFilteredScope->getType($targetExpr); + // the operands were processed during processExpr; read their stored + // results on these filtered scopes instead of re-walking via getType(). + $originalType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $scope); + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $leftFilteredScope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $rightFilteredScope); if ($leftType->equals($originalType) || !$originalType->isSuperTypeOf($leftType)->yes()) { continue; @@ -141,7 +144,7 @@ public function mergeConditionalHolders(array $holderLists): array /** * @return array */ - public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, Scope $rightScope, ?Expr $holderSideExpr = null): array + public function processBooleanConditionalTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, MutatingScope $rightScope, ?Expr $holderSideExpr = null): array { // The condition side asserts that its sub-expression evaluates truthy. // When that sub-expression is itself a compound boolean (e.g. `$a && $b`), @@ -156,7 +159,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con continue; } - $scopeType = $scope->getType($expr); + $scopeType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); $conditionType = TypeCombinator::remove($scopeType, $type); if ($scopeType->equals($conditionType)) { continue; @@ -174,7 +177,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( $expr, - TypeCombinator::intersect($scope->getType($expr), $type), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $type), ); } @@ -213,7 +216,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con } $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope; - $targetType = $targetScope->getType($expr); + $targetType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $targetScope); $holderType = $holdersFromSureTypes ? TypeCombinator::intersect($targetType, $type) : TypeCombinator::remove($targetType, $type); diff --git a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php new file mode 100644 index 00000000000..84395b4b3d3 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php @@ -0,0 +1,116 @@ +` - they + * emit the plain-chain variant alongside their own key once, and every parent + * simply composes their results. No recursive chain-walking, no type ask. + */ +#[AutowiredService] +final class DefaultNarrowingHelper +{ + + public function __construct( + private ExprPrinter $exprPrinter, + private TypeSpecifier $typeSpecifier, + ) + { + } + + /** + * The narrowing of an already-processed child expression in the given + * boolean context: answered by the child result's specifyTypesCallback. + * Until the child's handler migrates its narrowing - or when the child + * is a synthetic node with no result - this bridges through the + * old-world dispatcher, which answers converted handlers from stored + * results, so the bridge terminates. The bridge dies in 3.0 together + * with TypeSpecifier::specifyTypesInCondition(). + */ + public function getChildSpecifiedTypes(MutatingScope $s, Expr $childExpr, ?ExpressionResult $childResult, TypeSpecifierContext $context): SpecifiedTypes + { + if ($childResult !== null) { + $types = $childResult->getSpecifiedTypesForScope($s, $context); + if ($types !== null) { + return $types; + } + } + + return $this->typeSpecifier->specifyTypesInCondition($s, $childExpr, $context); + } + + public function specifyDefaultTypes(Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + { + if ($context->null()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + if (!$context->truthy()) { + $removedType = StaticTypeFactory::truthy(); + } elseif (!$context->falsey()) { + $removedType = StaticTypeFactory::falsey(); + } else { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return (new SpecifiedTypes(sureNotTypes: [ + $this->exprPrinter->printExpr($expr) => [$expr, $removedType], + ]))->setRootExpr($expr); + } + + /** + * A greatly simplified TypeSpecifier::create() for a subject the calling + * handler has already processed: one sure (truthy) or sureNot (falsey) + * entry for the subject node. A coalesce subject narrows its left side + * when the narrowed type rules the right side in or out. No purity gates, + * no nullsafe chain-walking, no assignment fan-out - an entry about an + * assignment narrows the assigned variables in the appliers, and the + * subject's own narrowing composes in through + * ExpressionResult::getSpecifiedTypesForScope() at the call site. + */ + /** + * A greatly simplified TypeSpecifier::create() for a subject the calling + * handler has already processed: the subject's own result says how a type + * constraint on it translates into entries (an assignment fans out to the + * assigned variable, a coalesce delegates to its left side); without a + * createTypesCallback a single sure (truthy) or sureNot (falsey) entry + * for the subject node is emitted. No purity gates, no nullsafe + * chain-walking, no structural unwrapping - the handlers that own those + * nodes compose their children's results inside-out. + */ + public function createSubjectTypes(MutatingScope $s, Expr $subject, ?ExpressionResult $subjectResult, Type $type, TypeSpecifierContext $context): SpecifiedTypes + { + if ($subjectResult !== null) { + $createdTypes = $subjectResult->getCreatedTypesForScope($s, $type, $context); + if ($createdTypes !== null) { + return $createdTypes; + } + } + + $exprString = $this->exprPrinter->printExpr($subject); + if ($context->true()) { + return new SpecifiedTypes([$exprString => [$subject, $type]], []); + } + if ($context->false()) { + return new SpecifiedTypes(sureNotTypes: [$exprString => [$subject, $type]]); + } + + return new SpecifiedTypes([], []); + } + +} diff --git a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php index c5162b0830e..ee541152646 100644 --- a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php +++ b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php @@ -10,6 +10,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -67,9 +68,9 @@ public function __construct( { } - public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + public function specifyTypesForEqual(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + $expressions = $this->findTypeExpressionsFromBinaryOperation($nodeScopeResolver, $scope, $expr); if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; @@ -181,8 +182,10 @@ public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, Ty } } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + // the operands were processed during processExpr; read their stored results + // instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope->toMutatingScope()); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope->toMutatingScope()); $leftBooleanType = $leftType->toBoolean(); if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) { @@ -246,22 +249,22 @@ public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, Ty return $context->true() ? $leftTypes->unionWith($rightTypes) - : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + : $leftTypes->normalize($scope, $nodeScopeResolver)->intersectWith($rightTypes->normalize($scope, $nodeScopeResolver)); } - public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + public function specifyTypesForIdentical(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $leftExpr = $expr->left; $rightExpr = $expr->right; // Normalize to: fn() === expr if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { - $specifiedTypes = $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $specifiedTypes = $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $rightExpr, $leftExpr, ), $scope, $context); } else { - $specifiedTypes = $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $specifiedTypes = $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $leftExpr, $rightExpr, ), $scope, $context); @@ -270,7 +273,7 @@ public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $s // merge result of fn1() === fn2() and fn2() === fn1() if ($rightExpr instanceof FuncCall && $leftExpr instanceof FuncCall) { return $specifiedTypes->unionWith( - $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $rightExpr, $leftExpr, ), $scope, $context), @@ -280,7 +283,7 @@ public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $s return $specifiedTypes; } - private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + private function specifyTypesForNormalizedIdentical(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $leftExpr = $expr->left; $rightExpr = $expr->right; @@ -294,7 +297,11 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp $unwrappedRightExpr = $rightExpr->getExpr(); } - $rightType = $scope->getType($rightExpr); + // the operands and their subexpressions were processed during processExpr; + // read their stored results instead of re-walking via Scope::getType(). + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope->toMutatingScope()); + + $rightType = $getType($rightExpr); // (count($a) === $expr) if ( @@ -315,16 +322,16 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp && in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true) && count($unwrappedRightExpr->getArgs()) >= 1 ) { - $argType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); - $sizeType = $scope->getType($leftExpr); + $argType = $getType($unwrappedRightExpr->getArgs()[0]->value); + $sizeType = $getType($leftExpr); $specifiedTypes = $this->typeSpecifier->specifyTypesForCountFuncCall($unwrappedRightExpr, $argType, $sizeType, $context, $scope, $expr); if ($specifiedTypes !== null) { return $specifiedTypes; } - $leftArrayType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); - $rightArrayType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); + $leftArrayType = $getType($unwrappedLeftExpr->getArgs()[0]->value); + $rightArrayType = $getType($unwrappedRightExpr->getArgs()[0]->value); if ( $leftArrayType->isArray()->yes() && $rightArrayType->isArray()->yes() @@ -342,7 +349,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp return $this->typeSpecifier->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); } - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); if ($isZero->yes()) { $funcTypes = $this->typeSpecifier->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); @@ -405,7 +412,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); if ($argType->isString()->yes()) { $funcTypes = $this->typeSpecifier->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); @@ -435,7 +442,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp $notNullOnly = $funcName === 'array_find_key'; if ($bothDirections || $notNullOnly) { $args = $unwrappedLeftExpr->getArgs(); - $argType = $scope->getType($args[0]->value); + $argType = $getType($args[0]->value); if ($argType->isArray()->yes()) { if ($bothDirections) { return $this->typeSpecifier->create($args[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); @@ -503,7 +510,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp && isset($unwrappedLeftExpr->getArgs()[0]) && $rightType->isNonEmptyString()->yes() ) { - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); if ($argType->isString()->yes()) { $specifiedTypes = new SpecifiedTypes(); @@ -545,7 +552,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp if ($rightType->isString()->yes()) { $types = null; foreach ($rightType->getConstantStrings() as $constantString) { - $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr); + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($nodeScopeResolver, $unwrappedLeftExpr, $constantString, $context, $scope, $expr); if ($specifiedType === null) { continue; @@ -566,7 +573,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } } - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + $expressions = $this->findTypeExpressionsFromBinaryOperation($nodeScopeResolver, $scope, $expr); if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; @@ -617,7 +624,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } } - $leftType = $scope->getType($leftExpr); + $leftType = $getType($leftExpr); // 'Foo' === $a::class if ( @@ -651,7 +658,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } if ($context->false()) { - $identicalType = $scope->getType($expr); + $identicalType = $getType($expr); if ($identicalType instanceof ConstantBooleanType) { $never = new NeverType(); $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; @@ -750,8 +757,8 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } return $leftTypes->unionWith($rightTypes); } elseif ($context->false()) { - return $this->typeSpecifier->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope) - ->intersectWith($this->typeSpecifier->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope)); + return $this->typeSpecifier->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope, $nodeScopeResolver) + ->intersectWith($this->typeSpecifier->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope, $nodeScopeResolver)); } return (new SpecifiedTypes([], []))->setRootExpr($expr); @@ -760,10 +767,12 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp /** * @return array{Expr, ConstantScalarType, Type}|null */ - private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array + private function findTypeExpressionsFromBinaryOperation(NodeScopeResolver $nodeScopeResolver, Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { - $leftType = $scope->getType($binaryOperation->left); - $rightType = $scope->getType($binaryOperation->right); + // the operands were processed during processExpr; read their stored results + // instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($binaryOperation->left, $scope->toMutatingScope()); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($binaryOperation->right, $scope->toMutatingScope()); $rightExpr = $binaryOperation->right; if ($rightExpr instanceof AlwaysRememberedExpr) { @@ -828,6 +837,7 @@ private function specifyTypesForConstantBinaryExpression( } private function specifyTypesForConstantStringBinaryExpression( + NodeScopeResolver $nodeScopeResolver, Expr $exprNode, Type $constantType, TypeSpecifierContext $context, @@ -889,7 +899,9 @@ private function specifyTypesForConstantStringBinaryExpression( && strtolower((string) $exprNode->name) === 'get_parent_class' && isset($exprNode->getArgs()[0]) ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); + // the argument was processed during processExpr; read its stored result + // instead of re-walking via Scope::getType(). + $argType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprNode->getArgs()[0]->value, $scope->toMutatingScope()); $objectType = new ObjectType($constantStringValue); $classStringType = new GenericClassStringType($objectType); @@ -932,7 +944,9 @@ private function specifyTypesForConstantStringBinaryExpression( && $constantStringValue === '' ) { $argValue = $exprNode->getArgs()[0]->value; - $argType = $scope->getType($argValue); + // the argument was processed during processExpr; read its stored result + // instead of re-walking via Scope::getType(). + $argType = $nodeScopeResolver->readStoredOrPriceOnDemand($argValue, $scope->toMutatingScope()); if ($argType->isString()->yes()) { return $this->typeSpecifier->create( $argValue, diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index eab62846f88..5a2a1ee138b 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -6,8 +6,10 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use function sprintf; @@ -19,24 +21,36 @@ final class ImplicitToStringCallHelper public function __construct( private PhpVersion $phpVersion, private MethodThrowPointHelper $methodThrowPointHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } - public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): ExpressionResult + /** + * @param ExpressionResult|null $exprResult the already-computed result of $expr, + * passed by callers that processed it on $scope so this helper reads its type + * directly instead of re-walking via Scope::getType(); callers that do not + * hold the result (only the Expr) pass null and the type is read from the + * stored result or priced on demand + */ + public function processImplicitToStringCall(NodeScopeResolver $nodeScopeResolver, Expr $expr, MutatingScope $scope, ?ExpressionResult $exprResult = null): ExpressionResult { $throwPoints = []; $impurePoints = []; - $exprType = $scope->getType($expr); + $exprType = $exprResult !== null + ? $exprResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); $toStringMethod = null; if (!$exprType->isObject()->no()) { $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); } if ($toStringMethod === null) { - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -55,20 +69,26 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E } if ($this->phpVersion->throwsOnStringCast()) { + // the __toString() call is a synthetic node - price it on demand to + // resolve its return type instead of re-walking via Scope::getType(). + $toStringCall = new Expr\MethodCall($expr, new Identifier('__toString')); $throwPoint = $this->methodThrowPointHelper->getThrowPoint( $toStringMethod, $toStringMethod->getOnlyVariant(), - new Expr\MethodCall($expr, new Identifier('__toString')), + $toStringCall, $scope, ExpressionContext::createDeep(), + $nodeScopeResolver->priceSyntheticOnDemand($toStringCall, $scope), ); if ($throwPoint !== null) { $throwPoints[] = $throwPoint; } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php b/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php index da565b78d8d..b34386598ce 100644 --- a/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -29,6 +30,7 @@ public function methodCallReturnType( Type $typeWithMethod, string $methodName, MethodCall|Expr\StaticCall $methodCall, + ?ParametersAcceptor $preResolvedAcceptor = null, ): ?Type { $typeWithMethod = $scope->filterTypeWithMethod($typeWithMethod, $methodName); @@ -37,7 +39,7 @@ public function methodCallReturnType( } $methodReflection = $typeWithMethod->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), $methodReflection->getVariants(), diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php index 08ff8735587..a786153f2d0 100644 --- a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use ReflectionFunction; use ReflectionMethod; use Throwable; @@ -31,12 +32,19 @@ public function __construct( { } + /** + * @param Type $methodCallReturnType the resolved return type of $normalizedMethodCall; + * passed in by the caller so this helper never asks Scope::getType() itself + * (the old-world call handlers resolve it directly, the new-world toString + * path prices the synthetic call on demand) + */ public function getThrowPoint( MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall|StaticCall $normalizedMethodCall, MutatingScope $scope, ExpressionContext $context, + Type $methodCallReturnType, ): ?InternalThrowPoint { if ($normalizedMethodCall instanceof MethodCall) { @@ -77,8 +85,7 @@ public function getThrowPoint( $throwType = $methodReflection->getThrowType(); if ($throwType === null) { - $returnType = $scope->getType($normalizedMethodCall); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { + if ($methodCallReturnType instanceof NeverType && $methodCallReturnType->isExplicit()) { $throwType = new ObjectType(Throwable::class); } } @@ -88,8 +95,7 @@ public function getThrowPoint( return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); } } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($methodCallReturnType)->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } } diff --git a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php index 79f5fe0b67c..c3eb36a1778 100644 --- a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php +++ b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php @@ -11,7 +11,7 @@ use PHPStan\Analyser\EnsuredNonNullabilityResult; use PHPStan\Analyser\EnsuredNonNullabilityResultExpression; use PHPStan\Analyser\MutatingScope; -use PHPStan\Analyser\Scope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\TrinaryLogic; use PHPStan\Type\TypeCombinator; @@ -20,9 +20,12 @@ final class NonNullabilityHelper { - public function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult + public function ensureShallowNonNullability(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult { - $exprType = $scope->getType($exprToSpecify); + // the expression has not been processed into the storage yet (this runs + // before processExprNode), so read its type from the stored result or + // price it on demand instead of re-walking via Scope::getType(). + $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $scope); $isNull = $exprType->isNull(); if ($isNull->yes()) { return new EnsuredNonNullabilityResult($scope, []); @@ -32,9 +35,9 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); if ($exprType->equals($exprTypeWithoutNull)) { - $originalExprType = $originalScope->getType($exprToSpecify); + $originalExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $originalScope); if (!$originalExprType->equals($exprTypeWithoutNull)) { - $originalNativeType = $originalScope->getNativeType($exprToSpecify); + $originalNativeType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $originalScope->doNotTreatPhpDocTypesAsCertain()); return new EnsuredNonNullabilityResult($scope, [ new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $hasExpressionType), @@ -52,8 +55,8 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $parentExpr = $exprToSpecify->var; $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression( $parentExpr, - $scope->getType($parentExpr), - $scope->getNativeType($parentExpr), + $nodeScopeResolver->readStoredOrPriceOnDemand($parentExpr, $scope), + $nodeScopeResolver->readStoredOrPriceOnDemand($parentExpr, $scope->doNotTreatPhpDocTypesAsCertain()), $originalScope->hasExpressionType($parentExpr), ); } @@ -64,7 +67,7 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $certainty = $hasExpressionType; } - $nativeType = $scope->getNativeType($exprToSpecify); + $nativeType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $scope->doNotTreatPhpDocTypesAsCertain()); $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty); $scope = $scope->specifyExpressionType( $exprToSpecify, @@ -79,12 +82,12 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina ); } - public function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult + public function ensureNonNullability(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult { $specifiedExpressions = []; $originalScope = $scope; - $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) { - $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr); + $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope, $nodeScopeResolver) { + $result = $this->ensureShallowNonNullability($nodeScopeResolver, $scope, $originalScope, $expr); foreach ($result->getSpecifiedExpressions() as $specifiedExpression) { $specifiedExpressions[] = $specifiedExpression; } diff --git a/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php b/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php deleted file mode 100644 index 72710dd93b5..00000000000 --- a/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php +++ /dev/null @@ -1,54 +0,0 @@ -getType($expr->var); - if (TypeCombinator::containsNull($varType)) { - return TypeCombinator::addNull($type); - } - - return $type; - } - - if ($expr instanceof ArrayDimFetch) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof PropertyFetch) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { - return self::getType($scope, $expr->class, $type); - } - - if ($expr instanceof MethodCall) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof StaticCall && $expr->class instanceof Expr) { - return self::getType($scope, $expr->class, $type); - } - - return $type; - } - -} diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 788a7c16522..0b0f4173821 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -7,15 +7,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; @@ -30,34 +29,36 @@ final class IncludeHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof Include_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - return new MixedType(); + return $expr instanceof Include_; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $identifier = in_array($expr->type, [Include_::TYPE_INCLUDE, Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require'; $scope = $exprResult->getScope()->afterExtractCall(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, $identifier, $identifier, true)]), + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 36933e466b3..1ada2bd80b7 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -8,15 +8,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\MixedType; @@ -38,6 +39,13 @@ final class InstanceofHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Instanceof_; @@ -45,12 +53,14 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); $impurePoints = $exprResult->getImpurePoints(); $isAlwaysTerminating = $exprResult->isAlwaysTerminating(); $scope = $exprResult->getScope(); + $classResult = null; if (!$expr->class instanceof Name) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); @@ -60,112 +70,120 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $expressionType = $scope->getType($expr->expr); - if ( - $scope->isInTrait() - && TypeUtils::findThisType($expressionType) !== null - ) { - return new BooleanType(); - } - if ($expressionType instanceof NeverType) { - return new ConstantBooleanType(false); - } - - $uncertainty = false; - - if ($expr->class instanceof Name) { - $unresolvedClassName = $expr->class->toString(); - if ( - strtolower($unresolvedClassName) === 'static' - && $scope->isInClass() - ) { - $classType = new StaticType($scope->getClassReflection()); - } else { - $className = $scope->resolveName($expr->class); - $classType = new ObjectType($className); - } - } else { - $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); - $classType = $result->type; - $uncertainty = $result->uncertainty; - } + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $classResult): Type { + $expressionType = $exprResult->getTypeForScope($s); + if ( + $s->isInTrait() + && TypeUtils::findThisType($expressionType) !== null + ) { + return new BooleanType(); + } + if ($expressionType instanceof NeverType) { + return new ConstantBooleanType(false); + } - if ($classType->isSuperTypeOf(new MixedType())->yes()) { - return new BooleanType(); - } + $uncertainty = false; + + if ($expr->class instanceof Name) { + $unresolvedClassName = $expr->class->toString(); + if ( + strtolower($unresolvedClassName) === 'static' + && $s->isInClass() + ) { + $classType = new StaticType($s->getClassReflection()); + } else { + $className = $s->resolveName($expr->class); + $classType = new ObjectType($className); + } + } else { + // this branch is only reached when $expr->class is an Expr, + // which is exactly when $classResult was set in processExpr + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + $classNameType = $classResult->getTypeForScope($s); + $result = $classNameType->toObjectTypeForInstanceofCheck(); + $classType = $result->type; + $uncertainty = $result->uncertainty; + } - $isSuperType = $classType->isSuperTypeOf($expressionType); + if ($classType->isSuperTypeOf(new MixedType())->yes()) { + return new BooleanType(); + } - if ($isSuperType->no()) { - return new ConstantBooleanType(false); - } elseif ($isSuperType->yes() && !$uncertainty) { - return new ConstantBooleanType(true); - } + $isSuperType = $classType->isSuperTypeOf($expressionType); - return new BooleanType(); - } + if ($isSuperType->no()) { + return new ConstantBooleanType(false); + } elseif ($isSuperType->yes() && !$uncertainty) { + return new ConstantBooleanType(true); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - $exprNode = $expr->expr; - if ($expr->class instanceof Name) { - $className = (string) $expr->class; - $lowercasedClassName = strtolower($className); - if ($lowercasedClassName === 'self' && $scope->isInClass()) { - $type = new ObjectType($scope->getClassReflection()->getName()); - } elseif ($lowercasedClassName === 'static' && $scope->isInClass()) { - $type = new StaticType($scope->getClassReflection()); - } elseif ($lowercasedClassName === 'parent') { - if ( - $scope->isInClass() - && $scope->getClassReflection()->getParentClass() !== null - ) { - $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName()); - } else { - $type = new NonexistentParentClassType(); + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $exprResult, $classResult): SpecifiedTypes { + $exprNode = $expr->expr; + if ($expr->class instanceof Name) { + $className = (string) $expr->class; + $lowercasedClassName = strtolower($className); + if ($lowercasedClassName === 'self' && $s->isInClass()) { + $type = new ObjectType($s->getClassReflection()->getName()); + } elseif ($lowercasedClassName === 'static' && $s->isInClass()) { + $type = new StaticType($s->getClassReflection()); + } elseif ($lowercasedClassName === 'parent') { + if ( + $s->isInClass() + && $s->getClassReflection()->getParentClass() !== null + ) { + $type = new ObjectType($s->getClassReflection()->getParentClass()->getName()); + } else { + $type = new NonexistentParentClassType(); + } + } else { + $type = new ObjectType($className); + } + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); } - } else { - $type = new ObjectType($className); - } - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); - } - $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); - $type = $result->type; - $uncertainty = $result->uncertainty; - - if (!$type->isSuperTypeOf(new MixedType())->yes()) { - if ($context->true()) { - $type = TypeCombinator::intersect( - $type, - new ObjectWithoutClassType(), - ); - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); - } elseif ($context->false() && !$uncertainty) { - $exprType = $scope->getType($expr->expr); - if (!$type->isSuperTypeOf($exprType)->yes()) { - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); + // this branch is only reached when $expr->class is an Expr, + // which is exactly when $classResult was set in processExpr + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + $classNameType = $classResult->getTypeForScope($s); + $result = $classNameType->toObjectTypeForInstanceofCheck(); + $type = $result->type; + $uncertainty = $result->uncertainty; + + if (!$type->isSuperTypeOf(new MixedType())->yes()) { + if ($context->true()) { + $type = TypeCombinator::intersect( + $type, + new ObjectWithoutClassType(), + ); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); + } elseif ($context->false() && !$uncertainty) { + $exprType = $exprResult->getTypeForScope($s); + if (!$type->isSuperTypeOf($exprType)->yes()) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); + } + } + } + if ($context->true()) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, new ObjectWithoutClassType(), $context)->setRootExpr($exprNode); } - } - } - if ($context->true()) { - return $typeSpecifier->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode); - } - return (new SpecifiedTypes([], []))->setRootExpr($expr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); + }, + ); } } diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 51de44f579f..577938afa3f 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -8,20 +8,20 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use function array_merge; +use function spl_object_id; /** * @implements ExprHandler @@ -33,6 +33,8 @@ final class InterpolatedStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -44,20 +46,24 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + /** @var array $partResults */ + $partResults = []; foreach ($expr->parts as $part) { if (!$part instanceof Expr) { continue; } $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $partResults[spl_object_id($part)] = $partResult; $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $part, $scope, $partResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); @@ -65,38 +71,34 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $partResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $scope) use ($expr, $partResults): Type { + $resultType = null; + foreach ($expr->parts as $part) { + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $partResults[spl_object_id($part)]->getTypeForScope($scope)->toString(); + } + if ($resultType === null) { + $resultType = $partType; + continue; + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $resultType = null; - foreach ($expr->parts as $part) { - if ($part instanceof InterpolatedStringPart) { - $partType = new ConstantStringType($part->value); - } else { - $partType = $scope->getType($part)->toString(); - } - if ($resultType === null) { - $resultType = $partType; - continue; - } + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + } - $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); - } - - return $resultType ?? new ConstantStringType(''); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $resultType ?? new ConstantStringType(''); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index da8e48431fd..89589a74bfb 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -15,14 +15,15 @@ use PhpParser\Node\VarLikeIdentifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -51,14 +52,15 @@ use function is_string; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class IssetHandler implements ExprHandler +final class IssetHandler implements TypeResolvingExprHandler { public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -344,13 +346,14 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; $nonNullabilityResults = []; $isAlwaysTerminating = false; foreach ($expr->vars as $var) { - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $var); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $var); $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $varResult->getScope(); @@ -385,14 +388,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 90d4714a5af..047d0acc6cf 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -16,14 +16,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -57,6 +57,8 @@ final class MatchHandler implements ExprHandler public function __construct( #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -66,16 +68,6 @@ public function supports(Expr $expr): bool return $expr instanceof Match_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $types = []; - foreach ($this->getArmScopesAndTypes($scope, $expr) as [$armScope, $armType]) { - $types[] = $armType; - } - - return TypeCombinator::union(...$types); - } - /** * For each reachable match arm, returns the arm's body type together with the * scope in which the match subject is narrowed to that arm's condition. This @@ -85,10 +77,12 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type * * @return list */ - public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array + public function getArmScopesAndTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Match_ $expr): array { $cond = $expr->cond; - $condType = $scope->getType($cond); + // the subject was processed before this shadow walk runs; read its stored + // result on the incoming scope instead of re-walking via Scope::getType(). + $condType = $nodeScopeResolver->readStoredOrPriceOnDemand($cond, $scope); $armScopesAndTypes = []; $matchScope = $scope; @@ -150,7 +144,10 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array $cond, $conditionCaseType, ); - $armScopesAndTypes[] = [$armScope, $armScope->getType($arm->body)]; + // the arm body is read on the subject-narrowed scope this shadow + // walk built; that (body, narrowed-scope) pair is not stored, so + // price the body on demand against the current storage. + $armScopesAndTypes[] = [$armScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $armScope)]; unset($arms[$i]); } @@ -179,7 +176,7 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array if ($expr->hasAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)) { $arm->body->setAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME, $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)); } - $armScopesAndTypes[] = [$matchScope, $matchScope->getType($arm->body)]; + $armScopesAndTypes[] = [$matchScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $matchScope)]; continue; } @@ -189,14 +186,16 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array $filteringExpr = $this->getFilteringExprForMatchArm($expr, $arm->conds); - $filteringExprType = $matchScope->getType($filteringExpr); + // the filtering expression is synthetic - price it on demand against the + // current storage instead of re-walking via Scope::getType(). + $filteringExprType = $nodeScopeResolver->priceSyntheticOnDemand($filteringExpr, $matchScope); if (!$filteringExprType->isFalse()->yes()) { $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); if ($expr->hasAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)) { $arm->body->setAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME, $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)); } - $armScopesAndTypes[] = [$truthyScope, $truthyScope->getType($arm->body)]; + $armScopesAndTypes[] = [$truthyScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $truthyScope)]; } $matchScope = $matchScope->filterByFalseyValue($filteringExpr); @@ -207,10 +206,13 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $deepContext = $context->enterDeep(); - $condType = $scope->getType($expr->cond); - $condNativeType = $scope->getNativeType($expr->cond); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); + // the subject was just processed on this scope; read its result instead of + // re-walking via Scope::getType(). + $condType = $condResult->getTypeForScope($scope); + $condNativeType = $condResult->getNativeTypeForScope($scope); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); @@ -223,6 +225,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arms = $expr->arms; $armCondsToSkip = []; $armBodyScopes = []; + // Capture, for each reachable arm, the body's already-computed + // ExpressionResult together with the scope it was processed on and the + // body node itself. The typeCallback unions these inside-out instead of + // re-walking the arms (which getArmScopesAndTypes/the old resolveType + // did). The set of contributing arms mirrors getArmScopesAndTypes + // exactly. The body node is kept so the keepVoid projection (the only + // caller is getKeepVoidType, via a synthetic clone of the match) can be + // computed for it. + /** @var list $armTypeResults */ + $armTypeResults = []; if ($condType->isEnum()->yes()) { // enum match analysis would work even without this if branch // but would be much slower @@ -364,6 +376,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + $armTypeResults[] = [$armResult, $matchArmBodyScope, $arm->body]; unset($arms[$i]); } @@ -390,6 +403,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex foreach ($arms as $i => $arm) { if ($arm->conds === null) { $hasDefaultCond = true; + $defaultArmBodyScope = $matchScope; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); $armResult = $nodeScopeResolver->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); @@ -400,6 +414,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if (!$armResult->isAlwaysTerminating()) { $armBodyScopes[] = $matchScope; } + $armTypeResults[] = [$armResult, $defaultArmBodyScope, $arm->body]; continue; } @@ -423,7 +438,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); $armCondResultScope = $armCondResult->getScope(); - $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + // the `subject === cond` comparison is synthetic - price it on demand + // against the current storage instead of re-walking via Scope::getType(). + $armCondType = $this->treatPhpDocTypesAsCertain + ? $nodeScopeResolver->priceSyntheticOnDemand($armCondExpr, $armCondResultScope) + : $nodeScopeResolver->priceSyntheticOnDemand($armCondExpr, $armCondResultScope->doNotTreatPhpDocTypesAsCertain()); if ($armCondType->isTrue()->yes()) { $hasAlwaysTrueCond = true; } @@ -456,6 +475,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + // Mirror getArmScopesAndTypes: an arm whose filtering expression is + // always false is unreachable and does not contribute to the result + // type. The filtering expression is synthetic - price it on demand + // against the current storage instead of re-walking via Scope::getType(). + $filteringExprType = $nodeScopeResolver->priceSyntheticOnDemand($filteringExpr, $matchScope); + if (!$filteringExprType->isFalse()->yes()) { + $armTypeResults[] = [$armResult, $bodyScope, $arm->body]; + } $matchScope = $armCondScope->filterByFalseyValue($filteringExpr); } @@ -470,7 +497,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond; if (!$isExhaustive) { - $remainingType = $matchScope->getType($expr->cond); + // the subject was processed above ($condResult); read its type on the + // arm-narrowed scope instead of re-walking via Scope::getType(). + $remainingType = $condResult->getTypeForScope($matchScope); if ($remainingType instanceof NeverType) { $isExhaustive = true; } @@ -501,12 +530,38 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->cond = $expr->cond->getExpr(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // Each arm body was already processed on the scope where the subject + // is narrowed to that arm's condition - those captured scopes are the + // evaluation points, so the result type is just the union of the arm + // body types, no re-walk of the arms needed. + typeCallback: static function (MutatingScope $s) use ($expr, $armTypeResults): Type { + $keepVoid = $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME) === true; + $types = []; + foreach ($armTypeResults as [$armResult, $bodyScope, $armBody]) { + if ($s->nativeTypesPromoted) { + $bodyScope = $bodyScope->doNotTreatPhpDocTypesAsCertain(); + } + if ($keepVoid) { + // The only caller is getKeepVoidType (via a synthetic + // clone of the match) - it keeps void in the arm bodies + // instead of transforming it to null. + $types[] = $bodyScope->getKeepVoidType($armBody); + } else { + $types[] = $armResult->getTypeForScope($bodyScope); + } + } + + return TypeCombinator::union(...$types); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } @@ -585,9 +640,4 @@ private function scopeHasNeverVariable(MutatingScope $scope, array $varNames): b return false; } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 1f661f5a980..5886fa810aa 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -11,16 +11,16 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -30,6 +30,7 @@ use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ErrorType; @@ -60,6 +61,9 @@ public function __construct( private ReflectionProvider $reflectionProvider, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -71,6 +75,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 { + $beforeScope = $scope; $originalScope = $scope; if ( ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction) @@ -94,24 +99,28 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $methodReflection = null; - $calledOnType = $scope->getType($expr->var); + $nameResult = null; + // the var was processed above as the receiver; read its already-computed + // result instead of re-walking via Scope::getType(). + $calledOnType = $varResult->getTypeForScope($scope); if ($expr->name instanceof Identifier) { $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), - ); - + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw point - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } else { - $methodNameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); - $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); - $scope = $methodNameResult->getScope(); + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $scope = $nameResult->getScope(); } if ($methodReflection !== null) { @@ -140,17 +149,79 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $stmt, $methodReflection, $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, - $parametersAcceptor, + $variants, + $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, ); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is + // re-derived from the already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $varResult, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $varResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw point is computed: the method throw point resolves the return type + // (resolveReturnType below) through dynamic return type extensions, which can + // narrow this very call on demand. Without a stored result that narrowing + // would re-process this MethodCall on demand and recurse. The callbacks are + // scope-independent, so the preliminary result answers those asks correctly; + // the final result below overwrites it with the resolved scope and + // throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + containsNullsafe: $varResult->containsNullsafe(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + // The early structural check above only sees the unresolved acceptor + // return type; a conditional-return never (e.g. `($x is Foo ? never : + // string)`) only resolves to never once the actual argument types are + // folded in by the type-driven resolved acceptor. + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via the receiver/name results and + // readStoredOrPriceOnDemand, never re-running processArgs) - asking + // Scope::getType() for the MethodCall here would re-enter this handler on + // demand, as its final result is not stored yet. + $methodCallReturnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $methodCallReturnType); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -159,21 +230,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage); $scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass()); } elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin()) { + // the remembered call value and the @phpstan-self-out type are + // generic-sensitive: resolve them from the type-driven acceptor + // processArgs() selected (generics resolved against the actual arg + // types), falling back to the structural acceptor for dynamic callees. + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr->var, sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())), - $parametersAcceptor->getReturnType(), + $acceptorForGenerics->getReturnType(), new MixedType(), ); } if (!$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); if ($selfOutType !== null) { + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( $normalizedExpr->var, TemplateTypeHelper::resolveTemplateTypes( $selfOutType, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptorForGenerics->getResolvedTemplateTypeMap(), + $acceptorForGenerics instanceof ExtendedParametersAcceptor ? $acceptorForGenerics->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createCovariant(), ), $scope->getNativeType($normalizedExpr->var), @@ -191,17 +268,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - $result = new ExpressionResult( + $result = $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: $varResult->containsNullsafe(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); - $calledOnType = $originalScope->getType($expr->var); + // the var was processed above as the receiver; read its already-computed + // result on the original scope instead of re-walking via Scope::getType(). + $calledOnType = $varResult->getTypeForScope($originalScope); if (!$expr->name instanceof Identifier) { return $result; } @@ -219,14 +301,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $calledMethodScope = $nodeScopeResolver->processCalledMethod($methodReflection); if ($calledMethodScope !== null) { $scope = $scope->mergeInitializedProperties($calledMethodScope); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: $varResult->containsNullsafe(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } } @@ -234,12 +319,32 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $result; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * on-demand / synthetic pricing) it falls back to re-selecting from the args via + * MethodCallReturnTypeHelper on the asking scope. + * + * The receiver/name were processed during processExpr; their already computed + * results are read instead of re-walking via Scope::getType(). The dynamic-name + * branch builds a synthetic MethodCall priced on demand by the resolver. + * + * @param MethodCall $expr + */ + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ExpressionResult $varResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // a call on a nullsafe chain whose receiver is currently nullable + // short-circuits to null - the receiver result carries whether the chain + // contains a ?-> (a plain nullable receiver does not propagate). + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($varResult->getTypeForScope($scope)) + ? TypeCombinator::addNull($type) + : $type; + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { $methodReflection = $scope->getMethodReflection( - $scope->getNativeType($expr->var), + $varResult->getNativeTypeForScope($scope), $expr->name->name, ); if ($methodReflection === null) { @@ -248,51 +353,70 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); + return $shortCircuit($returnType); } $returnType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, - $scope->getType($expr->var), + $varResult->getTypeForScope($scope), $expr->name->name, $expr, + $preResolvedAcceptor, ); if ($returnType === null) { $returnType = new ErrorType(); } - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); + return $shortCircuit($returnType); } - $nameType = $scope->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($scope) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new MethodCall($expr->var, new Identifier($constantString->getValue()), $expr->args)), $nameType->getConstantStrings()), + ...array_map(static function ($constantString) use ($expr, $scope, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a method call with a concrete name on the name-pinned scope + // is synthetic. + $truthyScope = $scope->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( + new MethodCall($expr->var, new Identifier($constantString->getValue()), $expr->args), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); } return new MixedType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * MethodTypeSpecifyingExtensions, conditional-return-type and @phpstan-assert + * narrowing are invoked on the already-processed argument results. The acceptor + * is $resolvedParametersAcceptor (type-driven, generics resolved by processArgs) + * rather than re-selected from the args on the asking scope. The subject's own + * default narrowing comes from DefaultNarrowingHelper instead of + * TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would re-enter this + * expression through TypeSpecifier::create(). + * + * @param MethodCall $expr + * @param MethodCall $normalizedExpr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ExpressionResult $varResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if (!$expr->name instanceof Identifier) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultMethodCallNarrowing($scope, $expr, $varResult, $context); } - $methodCalledOnType = $scope->getType($expr->var); + // the var was processed during processExpr; read its already-computed + // result instead of re-walking via Scope::getType(). + $methodCalledOnType = $varResult->getTypeForScope($scope); $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name); if ($methodReflection !== null) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; - } $referencedClasses = $methodCalledOnType->getObjectClassNames(); if ( @@ -300,7 +424,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e && $this->reflectionProvider->hasClass($referencedClasses[0]) ) { $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - foreach ($typeSpecifier->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { + foreach ($this->typeSpecifier->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { if (!$extension->isMethodSupported($methodReflection, $normalizedExpr, $context)) { continue; } @@ -309,33 +433,71 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $methodReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultMethodCallNarrowing($scope, $expr, $varResult, $context); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a method with side + * effects (or an unknown method whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s MethodCall handling inside-out, without + * re-entering this expression through create(). + * + * @param MethodCall $expr + */ + private function defaultMethodCallNarrowing(MutatingScope $scope, Expr $expr, ExpressionResult $varResult, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isMethodCallNarrowable($scope, $expr, $varResult)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + /** @param MethodCall $expr */ + private function isMethodCallNarrowable(MutatingScope $scope, Expr $expr, ExpressionResult $varResult): bool + { + if (!$expr->name instanceof Identifier) { + return true; + } + + $calledOnType = $varResult->getTypeForScope($scope); + $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + + $hasSideEffects = $methodReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); } } diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 2360ccd8076..4ec578a6650 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -11,8 +11,10 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -34,6 +36,7 @@ use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\DummyConstructorReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -77,6 +80,9 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -88,6 +94,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 { + $beforeScope = $scope; $parametersAcceptor = null; $constructorReflection = null; $classReflection = null; @@ -97,6 +104,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $isAlwaysTerminating = false; $normalizedExpr = $expr; + $className = null; if ($expr->class instanceof Name) { $className = $scope->resolveName($expr->class); @@ -111,12 +119,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization and the throw point - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; @@ -160,9 +166,24 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } else { $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); } + + if ($parametersAcceptor !== null) { + $normalizedExpr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } } else { $isDynamic = true; - $objectClasses = $scope->getType($expr)->getObjectClassNames(); + + $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); + $scope = $classResult->getScope(); + $hasYield = $classResult->hasYield(); + $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); + + // The instantiated object type derives from the class expression - read + // its already-processed result rather than asking Scope::getType() for + // the not-yet-stored New_ node, which would re-enter this handler. + $objectClasses = $classResult->getTypeForScope($scope)->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); if (count($objectClasses) === 1) { $objectExprResult = $nodeScopeResolver->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); $className = $objectClasses[0]; @@ -172,12 +193,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; } - $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $classResult->getScope(); - $hasYield = $classResult->hasYield(); - $throwPoints = $classResult->getThrowPoints(); - $impurePoints = $classResult->getImpurePoints(); - $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $throwPoints = array_merge($throwPoints, $additionalThrowPoints); if ($className !== null) { @@ -198,13 +213,56 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $variants = $constructorReflection !== null ? $constructorReflection->getVariants() : []; + $namedArgumentsVariants = $constructorReflection !== null ? $constructorReflection->getNamedArgumentsVariants() : null; + $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $hasYield || $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); + // The new-expression type is derived from $resolvedParametersAcceptor - the + // constructor acceptor processArgs() selected from the arg types gathered on + // the arg-to-arg evolving scope (type-driven, resolves the class's @template + // parameters from constructor args). When null (native-types-promoted, or + // on-demand / synthetic pricing), resolveReturnType() re-selects a structural + // acceptor from the args on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $s, + $expr, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw-point return type is computed: getConstructorThrowPoint() and the + // exact-instantiation return type resolution can re-enter on demand (e.g. a + // dynamic static-method return type extension narrowing this very + // instantiation). Without a stored result that narrowing would re-process + // this New_ on demand and recurse. The callbacks are scope-independent, so + // the preliminary result answers those asks correctly; the final result + // below overwrites it with the resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($constructorReflection !== null && $parametersAcceptor !== null) { $className ??= $constructorReflection->getDeclaringClass()->getName(); $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope, $context); @@ -215,17 +273,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } /** - * @return array{?MethodReflection, ?ClassReflection, ?ParametersAcceptor, ImpurePoint[]} + * @return array{?ExtendedMethodReflection, ?ClassReflection, ?ParametersAcceptor, ImpurePoint[]} */ private function processConstructorReflection(string $className, New_ $expr, MutatingScope $scope, bool $isDynamic): array { @@ -238,12 +300,10 @@ private function processConstructorReflection(string $className, New_ $expr, Mut $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization and the throw point - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); } } @@ -315,10 +375,19 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return null; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * The stored new-expression type is derived from $preResolvedAcceptor - the + * constructor acceptor processArgs() selected from the arg types gathered on + * the arg-to-arg evolving scope (resolves the class's @template parameters + * from constructor args). Null falls back to re-selecting a structural acceptor + * from the args on the asking scope (on-demand / synthetic pricing). + * + * @param New_ $expr + */ + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type { if ($expr->class instanceof Name) { - return $this->exactInstantiation($scope, $expr, $expr->class); + return $this->exactInstantiation($scope, $expr, $expr->class, $preResolvedAcceptor); } if ($expr->class instanceof Node\Stmt\Class_) { $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); @@ -326,11 +395,13 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return new ObjectType($anonymousClassReflection->getName()); } - $exprType = $scope->getType($expr->class); + // the class expression was processed during processExpr; read its already + // computed result instead of re-walking via Scope::getType(). + $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); return $exprType->getObjectTypeOrClassStringObjectType(); } - private function exactInstantiation(MutatingScope $scope, New_ $node, Name $className): Type + private function exactInstantiation(MutatingScope $scope, New_ $node, Name $className, ?ParametersAcceptor $preResolvedAcceptor): Type { $resolvedClassName = $scope->resolveName($className); $isStatic = false; @@ -376,8 +447,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas $node->getArgs(), ); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::combineVariantsForNormalization( $methodCall->getArgs(), $constructorMethod->getVariants(), $constructorMethod->getNamedArgumentsVariants(), @@ -407,6 +477,9 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas return TypeCombinator::union(...$resolvedTypes); } + // $methodCall is a synthetic StaticCall the handler built - it is not a + // source node, so Scope::getType() prices it on demand (the constructor's + // own never-returning conditional return type). $methodResult = $scope->getType($methodCall); if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { return $methodResult; @@ -612,13 +685,24 @@ classReflection: $classReflection->withTypes($types)->asFinal(), return TypeTraverser::map($newGenericType, new GenericTypeTemplateTraverser($resolvedTemplateTypeMap)); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * constructor's @phpstan-assert narrowing is invoked on the already-processed + * argument results. The acceptor is $resolvedParametersAcceptor (type-driven, + * generics resolved by processArgs) rather than re-selected from the args on + * the asking scope. The subject's own default narrowing comes from + * DefaultNarrowingHelper instead of TypeSpecifier::specifyDefaultTypes(), which + * would re-enter this expression through TypeSpecifier::create(). + * + * @param New_ $expr + */ + private function specifyTypes(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if ( !$expr->class instanceof Name || !$this->reflectionProvider->hasClass($expr->class->toString()) ) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); @@ -627,17 +711,15 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e $methodReflection = $classReflection->getConstructor(); $asserts = $methodReflection->getAsserts(); - if ($asserts->getAll() !== []) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - + if ($asserts->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -645,6 +727,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } + // A known class without (applicable) constructor asserts contributes no + // narrowing entry, mirroring the old handler's empty return for this path + // (a `new X()` is always a truthy object, so the default truthy/falsey + // removal that path 1 emits would be a no-op here anyway). return (new SpecifiedTypes([], []))->setRootExpr($expr); } diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 864a4859275..e0de6903c97 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -12,12 +12,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -37,6 +38,9 @@ final class NullsafeMethodCallHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -46,58 +50,23 @@ public function supports(Expr $expr): bool return $expr instanceof NullsafeMethodCall; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->var); - if ($varType->isNull()->yes()) { - return new NullType(); - } - if (!TypeCombinator::containsNull($varType)) { - return $scope->getType(new MethodCall($expr->var, $expr->name, $expr->args)); - } - - return TypeCombinator::union( - $scope->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new MethodCall($expr->var, $expr->name, $expr->args)), - new NullType(), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - $types = $typeSpecifier->specifyTypesInCondition( - $scope, - new BooleanAnd( - new NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new MethodCall($expr->var, $expr->name, $expr->args), - ), - $context, - )->setRootExpr($expr); - - $nullSafeTypes = $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $scopeBeforeNullsafe = $scope; - $varType = $scope->getType($expr->var); - $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($nodeScopeResolver, $scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); + $methodCall = new MethodCall( + $expr->var, + $expr->name, + $expr->args, + $attributes, + ); $exprResult = $nodeScopeResolver->processExprNode( $stmt, - new MethodCall( - $expr->var, - $expr->name, - $expr->args, - $attributes, - ), + $methodCall, $nonNullabilityResult->getScope(), $storage, $nodeCallback, @@ -105,6 +74,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); + // the var was processed above as the receiver of $methodCall; read its + // stored result on the original scope instead of re-walking via getType(). + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $scopeBeforeNullsafe); + $varIsNull = $varType->isNull(); if ($varIsNull->yes()) { // Arguments are never evaluated when the var is always null. @@ -115,14 +88,50 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->mergeWith($scopeBeforeNullsafe); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: true, + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $nodeScopeResolver): Type { + // the var was processed above as the receiver of $methodCall. + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $s); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($s); + } + + // the plain method call on the null-removed scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))); + + return TypeCombinator::union( + $nodeScopeResolver->priceSyntheticOnDemand(new MethodCall($expr->var, $expr->name, $expr->args), $truthyScope), + new NullType(), + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $methodCall, $nodeScopeResolver): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + $types = $this->typeSpecifier->specifyTypesInCondition( + $s, + new BooleanAnd( + new NotIdentical($expr->var, new ConstFetch(new Name('null'))), + $methodCall, + ), + $context, + )->setRootExpr($expr); + + $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s, $nodeScopeResolver)->intersectWith($nullSafeTypes->normalize($s, $nodeScopeResolver)); + }, ); } diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 0c9e190ff47..d7b94e6a7b6 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -12,12 +12,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -37,6 +38,9 @@ final class NullsafePropertyFetchHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -46,62 +50,65 @@ public function supports(Expr $expr): bool return $expr instanceof NullsafePropertyFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->var); - if ($varType->isNull()->yes()) { - return new NullType(); - } - if (!TypeCombinator::containsNull($varType)) { - return $scope->getType(new PropertyFetch($expr->var, $expr->name)); - } - - return TypeCombinator::union( - $scope->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new PropertyFetch($expr->var, $expr->name)), - new NullType(), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - $types = $typeSpecifier->specifyTypesInCondition( - $scope, - new BooleanAnd( - new NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new PropertyFetch($expr->var, $expr->name), - ), - $context, - )->setRootExpr($expr); - - $nullSafeTypes = $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); + $beforeScope = $scope; + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($nodeScopeResolver, $scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); - $exprResult = $nodeScopeResolver->processExprNode($stmt, new PropertyFetch( + $propertyFetch = new PropertyFetch( $expr->var, $expr->name, $attributes, - ), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); + ); + $exprResult = $nodeScopeResolver->processExprNode($stmt, $propertyFetch, $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: true, + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $nodeScopeResolver): Type { + // the var was processed above as the receiver of $propertyFetch - + // read its stored result instead of re-walking via Scope::getType(). + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $s); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($s); + } + + // the plain property fetch on the null-removed scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))); + + return TypeCombinator::union( + $nodeScopeResolver->priceSyntheticOnDemand(new PropertyFetch($expr->var, $expr->name), $truthyScope), + new NullType(), + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $propertyFetch, $nodeScopeResolver): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + $types = $this->typeSpecifier->specifyTypesInCondition( + $s, + new BooleanAnd( + new NotIdentical($expr->var, new ConstFetch(new Name('null'))), + $propertyFetch, + ), + $context, + )->setRootExpr($expr); + + $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s, $nodeScopeResolver)->intersectWith($nullSafeTypes->normalize($s, $nodeScopeResolver)); + }, ); } diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 93bd3554ef7..54073bc043b 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -11,16 +11,19 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\FunctionCallableNode; +use PHPStan\Node\MethodCallableNode; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Parser\ReversePipeTransformerVisitor; use PHPStan\Type\Type; use function array_merge; @@ -32,30 +35,16 @@ final class PipeHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof Pipe; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new FuncCall($expr->right->name, [ - new Arg($expr->left), - ])); - } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new MethodCall($expr->right->var, $expr->right->name, [ - new Arg($expr->left), - ])); - } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new StaticCall($expr->right->class, $expr->right->name, [ - new Arg($expr->left), - ])); - } - - return $scope->getType(new FuncCall($expr->right, [ - new Arg($expr->left), - ])); + return $expr instanceof Pipe; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult @@ -64,38 +53,60 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex unset($rightAttributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); $argAttributes = $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, []); + $firstClassCallableNode = null; if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { $callExpr = new FuncCall($expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $firstClassCallableNode = new FunctionCallableNode($expr->right->name, $expr->right); } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { $callExpr = new MethodCall($expr->right->var, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $firstClassCallableNode = new MethodCallableNode($expr->right->var, $expr->right->name, $expr->right); } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { $callExpr = new StaticCall($expr->right->class, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $firstClassCallableNode = new StaticMethodCallableNode($expr->right->class, $expr->right->name, $expr->right); } else { $callExpr = new FuncCall($expr->right, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); } + if ($firstClassCallableNode !== null) { + // store a result for $expr->right so node callbacks asking about its + // type can be resumed. Its closure type lives on the matching + // *CallableNode, processed here (storage is available, so the result - + // not the storage - is captured) and read back in the typeCallback. + $callableNodeResult = $nodeScopeResolver->processExprOnDemand($firstClassCallableNode, $scope, $storage); + $nodeScopeResolver->storeExpressionResult($storage, $expr->right, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr->right, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $callableNodeResult->getTypeForScope($s), + )); + } + $callResult = $nodeScopeResolver->processExprNode($stmt, $callExpr, $scope, $storage, $nodeCallback, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $callResult->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $callResult->hasYield(), isAlwaysTerminating: $callResult->isAlwaysTerminating(), throwPoints: $callResult->getThrowPoints(), impurePoints: $callResult->getImpurePoints(), + // the pipe evaluates to its rewritten call - read that child's result + typeCallback: static fn (MutatingScope $s): Type => $callResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 6c38de4a62e..d3fafe25b50 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -8,13 +8,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; @@ -26,6 +26,13 @@ final class PostDecHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostDec; @@ -35,32 +42,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - new PreDec($expr->var), - $nodeCallback, - )->getScope(); - - return new ExpressionResult( - $scope, + return $this->expressionResultFactory->create( + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + new PreDec($expr->var), + $nodeCallback, + )->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + // post-decrement evaluates to the variable's pre-mutation value + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->var); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index f26c45c50fe..a16eb22282f 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -8,13 +8,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; @@ -26,6 +26,13 @@ final class PostIncHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostInc; @@ -35,32 +42,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - new PreInc($expr->var), - $nodeCallback, - )->getScope(); - - return new ExpressionResult( - $scope, + return $this->expressionResultFactory->create( + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + new PreInc($expr->var), + $nodeCallback, + )->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + // post-increment evaluates to the variable's pre-mutation value + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->var); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 4abde925a80..b21eb79a145 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -3,23 +3,25 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\Minus; use PhpParser\Node\Expr\PreDec; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -40,85 +42,115 @@ final class PreDecHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreDec; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $varType = $scope->getType($expr->var); - $varScalars = $varType->getConstantScalarValues(); - - if (count($varScalars) > 0) { - $newTypes = []; - - foreach ($varScalars as $varValue) { - if ($varValue === '') { - $varValue = -1; - } elseif (is_string($varValue) && !is_numeric($varValue)) { - try { - $varValue = str_decrement($varValue); - } catch (ValueError) { - return new NeverType(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + + $typeCallback = function (MutatingScope $s) use ($expr, $varResult): Type { + $varType = $varResult->getTypeForScope($s); + $varScalars = $varType->getConstantScalarValues(); + + if (count($varScalars) > 0) { + $newTypes = []; + + foreach ($varScalars as $varValue) { + if ($varValue === '') { + $varValue = -1; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_decrement($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (is_numeric($varValue)) { + --$varValue; } - } elseif (is_numeric($varValue)) { - --$varValue; + + $newTypes[] = $s->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } elseif ($varType->isString()->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); } - $newTypes[] = $scope->getTypeFromValue($varValue); - } - return TypeCombinator::union(...$newTypes); - } elseif ($varType->isString()->yes()) { - if ($varType->isLiteralString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryLiteralStringType(), - ]); - } + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } - if ($varType->isNumericString()->yes()) { return new BenevolentUnionType([ + new StringType(), new IntegerType(), new FloatType(), ]); } - return new BenevolentUnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - ]); - } - - return $scope->getType(new Minus($expr->var, new Int_(1))); - } + $one = new Int_(1); + return $this->initializerExprTypeResolver->getMinusType($expr->var, $one, static function (Expr $e) use ($s, $expr, $varResult, $one): Type { + if ($e === $expr->var) { + return $varResult->getTypeForScope($s); + } + if ($e === $one) { + return new ConstantIntegerType(1); + } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + throw new ShouldNotHappenException(); + }); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - $scope = $nodeScopeResolver->processVirtualAssign( + // processVirtualAssign asks getType($expr) for the value to assign; store + // this result first so that resolves from the typeCallback below rather + // than re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - )->getScope(); - - return new ExpressionResult( - $scope, + beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + + return $this->expressionResultFactory->create( + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + )->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $varResult->hasYield(), + isAlwaysTerminating: $varResult->isAlwaysTerminating(), + throwPoints: $varResult->getThrowPoints(), + impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 1750b876542..5391bffb825 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -3,23 +3,25 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\PreInc; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -41,85 +43,115 @@ final class PreIncHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreInc; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $varType = $scope->getType($expr->var); - $varScalars = $varType->getConstantScalarValues(); - - if (count($varScalars) > 0) { - $newTypes = []; - - foreach ($varScalars as $varValue) { - if ($varValue === '') { - $varValue = '1'; - } elseif (is_string($varValue) && !is_numeric($varValue)) { - try { - $varValue = str_increment($varValue); - } catch (ValueError) { - return new NeverType(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + + $typeCallback = function (MutatingScope $s) use ($expr, $varResult): Type { + $varType = $varResult->getTypeForScope($s); + $varScalars = $varType->getConstantScalarValues(); + + if (count($varScalars) > 0) { + $newTypes = []; + + foreach ($varScalars as $varValue) { + if ($varValue === '') { + $varValue = '1'; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_increment($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (!is_bool($varValue)) { + ++$varValue; } - } elseif (!is_bool($varValue)) { - ++$varValue; + + $newTypes[] = $s->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } elseif ($varType->isString()->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); } - $newTypes[] = $scope->getTypeFromValue($varValue); - } - return TypeCombinator::union(...$newTypes); - } elseif ($varType->isString()->yes()) { - if ($varType->isLiteralString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryLiteralStringType(), - ]); - } + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } - if ($varType->isNumericString()->yes()) { return new BenevolentUnionType([ + new StringType(), new IntegerType(), new FloatType(), ]); } - return new BenevolentUnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - ]); - } - - return $scope->getType(new Plus($expr->var, new Int_(1))); - } + $one = new Int_(1); + return $this->initializerExprTypeResolver->getPlusType($expr->var, $one, static function (Expr $e) use ($s, $expr, $varResult, $one): Type { + if ($e === $expr->var) { + return $varResult->getTypeForScope($s); + } + if ($e === $one) { + return new ConstantIntegerType(1); + } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + throw new ShouldNotHappenException(); + }); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - $scope = $nodeScopeResolver->processVirtualAssign( + // processVirtualAssign asks getType($expr) for the value to assign; store + // this result first so that resolves from the typeCallback below rather + // than re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - )->getScope(); - - return new ExpressionResult( - $scope, + beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + + return $this->expressionResultFactory->create( + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + )->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $varResult->hasYield(), + isAlwaysTerminating: $varResult->isAlwaysTerminating(), + throwPoints: $varResult->getThrowPoints(), + impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 9d8b220ccfe..8bee2ebdb2d 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -7,15 +7,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantIntegerType; @@ -31,6 +30,8 @@ final class PrintHandler implements ExprHandler public function __construct( private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -40,35 +41,30 @@ public function supports(Expr $expr): bool return $expr instanceof Print_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new ConstantIntegerType(1); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $exprResult->getThrowPoints(); $impurePoints = $exprResult->getImpurePoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope, $exprResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: array_merge($impurePoints, [new ImpurePoint($scope, $expr, 'print', 'print', true)]), + typeCallback: static fn (MutatingScope $scope): Type => new ConstantIntegerType(1), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 9be0c5c28be..bf7069449c0 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -9,18 +9,19 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -40,6 +41,8 @@ final class PropertyFetchHandler implements ExprHandler public function __construct( private PhpVersion $phpVersion, private PropertyReflectionFinder $propertyReflectionFinder, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,6 +54,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 { + $beforeScope = $scope; $scopeBeforeVar = $scope; $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $varResult->hasYield(); @@ -58,9 +62,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $varResult->getImpurePoints(); $isAlwaysTerminating = $varResult->isAlwaysTerminating(); $scope = $varResult->getScope(); + $nameResult = null; if ($expr->name instanceof Identifier) { $propertyName = $expr->name->toString(); - $propertyHolderType = $scopeBeforeVar->getType($expr->var); + $propertyHolderType = $varResult->getTypeForScope($scopeBeforeVar); $propertyReflection = $scopeBeforeVar->getInstancePropertyReflection($propertyHolderType, $propertyName); if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); @@ -81,60 +86,75 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof Identifier) { - if ($scope->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); - if ($propertyReflection === null) { - return new ErrorType(); + containsNullsafe: $varResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::property($varResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), + typeCallback: function (MutatingScope $s) use ($expr, $varResult, $nameResult, $nodeScopeResolver): Type { + // a fetch on a nullsafe chain whose receiver is currently nullable + // short-circuits to null - the receiver result carries whether the + // chain contains a ?-> (a plain nullable receiver does not propagate) + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($varResult->getTypeForScope($s)) + ? TypeCombinator::addNull($type) + : $type; + + if ($expr->name instanceof Identifier) { + if ($s->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s); + if ($propertyReflection === null) { + return new ErrorType(); + } + + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $shortCircuit($propertyReflection->getNativeType()); + } + + $returnType = $this->propertyFetchType( + $s, + $varResult->getTypeForScope($s), + $expr->name->name, + $expr, + ); + if ($returnType === null) { + $returnType = new ErrorType(); + } + + return $shortCircuit($returnType); } - if (!$propertyReflection->hasNativeType()) { - return new MixedType(); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $s); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(static function ($constantString) use ($expr, $s, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a property fetch with a concrete name on the + // name-pinned scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( + new PropertyFetch($expr->var, new Identifier($constantString->getValue())), + $truthyScope, + ); + }, $nameType->getConstantStrings()), + ); } - $nativeType = $propertyReflection->getNativeType(); - - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $nativeType); - } - - $returnType = $this->propertyFetchType( - $scope, - $scope->getType($expr->var), - $expr->name->name, - $expr, - ); - if ($returnType === null) { - $returnType = new ErrorType(); - } - - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); - } - - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))) - ->getType( - new PropertyFetch($expr->var, new Identifier($constantString->getValue())), - ), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); + return new MixedType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, string $propertyName, PropertyFetch $propertyFetch): ?Type @@ -151,9 +171,4 @@ private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, st return $propertyReflection->getReadableType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index abd0dc63a4f..690ec8b8dd6 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -8,18 +8,15 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Type\Type; /** * @implements ExprHandler @@ -30,6 +27,7 @@ final class ScalarHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,23 +39,17 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - return new ExpressionResult( + // TODO $typeSpecifier->specifyDefaultTypes($scope, $expr, $context) OR noop + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: fn (Scope $scope) => $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 872fac5c790..5476f827fc9 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -14,17 +14,17 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -34,6 +34,7 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ErrorType; @@ -43,15 +44,12 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; use ReflectionProperty; use function array_map; use function array_merge; use function count; -use function in_array; use function sprintf; use function strtolower; @@ -68,6 +66,9 @@ public function __construct( private ReflectionProvider $reflectionProvider, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -79,10 +80,14 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $containsNullsafe = false; + $classResult = null; + $nameResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -91,9 +96,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $scope = $classResult->getScope(); + $containsNullsafe = $classResult->containsNullsafe(); } $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $methodReflection = null; $closureBindScope = null; if ($expr->name instanceof Identifier) { @@ -102,12 +110,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $methodName = $expr->name->name; if ($classType->hasMethod($methodName)->yes()) { $methodReflection = $classType->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), - ); + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw point - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $declaringClass = $methodReflection->getDeclaringClass(); if ( @@ -155,16 +163,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } } elseif ($expr->class instanceof Expr) { - $classType = $scope->getType($expr->class)->getObjectTypeOrClassStringObjectType(); + // the class expr was processed above as the receiver; read its + // already-computed result instead of re-walking via Scope::getType(). + $classType = $classResult->getTypeForScope($scope)->getObjectTypeOrClassStringObjectType(); $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($classType, $methodName); if ($methodReflection !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), - ); + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } } else { @@ -176,7 +183,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($expr->class instanceof Expr) { - $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); + // the class expr was processed above as the receiver; read its + // already-computed result instead of re-walking via Scope::getType(). + $objectClasses = $classResult->getTypeForScope($scope)->getObjectClassNames(); if (count($objectClasses) !== 1) { $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); } @@ -212,12 +221,73 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } - $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); + $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $scopeFunction = $scope->getFunction(); + // The early structural check above only sees the unresolved acceptor return + // type; a conditional-return never (e.g. `($x is Foo ? never : string)`) + // only resolves to never once the actual argument types are folded in by the + // type-driven resolved acceptor. + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is + // re-derived from the already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $classResult, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $classResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw point is computed: the method throw point resolves the return type + // (resolveReturnType below) through dynamic static-method return type + // extensions, which can narrow this very call on demand. Without a stored + // result that narrowing would re-process this StaticCall on demand and + // recurse. The callbacks are scope-independent, so the preliminary result + // answers those asks correctly; the final result below overwrites it with the + // resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + containsNullsafe: $containsNullsafe, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via the class/name results and + // readStoredOrPriceOnDemand, never re-running processArgs) - asking + // Scope::getType() for the StaticCall here would re-enter this handler on + // demand, as its final result is not stored yet. + $staticCallReturnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $staticCallReturnType); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -246,9 +316,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin() ) { + // the remembered call value is generic-sensitive: resolve it from the + // type-driven acceptor processArgs() selected (generics resolved against + // the actual arg types), falling back to the structural acceptor. + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( new PossiblyImpureCallExpr($normalizedExpr, new Variable('this'), sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())), - $parametersAcceptor->getReturnType(), + $acceptorForGenerics->getReturnType(), new MixedType(), ); } @@ -279,25 +353,53 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + containsNullsafe: $containsNullsafe, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * on-demand / synthetic pricing) it falls back to re-selecting from the args via + * MethodCallReturnTypeHelper on the asking scope. + * + * The class/name were processed during processExpr; their already computed + * results are read instead of re-walking via Scope::getType(). The dynamic-name + * branch builds a synthetic StaticCall priced on demand by the resolver. + * + * @param StaticCall $expr + */ + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // a call on a nullsafe chain whose class-receiver is currently nullable + // short-circuits to null - the class result carries whether the chain + // contains a ?-> (a plain nullable receiver does not propagate). + $shortCircuit = static fn (Type $type): Type => $expr->class instanceof Expr + && $classResult !== null + && $classResult->containsNullsafe() + && TypeCombinator::containsNull($classResult->getTypeForScope($scope)) + ? TypeCombinator::addNull($type) + : $type; + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { if ($expr->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); + $staticMethodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $staticMethodCalledOnType = $scope->getNativeType($expr->class); + $staticMethodCalledOnType = $classResult !== null + ? $classResult->getNativeTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemandNative($expr->class, $scope); } $methodReflection = $scope->getMethodReflection( $staticMethodCalledOnType, @@ -309,17 +411,16 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $callType); - } - - return $callType; + return $shortCircuit($callType); } if ($expr->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); + $staticMethodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $classType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); + $staticMethodCalledOnType = TypeCombinator::removeNull($classType)->getObjectTypeOrClassStringObjectType(); } $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( @@ -327,73 +428,70 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $staticMethodCalledOnType, $expr->name->toString(), $expr, + $preResolvedAcceptor, ); if ($callType === null) { $callType = new ErrorType(); } - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $callType); - } - - return $callType; + return $shortCircuit($callType); } - $nameType = $scope->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($scope) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new Expr\StaticCall($expr->class, new Identifier($constantString->getValue()), $expr->args)), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); - } + ...array_map(static function ($constantString) use ($expr, $scope, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } - private function resolveTypeByNameWithLateStaticBinding(MutatingScope $scope, Name $class, Identifier $name): TypeWithClassName - { - $classType = $scope->resolveTypeByName($class); + // a static call with a concrete name on the name-pinned scope + // is synthetic. + $truthyScope = $scope->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); - if ( - $classType instanceof StaticType - && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true) - ) { - $methodReflectionCandidate = $scope->getMethodReflection( - $classType, - $name->name, + return $nodeScopeResolver->priceSyntheticOnDemand( + new StaticCall($expr->class, new Identifier($constantString->getValue()), $expr->args), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); - if ($methodReflectionCandidate !== null && $methodReflectionCandidate->isStatic()) { - $classType = $classType->getStaticObjectType(); - } } - return $classType; + return new MixedType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * StaticMethodTypeSpecifyingExtensions, conditional-return-type and assert + * narrowing are invoked on the already-processed argument + * results. The acceptor is $resolvedParametersAcceptor (type-driven, generics + * resolved by processArgs) rather than re-selected from the args on the asking + * scope. The subject's own default narrowing comes from DefaultNarrowingHelper + * instead of TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would + * re-enter this expression through TypeSpecifier::create(). + * + * @param StaticCall $expr + * @param StaticCall $normalizedExpr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ?ExpressionResult $classResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if (!$expr->name instanceof Identifier) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } if ($expr->class instanceof Name) { $calleeType = $scope->resolveTypeByName($expr->class); } else { - $calleeType = $scope->getType($expr->class); + // the class expr was processed during processExpr; read its + // already-computed result instead of re-walking via Scope::getType(). + $calleeType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); } $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; - } $referencedClasses = $calleeType->getObjectClassNames(); if ( @@ -401,7 +499,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e && $this->reflectionProvider->hasClass($referencedClasses[0]) ) { $staticMethodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - foreach ($typeSpecifier->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) { + foreach ($this->typeSpecifier->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) { if (!$extension->isStaticMethodSupported($staticMethodReflection, $normalizedExpr, $context)) { continue; } @@ -410,33 +508,78 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $staticMethodReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultStaticCallNarrowing($scope, $expr, $classResult, $nodeScopeResolver, $context); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a static method with + * side effects (or an unknown method whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s StaticCall handling inside-out, without + * re-entering this expression through create(). + * + * @param StaticCall $expr + */ + private function defaultStaticCallNarrowing(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isStaticCallNarrowable($scope, $expr, $classResult, $nodeScopeResolver)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + /** @param StaticCall $expr */ + private function isStaticCallNarrowable(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver): bool + { + if (!$expr->name instanceof Identifier) { + return true; + } + + if ($expr->class instanceof Name) { + $calleeType = $scope->resolveTypeByName($expr->class); + } else { + $calleeType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); + } + + $methodReflection = $scope->getMethodReflection($calleeType, $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + + $hasSideEffects = $methodReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); } } diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index ec36cf64388..8454caea4be 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -11,17 +11,18 @@ use PhpParser\Node\VarLikeIdentifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -40,6 +41,8 @@ final class StaticPropertyFetchHandler implements ExprHandler public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,6 +54,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 { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = [ @@ -63,6 +67,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), ]; $isAlwaysTerminating = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -71,6 +76,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $scope = $classResult->getScope(); } + $nameResult = null; if (!$expr->name instanceof VarLikeIdentifier) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $nameResult->hasYield(); @@ -80,71 +86,78 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $nameResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof VarLikeIdentifier) { - if ($scope->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); - if ($propertyReflection === null) { - return new ErrorType(); - } - if (!$propertyReflection->hasNativeType()) { - return new MixedType(); + containsNullsafe: $classResult !== null && $classResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::property($classResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), + typeCallback: function (MutatingScope $s) use ($expr, $classResult, $nameResult, $nodeScopeResolver): Type { + $shortCircuit = static fn (Type $type): Type => $classResult !== null && $classResult->containsNullsafe() && TypeCombinator::containsNull($classResult->getTypeForScope($s)) + ? TypeCombinator::addNull($type) + : $type; + + if ($expr->name instanceof VarLikeIdentifier) { + if ($s->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s); + if ($propertyReflection === null) { + return new ErrorType(); + } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $shortCircuit($propertyReflection->getNativeType()); + } + + if ($expr->class instanceof Name) { + $staticPropertyFetchedOnType = $s->resolveTypeByName($expr->class); + } else { + $classType = $classResult !== null ? $classResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $s); + $staticPropertyFetchedOnType = TypeCombinator::removeNull($classType)->getObjectTypeOrClassStringObjectType(); + } + + $fetchType = $this->propertyFetchType( + $s, + $staticPropertyFetchedOnType, + $expr->name->toString(), + $expr, + ); + if ($fetchType === null) { + $fetchType = new ErrorType(); + } + + return $shortCircuit($fetchType); } - $nativeType = $propertyReflection->getNativeType(); - - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $nativeType); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $s); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(static function ($constantString) use ($expr, $s, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a static property fetch with a concrete name on the + // name-pinned scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( + new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue())), + $truthyScope, + ); + }, $nameType->getConstantStrings()), + ); } - return $nativeType; - } - - if ($expr->class instanceof Name) { - $staticPropertyFetchedOnType = $scope->resolveTypeByName($expr->class); - } else { - $staticPropertyFetchedOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); - } - - $fetchType = $this->propertyFetchType( - $scope, - $staticPropertyFetchedOnType, - $expr->name->toString(), - $expr, - ); - if ($fetchType === null) { - $fetchType = new ErrorType(); - } - - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $fetchType); - } - - return $fetchType; - } - - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); + return new MixedType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, string $propertyName, StaticPropertyFetch $propertyFetch): ?Type @@ -161,9 +174,4 @@ private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, st return $propertyReflection->getReadableType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 3dcc769ad36..861429e4392 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -9,14 +9,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NeverType; @@ -32,7 +31,8 @@ final class TernaryHandler implements ExprHandler { public function __construct( - private NodeScopeResolver $nodeScopeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -42,62 +42,6 @@ public function supports(Expr $expr): bool return $expr instanceof Ternary; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $condResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->cond), $expr->cond, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - if ($expr->if === null) { - $conditionType = $scope->getType($expr->cond); - $booleanConditionType = $conditionType->toBoolean(); - if ($booleanConditionType->isTrue()->yes()) { - return $condResult->getTruthyScope()->getType($expr->cond); - } - - if ($booleanConditionType->isFalse()->yes()) { - return $condResult->getFalseyScope()->getType($expr->else); - } - - return TypeCombinator::union( - TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($expr->cond)), - $condResult->getFalseyScope()->getType($expr->else), - ); - } - - $booleanConditionType = $scope->getType($expr->cond)->toBoolean(); - if ($booleanConditionType->isTrue()->yes()) { - return $condResult->getTruthyScope()->getType($expr->if); - } - - if ($booleanConditionType->isFalse()->yes()) { - return $condResult->getFalseyScope()->getType($expr->else); - } - - return TypeCombinator::union( - $condResult->getTruthyScope()->getType($expr->if), - $condResult->getFalseyScope()->getType($expr->else), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr->cond instanceof Ternary || $context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if ($expr->if !== null) { - $conditionExpr = new BooleanOr( - new BooleanAnd($expr->cond, $expr->if), - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), - ); - } else { - $conditionExpr = new BooleanOr( - $expr->cond, - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), - ); - } - - return $typeSpecifier->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $ternaryCondResult = $nodeScopeResolver->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -106,7 +50,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; + $ifResult = null; + $ifProcessingScope = $ifTrueScope; + $elseProcessingScope = $ifFalseScope; if ($expr->if === null) { $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); @@ -117,7 +64,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); - $ifTrueType = $ifTrueScope->getType($expr->if); + $ifTrueType = $ifResult->getTypeForScope($ifTrueScope); $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); @@ -125,7 +72,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $ifFalseScope = $elseResult->getScope(); } - $condType = $scope->getType($expr->cond); + $condType = $ternaryCondResult->getTypeForScope($scope); if ($condType->isTrue()->yes()) { $finalScope = $ifTrueScope; } elseif ($condType->isFalse()->yes()) { @@ -134,7 +81,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { $finalScope = $ifFalseScope; } else { - $ifFalseType = $ifFalseScope->getType($expr->else); + $ifFalseType = $elseResult->getTypeForScope($ifFalseScope); if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { $finalScope = $ifTrueScope; @@ -144,14 +91,75 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $finalScope, + beforeScope: $scope, + expr: $expr, hasYield: $ternaryCondResult->hasYield(), isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), + // the branches were processed on the cond-truthy/cond-falsey scopes + // including the condition's side effects - those captured scopes + // are the evaluation points, no re-walk needed + typeCallback: static function (MutatingScope $s) use ($expr, $ternaryCondResult, $ifResult, $elseResult, $ifProcessingScope, $elseProcessingScope): Type { + if ($s->nativeTypesPromoted) { + $ifProcessingScope = $ifProcessingScope->doNotTreatPhpDocTypesAsCertain(); + $elseProcessingScope = $elseProcessingScope->doNotTreatPhpDocTypesAsCertain(); + } + $booleanConditionType = $ternaryCondResult->getTypeForScope($s)->toBoolean(); + $elseType = $elseResult->getTypeForScope($elseProcessingScope); + if ($expr->if === null || $ifResult === null) { + $condTruthyType = $ternaryCondResult->getTypeForScope($ifProcessingScope); + if ($booleanConditionType->isTrue()->yes()) { + return $condTruthyType; + } + + if ($booleanConditionType->isFalse()->yes()) { + return $elseType; + } + + return TypeCombinator::union( + TypeCombinator::removeFalsey($condTruthyType), + $elseType, + ); + } + + $ifType = $ifResult->getTypeForScope($ifProcessingScope); + if ($booleanConditionType->isTrue()->yes()) { + return $ifType; + } + + if ($booleanConditionType->isFalse()->yes()) { + return $elseType; + } + + return TypeCombinator::union( + $ifType, + $elseType, + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($expr->cond instanceof Ternary || $context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + if ($expr->if !== null) { + $conditionExpr = new BooleanOr( + new BooleanAnd($expr->cond, $expr->if), + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), + ); + } else { + $conditionExpr = new BooleanOr( + $expr->cond, + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), + ); + } + + // the synthetic condition takes the on-demand bridge; its real + // subnodes answer from stored results + return $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $conditionExpr, null, $context)->setRootExpr($expr); + }, ); } diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 63c9b4720e8..89a3860f061 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -7,14 +7,13 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; @@ -28,6 +27,13 @@ final class ThrowHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Throw_; @@ -37,23 +43,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()->enterThrow()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: true, - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getTypeForScope($scope), $expr, false)]), impurePoints: $exprResult->getImpurePoints(), + typeCallback: static fn (MutatingScope $scope): Type => new NonAcceptingNeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new NonAcceptingNeverType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index 2bc2d872380..ff6dd498e1a 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -7,13 +7,12 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -28,6 +27,8 @@ final class UnaryMinusHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -41,23 +42,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult, $nodeScopeResolver): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + // a synthetic node ($expr->expr * -1, derived for an IntegerRangeType + // operand) created inside getUnaryMinusType - priced on demand + return $nodeScopeResolver->priceSyntheticOnDemand($e, $scope); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 676110b904f..994f1f2500b 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -7,16 +7,16 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; /** @@ -28,6 +28,8 @@ final class UnaryPlusHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -41,23 +43,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index e104b9fed42..984640d5241 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler; +use Closure; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\Variable; @@ -9,16 +10,19 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -34,49 +38,74 @@ final class VariableHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Variable; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * Evaluates the variable as a read on the asking scope. Also used by + * AssignHandler for the placeholder result it stores for an assignment + * target - every stored result for a Variable node must carry a + * typeCallback now that this handler no longer implements + * TypeResolvingExprHandler. + * + * @return Closure(MutatingScope): Type + */ + public static function createTypeCallback(Variable $expr, ?ExpressionResult $nameResult = null): Closure { - if (is_string($expr->name)) { - if ($scope->hasVariableType($expr->name)->no()) { - return new ErrorType(); + return static function (MutatingScope $s) use ($expr, $nameResult): Type { + if (is_string($expr->name)) { + if ($s->hasVariableType($expr->name)->no()) { + return new ErrorType(); + } + + return $s->getVariableType($expr->name); } - return $scope->getVariableType($expr->name); - } + // this branch is only reached when $expr->name is an Expr, which is + // exactly when the caller (processExpr) set $nameResult + if ($nameResult === null) { + throw new ShouldNotHappenException(); + } + $nameType = $nameResult->getTypeForScope($s); + if (count($nameType->getConstantStrings()) > 0) { + $types = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $variableScope = $s + ->filterByTruthyValue( + new Identical($expr->name, new String_($constantString->getValue())), + ); + if ($variableScope->hasVariableType($constantString->getValue())->no()) { + $types[] = new ErrorType(); + continue; + } - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - $types = []; - foreach ($nameType->getConstantStrings() as $constantString) { - $variableScope = $scope - ->filterByTruthyValue( - new Identical($expr->name, new String_($constantString->getValue())), - ); - if ($variableScope->hasVariableType($constantString->getValue())->no()) { - $types[] = new ErrorType(); - continue; + $types[] = $variableScope->getVariableType($constantString->getValue()); } - $types[] = $variableScope->getVariableType($constantString->getValue()); + return TypeCombinator::union(...$types); } - return TypeCombinator::union(...$types); - } - - return new MixedType(); + return new MixedType(); + }; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $nameResult = null; if (is_string($expr->name)) { if (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); @@ -89,20 +118,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); $scope = $nameResult->getScope(); } - return new ExpressionResult( + + return $this->expressionResultFactory->create( $scope, - $hasYield, - $isAlwaysTerminating, - $throwPoints, - $impurePoints, - static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + issetabilityDescriptor: is_string($expr->name) ? IssetabilityDescriptor::variable($expr->name) : null, + typeCallback: self::createTypeCallback($expr, $nameResult), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index fcf77547e33..350584cd091 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -6,13 +6,12 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; @@ -25,6 +24,13 @@ final class AlwaysRememberedExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof AlwaysRememberedExpr; @@ -40,29 +46,22 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { + $beforeScope = $scope; $innerExpr = $expr->getExpr(); $innerResult = $nodeScopeResolver->processExprNode($stmt, $innerExpr, $scope, $storage, $nodeCallback, $context); $scope = $innerResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $innerResult->hasYield(), isAlwaysTerminating: $innerResult->isAlwaysTerminating(), throwPoints: $innerResult->getThrowPoints(), impurePoints: $innerResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($innerExpr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($innerExpr), + typeCallback: static fn (MutatingScope $scope): Type => $scope->nativeTypesPromoted ? $expr->getNativeExprType() : $expr->getExprType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->nativeTypesPromoted ? $expr->getNativeExprType() : $expr->getExprType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index c2bf0d37fab..5645642ccfc 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -6,14 +6,11 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Type\Type; @@ -25,6 +22,10 @@ final class ExistingArrayDimFetchHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ExistingArrayDimFetch; @@ -32,26 +33,24 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( + // virtual node: callers only read the type, computed lazily by the + // typeCallback. The plain array dim fetch is processed here (its real + // leaves are already stored by on-demand time) so the typeCallback reads + // its ExpressionResult instead of Scope::getType(). A null + // specifyTypesCallback falls back to default narrowing in TypeSpecifier, + // matching the old specifyDefaultTypes(). + $arrayDimFetchResult = $nodeScopeResolver->processExprNode($stmt, new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim()), $scope, $storage, $nodeCallback, $context); + + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $arrayDimFetchResult->getTypeForScope($s), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType(new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index b358f70c8df..80970e939f9 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -2,21 +2,24 @@ namespace PHPStan\Analyser\ExprHandler\Virtual; +use Closure; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FunctionCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; /** @@ -26,6 +29,14 @@ final class FunctionCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof FunctionCallableNode; @@ -33,10 +44,12 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; $isAlwaysTerminating = false; + $nameResult = null; if ($expr->getName() instanceof Expr) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); @@ -46,25 +59,41 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr, $nameResult), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type + private function resolveType(MutatingScope $scope, FunctionCallableNode $expr, ?ExpressionResult $nameResult): Type { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableFuncCallHandler - return new MixedType(); - } + $originalNode = $expr->getOriginalNode(); + if ($originalNode->name instanceof Expr) { + // $originalNode->name is the same node as $expr->getName(), processed + // in processExpr exactly in this branch - read its ExpressionResult + if ($nameResult === null) { + throw new ShouldNotHappenException(); + } + $callableType = $nameResult->getTypeForScope($scope); + if (!$callableType->isCallable()->yes()) { + return new ObjectType(Closure::class); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->initializerExprTypeResolver->createFirstClassCallable( + null, + $callableType->getCallableParametersAcceptors($scope), + $scope->nativeTypesPromoted, + ); + } + + return $this->initializerExprTypeResolver->getFirstClassCallableType($originalNode, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); } } diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php deleted file mode 100644 index a9de984485e..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -#[AutowiredService] -final class GetIterableKeyTypeExprHandler implements ExprHandler -{ - - public function supports(Expr $expr): bool - { - return $expr instanceof GetIterableKeyTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( - $scope, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getIterableKeyType($scope->getType($expr->getExpr())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php deleted file mode 100644 index 261c364ffd3..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -#[AutowiredService] -final class GetIterableValueTypeExprHandler implements ExprHandler -{ - - public function supports(Expr $expr): bool - { - return $expr instanceof GetIterableValueTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( - $scope, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getIterableValueType($scope->getType($expr->getExpr())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php deleted file mode 100644 index 09922c7daa7..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -#[AutowiredService] -final class GetOffsetValueTypeExprHandler implements ExprHandler -{ - - public function supports(Expr $expr): bool - { - return $expr instanceof GetOffsetValueTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( - $scope, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->getVar())->getOffsetValueType($scope->getType($expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index eb552e3a544..c7ee4040a98 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -6,17 +6,17 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\InstantiationCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; /** @@ -26,6 +26,14 @@ final class InstantiationCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof InstantiationCallableNode; @@ -33,6 +41,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 { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; @@ -46,25 +55,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: fn (MutatingScope $scope): Type => $this->initializerExprTypeResolver->getFirstClassCallableType($expr->getOriginalNode(), InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableNewHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index f2d224bc91a..947d299200c 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -2,21 +2,23 @@ namespace PHPStan\Analyser\ExprHandler\Virtual; +use Closure; use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\MethodCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use function array_merge; @@ -27,6 +29,14 @@ final class MethodCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof MethodCallableNode; @@ -34,6 +44,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 { + $beforeScope = $scope; $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $varResult->getScope(); $hasYield = $varResult->hasYield(); @@ -49,25 +60,39 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr, $varResult), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type + private function resolveType(MutatingScope $scope, MethodCallableNode $expr, ExpressionResult $varResult): Type { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableMethodCallHandler - return new MixedType(); - } + $originalNode = $expr->getOriginalNode(); + if (!$originalNode->name instanceof Identifier) { + return new ObjectType(Closure::class); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + // $originalNode->var is the same node as $expr->getVar(), processed in + // processExpr - read its ExpressionResult instead of Scope::getType() + $varType = $varResult->getTypeForScope($scope); + $method = $scope->getMethodReflection($varType, $originalNode->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); + } + + return $this->initializerExprTypeResolver->createFirstClassCallable( + $method, + $method->getVariants(), + $scope->nativeTypesPromoted, + ); } } diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index c952fcc1297..6ef2fa033f6 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -6,13 +6,12 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\NativeTypeExpr; @@ -25,6 +24,13 @@ final class NativeTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof NativeTypeExpr; @@ -35,26 +41,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $scope): Type => $scope->nativeTypesPromoted ? $expr->getNativeType() : $expr->getPhpDocType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($scope->nativeTypesPromoted) { - return $expr->getNativeType(); - } - return $expr->getPhpDocType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php deleted file mode 100644 index 7893990b1a3..00000000000 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -#[AutowiredService] -final class OriginalPropertyTypeExprHandler implements ExprHandler -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof OriginalPropertyTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( - $scope, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr->getPropertyFetch(), $scope); - if ($propertyReflection === null) { - return new ErrorType(); - } - - return $propertyReflection->getReadableType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index cd764c40dbf..59ed410e1c9 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -6,19 +6,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; /** * @implements ExprHandler @@ -27,6 +22,10 @@ final class SetExistingOffsetValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetExistingOffsetValueTypeExpr; @@ -34,37 +33,29 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( + // virtual node: callers only read the type, computed lazily by the + // typeCallback. The (synthetic) sub-expressions are processed here - by + // on-demand time their real leaves are already stored, so this reads them + // back; the typeCallback then reads the ExpressionResults instead of + // Scope::getType(). A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context); + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), $scope, $storage, $nodeCallback, $context); + + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->setExistingOffsetValueType( + $dimResult->getTypeForScope($s), + $valueResult->getTypeForScope($s), + ), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varNode = $expr->getVar(); - $varType = $scope->getType($varNode); - if ($varNode instanceof OriginalPropertyTypeExpr) { - $currentPropertyType = $scope->getType($varNode->getPropertyFetch()); - if ($varType instanceof UnionType) { - $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); - } - } - return $varType->setExistingOffsetValueType( - $scope->getType($expr->getDim()), - $scope->getType($expr->getValue()), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index 92c41c6b516..1f1ca8e113f 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -6,19 +6,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; /** * @implements ExprHandler @@ -27,6 +22,10 @@ final class SetOffsetValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetOffsetValueTypeExpr; @@ -34,37 +33,30 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( + // virtual node: callers only read the type, computed lazily by the + // typeCallback. The (synthetic) sub-expressions are processed here so the + // typeCallback reads their ExpressionResults instead of Scope::getType(). + // A null specifyTypesCallback falls back to default narrowing in + // TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $expr->getDim() !== null + ? $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context) + : null; + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), $scope, $storage, $nodeCallback, $context); + + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->setOffsetValueType( + $dimResult !== null ? $dimResult->getTypeForScope($s) : null, + $valueResult->getTypeForScope($s), + ), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varNode = $expr->getVar(); - $varType = $scope->getType($varNode); - if ($varNode instanceof OriginalPropertyTypeExpr) { - $currentPropertyType = $scope->getType($varNode->getPropertyFetch()); - if ($varType instanceof UnionType) { - $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); - } - } - return $varType->setOffsetValueType( - $expr->getDim() !== null ? $scope->getType($expr->getDim()) : null, - $scope->getType($expr->getValue()), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 10467a171a5..33583ff0ec5 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -6,17 +6,17 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\StaticMethodCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; use function array_merge; @@ -27,6 +27,14 @@ final class StaticMethodCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof StaticMethodCallableNode; @@ -34,6 +42,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 { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; @@ -55,25 +64,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: fn (MutatingScope $scope): Type => $this->initializerExprTypeResolver->getFirstClassCallableType($expr->getOriginalNode(), InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableStaticCallHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 81c6c9f08f8..1efa5f6db19 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -6,13 +6,12 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; @@ -25,6 +24,13 @@ final class TypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof TypeExpr; @@ -35,23 +41,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $scope): Type => $expr->getExprType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $expr->getExprType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 0c2c4741831..c81be133e7b 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -6,14 +6,11 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Type\Type; @@ -25,6 +22,10 @@ final class UnsetOffsetExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof UnsetOffsetExpr; @@ -32,26 +33,25 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return new ExpressionResult( + // virtual node: callers only read the type, computed lazily by the + // typeCallback. The (synthetic) sub-expressions are processed here - by + // on-demand time their real leaves are already stored, so this reads them + // back; the typeCallback then reads the ExpressionResults instead of + // Scope::getType(). A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context); + + return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->unsetOffset($dimResult->getTypeForScope($s)), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->getVar())->unsetOffset($scope->getType($expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 7b86f00abb2..1de51432932 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -8,15 +8,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; @@ -31,39 +30,43 @@ final class YieldFromHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof YieldFrom; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - $yieldFromType = $scope->getType($expr->expr); - $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); - if ($generatorReturnType instanceof ErrorType) { - return new MixedType(); - } - - return $generatorReturnType; + return $expr instanceof YieldFrom; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: true, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'yieldFrom', 'yield from', true)]), - ); - } + typeCallback: static function (MutatingScope $scope) use ($exprResult): Type { + $yieldFromType = $exprResult->getTypeForScope($scope); + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $generatorReturnType; + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index 48fb166ee70..6ec1d13fc57 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -8,15 +8,14 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; @@ -31,29 +30,21 @@ final class YieldHandler implements ExprHandler { - public function supports(Expr $expr): bool + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { - return $expr instanceof Yield_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function supports(Expr $expr): bool { - $functionReflection = $scope->getFunction(); - if ($functionReflection === null) { - return new MixedType(); - } - - $returnType = $functionReflection->getReturnType(); - $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); - if ($generatorSendType instanceof ErrorType) { - return new MixedType(); - } - - return $generatorSendType; + return $expr instanceof Yield_; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $throwPoints = [ InternalThrowPoint::createImplicit($scope, $expr), ]; @@ -82,18 +73,30 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, + expr: $expr, hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: static function (MutatingScope $scope): Type { + $functionReflection = $scope->getFunction(); + if ($functionReflection === null) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + $returnType = $functionReflection->getReturnType(); + $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); + if ($generatorSendType instanceof ErrorType) { + return new MixedType(); + } + + return $generatorSendType; + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 746c518953d..9bc2dc335b5 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,9 +2,25 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; +use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; +use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; + +#[GenerateFactory(interface: ExpressionResultFactory::class)] final class ExpressionResult { + /** @var (callable(MutatingScope, Expr): Type)|null */ + private $typeCallback; + + /** @var (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null */ + private $specifyTypesCallback; + + /** @var (callable(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes)|null */ + private $createTypesCallback; + /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -15,24 +31,42 @@ final class ExpressionResult private ?MutatingScope $falseyScope = null; + private ?Type $cachedType = null; + + private ?Type $cachedNativeType = null; + /** * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints + * @param (callable(MutatingScope, Expr): Type)|null $typeCallback + * @param (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null $specifyTypesCallback + * @param (callable(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes)|null $createTypesCallback * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ public function __construct( + private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider, private MutatingScope $scope, + private MutatingScope $beforeScope, + private Expr $expr, private bool $hasYield, private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, + private bool $containsNullsafe = false, + private ?IssetabilityDescriptor $issetabilityDescriptor = null, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, + ?callable $typeCallback = null, + ?callable $specifyTypesCallback = null, + ?callable $createTypesCallback = null, ) { $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; + $this->typeCallback = $typeCallback; + $this->specifyTypesCallback = $specifyTypesCallback; + $this->createTypesCallback = $createTypesCallback; } public function getScope(): MutatingScope @@ -40,11 +74,86 @@ public function getScope(): MutatingScope return $this->scope; } + public function getExpr(): Expr + { + return $this->expr; + } + + public function getBeforeScope(): MutatingScope + { + return $this->beforeScope; + } + public function hasYield(): bool { return $this->hasYield; } + /** + * Whether this expression's chain contains a nullsafe operator (?->). A + * fetch/call on a receiver whose chain short-circuits propagates null, + * which a plain nullable receiver (e.g. a nullable variable) does not - + * this flag is what tells them apart. + */ + public function containsNullsafe(): bool + { + return $this->containsNullsafe; + } + + /** + * The isset/empty/?? chain descriptor for this expression, or null when the + * expression is not a variable / array dim fetch / property fetch chain link + * (in which case isset() falls back to the leaf type check). + */ + public function getIssetabilityDescriptor(): ?IssetabilityDescriptor + { + return $this->issetabilityDescriptor; + } + + /** + * Whether isset($expr) holds: folds the isset/empty/?? chain descriptor, or + * applies the leaf type check when the expression is not a chain link. + * + * @param callable(Type): ?bool $typeCallback + */ + public function issetCheck(MutatingScope $scope, callable $typeCallback, ?bool $result = null): ?bool + { + if ($this->issetabilityDescriptor !== null) { + return $this->issetabilityDescriptor->check($scope, $typeCallback, $result); + } + + return $result ?? $typeCallback($this->getTypeForScope($scope)); + } + + public function issetCheckUndefined(MutatingScope $scope): ?bool + { + return $this->issetabilityDescriptor?->checkUndefined($scope); + } + + /** + * Whether $expr is definitely set-and-non-falsey (i.e. the negation of + * empty($expr)); null = maybe. EmptyHandler negates the result. + */ + public function empty(MutatingScope $scope): ?bool + { + return $this->issetCheck($scope, static function (Type $type): ?bool { + $isNull = $type->isNull(); + $isFalsey = $type->toBoolean()->isFalse(); + if ($isNull->maybe()) { + return null; + } + if ($isFalsey->maybe()) { + return null; + } + + if ($isNull->yes()) { + return $isFalsey->no(); + } + + return !$isFalsey->yes(); + }); + } + /** * @return InternalThrowPoint[] */ @@ -63,32 +172,42 @@ public function getImpurePoints(): array public function getTruthyScope(): MutatingScope { - if ($this->truthyScopeCallback === null) { - return $this->scope; - } - if ($this->truthyScope !== null) { return $this->truthyScope; } + if ($this->truthyScopeCallback === null) { + if ($this->specifyTypesCallback !== null) { + return $this->truthyScope = $this->scope->applySpecifiedTypes( + ($this->specifyTypesCallback)($this->scope, TypeSpecifierContext::createTruthy()), + ); + } + + return $this->truthyScope = $this->scope->filterByTruthyValue($this->expr); + } + $callback = $this->truthyScopeCallback; - $this->truthyScope = $callback(); - return $this->truthyScope; + return $this->truthyScope = $callback(); } public function getFalseyScope(): MutatingScope { - if ($this->falseyScopeCallback === null) { - return $this->scope; - } - if ($this->falseyScope !== null) { return $this->falseyScope; } + if ($this->falseyScopeCallback === null) { + if ($this->specifyTypesCallback !== null) { + return $this->falseyScope = $this->scope->applySpecifiedTypes( + ($this->specifyTypesCallback)($this->scope, TypeSpecifierContext::createFalsey()), + ); + } + + return $this->falseyScope = $this->scope->filterByFalseyValue($this->expr); + } + $callback = $this->falseyScopeCallback; - $this->falseyScope = $callback(); - return $this->falseyScope; + return $this->falseyScope = $callback(); } public function isAlwaysTerminating(): bool @@ -96,4 +215,114 @@ public function isAlwaysTerminating(): bool return $this->isAlwaysTerminating; } + public function getType(): Type + { + if ($this->cachedType !== null) { + return $this->cachedType; + } + + foreach ($this->expressionTypeResolverExtensionRegistryProvider->getRegistry()->getExtensions() as $extension) { + $type = $extension->getType($this->expr, $this->beforeScope); + if ($type !== null) { + return $this->cachedType = $type; + } + } + + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($this->beforeScope)) { + return $this->cachedType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope, $this->expr)); + } + + return $this->cachedType = $this->beforeScope->getType($this->expr); + } + + public function getNativeType(): Type + { + if ($this->cachedNativeType !== null) { + return $this->cachedNativeType; + } + + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($this->beforeScope->doNotTreatPhpDocTypesAsCertain())) { + return $this->cachedNativeType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope->doNotTreatPhpDocTypesAsCertain(), $this->expr)); + } + + return $this->cachedNativeType = $this->beforeScope->getNativeType($this->expr); + } + + /** + * A narrowed or ensured type tracked for the whole expression (e.g. the + * nullsafe handlers ensure `($x ?? null)` is not null before processing + * the chain) wins over recomputing the type - mirrors the tracked-holder + * early return in MutatingScope::resolveType(). Asking the scope is safe: + * its own early return answers from the holder without dispatching back. + */ + private function hasTrackedExpressionType(MutatingScope $scope): bool + { + return !$this->expr instanceof Expr\Variable + && !$this->expr instanceof Expr\Closure + && !$this->expr instanceof Expr\ArrowFunction + && $scope->hasExpressionType($this->expr)->yes(); + } + + public function hasTypeCallback(): bool + { + return $this->typeCallback !== null; + } + + /** + * Re-evaluates the narrowing on a different scope (e.g. the one an old-world + * caller holds). Returns null when the handler wired no specifyTypesCallback - + * the caller falls back to default truthy/falsey narrowing. + */ + public function getSpecifiedTypesForScope(MutatingScope $scope, TypeSpecifierContext $context): ?SpecifiedTypes + { + if ($this->specifyTypesCallback === null) { + return null; + } + + return ($this->specifyTypesCallback)($scope, $context); + } + + /** + * How a type constraint on this expression translates into narrowing + * entries - the inside-out counterpart of TypeSpecifier::create(). The + * handler that produced this result knows the structure: an assignment + * fans out to the assigned variable and the assigned expression + * (recursing through the assigned expression's own result), a coalesce + * delegates to its left side when the type rules the right side in or + * out. Returns null when the handler wired no createTypesCallback - the + * caller emits a single entry for the expression itself. + */ + public function getCreatedTypesForScope(MutatingScope $scope, Type $type, TypeSpecifierContext $context): ?SpecifiedTypes + { + if ($this->createTypesCallback === null) { + return null; + } + + return ($this->createTypesCallback)($scope, $type, $context); + } + + /** + * Re-evaluates the expression type on a different scope (e.g. a narrowed one). + * Unlike getType(), the result is not cached. + */ + public function getTypeForScope(MutatingScope $scope): Type + { + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($scope)) { + return TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($scope, $this->expr)); + } + + return $scope->getType($this->expr); + } + + /** Native counterpart of getTypeForScope(). */ + public function getNativeTypeForScope(MutatingScope $scope): Type + { + $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($nativeScope)) { + return TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($nativeScope, $this->expr)); + } + + return $scope->getNativeType($this->expr); + } + } diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php new file mode 100644 index 00000000000..b996c62ce30 --- /dev/null +++ b/src/Analyser/ExpressionResultFactory.php @@ -0,0 +1,37 @@ + */ - private SplObjectStorage $scopes; + /** @var SplObjectStorage */ + private SplObjectStorage $exprResults; - /** @var array, request: BeforeScopeForExprRequest}> */ + /** + * Read-only fallback - writes never reach it. Makes duplicate() O(1) + * instead of copying all stored results. + */ + private ?self $fallback = null; + + /** @var array, request: ExpressionResultRequest}> */ public array $pendingFibers = []; - /** @var list> */ + /** @var list> */ public array $parkedFibers = []; public function __construct() { - $this->scopes = new SplObjectStorage(); + $this->exprResults = new SplObjectStorage(); } public function duplicate(): self { $new = new self(); - $new->scopes->addAll($this->scopes); + $new->fallback = $this; return $new; } - public function storeBeforeScope(Expr $expr, Scope $scope): void + public function mergeResults(self $other): void + { + $this->exprResults->addAll($other->exprResults); + } + + public function storeExpressionResult(Expr $expr, ExpressionResult $expressionResult): void { - $this->scopes[$expr] = $scope; + $this->exprResults[$expr] = $expressionResult; } - public function findBeforeScope(Expr $expr): ?Scope + public function findExpressionResult(Expr $expr): ?ExpressionResult { - return $this->scopes[$expr] ?? null; + return $this->exprResults[$expr] ?? ($this->fallback !== null ? $this->fallback->findExpressionResult($expr) : null); } } diff --git a/src/Analyser/ExpressionResultStorageStack.php b/src/Analyser/ExpressionResultStorageStack.php new file mode 100644 index 00000000000..9d0e37f4e71 --- /dev/null +++ b/src/Analyser/ExpressionResultStorageStack.php @@ -0,0 +1,57 @@ + results -> scopes -> storage) + * that never gets collected because the cycle collector is disabled + * in bin/phpstan. + * + * NodeScopeResolver pushes a storage for the duration of an analysis (file, + * statement list, trait pass, on-demand expression) through + * MutatingScope::pushExpressionResultStorage() and must always pop it + * in a finally block. Old-world type questions about expressions whose + * handler no longer implements TypeResolvingExprHandler are answered from + * the current storage (see MutatingScope::resolveTypeOfNewWorldHandlerNode()). + * A scope used outside any running analysis simply misses here and resolves + * on demand with a throwaway storage. + */ +final class ExpressionResultStorageStack +{ + + /** @var list */ + private array $stack = []; + + public function push(ExpressionResultStorage $storage): void + { + $this->stack[] = $storage; + } + + public function pop(): void + { + if (count($this->stack) === 0) { + throw new ShouldNotHappenException('Unbalanced ExpressionResultStorageStack pop.'); + } + + array_pop($this->stack); + } + + public function getCurrent(): ?ExpressionResultStorage + { + if (count($this->stack) === 0) { + return null; + } + + return $this->stack[count($this->stack) - 1]; + } + +} diff --git a/src/Analyser/Fiber/BeforeScopeForExprRequest.php b/src/Analyser/Fiber/ExpressionResultRequest.php similarity index 61% rename from src/Analyser/Fiber/BeforeScopeForExprRequest.php rename to src/Analyser/Fiber/ExpressionResultRequest.php index 0fc6ecd35cd..4f3ecabdf97 100644 --- a/src/Analyser/Fiber/BeforeScopeForExprRequest.php +++ b/src/Analyser/Fiber/ExpressionResultRequest.php @@ -3,12 +3,11 @@ namespace PHPStan\Analyser\Fiber; use PhpParser\Node\Expr; -use PHPStan\Analyser\MutatingScope; -final class BeforeScopeForExprRequest +final class ExpressionResultRequest { - public function __construct(public readonly Expr $expr, public readonly MutatingScope $scope) + public function __construct(public readonly Expr $expr, public readonly FiberScope $scope) { } diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index e8d160f6a1d..6e7d2cd8df8 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -5,15 +5,20 @@ use Fiber; use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; use function array_pop; use function count; +use function get_class; use function get_debug_type; +use function spl_object_id; +use function sprintf; #[AutowiredService(as: FiberNodeScopeResolver::class)] final class FiberNodeScopeResolver extends NodeScopeResolver @@ -29,6 +34,12 @@ public function callNodeCallback( ExpressionResultStorage $storage, ): void { + if ($nodeCallback instanceof NoopNodeCallback) { + // fibers exist solely to let node callbacks ask about types, + // a noop callback does not need one + return; + } + if (Fiber::getCurrent() !== null) { $nodeCallback($node, $scope->toFiberScope()); return; @@ -48,26 +59,26 @@ public function callNodeCallback( $this->runFiberForNodeCallback($storage, $fiber, $request); } - public function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void + public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { - $storage->storeBeforeScope($expr, $beforeScope); - $this->processPendingFibersForRequestedExpr($storage, $expr, $beforeScope); + parent::storeExpressionResult($storage, $expr, $expressionResult); + $this->processPendingFibersForRequestedExpr($storage, $expr, $expressionResult); } /** - * @param Fiber $fiber + * @param Fiber $fiber */ private function runFiberForNodeCallback( ExpressionResultStorage $storage, Fiber $fiber, - BeforeScopeForExprRequest|ParkFiberRequest|null $request, + ExpressionResultRequest|ParkFiberRequest|null $request, ): void { while (!$fiber->isTerminated()) { - if ($request instanceof BeforeScopeForExprRequest) { - $beforeScope = $storage->findBeforeScope($request->expr); - if ($beforeScope !== null) { - $request = $fiber->resume($beforeScope); + if ($request instanceof ExpressionResultRequest) { + $expressionResult = $storage->findExpressionResult($request->expr); + if ($expressionResult !== null) { + $request = $fiber->resume($expressionResult); continue; } @@ -100,16 +111,48 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void foreach ($storage->pendingFibers as $key => $pending) { $request = $pending['request']; - $beforeScope = $storage->findBeforeScope($request->expr); - if ($beforeScope !== null) { + // A fiber suspended on an expression that is still being processed + // must not be flushed here: this boundary is a nested statement list + // inside that very expression (e.g. an immediately-invoked closure's + // body). The fiber is resumed when the enclosing processExprNode + // stores the result. + if (isset($this->processingExprIds[spl_object_id($request->expr)])) { + continue; + } + + $expressionResult = $storage->findExpressionResult($request->expr); + + if ($expressionResult !== null) { throw new ShouldNotHappenException('Pending fibers at the end should be about synthetic nodes'); } + // Only nodes built during analysis (rules constructing synthetic + // comparisons, ArgumentsNormalizer rewrites, ...) should reach the + // on-demand path here. A node from the file's parsed AST left pending + // means a rule asked about its type but it was never processed and + // stored during natural traversal - a gap to fix at the producing + // handler. Guard kept dormant; enable with PHPSTAN_GUARD_NW=1. + if (self::$guardNewWorld && isset(self::$guardRealExprIds[spl_object_id($request->expr)])) { + throw new ShouldNotHappenException(sprintf( + 'Pending fiber about real AST node %s on line %d - it should have been processed and its result stored during natural traversal.', + get_class($request->expr), + $request->expr->getStartLine(), + )); + } + unset($storage->pendingFibers[$key]); $fiber = $pending['fiber']; - $request = $fiber->resume($request->scope); + + // Process the synthetic node with a duplicated storage so that the result + // computed from the asker's scope does not poison the real storage. + $expressionResult = $this->processExprOnDemand( + $request->expr, + $request->scope->toMutatingScope(), + $storage->duplicate(), + ); + $request = $fiber->resume($expressionResult); $this->runFiberForNodeCallback($storage, $fiber, $request); // Break and restart the loop since the array may have been modified @@ -117,7 +160,7 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void } } - private function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, Scope $result): void + private function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { start: @@ -130,7 +173,7 @@ private function processPendingFibersForRequestedExpr(ExpressionResultStorage $s unset($storage->pendingFibers[$key]); $fiber = $pending['fiber']; - $request = $fiber->resume($result); + $request = $fiber->resume($expressionResult); $this->runFiberForNodeCallback($storage, $fiber, $request); // Break and restart the loop since the array may have been modified diff --git a/src/Analyser/Fiber/FiberScope.php b/src/Analyser/Fiber/FiberScope.php index b02f322c358..698cf7fa1d0 100644 --- a/src/Analyser/Fiber/FiberScope.php +++ b/src/Analyser/Fiber/FiberScope.php @@ -4,6 +4,7 @@ use Fiber; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; @@ -11,6 +12,7 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Type\Type; use function array_pop; +use function count; final class FiberScope extends MutatingScope { @@ -57,12 +59,20 @@ public function toMutatingScope(): MutatingScope /** @api */ public function getType(Expr $node): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($node, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($node, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + if ( + !$this->nativeTypesPromoted + && count($this->truthyValueExprs) === 0 + && count($this->falseyValueExprs) === 0 + ) { + return $expressionResult->getType(); + } + + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getType($node); } @@ -79,23 +89,31 @@ public function getScopeNativeType(Expr $expr): Type /** @api */ public function getNativeType(Expr $expr): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($expr, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($expr, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + if ( + !$this->nativeTypesPromoted + && count($this->truthyValueExprs) === 0 + && count($this->falseyValueExprs) === 0 + ) { + return $expressionResult->getNativeType(); + } + + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getNativeType($expr); } public function getKeepVoidType(Expr $node): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($node, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($node, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getKeepVoidType($node); } diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php new file mode 100644 index 00000000000..869d4654d8c --- /dev/null +++ b/src/Analyser/IssetabilityDescriptor.php @@ -0,0 +1,274 @@ +kind === self::KIND_VARIABLE; + } + + public function isOffset(): bool + { + return $this->kind === self::KIND_OFFSET; + } + + public function isProperty(): bool + { + return $this->kind === self::KIND_PROPERTY; + } + + public function getVariableName(): ?string + { + return $this->variableName; + } + + public function getVarResult(): ?ExpressionResult + { + return $this->varResult; + } + + public function getDimResult(): ?ExpressionResult + { + return $this->dimResult; + } + + public function getInnerResult(): ?ExpressionResult + { + return $this->innerResult; + } + + public function resolvePropertyReflection(MutatingScope $scope): ?FoundPropertyReflection + { + if ($this->reflectionResolver === null) { + throw new ShouldNotHappenException(); + } + + return ($this->reflectionResolver)($scope); + } + + /** + * @return PropertyFetch|StaticPropertyFetch|null + */ + public function getPropertyFetch(): ?Expr + { + return $this->propertyFetch; + } + + /** + * @param callable(Type): ?bool $typeCallback + */ + public function check(MutatingScope $scope, callable $typeCallback, ?bool $result = null): ?bool + { + if ($this->kind === self::KIND_VARIABLE) { + $variableName = $this->variableName; + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); + if ($hasVariable->maybe()) { + return null; + } + + if ($result === null) { + if ($hasVariable->yes()) { + if ($variableName === '_SESSION') { + return null; + } + + return $typeCallback($scope->getVariableType($variableName)); + } + + return false; + } + + return $result; + } + + if ($this->kind === self::KIND_OFFSET) { + $varResult = $this->varResult; + $dimResult = $this->dimResult; + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $varResult->getTypeForScope($scope); + if (!$type->isOffsetAccessible()->yes()) { + return $result ?? $this->checkUndefinedInner($varResult, $scope); + } + + $dimType = $dimResult->getTypeForScope($scope); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if ($hasOffsetValue->no()) { + return false; + } + + // If offset cannot be null, store this error message and see if one of the earlier offsets is. + // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. + if ($hasOffsetValue->yes()) { + $result = $typeCallback($type->getOffsetValueType($dimType)); + + if ($result !== null) { + return $this->checkInner($varResult, $scope, $typeCallback, $result); + } + } + + // Has offset, it is nullable + return null; + } + + $reflectionResolver = $this->reflectionResolver; + $propertyFetch = $this->propertyFetch; + if ($reflectionResolver === null || $propertyFetch === null) { + throw new ShouldNotHappenException(); + } + $innerResult = $this->innerResult; + + $propertyReflection = $reflectionResolver($scope); + if ($propertyReflection === null) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + if (!$propertyReflection->isNative()) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if (!$scope->hasExpressionType($propertyFetch)->yes()) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null || !$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked())) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + } + } + + if ($result !== null) { + return $innerResult !== null ? $this->checkInner($innerResult, $scope, $typeCallback, $result) : $result; + } + + $result = $typeCallback($propertyReflection->getWritableType()); + if ($result !== null && $innerResult !== null) { + return $this->checkInner($innerResult, $scope, $typeCallback, $result); + } + + return $result; + } + + public function checkUndefined(MutatingScope $scope): ?bool + { + if ($this->kind === self::KIND_VARIABLE) { + $variableName = $this->variableName; + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); + if (!$hasVariable->no()) { + return null; + } + + return false; + } + + if ($this->kind === self::KIND_OFFSET) { + $varResult = $this->varResult; + $dimResult = $this->dimResult; + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $varResult->getTypeForScope($scope); + if (!$type->isOffsetAccessible()->yes()) { + return $this->checkUndefinedInner($varResult, $scope); + } + + $dimType = $dimResult->getTypeForScope($scope); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$hasOffsetValue->no()) { + return $this->checkUndefinedInner($varResult, $scope); + } + + return false; + } + + $innerResult = $this->innerResult; + + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + /** + * @param callable(Type): ?bool $typeCallback + */ + private function checkInner(ExpressionResult $inner, MutatingScope $scope, callable $typeCallback, ?bool $result): ?bool + { + return $inner->issetCheck($scope, $typeCallback, $result); + } + + private function checkUndefinedInner(ExpressionResult $inner, MutatingScope $scope): ?bool + { + return $inner->issetCheckUndefined($scope); + } + +} diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 30bad59a9a4..b554cc1a3dd 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -41,6 +41,8 @@ final class LazyInternalScopeFactory implements InternalScopeFactory private ?ConstantResolver $constantResolver = null; + private ExpressionResultStorageStack $expressionResultStorageStack; + private ?PhpVersion $phpVersionType = null; private ?AttributeReflectionFactory $attributeReflectionFactory = null; @@ -52,10 +54,12 @@ public function __construct( private Container $container, private $nodeCallback, private bool $fiber = false, + ?ExpressionResultStorageStack $expressionResultStorageStack = null, ) { $this->phpVersion = $this->container->getParameter('phpVersion'); $this->currentSimpleVersionParser = $this->container->getService('currentPhpVersionSimpleParser'); + $this->expressionResultStorageStack = $expressionResultStorageStack ?? new ExpressionResultStorageStack(); } public function create( @@ -105,6 +109,7 @@ public function create( $this->propertyReflectionFinder, $this->currentSimpleVersionParser, $this->constantResolver, + $this->expressionResultStorageStack, $context, $this->phpVersionType, $this->attributeReflectionFactory, @@ -130,12 +135,12 @@ public function create( public function toFiberFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, true); + return new self($this->container, $this->nodeCallback, true, $this->expressionResultStorageStack); } public function toMutatingFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, false); + return new self($this->container, $this->nodeCallback, false, $this->expressionResultStorageStack); } } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e02c18b0da3..05f8326d6ef 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -28,8 +28,8 @@ use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\CloneReinitializationExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; @@ -126,6 +126,7 @@ use function is_string; use function ltrim; use function md5; +use function spl_object_id; use function sprintf; use function str_contains; use function str_starts_with; @@ -186,6 +187,7 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private ConstantResolver $constantResolver, + private ExpressionResultStorageStack $expressionResultStorageStack, protected ScopeContext $context, private PhpVersion $phpVersion, private AttributeReflectionFactory $attributeReflectionFactory, @@ -896,6 +898,18 @@ public function getAnonymousFunctionReturnType(): ?Type /** @api */ public function getType(Expr $node): Type { + if ( + NodeScopeResolver::$guardNewWorld + && isset(NodeScopeResolver::$guardRealExprIds[spl_object_id($node)]) + && !isset(NodeScopeResolver::$guardProcessedExprIds[spl_object_id($node)]) + ) { + throw new ShouldNotHappenException(sprintf( + 'getType() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.', + get_class($node), + $node->getStartLine(), + )); + } + $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { @@ -984,24 +998,250 @@ private function resolveType(string $exprString, Expr $node): Type return $this->expressionTypes[$exprString]->getType(); } + // NodeScopeResolver intercepts a first-class callable CallLike before the + // ExprHandler loop - no handler supports the original node, its closure + // type lives on the stored result's typeCallback (see the *CallableNode + // handlers), mirroring TypeSpecifier::specifyTypesInCondition(). + if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + return $this->resolveTypeOfNewWorldHandlerNode($node); + } + /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { if (!$exprHandler->supports($node)) { continue; } - return $exprHandler->resolveType($this, $node); + if ($exprHandler instanceof TypeResolvingExprHandler) { + // A call handler that processed this node wires a typeCallback onto + // its stored ExpressionResult carrying the acceptor resolved from + // the arg types gathered on the arg-to-arg evolving scope. Prefer it + // over resolveType(), whose own re-selection would lose generics + // inferred from sibling args. resolveType() still answers synthetic / + // not-yet-processed nodes. + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null && $result->hasTypeCallback()) { + return $result->getTypeForScope($this->toMutatingScope()); + } + } + + return $exprHandler->resolveType($this, $node); + } + + return $this->resolveTypeOfNewWorldHandlerNode($node); } return new MixedType(); } + /** + * The handler of the node no longer implements TypeResolvingExprHandler. + * The answer comes from the ExpressionResult stored during the analysis + * currently in progress, or from processing the node on demand (synthetic + * nodes, or no analysis in progress at all). + * + * The scope deliberately does not reference the storage - that would create + * a reference cycle that never gets collected (see ExpressionResultStorageStack). + */ + private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type + { + // the hooks are the boundary between the rule-facing world and the + // engine - a rule's FiberScope must not flow into result callbacks or + // on-demand processing, where its suspending type asks crash outside + // a fiber + $scope = $this->toMutatingScope(); + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null) { + if (!$result->hasTypeCallback()) { + throw new ShouldNotHappenException(sprintf( + 'ExprHandler for %s does not implement TypeResolvingExprHandler but its ExpressionResult is missing a typeCallback.', + get_class($node), + )); + } + + return $result->getTypeForScope($scope); + } + } + + // a synthetic node, or no analysis in progress + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $node, + $scope, + $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + ); + + return $onDemandResult->getTypeForScope($scope); + } + + /** + * Prices the current (phpdoc, native) type pair of an expression that + * applySpecifiedTypes() needs to intersect with or subtract from but that + * is not tracked in the scope. Old-world filterBySpecifiedTypes() asked + * Scope::getType() here; pricing from the stored ExpressionResult answers + * through the typeCallback for converted handlers. A synthetic node the + * analysis never processed - e.g. the plain-chain variant a nullsafe + * narrowing emits ($a->b() alongside $a?->b()) - is priced on demand, + * mirroring resolveTypeOfNewWorldHandlerNode(); its real subnodes answer + * from stored results so the on-demand walk terminates. Returns null only + * when there is no analysis in progress to price against. + * + * @return array{Type, Type}|null + */ + private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array + { + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage === null) { + return null; + } + + $result = $storage->findExpressionResult($expr); + if ($result === null) { + // a synthetic node - price it on demand, see + // resolveTypeOfNewWorldHandlerNode() + $scope = $this->toMutatingScope(); + $result = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $expr, + $scope, + $storage->duplicate(), + ); + + return [ + $result->getTypeForScope($scope), + $result->getNativeTypeForScope($scope), + ]; + } + + // re-evaluate on the asking scope, not the stored beforeScope: a handler + // (e.g. isset/empty via NonNullabilityHelper) may have processed the + // inner expression on a scope that strips null, so the cached type would + // be stale for the narrowing the caller is applying + return [ + $result->getTypeForScope($this), + $result->getNativeTypeForScope($this), + ]; + } + + /** + * Narrowing counterpart of resolveTypeOfNewWorldHandlerNode() - the old-world + * TypeSpecifier dispatcher asks here for nodes whose handler no longer + * implements TypeResolvingExprHandler. Returns null when the ExpressionResult + * carries no specifyTypesCallback - the dispatcher falls back to default + * truthy/falsey narrowing, which is what such handlers used to implement. + * + * @internal + */ + public function specifyTypesOfNewWorldHandlerNode(Expr $node, TypeSpecifierContext $context): ?SpecifiedTypes + { + // see resolveTypeOfNewWorldHandlerNode() - rules ask the dispatcher + // with their FiberScope (e.g. ImpossibleCheckTypeHelper), the engine + // side of the boundary works with the mutating flavor + $scope = $this->toMutatingScope(); + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null) { + return $result->getSpecifiedTypesForScope($scope, $context); + } + } + + // a synthetic node, or no analysis in progress + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $node, + $scope, + $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + ); + + return $onDemandResult->getSpecifiedTypesForScope($scope, $context); + } + + /** + * Makes the storage answer type questions asked on this scope (and every + * scope sharing its ExpressionResultStorageStack) for the duration of an + * analysis. The caller must pop in a finally block. + */ + public function pushExpressionResultStorage(ExpressionResultStorage $storage): void + { + $this->expressionResultStorageStack->push($storage); + } + + public function popExpressionResultStorage(): void + { + $this->expressionResultStorageStack->pop(); + } + + /** + * The ExpressionResultStorage of the analysis currently in progress, the one + * resolveTypeOfNewWorldHandlerNode() prices synthetic nodes against. A handler + * pricing a synthetic node from a lazily-invoked typeCallback must use this + * (not a storage captured at processExpr() time): a later re-evaluation + * (e.g. findEarlyTerminatingExpr()) runs under a different current storage, + * and the captured one would resolve the synthetic node's real subnodes from + * stale stored results. + * + * @internal + */ + public function getCurrentExpressionResultStorage(): ?ExpressionResultStorage + { + return $this->expressionResultStorageStack->getCurrent(); + } + + /** + * The isset/empty/?? chain descriptor PHPStan\Rules\IssetCheck folds. Reads + * it from the current expression-result storage; when the rule asks before + * the engine has stored the expression's result (the rule callback fires + * before the chain-link handlers run), the expression is processed on demand + * just like resolveTypeOfNewWorldHandlerNode(). + * + * @internal + */ + public function getIssetabilityDescriptor(Expr $expr): ?IssetabilityDescriptor + { + $scope = $this->toMutatingScope(); + $storage = $this->expressionResultStorageStack->getCurrent(); + $onDemandStorage = $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(); + if ($storage !== null) { + $result = $storage->findExpressionResult($expr); + if ($result !== null) { + $descriptor = $result->getIssetabilityDescriptor(); + if ($descriptor !== null) { + return $descriptor; + } + + // a placeholder result (e.g. the var of `$x['k'] ??= …`, stored + // as an assignment target) carries no descriptor; re-process on a + // fresh storage so the placeholder doesn't shadow the real one + // (processExprOnDemand returns stored results, incl. the placeholder) + $onDemandStorage = new ExpressionResultStorage(); + } + } + + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $expr, + $scope, + $onDemandStorage, + ); + + return $onDemandResult->getIssetabilityDescriptor(); + } + /** * @param callable(Type): ?bool $typeCallback */ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool { // mirrored in PHPStan\Rules\IssetCheck + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $exprResult = $storage->findExpressionResult($expr); + if ($exprResult !== null && $exprResult->getIssetabilityDescriptor() !== null) { + return $exprResult->issetCheck($this, $typeCallback, $result); + } + } + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $this->hasVariableType($expr->name); if ($hasVariable->maybe()) { @@ -1195,7 +1435,7 @@ public function getKeepVoidType(Expr $node): Type return $this->getType($clonedNode); } - public function doNotTreatPhpDocTypesAsCertain(): Scope + public function doNotTreatPhpDocTypesAsCertain(): self { return $this->promoteNativeTypes(); } @@ -2436,7 +2676,10 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN $scope = $scope->assignExpression( new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetExistingOffsetValueTypeExpr( $iteratee, - new GetIterableKeyTypeExpr($iteratee), + new NativeTypeExpr( + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + ), new Variable($valueName), )), $valueType, @@ -3385,6 +3628,17 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } + if ( + !$typeSpecification['sure'] + && $expr instanceof Variable && is_string($expr->name) + && $scope->hasVariableType($expr->name)->no() + ) { + // removing type from a certainly-undefined variable cannot make + // it defined; a sure specification (e.g. is_string($a)) still can - + // the condition can only hold for a defined variable + continue; + } + if ($typeSpecification['sure']) { if ($specifiedTypes->shouldOverwrite()) { $scope = $scope->assignExpression($expr, $type, $type); @@ -3397,6 +3651,198 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getScopeType($expr)); } + $scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions); + + /** @var static */ + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, + ); + } + + /** + * New-world counterpart of filterBySpecifiedTypes. + * + * The types inside SpecifiedTypes were already computed from ExpressionResults + * by the specifyTypesCallback of an ExprHandler. This method must never call + * Scope::getType() - it only combines the given types with already-tracked + * expression type holders. + */ + public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self + { + $typeSpecifications = []; + foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => true, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => false, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + + usort($typeSpecifications, static function (array $a, array $b): int { + $length = strlen($a['exprString']) - strlen($b['exprString']); + if ($length !== 0) { + return $length; + } + + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric + }); + + $scope = $this; + $specifiedExpressions = []; + foreach ($typeSpecifications as $typeSpecification) { + $expr = $typeSpecification['expr']; + $type = $typeSpecification['type']; + $exprString = $typeSpecification['exprString']; + + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); + + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); + } + + continue; + } + + if ( + !$typeSpecification['sure'] + && $expr instanceof Variable && is_string($expr->name) + && $scope->hasVariableType($expr->name)->no() + ) { + // removing type from a certainly-undefined variable cannot make + // it defined; a sure specification (e.g. is_string($a)) still can - + // the condition can only hold for a defined variable + continue; + } + + // only Yes-certainty holders hold the current type of the expression - + // a Maybe-certainty holder holds the when-defined type (e.g. after + // merging a branch where the expression was never assigned), which + // the certainty-aware Scope::getType() of the old world never returned + $trackedType = null; + $trackedNativeType = null; + if ( + array_key_exists($exprString, $scope->expressionTypes) + && $scope->expressionTypes[$exprString]->getCertainty()->yes() + ) { + $trackedType = $scope->expressionTypes[$exprString]->getType(); + } + if ( + array_key_exists($exprString, $scope->nativeExpressionTypes) + && $scope->nativeExpressionTypes[$exprString]->getCertainty()->yes() + ) { + $trackedNativeType = $scope->nativeExpressionTypes[$exprString]->getType(); + } + if ($trackedType === null) { + $currentTypes = $scope->getCurrentTypesOfSpecifiedExpr($expr); + if ($currentTypes !== null) { + if ($scope->isComplexUnionType($currentTypes[0])) { + continue; + } + + $trackedType = $currentTypes[0]; + $trackedNativeType ??= $currentTypes[1]; + } + } + + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $newType = $trackedType !== null ? TypeCombinator::intersect($type, $trackedType) : $type; + $newNativeType = $trackedNativeType !== null ? TypeCombinator::intersect($type, $trackedNativeType) : $type; + $scope = $scope->specifyExpressionType($expr, $newType, $newNativeType, TrinaryLogic::createYes()); + } + } else { + if ($type instanceof NeverType || $trackedType instanceof NeverType) { + continue; + } + $newType = $trackedType !== null ? TypeCombinator::remove($trackedType, $type) : null; + if ($newType === null) { + // the expression is not tracked - there is nothing to subtract from + continue; + } + $newNativeType = $trackedNativeType !== null ? TypeCombinator::remove($trackedNativeType, $type) : $newType; + $scope = $scope->specifyExpressionType($expr, $newType, $newNativeType, TrinaryLogic::createYes()); + } + + $holderType = array_key_exists($exprString, $scope->expressionTypes) + ? $scope->expressionTypes[$exprString]->getType() + : $type; + $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType); + } + + $scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions); + + /** @var static */ + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, + ); + } + + /** + * Matches already-registered conditional expressions against the just-specified + * expression type holders and applies the matching consequences. + * + * Mutates and returns $this - only to be called on an intermediate scope + * that is about to be rebuilt through the scope factory. + * + * @param array $specifiedExpressions + */ + private function processConditionalExpressionsAfterSpecifying(array $specifiedExpressions): self + { + $scope = $this; $conditions = []; $originallySpecifiedExprStrings = $specifiedExpressions; $prevSpecifiedCount = -1; @@ -3475,25 +3921,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } - /** @var static */ - return $scope->scopeFactory->create( - $scope->context, - $scope->isDeclareStrictTypes(), - $scope->getFunction(), - $scope->getNamespace(), - $scope->expressionTypes, - $scope->nativeExpressionTypes, - $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), - $scope->inClosureBindScopeClasses, - $scope->anonymousFunctionReflection, - $scope->inFirstLevelStatement, - $scope->currentlyAssignedExpressions, - $scope->currentlyAllowedUndefinedExpressions, - $scope->inFunctionCallsStack, - $scope->afterExtractCall, - $scope->parentScope, - $scope->nativeTypesPromoted, - ); + return $scope; } /** diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3f63434d03f..f48dd539088 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -51,6 +51,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; +use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; @@ -78,8 +79,7 @@ use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -175,10 +175,12 @@ use function array_slice; use function array_values; use function count; +use function getenv; use function in_array; use function is_array; use function is_int; use function is_string; +use function spl_object_id; use function sprintf; use function strtolower; use function trim; @@ -206,6 +208,44 @@ class NodeScopeResolver /** @var array */ private array $calledMethodResults = []; + /** + * When processing a synthetic node on demand (for a Fiber request), real AST + * nodes contained in it were already processed and must not be processed again. + */ + protected bool $returnStoredExpressionResults = false; + + /** + * spl_object_id => recursion depth of the expressions currently being + * processed by processExprNode. A fiber pending on one of them must not be + * flushed at a nested statement-list boundary inside that expression - it + * is resumed when the expression's own processing stores its result. + * + * @var array + */ + protected array $processingExprIds = []; + + /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */ + public static bool $guardNewWorld = 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 + * tell a real AST node from a node a rule built during analysis (which + * legitimately resolves on demand). Static so MutatingScope can read it. + * + * @var array + */ + public static array $guardRealExprIds = []; + + /** + * spl_object_id => true of every Expr already processed by processExprNode + * in the current file. Used by the MutatingScope::getType guard to detect a + * real AST node whose type is asked before it was processed. + * + * @var array + */ + public static array $guardProcessedExprIds = []; + /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls @@ -244,6 +284,7 @@ public function __construct( #[AutowiredParameter] private readonly bool $treatPhpDocTypesAsCertain, private readonly ImplicitToStringCallHelper $implicitToStringCallHelper, + private readonly ExpressionResultFactory $expressionResultFactory, ) { $earlyTerminatingMethodNames = []; @@ -253,6 +294,8 @@ public function __construct( } } $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; + + self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; } /** @@ -275,7 +318,34 @@ public function processNodes( callable $nodeCallback, ): void { + if (self::$guardNewWorld) { + self::$guardRealExprIds = []; + self::$guardProcessedExprIds = []; + foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) { + self::$guardRealExprIds[spl_object_id($realExpr)] = true; + } + } + $expressionResultStorage = new ExpressionResultStorage(); + $scope->pushExpressionResultStorage($expressionResultStorage); + try { + $this->processNodesWithStorage($nodes, $scope, $expressionResultStorage, $nodeCallback); + } finally { + $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 = []; @@ -353,8 +423,14 @@ public function processNodes( $this->processPendingFibers($expressionResultStorage); } - public function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void + public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { + if (self::$guardNewWorld) { + self::$guardProcessedExprIds[spl_object_id($expr)] = true; + } + // converted handlers (no TypeResolvingExprHandler) are answered from + // stored results in both worlds - storing must not depend on fibers + $storage->storeExpressionResult($expr, $expressionResult); } protected function processPendingFibers(ExpressionResultStorage $storage): void @@ -491,14 +567,19 @@ public function processStmtNodes( ): StatementResult { $storage = new ExpressionResultStorage(); - return $this->processStmtNodesInternal( - $parentNode, - $stmts, - $scope, - $storage, - $nodeCallback, - $context, - )->toPublic(); + $scope->pushExpressionResultStorage($storage); + try { + return $this->processStmtNodesInternal( + $parentNode, + $stmts, + $scope, + $storage, + $nodeCallback, + $context, + )->toPublic(); + } finally { + $scope->popExpressionResultStorage(); + } } /** @@ -522,7 +603,22 @@ private function processStmtNodesInternal( $nodeCallback, $context, ); - $this->processPendingFibers($storage); + // Flush pending fibers only at a scope boundary - a function/method body, + // a class/trait body, a namespace. Nested control-flow statement lists + // (if/else branches, loop and switch/try bodies) must NOT flush: a rule + // invoked at the scope's entry node (e.g. UnusedConstructorParametersRule + // on InClassMethodNode) asks the types of expressions appearing later in + // the body, and a flush at an earlier branch would resolve those fibers + // on the asker's scope before natural traversal stores the results. Such + // fibers are resumed when their expression stores its result, or at this + // scope boundary once the whole body is processed. + if ( + $parentNode instanceof Node\FunctionLike + || $parentNode instanceof Node\Stmt\ClassLike + || $parentNode instanceof Node\Stmt\Namespace_ + ) { + $this->processPendingFibers($storage); + } return $statementResult; } @@ -957,7 +1053,7 @@ public function processStmtNode( $gatheredYieldStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { + $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -971,7 +1067,7 @@ public function processStmtNode( && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection && $scope->getFunction()->getDeclaringClass()->hasConstructor() && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() - && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + && TypeUtils::findThisType($this->readStoredOrPriceOnDemand($node->getPropertyFetch()->var, $scope->toMutatingScope())) !== null ) { return; } @@ -1058,7 +1154,7 @@ public function processStmtNode( $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($this, $echoExpr, $scope, $result); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $result->getScope(); @@ -1300,9 +1396,9 @@ public function processStmtNode( $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage); } } elseif ($stmt instanceof If_) { - $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); - $ifAlwaysTrue = $conditionType->isTrue()->yes(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $ifAlwaysTrue = $conditionType->isTrue()->yes(); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); @@ -1336,8 +1432,8 @@ public function processStmtNode( $condScope = $scope; foreach ($stmt->elseifs as $elseif) { $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage); - $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($condScope) : $condResult->getNativeTypeForScope($scope))->toBoolean(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); @@ -1414,10 +1510,20 @@ public function processStmtNode( $throwPoints = []; $impurePoints = []; - $traitStorage = $storage->duplicate(); - $traitStorage->pendingFibers = []; - $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback); - $this->processPendingFibers($traitStorage); + // fresh storage - the same trait node objects are processed once per + // using class and fibers must not see results from a previous pass + $traitStorage = new ExpressionResultStorage(); + $scope->pushExpressionResultStorage($traitStorage); + try { + $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback); + $this->processPendingFibers($traitStorage); + } finally { + $scope->popExpressionResultStorage(); + } + + // class-level node callbacks (like ClassMethodsNode) are invoked with + // the outer storage but ask about expressions inside the used trait + $storage->mergeResults($traitStorage); } elseif ($stmt instanceof Foreach_) { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); @@ -1434,14 +1540,28 @@ public function processStmtNode( $originalScope = $scope; $bodyScope = $scope; + $foreachIterateeType = $condResult->getTypeForScope($originalScope); + $foreachNativeIterateeType = $condResult->getNativeTypeForScope($originalScope); + if ($stmt->keyVar instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope, $storage); + $keyTypeExpr = new NativeTypeExpr( + $originalScope->getIterableKeyType($foreachIterateeType), + $originalScope->getIterableKeyType($foreachNativeIterateeType), + ); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, $keyTypeExpr), $originalScope, $storage); } if ($stmt->valueVar instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope, $storage); + $valueTypeExpr = new NativeTypeExpr( + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), + ); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, $valueTypeExpr), $originalScope, $storage); } elseif ($stmt->valueVar instanceof List_) { - $virtualAssign = new Assign($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)); + $virtualAssign = new Assign($stmt->valueVar, new NativeTypeExpr( + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), + )); $virtualAssign->setAttributes($stmt->valueVar->getAttributes()); $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage); } @@ -1453,19 +1573,21 @@ public function processStmtNode( $storage = $originalStorage->duplicate(); $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; - $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context); + $foreachIterateeType = $condResult->getTypeForScope($originalScope); + $foreachNativeIterateeType = $condResult->getNativeTypeForScope($originalScope); + $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType); if ($unrolledResult !== null) { $bodyScope = $unrolledResult['bodyScope']; $unrolledEndScope = $unrolledResult['endScope']; $unrolledTotalKeys = $unrolledResult['totalKeys']; } else { - $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $count = 0; do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); $storage = $originalStorage->duplicate(); - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -1485,7 +1607,7 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); $storage = $originalStorage; - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context; $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); @@ -1535,7 +1657,7 @@ public function processStmtNode( $finalScope = $unrolledEndScope; } - $exprType = $scope->getType($stmt->expr); + $exprType = $condResult->getTypeForScope($scope); $hasExpr = $scope->hasExpressionType($stmt->expr); if ( count($breakExitPoints) === 0 @@ -1553,8 +1675,8 @@ public function processStmtNode( foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { if ($keyVarExpr !== null) { $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $keyVarExpr); - $dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch); - $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch); + $dimFetchType = $this->priceSyntheticOnDemand($arrayExprDimFetch, $scopeWithIterableValueType); + $dimFetchNativeType = $this->priceSyntheticOnDemand($arrayExprDimFetch, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); // Condition-based narrowings like `is_string($type)` apply to the value // variable but not automatically to the array dim fetch, even though the // two describe the same element for a given iteration. If the value var @@ -1562,21 +1684,21 @@ public function processStmtNode( // the narrowed value-var type in place of the broader dim fetch type so // the loop's final array rewrite below picks up the sharper element type. if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) { - $valueVarType = $scopeWithIterableValueType->getType($stmt->valueVar); + $valueVarType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType); if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) { $dimFetchType = $valueVarType; } - $valueVarNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar); + $valueVarNativeType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) { $dimFetchNativeType = $valueVarNativeType; } } - $keyLoopTypes[] = $scopeWithIterableValueType->getType($keyVarExpr); - $keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($keyVarExpr); + $keyLoopTypes[] = $this->readStoredOrPriceOnDemand($keyVarExpr, $scopeWithIterableValueType); + $keyLoopNativeTypes[] = $this->readStoredOrPriceOnDemand($keyVarExpr, $scopeWithIterableValueType); } else { // No key variable: the narrowed value var is the array element type directly. - $dimFetchType = $scopeWithIterableValueType->getType($stmt->valueVar); - $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar); + $dimFetchType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType); + $dimFetchNativeType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); } $arrayDimFetchLoopTypes[] = $dimFetchType; $arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType; @@ -1588,7 +1710,7 @@ public function processStmtNode( $valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType()); $keyTypeChanged = false; $keyLoopType = $exprType->getIterableKeyType(); - $keyLoopNativeType = $scope->getNativeType($stmt->expr)->getIterableKeyType(); + $keyLoopNativeType = $condResult->getNativeTypeForScope($scope)->getIterableKeyType(); if ($keyVarExpr !== null) { $keyLoopType = TypeCombinator::union(...$keyLoopTypes); $keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes); @@ -1604,7 +1726,7 @@ public function processStmtNode( $newExprType = $newExprType->mapKeyType(static fn (Type $type): Type => $keyLoopType); } - $nativeExprType = $scope->getNativeType($stmt->expr); + $nativeExprType = $condResult->getNativeTypeForScope($scope); $newExprNativeType = $nativeExprType; if ($valueTypeChanged) { $newExprNativeType = $newExprNativeType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopNativeType); @@ -1652,7 +1774,7 @@ public function processStmtNode( $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } - $traversableThrowPoint = $this->getTraversableForeachThrowPoint($scope, $stmt->expr); + $traversableThrowPoint = $this->getTraversableForeachThrowPoint($scope, $stmt->expr, $exprType); if ($traversableThrowPoint !== null) { $throwPoints[] = $traversableThrowPoint; } @@ -1672,7 +1794,7 @@ public function processStmtNode( $originalStorage = $storage; $storage = $originalStorage->duplicate(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep()); - $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($scope) : $condResult->getNativeTypeForScope($scope))->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { if (!$this->polluteScopeWithLoopInitialAssignments) { @@ -1716,14 +1838,15 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; $storage = $originalStorage; - $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $bodyCondResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $bodyScope = $bodyCondResult->getTruthyScope(); $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyCondResult->getTypeForScope($bodyScopeMaybeRan) : $bodyCondResult->getTypeForScope($bodyScopeMaybeRan->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } @@ -1821,7 +1944,7 @@ public function processStmtNode( $alwaysIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScope) : $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScope->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); } @@ -1892,7 +2015,7 @@ public function processStmtNode( // 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 = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($condResultScope) : $condResult->getNativeTypeForScope($condResultScope))->toBoolean(); $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } @@ -1942,7 +2065,7 @@ public function processStmtNode( $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { - $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); + $alwaysIterates = $alwaysIterates->and($this->readStoredOrPriceOnDemand($lastCondExpr, $bodyScope)->toBoolean()->isTrue()); $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope); } @@ -2066,7 +2189,7 @@ public function processStmtNode( } } - $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType; + $exhaustive = $condResult->getTypeForScope($scopeForBranches) instanceof NeverType; if (!$hasDefaultCase && !$exhaustive) { $alwaysTerminating = false; @@ -2319,7 +2442,7 @@ public function processStmtNode( $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); if ($var instanceof ArrayDimFetch && $var->dim !== null) { - $varType = $scope->getType($var->var); + $varType = $this->readStoredOrPriceOnDemand($var->var, $scope); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( $stmt, @@ -2450,7 +2573,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } else { $constantName = new Name\FullyQualified($const->name->toString()); } - $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); + $scope = $scope->assignExpression(new ConstFetch($constantName), $constResult->getTypeForScope($scope), $constResult->getNativeTypeForScope($scope)); } } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; @@ -2466,8 +2589,8 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } $scope = $scope->assignExpression( new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), - $scope->getType($const->value), - $scope->getNativeType($const->value), + $constResult->getTypeForScope($scope), + $constResult->getNativeTypeForScope($scope), ); } } elseif ($stmt instanceof Node\Stmt\EnumCase) { @@ -2691,12 +2814,12 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) { if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { if ($expr instanceof MethodCall) { - $methodCalledOnType = $scope->getType($expr->var); + $methodCalledOnType = $this->readStoredOrPriceOnDemand($expr->var, $scope->toMutatingScope()); } else { if ($expr->class instanceof Name) { $methodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $methodCalledOnType = $scope->getType($expr->class); + $methodCalledOnType = $this->readStoredOrPriceOnDemand($expr->class, $scope->toMutatingScope()); } } @@ -2729,6 +2852,12 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr return $expr; } + // Scope::getType() must stay here (not a scope-state side effect): for a + // `$x[...] ??= []` expression it returns getType()'s cached resolvedTypes + // value, computed during loop convergence when the left side was maybe-set + // (so the coalesced value keeps its optional array{} branch). The + // side-effect-free helpers re-price on the converged scope and drop that + // branch, regressing bug-13623. See AssignHandler::processAssignVar. $exprType = $scope->getType($expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; @@ -2737,6 +2866,85 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr return null; } + /** + * 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. + */ + public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult + { + $this->returnStoredExpressionResults = true; + $scope->pushExpressionResultStorage($storage); + try { + return $this->processExprNode( + new Node\Stmt\Expression($expr), + $expr, + $scope, + $storage, + new NoopNodeCallback(), + ExpressionContext::createTopLevel(), + ); + } finally { + $scope->popExpressionResultStorage(); + $this->returnStoredExpressionResults = false; + } + } + + /** + * Reads the type, on the given scope, of a node an ExprHandler already + * processed (its ExpressionResult is in the storage of the analysis in + * progress). Used from lazily-invoked typeCallbacks instead of + * Scope::getType(): it reads the stored result rather than re-walking, and + * does not allocate a throwaway duplicate storage. Falls back to pricing the + * node as synthetic when it is not stored (e.g. a re-evaluation reached this + * before the original processing did). + */ + public function readStoredOrPriceOnDemand(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage(); + $result = $current?->findExpressionResult($expr); + if ($result !== null) { + return $result->getTypeForScope($scope); + } + + return $this->priceSyntheticOnDemand($expr, $scope); + } + + /** + * Prices 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 priceSyntheticOnDemand(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); + + return $this->processExprOnDemand($expr, $scope, $current->duplicate())->getTypeForScope($scope); + } + + /** Native counterpart of readStoredOrPriceOnDemand(). */ + public function readStoredOrPriceOnDemandNative(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage(); + $result = $current?->findExpressionResult($expr); + if ($result !== null) { + return $result->getNativeTypeForScope($scope); + } + + return $this->priceSyntheticOnDemandNative($expr, $scope); + } + + /** Native counterpart of priceSyntheticOnDemand(). */ + public function priceSyntheticOnDemandNative(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); + + return $this->processExprOnDemand($expr, $scope, $current->duplicate())->getNativeTypeForScope($scope); + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -2749,7 +2957,42 @@ public function processExprNode( ExpressionContext $context, ): ExpressionResult { - $this->storeBeforeScope($storage, $expr, $scope); + if ($this->returnStoredExpressionResults) { + $storedResult = $storage->findExpressionResult($expr); + if ($storedResult !== null) { + return $storedResult; + } + } + + // Track that this expression is being processed. A fiber suspended on it + // (a rule asked its type before processing reached it) must not be + // flushed at a nested statement-list boundary inside this very + // expression - e.g. an immediately-invoked closure's body. It is resumed + // when this processExprNode stores the result below. + $exprId = spl_object_id($expr); + $this->processingExprIds[$exprId] = ($this->processingExprIds[$exprId] ?? 0) + 1; + + try { + return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context); + } finally { + if (--$this->processingExprIds[$exprId] === 0) { + unset($this->processingExprIds[$exprId]); + } + } + } + + /** + * @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); @@ -2763,7 +3006,21 @@ public function processExprNode( throw new ShouldNotHappenException(); } - return $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); + $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(), + // the first-class callable closure type lives on the *CallableNode + // result; delegate so getType() of the original CallLike answers from it + typeCallback: static fn (MutatingScope $s): Type => $newExprResult->getTypeForScope($s), + ); + $this->storeExpressionResult($storage, $expr, $expressionResult); + return $expressionResult; } $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); @@ -2774,23 +3031,23 @@ public function processExprNode( continue; } - return $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); - } - - if ($expr instanceof List_) { - // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); + $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); + $this->storeExpressionResult($storage, $expr, $expressionResult); + return $expressionResult; } - return new ExpressionResult( + $expressionResult = $this->expressionResultFactory->create( $scope, + beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); + $this->storeExpressionResult($storage, $expr, $expressionResult); + + return $expressionResult; } /** @@ -2922,6 +3179,37 @@ public function processClosureNode( ?Type $passedToType, ?Type $nativePassedToType = null, ): ProcessClosureResult + { + // Closures reached as call arguments are processed here directly rather + // than through processExprNode (which tracks the node), so track the + // closure too: the dependency/node callbacks fired for it ask its type + // and suspend a fiber that must not be flushed at a nested boundary + // inside the closure body before the caller stores the closure result. + $exprId = spl_object_id($expr); + $this->processingExprIds[$exprId] = ($this->processingExprIds[$exprId] ?? 0) + 1; + + try { + return $this->processClosureNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); + } finally { + if (--$this->processingExprIds[$exprId] === 0) { + unset($this->processingExprIds[$exprId]); + } + } + } + + /** + * @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); @@ -2945,7 +3233,7 @@ public function processClosureNode( $inAssignRightSideVariableName === $use->var->name && $inAssignRightSideExpr !== null ) { - $inAssignRightSideType = $scope->getType($inAssignRightSideExpr); + $inAssignRightSideType = $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; } else { @@ -2956,7 +3244,7 @@ public function processClosureNode( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr); + $inAssignRightSideNativeType = $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); if ($inAssignRightSideNativeType instanceof ClosureType) { $variableNativeType = $inAssignRightSideNativeType; } else { @@ -3150,33 +3438,51 @@ public function processArrowFunctionNode( $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()); } /** * @param Node\Arg[]|null $args * @return ParameterReflection[]|null */ - public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + public function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, static fn (Scope $s, Expr $e) => $s->getType($e)); + 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(Scope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array + public function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e)); + 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 + * through its TypeResolvingExprHandler (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); + } + + return $this->readStoredOrPriceOnDemand($expr, $scope); } /** * @param Node\Arg[]|null $args - * @param Closure(Scope, Expr): Type $typeGetter + * @param Closure(MutatingScope, Expr): Type $typeGetter * @return ParameterReflection[]|null */ - private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array + private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array { $callableParameters = null; if ($args !== null) { @@ -3304,15 +3610,14 @@ private function processAttributeGroups( $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, + $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, $parametersAcceptor, $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); continue; } @@ -3500,26 +3805,61 @@ private function resolveClosureThisType( /** * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processArgs( Node\Stmt $stmt, $calleeReflection, ?ExtendedMethodReflection $nakedMethodReflection, - ?ParametersAcceptor $parametersAcceptor, + array $parametersAcceptors, + ?array $namedArgumentsVariants, CallLike $callLike, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?MutatingScope $closureBindScope = null, - ): ExpressionResult + ): ArgsResult { $args = $callLike->getArgs(); - $parameters = null; - if ($parametersAcceptor !== null) { - $parameters = $parametersAcceptor->getParameters(); + // 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; + + // Metadata acceptor: drives the per-arg by-ref/variadic matching. Selected + // ONCE from all arg types gathered on the initial scope (mirrors the + // original ParametersAcceptorSelector::selectFromArgs()), so multi-variant + // selection - which depends on the total argument count - is stable across + // the per-arg loop rather than flapping as the prefix grows. + $metadataAcceptor = null; + if ($parametersAcceptors !== []) { + $fastPath = count($parametersAcceptors) === 1 + && !ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($parametersAcceptors[0]) + && !ParametersAcceptorSelector::argsHaveIntrinsicArgOverride($args); + if ($fastPath) { + $metadataAcceptor = $parametersAcceptors[0]; + } else { + $metadataTypes = []; + $metadataUnpack = false; + $metadataHasName = false; + foreach ($args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { + $argType = $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope); + } else { + $argType = $this->readStoredOrPriceOnDemand($arg->value, $scope); + } + $this->addGatheredArgType($metadataTypes, $metadataUnpack, $metadataHasName, $originalArg, $i, $argType); + } + $metadataAcceptor = $this->selectArgsMetadataAcceptor($args, $metadataTypes, $parametersAcceptors, $namedArgumentsVariants, $metadataHasName, $metadataUnpack, $scope); + } } $hasYield = false; @@ -3532,33 +3872,50 @@ public function processArgs( $deferredByRefClosureResults = []; $processingOrder = array_keys($args); - $hasReorderedArgs = false; - foreach ($args as $arg) { - if ($arg->hasAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE)) { - $hasReorderedArgs = true; - break; + 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; } - } - if ($hasReorderedArgs) { - usort($processingOrder, static function (int $a, int $b) use ($args): int { - $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(); - }); - } + $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(); + }); 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; + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope)); + } + + $parameters = $metadataAcceptor?->getParameters(); + $assignByReference = false; $parameter = null; $parameterType = null; @@ -3584,7 +3941,7 @@ public function processArgs( $parameterNativeType = $matchedParameter->getNativeType(); } $parameter = $matchedParameter; - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + } elseif (count($parameters) > 0 && $metadataAcceptor->isVariadic()) { $lastParameter = array_last($parameters); $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); @@ -3656,7 +4013,15 @@ public function processArgs( $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); } - $this->storeBeforeScope($storage, $arg->value, $scopeToPass); + $this->storeExpressionResult($storage, $arg->value, $this->expressionResultFactory->create( + $closureResult->getScope(), + $scopeToPass, + $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); $uses = []; foreach ($arg->value->uses as $use) { @@ -3714,9 +4079,9 @@ public function processArgs( $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), $arrowFunctionResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); } - $this->storeBeforeScope($storage, $arg->value, $scopeToPass); + $this->storeExpressionResult($storage, $arg->value, $arrowFunctionResult); } else { - $exprType = $scope->getType($arg->value); + $exprType = $this->readStoredOrPriceOnDemand($arg->value, $scope); $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; if ($enterExpressionAssignForByRef) { $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); @@ -3745,6 +4110,8 @@ public function processArgs( } } } + + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $exprResult->getTypeForScope($scope)); } if ($assignByReference && $lookForUnset) { @@ -3770,14 +4137,27 @@ public function processArgs( $scope = $deferredClosureResult->applyByRefUseScope($scope); } - if ($parameters !== null) { + // 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(). + $resolvedAcceptor = null; + if ($parametersAcceptors !== []) { + $resolvedAcceptor = $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope); + } + + // The by-ref OUT writeback reads the metadata acceptor: it is selected from + // the full argument count (stable variant) and its parameters are already + // generic-resolved, so OUT types are correct without re-flapping the variant. + $writebackParameters = $metadataAcceptor?->getParameters(); + if ($writebackParameters !== null) { foreach ($args as $i => $arg) { $assignByReference = false; $currentParameter = null; - if (isset($parameters[$i])) { - $currentParameter = $parameters[$i]; - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { - $currentParameter = array_last($parameters); + if (isset($writebackParameters[$i])) { + $currentParameter = $writebackParameters[$i]; + } elseif (count($writebackParameters) > 0 && $metadataAcceptor->isVariadic()) { + $currentParameter = array_last($writebackParameters); } if ($currentParameter !== null) { @@ -3824,15 +4204,16 @@ public function processArgs( $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); } } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { - $argType = $scope->getType($arg->value); + $argType = $this->readStoredOrPriceOnDemand($arg->value, $scope); if (!$argType->isObject()->no()) { $nakedReturnType = null; if ($nakedMethodReflection !== null) { - $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $args, + $nakedParametersAcceptor = $this->selectArgsAcceptor( + $gatheredTypes, $nakedMethodReflection->getVariants(), $nakedMethodReflection->getNamedArgumentsVariants(), + $gatheredHasName, + $gatheredUnpack, ); $nakedReturnType = $nakedParametersAcceptor->getReturnType(); } @@ -3853,7 +4234,160 @@ public function processArgs( } // not storing this, it's scope after processing all args - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return new ArgsResult( + $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints), + $resolvedAcceptor, + ); + } + + /** + * 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; + } + } + + /** + * 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->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(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->readStoredOrPriceOnDemand($e, $scope), + fn (Expr $e): Type => $this->readStoredOrPriceOnDemandNative($e, $scope), + 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( + 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; + } + + $this->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + } } /** @@ -3990,7 +4524,7 @@ public function processVirtualAssign(MutatingScope $scope, ExpressionResultStora $assignedExpr, new VirtualAssignNodeCallback($nodeCallback), ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $assignedExpr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), false, ); } @@ -4058,14 +4592,14 @@ public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultS $scope = $scope->assignVariable( $name, $varTag->getType(), - $scope->getNativeType($variableNode), + $this->priceSyntheticOnDemand($variableNode, $scope->doNotTreatPhpDocTypesAsCertain()), $certainty, ); } } if (count($variableLessTags) === 1 && $defaultExpr !== null) { - $originalType = $scope->getType($defaultExpr); + $originalType = $this->readStoredOrPriceOnDemand($defaultExpr, $scope); $varTag = $variableLessTags[0]; if (!$originalType->equals($varTag->getType())) { $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); @@ -4131,6 +4665,8 @@ private function tryProcessUnrolledConstantArrayForeach( MutatingScope $originalScope, ExpressionResultStorage $originalStorage, StatementContext $context, + Type $iterateeType, + Type $nativeIterateeType, ): ?array { if ($stmt->byRef) { @@ -4143,7 +4679,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $iterateeType = $originalScope->getType($stmt->expr); if (!$iterateeType->isConstantArray()->yes()) { return null; } @@ -4169,7 +4704,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $nativeIterateeType = $originalScope->getNativeType($stmt->expr); $nativeConstantArrays = $nativeIterateeType->getConstantArrays(); $matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null; @@ -4305,7 +4839,7 @@ private function tryProcessUnrolledConstantArrayForeach( $prevLoopScope = $loopScope; $iterStorage = $originalStorage->duplicate(); $iterBodyScope = $loopScope->mergeWith($endScope); - $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, new NoopNodeCallback()); + $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback()); $iterBodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $loopScope = $iterBodyScopeResult->getScope(); foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -4330,9 +4864,8 @@ private function tryProcessUnrolledConstantArrayForeach( return ['bodyScope' => $bodyScope, 'endScope' => $endScope, 'totalKeys' => $totalKeys]; } - private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee): ?InternalThrowPoint + private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee, Type $exprType): ?InternalThrowPoint { - $exprType = $scope->getType($iteratee); $traversableType = new ObjectType(Traversable::class); if ($traversableType->isSuperTypeOf($exprType)->no()) { @@ -4364,13 +4897,12 @@ private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $ite /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope + private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, Type $iterateeType, Type $nativeIterateeType, callable $nodeCallback): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $originalScope->getType($stmt->expr); if ( ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) @@ -4393,7 +4925,10 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $storage, $stmt, $stmt->valueVar, - new GetIterableValueTypeExpr($stmt->expr), + new NativeTypeExpr( + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), + ), $nodeCallback, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); @@ -4408,7 +4943,10 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $storage, $stmt, $stmt->keyVar, - new GetIterableKeyTypeExpr($stmt->expr), + new NativeTypeExpr( + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + ), $nodeCallback, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); @@ -4471,8 +5009,8 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $arrayArg = $args[0]->value; $scope = $scope->assignExpression( new ArrayDimFetch($arrayArg, $stmt->valueVar), - $scope->getType($arrayArg)->getIterableValueType(), - $scope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $scope)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $scope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } @@ -4689,6 +5227,11 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection throw new ShouldNotHappenException(); } $traitScope = $scope->enterTrait($traitReflection); + + // attribute args are not processed as part of the trait statements + // but rules like TraitAttributesRule ask about their types + $this->processAttributeGroups($node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); + $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage); $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel()); return; @@ -4765,7 +5308,7 @@ public function processCalledMethod(MethodReflection $methodReflection): ?Mutati $statementResult = $executionEnd->getStatementResult(); $endNode = $executionEnd->getNode(); if ($endNode instanceof Node\Stmt\Expression) { - $exprType = $statementResult->getScope()->getType($endNode->expr); + $exprType = $this->readStoredOrPriceOnDemand($endNode->expr, $statementResult->getScope()->toMutatingScope()); if ($exprType instanceof NeverType && $exprType->isExplicit()) { continue; } @@ -5150,12 +5693,12 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin && $stmt->init[0]->var->name === $lastCondExpr->left->name ) { $arrayArg = $lastCondExpr->right->getArgs()[0]->value; - $arrayType = $bodyScope->getType($arrayArg); + $arrayType = $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope); if ($arrayType->isList()->yes()) { $bodyScope = $bodyScope->assignExpression( new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left), $arrayType->getIterableValueType(), - $bodyScope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } @@ -5175,12 +5718,12 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin && $stmt->init[0]->var->name === $lastCondExpr->right->name ) { $arrayArg = $lastCondExpr->left->getArgs()[0]->value; - $arrayType = $bodyScope->getType($arrayArg); + $arrayType = $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope); if ($arrayType->isList()->yes()) { $bodyScope = $bodyScope->assignExpression( new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right), $arrayType->getIterableValueType(), - $bodyScope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php index 132c1875809..557f4949469 100644 --- a/src/Analyser/RicherScopeGetTypeHelper.php +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -27,7 +27,7 @@ public function __construct( /** * @return TypeResult */ - public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + public function getIdenticalResult(Scope $scope, Identical $expr, ?NodeScopeResolver $nodeScopeResolver = null): TypeResult { if ( $expr->left instanceof Variable @@ -39,8 +39,15 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult return new TypeResult(new ConstantBooleanType(true), []); } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + // $nodeScopeResolver is passed from inside-out callbacks (e.g. BinaryOp's + // typeCallback) so the operands are read from their ExpressionResults + // instead of Scope::getType(); rules call this without it (BC). + $leftType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope->toMutatingScope()) + : $scope->getType($expr->left); + $rightType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope->toMutatingScope()) + : $scope->getType($expr->right); if ( ( @@ -78,9 +85,9 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult /** * @return TypeResult */ - public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult + public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr, ?NodeScopeResolver $nodeScopeResolver = null): TypeResult { - $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right), $nodeScopeResolver); $identicalType = $identicalResult->type; if ($identicalType instanceof ConstantBooleanType) { return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index 5cfa65dc53b..ad213042f35 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -218,13 +218,18 @@ public function unionWith(SpecifiedTypes $other): self return $result->setRootExpr($rootExpr); } - public function normalize(Scope $scope): self + public function normalize(Scope $scope, ?NodeScopeResolver $nodeScopeResolver = null): self { $sureTypes = $this->sureTypes; foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) { if (!isset($sureTypes[$exprString])) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)]; + // $nodeScopeResolver is passed from inside-out callbacks so the expr + // type is read from its ExpressionResult instead of Scope::getType(). + $exprType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($exprNode, $scope->toMutatingScope()) + : $scope->getType($exprNode); + $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($exprType, $sureNotType)]; continue; } diff --git a/src/Analyser/TypeResolvingExprHandler.php b/src/Analyser/TypeResolvingExprHandler.php new file mode 100644 index 00000000000..030a2dfb8a3 --- /dev/null +++ b/src/Analyser/TypeResolvingExprHandler.php @@ -0,0 +1,30 @@ + + */ +interface TypeResolvingExprHandler extends ExprHandler +{ + + /** + * @param T $expr + */ + public function resolveType(MutatingScope $scope, Expr $expr): Type; + + /** + * @param T $expr + */ + public function specifyTypes( + TypeSpecifier $typeSpecifier, + Scope $scope, + Expr $expr, + TypeSpecifierContext $context, + ): SpecifiedTypes; + +} diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 27156a8b3f0..b91b62fb2e0 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -95,7 +95,18 @@ public function specifyTypesInCondition( continue; } - return $exprHandler->specifyTypes($this, $scope, $expr, $context); + if ($exprHandler instanceof TypeResolvingExprHandler) { + return $exprHandler->specifyTypes($this, $scope, $expr, $context); + } + + if ($scope instanceof MutatingScope) { + $specifiedTypes = $scope->specifyTypesOfNewWorldHandlerNode($expr, $context); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + break; } return $this->specifyDefaultTypes($scope, $expr, $context); diff --git a/src/Node/Expr/GetIterableKeyTypeExpr.php b/src/Node/Expr/GetIterableKeyTypeExpr.php deleted file mode 100644 index 60731730539..00000000000 --- a/src/Node/Expr/GetIterableKeyTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -expr; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetIterableKeyTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Expr/GetIterableValueTypeExpr.php b/src/Node/Expr/GetIterableValueTypeExpr.php deleted file mode 100644 index 642eb4870ea..00000000000 --- a/src/Node/Expr/GetIterableValueTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -expr; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetIterableValueTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php deleted file mode 100644 index 38226855557..00000000000 --- a/src/Node/Expr/GetOffsetValueTypeExpr.php +++ /dev/null @@ -1,42 +0,0 @@ -var; - } - - public function getDim(): Expr - { - return $this->dim; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetOffsetValueTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php deleted file mode 100644 index d662dbe488f..00000000000 --- a/src/Node/Expr/OriginalPropertyTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -propertyFetch; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_OriginalPropertyTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 7409140e8c2..d513a5f901d 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -10,14 +10,10 @@ use PHPStan\Node\Expr\CloneReinitializationExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -50,36 +46,16 @@ protected function pPHPStan_Node_NativeTypeExpr(NativeTypeExpr $expr): string // return sprintf('__phpstanNativeType(%s, %s)', $expr->getPhpDocType()->describe(VerbosityLevel::precise()), $expr->getNativeType()->describe(VerbosityLevel::precise())); } - protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); - } - protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore { return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } - protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); - } - - protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); - } - protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore { return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } - protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); - } - protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index d918b512f22..b0307f8ee88 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -4,6 +4,7 @@ use Closure; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; @@ -80,6 +81,124 @@ public static function selectFromArgs( { $types = []; $unpack = false; + $parametersAcceptors = self::applyIntrinsicArgOverrides( + $args, + $parametersAcceptors, + $namedArgumentsVariants, + $scope, + static fn (Expr $e): Type => $scope->getType($e), + static fn (Expr $e): Type => $scope->getNativeType($e), + static fn (Type $t): Type => $scope->getIterableValueType($t), + static fn (Type $t): Type => $scope->getIterableKeyType($t), + ); + + if (count($parametersAcceptors) === 1) { + $acceptor = $parametersAcceptors[0]; + if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { + return $acceptor; + } + } + + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + if (!array_is_list($args)) { + // actually $args parameter should be typed to list but we can't atm, + // because its a BC break. + $args = array_values($args); + } + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; + } + + $hasName = false; + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = array_last($parameters); + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $rememberTypes = !$originalArg->value instanceof Node\Expr\Closure && !$originalArg->value instanceof Node\Expr\ArrowFunction; + $scope = $scope->pushInFunctionCall(null, $parameter, $rememberTypes); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + 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; + } + } + + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + + return self::selectFromTypes($types, $parametersAcceptors, $unpack); + } + + /** + * @internal + * @param Node\Arg[] $args + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + * @param Closure(Expr): Type $typeGetter + * @param Closure(Expr): Type $nativeTypeGetter + * @param Closure(Type): Type $iterableValueTypeGetter + * @param Closure(Type): Type $iterableKeyTypeGetter + * @return ParametersAcceptor[] + */ + public static function applyIntrinsicArgOverrides( + array $args, + array $parametersAcceptors, + ?array $namedArgumentsVariants, + Scope $scope, + Closure $typeGetter, + Closure $nativeTypeGetter, + Closure $iterableValueTypeGetter, + Closure $iterableKeyTypeGetter, + ): array + { if ( count($args) > 0 && count($parametersAcceptors) > 0 @@ -89,15 +208,15 @@ public static function selectFromArgs( $callbackParameters = []; $nativeCallbackParameters = []; foreach ($arrayMapArgs as $arg) { - $argType = $scope->getType($arg->value); - $nativeArgType = $scope->getNativeType($arg->value); + $argType = ($typeGetter)($arg->value); + $nativeArgType = ($nativeTypeGetter)($arg->value); if ($arg->unpack) { $constantArrays = $argType->getConstantArrays(); if (count($constantArrays) > 0) { foreach ($constantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } } @@ -106,13 +225,13 @@ public static function selectFromArgs( foreach ($nativeConstantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $nativeCallbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } } } else { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeCallbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } @@ -133,7 +252,7 @@ public static function selectFromArgs( } if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { - $optType = $scope->getType($args[1]->value); + $optType = ($typeGetter)($args[1]->value); $valueTypes = []; foreach ($optType->getConstantScalarValues() as $scalarValue) { @@ -177,7 +296,7 @@ public static function selectFromArgs( } if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { - $optArrayType = $scope->getType($args[1]->value); + $optArrayType = ($typeGetter)($args[1]->value); $hasTypes = false; $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -232,23 +351,23 @@ public static function selectFromArgs( $arrayFilterParameters = null; $nativeArrayFilterParameters = null; if (isset($args[2])) { - $mode = $scope->getType($args[2]->value); + $mode = ($typeGetter)($args[2]->value); if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('key', $scope->getIterableKeyType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('item', $scope->getIterableValueType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } } @@ -257,22 +376,22 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $arrayArgType = $scope->getType($args[0]->value); + $arrayArgType = ($typeGetter)($args[0]->value); $callableType = new UnionType([ new CallableType( $arrayFilterParameters ?? [ - new DummyParameter('item', $scope->getIterableValueType($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, ), new NullType(), ]); - $nativeArrayArgType = $scope->getNativeType($args[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); $nativeCallableType = new UnionType([ new CallableType( $nativeArrayFilterParameters ?? [ - new DummyParameter('item', $scope->getIterableValueType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, @@ -314,19 +433,19 @@ public static function selectFromArgs( } if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { - $arrayArgType = $scope->getType($args[0]->value); - $nativeArrayArgType = $scope->getNativeType($args[0]->value); + $arrayArgType = ($typeGetter)($args[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); $arrayWalkParameters = [ - new DummyParameter('item', $scope->getIterableValueType($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayWalkParameters = [ - new DummyParameter('item', $scope->getIterableValueType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; if (isset($args[2])) { - $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeArrayWalkParameters[] = new DummyParameter('arg', $scope->getNativeType($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } $acceptor = $parametersAcceptors[0]; @@ -343,20 +462,20 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $argType = $scope->getType($args[0]->value); + $argType = ($typeGetter)($args[0]->value); $callableType = new CallableType( [ - new DummyParameter('value', $scope->getIterableValueType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('value', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, ); - $nativeArgType = $scope->getNativeType($args[0]->value); + $nativeArgType = ($nativeTypeGetter)($args[0]->value); $nativeCallableType = new CallableType( [ - new DummyParameter('value', $scope->getIterableValueType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('value', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, @@ -371,7 +490,7 @@ public static function selectFromArgs( $closureBindToVar instanceof Node\Expr\Variable && is_string($closureBindToVar->name) ) { - $varType = $scope->getType($closureBindToVar); + $varType = ($typeGetter)($closureBindToVar); if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { $inFunction = $scope->getFunction(); if ($inFunction !== null) { @@ -458,92 +577,64 @@ public static function selectFromArgs( } } - if (count($parametersAcceptors) === 1) { - $acceptor = $parametersAcceptors[0]; - if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { - return $acceptor; - } + return $parametersAcceptors; + } + + /** + * Whether applyIntrinsicArgOverrides() could rewrite the acceptor's parameter + * types for these args (array_map/filter/walk/find, curl_setopt, implode, + * Closure::bind). When false the single-acceptor metadata is override-free and + * processArgs() can skip re-selecting it per argument. Mirrors the attribute + * dispatch in applyIntrinsicArgOverrides(). + * + * @internal + * @param Node\Arg[] $args + */ + public static function argsHaveIntrinsicArgOverride(array $args): bool + { + if (count($args) === 0) { + return false; } - $reorderedArgs = $args; - $parameters = null; - $singleParametersAcceptor = null; - if (count($parametersAcceptors) === 1) { - if (!array_is_list($args)) { - // actually $args parameter should be typed to list but we can't atm, - // because its a BC break. - $args = array_values($args); - } - $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); - $singleParametersAcceptor = $parametersAcceptors[0]; + if ($args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) !== null) { + return true; } - $hasName = false; - foreach ($reorderedArgs ?? $args as $i => $arg) { - $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $parameter = null; - if ($singleParametersAcceptor !== null) { - $parameters = $singleParametersAcceptor->getParameters(); - if (isset($parameters[$i])) { - $parameter = $parameters[$i]; - } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { - $parameter = array_last($parameters); - } - } + if ((bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { + return true; + } - if ($parameter !== null && $scope instanceof MutatingScope) { - $rememberTypes = !$originalArg->value instanceof Node\Expr\Closure && !$originalArg->value instanceof Node\Expr\ArrowFunction; - $scope = $scope->pushInFunctionCall(null, $parameter, $rememberTypes); - } + if (isset($args[1]) && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { + return true; + } - $type = $scope->getType($originalArg->value); + if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { + return true; + } - if ($parameter !== null && $scope instanceof MutatingScope) { - $scope = $scope->popInFunctionCall(); - } + if ((bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + return true; + } - 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; - } + if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { + return true; + } - $types[$valueIndex] = isset($types[$valueIndex]) - ? TypeCombinator::union($types[$valueIndex], $valueType) - : $valueType; - } - } - } else { - $types[$index] = $type->getIterableValueType(); - } - } else { - $types[$index] = $type; - } + if ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + return true; } - if ($hasName && $namedArgumentsVariants !== null) { - return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + if ($args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME) !== null) { + return true; } - return self::selectFromTypes($types, $parametersAcceptors, $unpack); + return $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null; } - private static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool + /** + * @internal + */ + public static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool { if ($acceptor->getReturnType()->hasTemplateOrLateResolvableType()) { return true; @@ -685,6 +776,34 @@ public static function selectFromTypes( return GenericParametersAcceptorResolver::resolve($types, self::combineAcceptors($winningAcceptors)); } + /** + * Picks the structural ParametersAcceptor (parameter names/positions/variadic + * only) that drives argument normalization / reordering. Unlike selectFromArgs() + * it never reads argument types from a Scope, so it is safe to call before the + * arguments have been processed - generics are resolved separately, type-driven. + * + * @internal + * @param Node\Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + public static function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor + { + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = $hasName && $namedArgumentsVariants !== null ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : self::combineAcceptors($selectedVariants); + } + /** * @param ParametersAcceptor[] $acceptors */ diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php index 4dc25092a9a..08cb915cad3 100644 --- a/src/Rules/Arrays/ArrayUnpackingRule.php +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -40,7 +40,10 @@ public function processNode(Node $node, Scope $scope): array $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - new GetIterableKeyTypeExpr($node->value), + new NativeTypeExpr( + $scope->getIterableKeyType($scope->getType($node->value)), + $scope->getIterableKeyType($scope->getNativeType($node->value)), + ), '', static fn (Type $type): bool => $type->isString()->no(), ); diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index 35195e7ea52..e0a2ccf409c 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -4,16 +4,18 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\IssetabilityDescriptor; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function is_string; use function sprintf; use function str_starts_with; @@ -26,7 +28,6 @@ final class IssetCheck public function __construct( private PropertyDescriptor $propertyDescriptor, - private PropertyReflectionFinder $propertyReflectionFinder, #[AutowiredParameter] private bool $checkAdvancedIsset, #[AutowiredParameter] @@ -41,16 +42,32 @@ public function __construct( */ public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { - // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); + $mutatingScope = $scope->toMutatingScope(); + + return $this->doCheck($mutatingScope->getIssetabilityDescriptor($expr), $expr, $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); + } + + /** + * @param ErrorIdentifier $identifier + * @param callable(Type): ?string $typeMessageCallback + */ + private function doCheck(?IssetabilityDescriptor $descriptor, Expr $expr, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error): ?IdentifierRuleError + { + // folds PHPStan\Analyser\IssetabilityDescriptor; mirrors PHPStan\Analyser\MutatingScope::issetCheck() + if ($descriptor !== null && $descriptor->isVariable()) { + $variableName = $descriptor->getVariableName(); + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); if ($hasVariable->maybe()) { return null; } if ($error === null) { if ($hasVariable->yes()) { - if ($expr->name === '_SESSION') { + if ($variableName === '_SESSION') { return null; } @@ -58,7 +75,7 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str if (!$type instanceof NeverType) { return $this->generateError( $type, - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), + sprintf('Variable $%s %s always exists and', $variableName, $operatorDescription), $typeMessageCallback, $identifier, 'variable', @@ -66,23 +83,29 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) ->identifier(sprintf('%s.variable', $identifier)) ->build(); } return $error; - } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + } elseif ($descriptor !== null && $descriptor->isOffset()) { + $varResult = $descriptor->getVarResult(); + $dimResult = $descriptor->getDimResult(); + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + $type = $this->treatPhpDocTypesAsCertain - ? $scope->getScopeType($expr->var) - : $scope->getScopeNativeType($expr->var); + ? $varResult->getTypeForScope($mutatingScope) + : $varResult->getNativeTypeForScope($mutatingScope); if (!$type->isOffsetAccessible()->yes()) { - return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $error ?? $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } $dimType = $this->treatPhpDocTypesAsCertain - ? $scope->getScopeType($expr->dim) - : $scope->getScopeNativeType($expr->dim); + ? $dimResult->getTypeForScope($mutatingScope) + : $dimResult->getNativeTypeForScope($mutatingScope); $hasOffsetValue = $type->hasOffsetValueType($dimType); if ($hasOffsetValue->no()) { if (!$this->checkAdvancedIsset) { @@ -114,54 +137,48 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); + return $this->doCheck($varResult->getIssetabilityDescriptor(), $varResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } // Has offset, it is nullable return null; - } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) { + } elseif ($descriptor !== null && $descriptor->isProperty()) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope->toMutatingScope()); - - if ($propertyReflection === null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } + $propertyFetch = $descriptor->getPropertyFetch(); + if ($propertyFetch === null) { + throw new ShouldNotHappenException(); + } + $innerResult = $descriptor->getInnerResult(); - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } + $propertyReflection = $descriptor->resolvePropertyReflection($mutatingScope); - return null; + if ($propertyReflection === null) { + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } if (!$propertyReflection->isNative()) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } - - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } - - return null; + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { if ( - $expr instanceof Node\Expr\PropertyFetch - && $expr->name instanceof Node\Identifier - && $expr->var instanceof Expr\Variable - && $expr->var->name === 'this' + $propertyFetch instanceof Node\Expr\PropertyFetch + && $propertyFetch->name instanceof Node\Identifier + && $propertyFetch->var instanceof Expr\Variable + && $propertyFetch->var->name === 'this' && $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes() ) { return $this->generateError( $propertyReflection->getNativeType(), sprintf( '%s %s', - $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr), + $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch), $operatorDescription, ), static function (Type $type) use ($typeMessageCallback): ?string { @@ -181,7 +198,7 @@ static function (Type $type) use ($typeMessageCallback): ?string { ); } - if (!$scope->hasExpressionType($expr)->yes()) { + if (!$scope->hasExpressionType($propertyFetch)->yes()) { $nativeReflection = $propertyReflection->getNativeReflection(); if ( $nativeReflection !== null @@ -193,29 +210,17 @@ static function (Type $type) use ($typeMessageCallback): ?string { } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - return $error; + return $innerResult !== null + ? $this->doCheck($innerResult->getIssetabilityDescriptor(), $innerResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error) + : $error; } if (!$this->checkAdvancedIsset) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } - - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } - - return null; + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } $error = $this->generateError( @@ -226,14 +231,8 @@ static function (Type $type) use ($typeMessageCallback): ?string { 'property', ); - if ($error !== null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } + if ($error !== null && $innerResult !== null) { + return $this->doCheck($innerResult->getIssetabilityDescriptor(), $innerResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); } return $error; @@ -276,29 +275,48 @@ static function (Type $type) use ($typeMessageCallback): ?string { /** * @param ErrorIdentifier $identifier */ - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError + private function checkUndefinedInner(ExpressionResult $inner, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier): ?IdentifierRuleError + { + return $this->checkUndefined($inner->getIssetabilityDescriptor(), $inner->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier); + } + + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(?IssetabilityDescriptor $descriptor, Expr $expr, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier): ?IdentifierRuleError { - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); + if ($descriptor !== null && $descriptor->isVariable()) { + $variableName = $descriptor->getVariableName(); + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); if (!$hasVariable->no()) { return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) ->identifier(sprintf('%s.variable', $identifier)) ->build(); } - if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->var) : $scope->getScopeNativeType($expr->var); - $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->dim) : $scope->getScopeNativeType($expr->dim); + if ($descriptor !== null && $descriptor->isOffset()) { + $varResult = $descriptor->getVarResult(); + $dimResult = $descriptor->getDimResult(); + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $this->treatPhpDocTypesAsCertain ? $varResult->getTypeForScope($mutatingScope) : $varResult->getNativeTypeForScope($mutatingScope); + $dimType = $this->treatPhpDocTypesAsCertain ? $dimResult->getTypeForScope($mutatingScope) : $dimResult->getNativeTypeForScope($mutatingScope); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } if (!$hasOffsetValue->no()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } return RuleErrorBuilder::message( @@ -311,12 +329,12 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri )->identifier(sprintf('%s.offset', $identifier))->build(); } - if ($expr instanceof Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } + if ($descriptor !== null && $descriptor->isProperty()) { + $innerResult = $descriptor->getInnerResult(); - if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } return null; diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index 4a6e8421d91..127501aa58c 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -8,7 +8,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDoc\TypeNodeResolver; @@ -78,7 +78,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra $dimExpr = $arrayItem->key; } - $itemErrors = $this->checkVarType($scope, $arrayItem->value, new GetOffsetValueTypeExpr($expr, $dimExpr), $varTags, $assignedVariables); + $itemErrors = $this->checkVarType($scope, $arrayItem->value, new TypeExpr($scope->getType($expr)->getOffsetValueType($scope->getType($dimExpr))), $varTags, $assignedVariables); foreach ($itemErrors as $error) { $errors[] = $error; } diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index 6296e12ca6c..fbd3f1c7bb2 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -7,8 +7,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; @@ -271,11 +270,19 @@ private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Exp $errors[] = $error; } if ($keyVar !== null) { - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, new GetIterableKeyTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $keyTypeExpr = new NativeTypeExpr( + $scope->getIterableKeyType($scope->getScopeType($iterateeExpr)), + $scope->getIterableKeyType($scope->getScopeNativeType($iterateeExpr)), + ); + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, $keyTypeExpr, $varTags, $variableNames) as $error) { $errors[] = $error; } } - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, new GetIterableValueTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $valueTypeExpr = new NativeTypeExpr( + $scope->getIterableValueType($scope->getScopeType($iterateeExpr)), + $scope->getIterableValueType($scope->getScopeNativeType($iterateeExpr)), + ); + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, $valueTypeExpr, $varTags, $variableNames) as $error) { $errors[] = $error; } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index d8d0ba20e1c..59cdbf3eab4 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\FileAnalyser; @@ -118,6 +119,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ImplicitToStringCallHelper::class), + self::getContainer()->getByType(ExpressionResultFactory::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 7723560fd06..3cda273ecf0 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\MutatingScope; @@ -93,6 +94,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 39da6a5be9e..ded70fca9e3 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -834,6 +834,7 @@ private function createAnalyser(): Analyser true, $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php index 0a1804341a6..fae07c69bb5 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser\Fiber; use PhpParser\Node; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -138,6 +139,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ImplicitToStringCallHelper::class), + self::getContainer()->getByType(ExpressionResultFactory::class), ); } diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php index ae9aa1ec4c1..608bfee99ec 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\Fiber; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; @@ -71,6 +72,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); } diff --git a/tests/PHPStan/Analyser/nsrt/assign-in-array.php b/tests/PHPStan/Analyser/nsrt/assign-in-array.php new file mode 100644 index 00000000000..955df512571 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assign-in-array.php @@ -0,0 +1,23 @@ +value) && is_null($b->value)) { + throw new \Exception(); + } + + assertType('int', $a->value ?? $b->value); + + return $a->value ?? $b->value; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12207.php b/tests/PHPStan/Analyser/nsrt/bug-12207.php new file mode 100644 index 00000000000..63990d759c4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12207.php @@ -0,0 +1,31 @@ +}> + */ + public function bar(): Generator + { + yield 'foo' => [ + $a = 'string', + ['string' => $a], + ]; + } + + public function baz(): void + { + $value = [ + $a = 'string', + ['string' => $a], + ]; + assertType("array{'string', array{string: 'string'}}", $value); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13253.php b/tests/PHPStan/Analyser/nsrt/bug-13253.php new file mode 100644 index 00000000000..5bc8ac64c39 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13253.php @@ -0,0 +1,80 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug13253; + +use Generator; +use ReflectionFunction; +use function PHPStan\Testing\assertType; + +/** + * @template TKey + * @template TValue + */ +class Pfline +{ + + /** @var iterable */ + private iterable $data; + + /** @param iterable $data */ + public function __construct(iterable $data) + { + $this->data = $data; + } + + /** + * @return iterable + */ + public function data(): iterable + { + return $this->data; + } + + /** + * @template TMapKey + * @template TMapValue + * @param null|callable(TValue): Generator $func + * @phpstan-self-out self + * @return self + */ + public function map(?callable $func = null): self + { + return $this; + } + +} + +function (): void { + $reflection = new ReflectionFunction('strlen'); + $params = $reflection->getParameters(); + + // Without chaining (each call advances the type through @phpstan-self-out) + $pipeline = new Pfline($params); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param; + }); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param->getName(); + }); + $pipeline->map(function ($param) { + assertType('non-empty-string', $param); + yield substr_count('.', $param); + }); + + // With chaining (the type must advance through @return self) + $pipeline = new Pfline($params); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param; + })->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param->getName(); + })->map(function ($param) { + assertType('non-empty-string', $param); + yield substr_count('.', $param); + }); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13944.php b/tests/PHPStan/Analyser/nsrt/bug-13944.php new file mode 100644 index 00000000000..ed1aeaf31b0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13944.php @@ -0,0 +1,48 @@ +, + * "when@stage"?: array, + * } $config + */ +function config(array $config): void +{ +} + +config([ + 'when@dev' => $does_not_work = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], + ], + 'when@stage' => $does_not_work, +]); + +assertType("array{'when@dev': array{controllers: array{resource: 'routing.controllers'}}, 'when@stage': array{controllers: array{resource: 'routing.controllers'}}}", [ + 'when@dev' => $does_not_work, + 'when@stage' => $does_not_work, +]); + +assertType("array{'when@dev': array{controllers: array{resource: 'routing.controllers'}}, 'when@stage': array{controllers: array{resource: 'routing.controllers'}}}", [ + 'when@dev' => $defined_inside = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], + ], + 'when@stage' => $defined_inside, +]); + +$does_work = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], +]; +config([ + 'when@dev' => $does_work, + 'when@stage' => $does_work, +]); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7155.php b/tests/PHPStan/Analyser/nsrt/bug-7155.php new file mode 100644 index 00000000000..13fc56e4830 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7155.php @@ -0,0 +1,16 @@ + + */ +class Bug14396Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInFunctionThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + [], + )), + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-14396.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-14396.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/bug-14396.neon b/tests/PHPStan/Rules/Exceptions/bug-14396.neon new file mode 100644 index 00000000000..feb290057aa --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/bug-14396.neon @@ -0,0 +1,5 @@ +parameters: + treatPhpDocTypesAsCertain: false + exceptions: + check: + missingCheckedExceptionInThrows: true diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-14396.php b/tests/PHPStan/Rules/Exceptions/data/bug-14396.php new file mode 100644 index 00000000000..bf8901f8127 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-14396.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug14396; + +enum Status { + case A; + case B; + case C; +} + +class Item { + public function __construct( + public ?Status $status + ) {} +} + +/** +* @param list $list +*/ +function countAFromCollection(array $list): int +{ + $count = 0; + + foreach ($list as $item) { + match ($item->status) { + Status::A => ++$count, + Status::B, + Status::C, + null => null, + }; + } + + return $count; +} + +function countAFromItem(Item $item): ?int { + return match ($item->status) { + Status::A => 1, + Status::B, + Status::C, + null => null, + }; +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 9f2e93fc72a..0b03c8d2907 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1701,4 +1701,26 @@ public function testBug10090(): void ]); } + 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 582fdeb1076..079b032e8ee 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -22,7 +21,6 @@ protected function getRule(): Rule { return new EmptyRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, )); diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 613e2688a5f..4847e879561 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -21,7 +20,6 @@ protected function getRule(): Rule { return new IssetRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, )); diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index e22565393fe..c07c746d3c8 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -20,7 +19,6 @@ protected function getRule(): Rule { return new NullCoalesceRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->shouldTreatPhpDocTypesAsCertain(), )); diff --git a/tests/PHPStan/Rules/Variables/data/bug-2032.php b/tests/PHPStan/Rules/Variables/data/bug-2032.php new file mode 100644 index 00000000000..65e9d1c5f62 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-2032.php @@ -0,0 +1,17 @@ +