Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
476839a
Introduce ExpressionResultFactory
ondrejmirtes Jun 11, 2026
26a165d
ExpressionResult - add beforeScope
ondrejmirtes Jun 11, 2026
b8157b6
ExpressionResult - add Expr
ondrejmirtes Jun 11, 2026
c8b3bda
Store ExpressionResult instead of before-Scope
ondrejmirtes Jun 11, 2026
0933ba3
Fill the missing gaps in expr processing
ondrejmirtes Jun 12, 2026
cfef4b2
Divide ExprHandler into TypeResolvingExprHandler
ondrejmirtes Jun 12, 2026
445afe8
ScalarHandler stops implementing TypeResolvingExprHandler
ondrejmirtes Jun 12, 2026
f729e60
ExpressionResultStorageStack - answer type questions from ExpressionR…
ondrejmirtes Jun 12, 2026
eba7bcf
Migrate ArrayHandler - per-item types from ExpressionResults
ondrejmirtes Jun 12, 2026
457689b
Throw on unbalanced ExpressionResultStorageStack pop
ondrejmirtes Jun 12, 2026
43bdaa4
Fix PHP 7.4 compat
ondrejmirtes Jun 12, 2026
342272d
Migrate VariableHandler and InstanceofHandler - narrowing from Expres…
ondrejmirtes Jun 12, 2026
d7b5a5f
Add regression tests for evaluation-point array item types
ondrejmirtes Jun 12, 2026
428d58e
Add regression test for certainty of undefined variables in loops
ondrejmirtes Jun 12, 2026
61d64c6
Store expressions even without FNSR
ondrejmirtes Jun 12, 2026
1731073
Only sureNot specifications skip certainly-undefined variables
ondrejmirtes Jun 12, 2026
630fa91
This is better
ondrejmirtes Jun 12, 2026
dd44b87
ExpressionResult::createTypesCallback - the inside-out TypeSpecifier:…
ondrejmirtes Jun 12, 2026
fb9b427
Coalesce, Ternary, BooleanAnd, BooleanOr stop implementing TypeResolv…
ondrejmirtes Jun 12, 2026
3fd7504
Add regression test for conditional holders narrowing coalesce of pro…
ondrejmirtes Jun 12, 2026
be672de
Never process expressions on a FiberScope
ondrejmirtes Jun 12, 2026
eb31077
Convert rule-facing FiberScope at the new-world hook boundary
ondrejmirtes Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ parameters:
-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 3
count: 5
path: src/Analyser/MutatingScope.php

-
Expand Down
30 changes: 13 additions & 17 deletions src/Analyser/DirectInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -77,6 +81,7 @@ public function create(
$this->propertyReflectionFinder,
$this->parser,
$this->constantResolver,
$this->expressionResultStorageStack,
$context,
$this->phpVersion,
$this->attributeReflectionFactory,
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this read withFiber?

{
return new self(
$this->container,
Expand All @@ -136,7 +131,8 @@ public function toMutatingFactory(): InternalScopeFactory
$this->configPhpVersion,
$this->nodeCallback,
$this->constantResolver,
false,
$fiber,
$this->expressionResultStorageStack,
);
}

Expand Down
16 changes: 0 additions & 16 deletions src/Analyser/ExprHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use PHPStan\Type\Type;

/**
* @template T of Expr
Expand All @@ -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;

}
24 changes: 15 additions & 9 deletions src/Analyser/ExprHandler/ArrayDimFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +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\NullsafeShortCircuitingHelper;
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;
Expand All @@ -29,12 +30,16 @@
use function array_merge;

/**
* @implements ExprHandler<ArrayDimFetch>
* @implements TypeResolvingExprHandler<ArrayDimFetch>
*/
#[AutowiredService]
final class ArrayDimFetchHandler implements ExprHandler
final class ArrayDimFetchHandler implements TypeResolvingExprHandler
{

public function __construct(private ExpressionResultFactory $expressionResultFactory)
{
}

public function supports(Expr $expr): bool
{
return $expr instanceof ArrayDimFetch;
Expand Down Expand Up @@ -76,18 +81,19 @@ 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
{
$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),
);
}

Expand All @@ -109,14 +115,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
)->getThrowPoints());
}

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),
);
}

Expand Down
81 changes: 47 additions & 34 deletions src/Analyser/ExprHandler/ArrayHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,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\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\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<Array_>
Expand All @@ -37,6 +36,7 @@ final class ArrayHandler implements ExprHandler

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private ExpressionResultFactory $expressionResultFactory,
)
{
}
Expand All @@ -46,33 +46,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 = [];
Expand All @@ -82,6 +60,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());
Expand All @@ -90,6 +69,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());
Expand All @@ -98,18 +78,51 @@ 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);
// getArrayType only asks about item keys and values - guarded
// legacy bridge just in case
return $s->getType($inner);
});

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()
&& $s->getType($isCallableCall)->isTrue()->yes()
) {
$type = TypeCombinator::intersect($type, new CallableType());
}
}

return $type;
},
);
}

}
12 changes: 8 additions & 4 deletions src/Analyser/ExprHandler/ArrowFunctionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArrowFunction>
* @implements TypeResolvingExprHandler<ArrowFunction>
*/
#[AutowiredService]
final class ArrowFunctionHandler implements ExprHandler
final class ArrowFunctionHandler implements TypeResolvingExprHandler
{

public function __construct(
private ClosureTypeResolver $closureTypeResolver,
private ExpressionResultFactory $expressionResultFactory,
)
{
}
Expand All @@ -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: [],
Expand Down
Loading
Loading