Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
b1a7bbd
Accept precomputed operand types in RicherScopeGetTypeHelper
ondrejmirtes Aug 14, 2026
214dc71
Let ExpressionResult own its expression's type and narrowing
ondrejmirtes Aug 14, 2026
f58150f
Store whole ExpressionResults instead of before-scopes
ondrejmirtes Aug 14, 2026
205bc3e
Compose default narrowing from walk results in DefaultNarrowingHelper
ondrejmirtes Aug 14, 2026
c754216
Replace EqualityTypeSpecifyingHelper with result-composed IdenticalNa…
ondrejmirtes Aug 14, 2026
0119cd2
Compose boolean narrowing from operand results
ondrejmirtes Aug 14, 2026
dad1995
Fold leaf handler type resolution into result callbacks
ondrejmirtes Aug 14, 2026
d663442
Fabricate virtual expression results equal to walked ones
ondrejmirtes Aug 14, 2026
3d7297e
Rework the call handlers around preliminary results and ArgsResult
ondrejmirtes Aug 14, 2026
b96496c
Read impossible-check verdicts from the call's own result
ondrejmirtes Aug 14, 2026
0202a67
Inline nullsafe short-circuiting and compose receiver narrowing
ondrejmirtes Aug 14, 2026
3d29bfa
Fetch handlers read operand results
ondrejmirtes Aug 14, 2026
659f52d
Resolve isset, empty and coalesce from chain results
ondrejmirtes Aug 14, 2026
5b077bc
Decompose ternary and match through composed narrowing
ondrejmirtes Aug 14, 2026
6ef627c
Build closure types eagerly from the single body walk
ondrejmirtes Aug 14, 2026
d5204bf
Thread assigned-value results through the assignment handlers
ondrejmirtes Aug 14, 2026
55b090e
Answer type and narrowing questions from stored ExpressionResults
ondrejmirtes Aug 14, 2026
34cde3a
Drop resolveType() and specifyTypes() from ExprHandler
ondrejmirtes Aug 14, 2026
7c6de54
Short-circuit ScopeOps scans over conditional expressions
ondrejmirtes Aug 14, 2026
cb0b6fa
Emit node callbacks after the node's results are stored
ondrejmirtes Aug 14, 2026
2a9ae83
Emit condition-statement and assign-target callbacks after their resu…
ondrejmirtes Aug 14, 2026
41babc9
Prime the storage for constructor return-type and type-specifying ext…
ondrejmirtes Aug 14, 2026
175b9e6
Sum the output-buffer level without walking a synthetic node
ondrejmirtes Aug 14, 2026
cca907f
Compose static-call and clone-with types without synthetic-node walks
ondrejmirtes Aug 14, 2026
6517ac1
Read $this from scope state and document the parent-instantiation walk
ondrejmirtes Aug 14, 2026
4452315
Memoize function and constant name resolution in BetterReflectionProv…
ondrejmirtes Aug 15, 2026
6f68473
Restore the non-type-driven fast path for the resolved acceptor
ondrejmirtes Aug 15, 2026
3b8169d
Degrade on-demand walks of handler-less nodes to mixed
ondrejmirtes Aug 15, 2026
0247b11
Guard FiberScope stored-result reads with the asking scope's variable…
ondrejmirtes Aug 15, 2026
5d4d3f9
Consult ExpressionTypeResolverExtensions in every ExpressionResult re…
ondrejmirtes Aug 15, 2026
c3e8cc7
Memoize a full ExpressionTypeResolverExtension decline per result
ondrejmirtes Aug 15, 2026
b770313
Anchor rule-started walks to the MutatingScope
ondrejmirtes Aug 15, 2026
12e29bd
Remove the fiber suspension machinery
ondrejmirtes Aug 15, 2026
2a6a30f
Serve node callbacks the storage-backed scope on every PHP version
ondrejmirtes Aug 15, 2026
04601da
Rename FiberScope to NodeCallbackScope
ondrejmirtes Aug 15, 2026
4df6ab1
Rename toMutatingScope to toWalkScope
ondrejmirtes Aug 15, 2026
556e6c7
Keep deprecated toMutatingScope() and reset per-file state at the fil…
ondrejmirtes Aug 15, 2026
a3772b3
Memoize node-callback type asks by node identity
ondrejmirtes Aug 15, 2026
10cde0d
Remove unnecessary EXTENSION_TAG from ExprHandler
ondrejmirtes Aug 16, 2026
5aa3c3a
Align the extracted stmt handlers with the single-pass engine seam
ondrejmirtes Aug 16, 2026
199f9c9
Read argument and gathered-expression types from stored ExpressionRes…
ondrejmirtes Aug 18, 2026
72d3217
Price NewHandler's synthetic nodes through the sanctioned on-demand walk
ondrejmirtes Aug 18, 2026
b3e167b
Extract function-call scope effects into FuncCallScopeEffectsHelper
ondrejmirtes Aug 18, 2026
b8917e5
Emit the closure and arrow-function argument callbacks after their re…
ondrejmirtes Aug 18, 2026
048a0de
Emit intermediate assign-target links after their dimensions are proc…
ondrejmirtes Aug 18, 2026
ac3734a
Store the AssignOp result before applyWrite() emits assignment nodes
ondrejmirtes Aug 18, 2026
c4652f3
Defer Unset_, ClassConst, Const_ and While_ statement callbacks past …
ondrejmirtes Aug 18, 2026
efeb005
Read tracked state and constant expressions without pricing in resolv…
ondrejmirtes Aug 18, 2026
cea137e
Give lazy property-type inference a sanctioned independent-pass walk
ondrejmirtes Aug 18, 2026
7203b0f
Build the offset-write chain after the root walk in doPrepareTarget()
ondrejmirtes Aug 18, 2026
2772130
Create method call impure points after the arguments are processed
ondrejmirtes Aug 18, 2026
e06394f
Read closure use-variable native types from scope state
ondrejmirtes Aug 18, 2026
03f0634
Read immediately-invoked closure arguments without walking ahead
ondrejmirtes Aug 18, 2026
135015e
Create constructor impure points after the arguments are processed
ondrejmirtes Aug 18, 2026
da2ace6
Consult the complex-union guard for tracked expressions in applySpeci…
ondrejmirtes Aug 18, 2026
0b26f7c
Short-circuit unchanged variables in askScopeVariableStateMatches()
ondrejmirtes Aug 18, 2026
e4bbca0
Adopt the upstream convergence replay implementation
ondrejmirtes Aug 24, 2026
84cad32
Adopt the upstream gatherer frames
ondrejmirtes Aug 24, 2026
698107f
Store the inc/dec results before the virtual assign emits assignment …
ondrejmirtes Aug 26, 2026
ebed19f
Resolve the arrow function body's return type from its stored result
ondrejmirtes Aug 26, 2026
a5cf15b
Re-price untracked isset chain links on the asking scope in rule-faci…
ondrejmirtes Aug 26, 2026
93a6003
Keep the nullsafe short-circuit's null in specified-type derivations …
ondrejmirtes Aug 26, 2026
7adb021
Derive truthy/falsey override scopes lazily from the source result
ondrejmirtes Aug 30, 2026
1185f1a
Require a call argument's captured ExpressionResult to exist
ondrejmirtes Aug 30, 2026
37dbf79
Carry the ending expression's result on ExecutionEndNode
ondrejmirtes Aug 30, 2026
94f4e9a
Decide the scope-state reads whose synthetic fallback never fires
ondrejmirtes Aug 30, 2026
66ac896
Read a conditional-holder condition through its own ExpressionResult
ondrejmirtes Aug 30, 2026
19b5348
Keep void out of the value type of everything but a resolved call
ondrejmirtes Aug 30, 2026
352b7e9
Resolve late-resolvable types when reading a tracked holder
ondrejmirtes Aug 30, 2026
af5fa04
Pass the storage argument positionally
ondrejmirtes Aug 31, 2026
9137fcf
Read a variable-variable from the tracked expressions
ondrejmirtes Aug 31, 2026
5e10d2d
Keep the tracked type when there is no void to keep
ondrejmirtes Aug 31, 2026
18af23a
Build a match arm's filtering haystack from the conditions' types
ondrejmirtes Aug 31, 2026
8a15862
Walk a closure argument of a nullsafe call's plain twin
ondrejmirtes Aug 31, 2026
769b091
Key the fully plain twin of a truthy nullsafe chain
ondrejmirtes Aug 31, 2026
ee96e88
Do not remember an impure call's value through its asserts
ondrejmirtes Aug 31, 2026
32983e5
Price a for loop's exit condition on the generalized scope
ondrejmirtes Aug 31, 2026
fbe41e8
Read count()'s mode argument from its stored result only
ondrejmirtes Aug 31, 2026
5ecb397
Answer a nullsafe chain link's state without walking it
ondrejmirtes Aug 31, 2026
7ff8020
Answer rule-side asks from results instead of the unwalked scope
ondrejmirtes Aug 31, 2026
709581b
Exempt variable reads and literals from the new-world guard
ondrejmirtes Aug 31, 2026
d1df285
Price literal arguments from scope state in findScopeStateType
ondrejmirtes Aug 31, 2026
ac6e554
Fire the list item callback after its key is processed
ondrejmirtes Aug 31, 2026
3796f04
Pin the node in object-id keyed per-file caches
ondrejmirtes Aug 31, 2026
46f742e
Device non-nullability of unpriceable chain links at the walk's compl…
ondrejmirtes Aug 31, 2026
91e195f
Walk an immediately invoked closure's arguments before the callee
ondrejmirtes Aug 31, 2026
321b3f3
Carry the assigned call's declared return type for by-ref closure uses
ondrejmirtes Aug 31, 2026
6598741
Answer a walked argument-less call from its stored result in the stat…
ondrejmirtes Aug 31, 2026
64b2122
Cache read-variable names on the AST node
ondrejmirtes Sep 1, 2026
2aca59b
Bump expected turbo version
ondrejmirtes Sep 1, 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
111 changes: 111 additions & 0 deletions build/PHPStan/Build/NoScopeTypeReadInEngineRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types = 1);

namespace PHPStan\Build;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_key_exists;
use function in_array;
use function sprintf;
use function str_starts_with;

/**
* The single-pass engine must not price expressions through Scope::getType()
* and friends - every walked node's type lives in its ExpressionResult
* (threaded via ArgsResult, storage, or gathered statement scopes). A scope
* read in engine code either re-walks a node on demand or silently diverges
* from the stored result. The allowlist below names the documented seams
* (rule-facing bridge asks and ask-ahead-of-walk reads).
*
* @implements Rule<MethodCall>
*/
final class NoScopeTypeReadInEngineRule implements Rule
{

private const ENGINE_NAMESPACES = [
'PHPStan\\Analyser\\NodeScopeResolver',
'PHPStan\\Analyser\\StmtHandler\\',
'PHPStan\\Analyser\\ExprHandler\\',
];

private const BANNED_METHODS = ['getType', 'getNativeType', 'getKeepVoidType'];

private const ALLOWED_SEAMS = [
// rule-facing bridge asks: the scope carries no storage
'PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver' => ['readExprType', 'resolveArrowFunctionReturnType'],
'PHPStan\\Analyser\\ExprHandler\\Helper\\CountNarrowingHelper' => ['isNormalCountCall'],
// immediately invoked closures: the invocation args are walked after the closure
'PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver#buildParametersAndAcceptors' => [],
];

public function getNodeType(): string
{
return MethodCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Identifier) {
return [];
}
$methodName = $node->name->toString();
if (!in_array($methodName, self::BANNED_METHODS, true)) {
return [];
}

$currentClassReflection = $scope->getClassReflection();
if ($currentClassReflection === null) {
return [];
}
$namespace = $currentClassReflection->getName();
$inEngine = false;
foreach (self::ENGINE_NAMESPACES as $prefix) {
if ($namespace === $prefix || str_starts_with($namespace, $prefix)) {
$inEngine = true;
break;
}
}
if (!$inEngine) {
return [];
}

$calledOnType = $scope->getType($node->var);
if ($calledOnType->getObjectClassNames() === []) {
return [];
}
$isScope = false;
foreach ($calledOnType->getObjectClassReflections() as $classReflection) {
if ($classReflection->is(Scope::class)) {
$isScope = true;
break;
}
}
if (!$isScope) {
return [];
}

$function = $scope->getFunction();
$functionName = $function !== null ? $function->getName() : null;
if (
array_key_exists($namespace, self::ALLOWED_SEAMS)
&& $functionName !== null
&& in_array($functionName, self::ALLOWED_SEAMS[$namespace], true)
) {
return [];
}
if ($functionName !== null && array_key_exists($namespace . '#' . $functionName, self::ALLOWED_SEAMS)) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(
'Engine code must not price expressions via Scope::%s() - read the expression\'s stored ExpressionResult (ArgsResult, storage, gathered scopes) instead.',
$methodName,
))->identifier('phpstanBuild.scopeTypeReadInEngine')->build(),
];
}

}
1 change: 1 addition & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ parameters:

rules:
- PHPStan\Build\FinalClassRule
- PHPStan\Build\NoScopeTypeReadInEngineRule
- PHPStan\Build\AttributeNamedArgumentsRule
- PHPStan\Build\NamedArgumentsRule
- PHPStan\Build\OverrideAttributeThirdPartyMethodRule
Expand Down
36 changes: 6 additions & 30 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,27 @@ parameters:
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
count: 2
path: src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.'
identifier: phpstanApi.instanceofType
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 3
path: src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php
path: src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 2
path: src/Analyser/ExprHandler/IssetHandler.php
path: src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php

-
rawMessage: 'Only numeric types are allowed in pre-increment, float|int|string|null given.'
identifier: preInc.nonNumeric
count: 1
path: src/Analyser/ExprHandler/PreIncHandler.php
path: src/Analyser/ExprHandler/Helper/IncDecTypeHelper.php

-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 1
count: 4
path: src/Analyser/MutatingScope.php

-
Expand Down Expand Up @@ -108,24 +96,12 @@ parameters:
count: 1
path: src/Analyser/RuleErrorTransformer.php

-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 2
path: src/Analyser/ScopeOps.php

-
rawMessage: 'Parameter #2 $node of method PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection::__invoke() expects PhpParser\Node\Expr\ArrowFunction|PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\FuncCall|PhpParser\Node\Stmt\Class_|PhpParser\Node\Stmt\Const_|PhpParser\Node\Stmt\Enum_|PhpParser\Node\Stmt\Function_|PhpParser\Node\Stmt\Interface_|PhpParser\Node\Stmt\Trait_, PhpParser\Node\Stmt\ClassLike given.'
identifier: argument.type
count: 1
path: src/Analyser/StmtHandler/ClassLikeHandler.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.'
identifier: phpstanApi.instanceofType
count: 2
path: src/Analyser/TypeSpecifier.php

-
rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().'
identifier: generics.variance
Expand Down
67 changes: 65 additions & 2 deletions src/Analyser/ArgsResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,76 @@

namespace PHPStan\Analyser;

use PhpParser\Node\Expr;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\ShouldNotHappenException;
use function get_class;
use function spl_object_id;
use function sprintf;

/**
* Result of NodeScopeResolver::processArgs(): the scope/throw/impure state after
* processing all arguments (wrapped ExpressionResult) plus the ParametersAcceptor
* resolved from the arg types gathered on the arg-to-arg evolving scope. The
* resolved acceptor is type-driven (selectFromTypes) so its generics are resolved
* against the actual argument types - callers wire it into the call's return
* type. Null when the call had no variants (dynamic callee).
* against the actual argument types - callers wire it into the call expression's
* stored return type. Null when the call had no variants (dynamic callee).
*/
final class ArgsResult
{

/**
* @param array<int, ExpressionResult> $argResults keyed by spl_object_id of each argument's value expression
*/
public function __construct(
private ExpressionResult $expressionResult,
private ?ParametersAcceptor $resolvedParametersAcceptor,
private array $argResults,
)
{
}

/**
* Membership query: the ExpressionResult of the given expression if it is one of
* the call's processed arguments. Null means the expression is not such an
* argument - either it is a different expression altogether (the callee name, a
* synthetic node, a narrowed subexpression), or it is a default-value argument
* ArgumentsNormalizer synthesized for an omitted optional parameter, which has no
* source code behind it and so was never processed. Call sites holding an actual
* argument of the call use requireArgResult() instead.
*/
public function findArgResult(Expr $argValue): ?ExpressionResult
{
return $this->argResults[spl_object_id($argValue)] ?? null;
}

/**
* @return array<int, ExpressionResult> keyed by spl_object_id of each argument's value expression
*/
public function getArgResults(): array
{
return $this->argResults;
}

/**
* The stored ExpressionResult of a call argument's value expression; the
* argument must have been processed by processArgs() - engine code reads
* argument types through this instead of re-asking the scope.
*/
public function requireArgResult(Expr $argValue): ExpressionResult
{
$result = $this->argResults[spl_object_id($argValue)] ?? null;
if ($result === null) {
throw new ShouldNotHappenException(sprintf(
'No stored ExpressionResult for a %s argument on line %d.',
get_class($argValue),
$argValue->getStartLine(),
));
}

return $result;
}

public function getScope(): MutatingScope
{
return $this->expressionResult->getScope();
Expand Down Expand Up @@ -53,6 +103,19 @@ public function getImpurePoints(): array
return $this->expressionResult->getImpurePoints();
}

/**
* The same processed arguments under another acceptor: an immediately
* invoked closure's arguments are walked on its declared signature, the
* acceptor the call resolves from is the walked closure's.
*/
public function withResolvedParametersAcceptor(?ParametersAcceptor $resolvedParametersAcceptor): self
{
$clone = clone $this;
$clone->resolvedParametersAcceptor = $resolvedParametersAcceptor;

return $clone;
}

public function getResolvedParametersAcceptor(): ?ParametersAcceptor
{
return $this->resolvedParametersAcceptor;
Expand Down
7 changes: 4 additions & 3 deletions src/Analyser/CalledMethodProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ public function processCalledMethod(NodeScopeResolver $nodeScopeResolver, Method
if ($returnStatement !== null) {
foreach ($returnStatement->getExecutionEnds() as $executionEnd) {
$statementResult = $executionEnd->getStatementResult();
$endNode = $executionEnd->getNode();
if ($endNode instanceof Node\Stmt\Expression) {
$exprType = $statementResult->getScope()->getType($endNode->expr);
$endExprResult = $executionEnd->getExprResult();
if ($endExprResult !== null) {
$walkScope = $statementResult->getScope()->toWalkScope();
$exprType = $endExprResult->getTypeOnScope($walkScope, $walkScope->nativeTypesPromoted);
if ($exprType instanceof NeverType && $exprType->isExplicit()) {
continue;
}
Expand Down
15 changes: 11 additions & 4 deletions src/Analyser/ConditionalExpressionHolderRecipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class ConditionalExpressionHolderRecipe
{

/**
* @param list<array{string, Expr, bool, Type}> $conditionEntries [exprString, expr, fromSureTypes, type]
* @param list<array{string, Expr, bool, Type, ?ExpressionResult}> $conditionEntries [exprString, expr, fromSureTypes, type, the operand walk's result for the expr]
* @param list<array{string, Expr, Type, ?Type}> $holderEntries [exprString, expr, type, target type pinned at compose time (null = read the applying scope)]
*/
public function __construct(
Expand All @@ -43,8 +43,15 @@ public function evaluate(MutatingScope $scope): array
// the unnarrowed type of each condition expression, for the
// dropped-self-condition complement below
$conditionOriginalTypes = [];
foreach ($this->conditionEntries as [$exprString, $expr, $fromSureTypes, $type]) {
$scopeType = $scope->getType($expr);
foreach ($this->conditionEntries as [$exprString, $expr, $fromSureTypes, $type, $conditionResult]) {
// through the expression's own result, which answers from the applying
// scope's state where that scope owns the expression - a narrowing
// subject no walk produced (an extension is free to specify a type for
// an expression the source never evaluated on its own) has no result
// and is read from the scope state directly
$scopeType = $conditionResult !== null
? $conditionResult->getTypeOnScope($scope, $scope->nativeTypesPromoted)
: $scope->getStateType($expr);
$conditionType = $fromSureTypes
? TypeCombinator::remove($scopeType, $type)
: TypeCombinator::intersect($scopeType, $type);
Expand Down Expand Up @@ -81,7 +88,7 @@ public function evaluate(MutatingScope $scope): array
continue;
}

$targetType = $pinnedTargetType ?? $scope->getType($expr);
$targetType = $pinnedTargetType ?? $scope->getStateType($expr);
$holderType = $this->holdersFromSureTypes
? TypeCombinator::intersect($targetType, $type)
: TypeCombinator::remove($targetType, $type);
Expand Down
Loading
Loading