Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e15c9ab
Accept precomputed operand types in RicherScopeGetTypeHelper
ondrejmirtes Aug 14, 2026
4607755
Let ExpressionResult own its expression's type and narrowing
ondrejmirtes Aug 14, 2026
c6a9684
Store whole ExpressionResults instead of before-scopes
ondrejmirtes Aug 14, 2026
ea3248e
Compose default narrowing from walk results in DefaultNarrowingHelper
ondrejmirtes Aug 14, 2026
94e34d0
Replace EqualityTypeSpecifyingHelper with result-composed IdenticalNa…
ondrejmirtes Aug 14, 2026
54d3a61
Compose boolean narrowing from operand results
ondrejmirtes Aug 14, 2026
0e8b7a5
Fold leaf handler type resolution into result callbacks
ondrejmirtes Aug 14, 2026
e621270
Fabricate virtual expression results equal to walked ones
ondrejmirtes Aug 14, 2026
08110a2
Rework the call handlers around preliminary results and ArgsResult
ondrejmirtes Aug 14, 2026
322fba8
Read impossible-check verdicts from the call's own result
ondrejmirtes Aug 14, 2026
b0d6ff0
Inline nullsafe short-circuiting and compose receiver narrowing
ondrejmirtes Aug 14, 2026
474f4d0
Fetch handlers read operand results
ondrejmirtes Aug 14, 2026
63f2abc
Resolve isset, empty and coalesce from chain results
ondrejmirtes Aug 14, 2026
6549848
Decompose ternary and match through composed narrowing
ondrejmirtes Aug 14, 2026
1e332de
Build closure types eagerly from the single body walk
ondrejmirtes Aug 14, 2026
214b055
Thread assigned-value results through the assignment handlers
ondrejmirtes Aug 14, 2026
d261dac
Answer type and narrowing questions from stored ExpressionResults
ondrejmirtes Aug 14, 2026
d73767e
Drop resolveType() and specifyTypes() from ExprHandler
ondrejmirtes Aug 14, 2026
c545ff3
Short-circuit ScopeOps scans over conditional expressions
ondrejmirtes Aug 14, 2026
4c68968
Bump expected turbo version
ondrejmirtes Aug 14, 2026
5af9cd0
Emit node callbacks after the node's results are stored
ondrejmirtes Aug 14, 2026
7f02dae
Emit condition-statement and assign-target callbacks after their resu…
ondrejmirtes Aug 14, 2026
2b2a6ca
Store boolean results before their virtual nodes and defer the foreac…
ondrejmirtes Aug 14, 2026
1762dd2
Prime the storage for constructor return-type and type-specifying ext…
ondrejmirtes Aug 14, 2026
5c4d593
Sum the output-buffer level without walking a synthetic node
ondrejmirtes Aug 14, 2026
540e7c5
Compose static-call and clone-with types without synthetic-node walks
ondrejmirtes Aug 14, 2026
ab8eff7
Read $this from scope state and document the parent-instantiation walk
ondrejmirtes Aug 14, 2026
ee8f944
Memoize function and constant name resolution in BetterReflectionProv…
ondrejmirtes Aug 15, 2026
2ebc5e7
Restore the non-type-driven fast path for the resolved acceptor
ondrejmirtes Aug 15, 2026
b94a40e
Degrade on-demand walks of handler-less nodes to mixed
ondrejmirtes Aug 15, 2026
4985db5
Guard FiberScope stored-result reads with the asking scope's variable…
ondrejmirtes Aug 15, 2026
1ac1737
Consult ExpressionTypeResolverExtensions in every ExpressionResult re…
ondrejmirtes Aug 15, 2026
6a8576c
Preserve surviving offsets when popping or shifting a list
ondrejmirtes Aug 15, 2026
587506b
Memoize a full ExpressionTypeResolverExtension decline per result
ondrejmirtes Aug 15, 2026
0f6be93
Anchor rule-started walks to the MutatingScope
ondrejmirtes Aug 15, 2026
2e4d711
Remove the fiber suspension machinery
ondrejmirtes Aug 15, 2026
648e6cb
Bump expected turbo version
ondrejmirtes Aug 17, 2026
bb8f113
Serve node callbacks the storage-backed scope on every PHP version
ondrejmirtes Aug 15, 2026
739a3f5
Rename FiberScope to NodeCallbackScope
ondrejmirtes Aug 15, 2026
6a637bd
Rename toMutatingScope to toWalkScope
ondrejmirtes Aug 15, 2026
4633f12
Bump expected turbo version
ondrejmirtes Aug 17, 2026
578b068
Keep deprecated toMutatingScope() and reset per-file state at the fil…
ondrejmirtes Aug 15, 2026
4d3e8cd
Memoize node-callback type asks by node identity
ondrejmirtes Aug 15, 2026
4fdbc50
Remove unnecessary EXTENSION_TAG from ExprHandler
ondrejmirtes Aug 16, 2026
ba5f7fb
Align the extracted stmt handlers with the single-pass engine seam
ondrejmirtes Aug 16, 2026
de50eea
Read argument and gathered-expression types from stored ExpressionRes…
ondrejmirtes Aug 18, 2026
00cf1a9
Price NewHandler's synthetic nodes through the sanctioned on-demand walk
ondrejmirtes Aug 18, 2026
b1c4b9e
Extract function-call scope effects into FuncCallScopeEffectsHelper
ondrejmirtes Aug 18, 2026
0209d3b
Emit the closure and arrow-function argument callbacks after their re…
ondrejmirtes Aug 18, 2026
38327c7
Emit intermediate assign-target links after their dimensions are proc…
ondrejmirtes Aug 18, 2026
04ee461
Store the AssignOp result before applyWrite() emits assignment nodes
ondrejmirtes Aug 18, 2026
05a3dd3
Emit array item callbacks after their key and value are processed
ondrejmirtes Aug 18, 2026
dd972cc
Defer Unset_, ClassConst, Const_ and While_ statement callbacks past …
ondrejmirtes Aug 18, 2026
bf902e8
Read tracked state and constant expressions without pricing in resolv…
ondrejmirtes Aug 18, 2026
98dcd1d
Price enum case values through InitializerExprTypeResolver in EnumSan…
ondrejmirtes Aug 18, 2026
dfff298
Give lazy property-type inference a sanctioned independent-pass walk
ondrejmirtes Aug 18, 2026
bf47338
Process class attributes before emitting InClassNode
ondrejmirtes Aug 18, 2026
1f28931
Build the offset-write chain after the root walk in doPrepareTarget()
ondrejmirtes Aug 18, 2026
a74e1c1
Create method call impure points after the arguments are processed
ondrejmirtes Aug 18, 2026
7c2cec7
Read closure use-variable native types from scope state
ondrejmirtes Aug 18, 2026
109c79b
Read immediately-invoked closure arguments without walking ahead
ondrejmirtes Aug 18, 2026
e6af36e
Create constructor impure points after the arguments are processed
ondrejmirtes Aug 18, 2026
7cba6be
Consult the complex-union guard for tracked expressions in applySpeci…
ondrejmirtes Aug 18, 2026
42bbd61
Short-circuit unchanged variables in askScopeVariableStateMatches()
ondrejmirtes Aug 18, 2026
4b8dc9a
Bump expected turbo version
ondrejmirtes Aug 23, 2026
0adedd1
Sort collected data by file before running CollectedDataNode rules
ondrejmirtes Aug 23, 2026
d9f0a15
Bump expected turbo version
ondrejmirtes Aug 23, 2026
ea0c332
Adopt the upstream convergence replay implementation
ondrejmirtes Aug 24, 2026
e4cfba9
Bump expected turbo version
ondrejmirtes Aug 24, 2026
79dacde
Adopt the upstream gatherer frames
ondrejmirtes Aug 24, 2026
f6fa312
Bump expected turbo version
ondrejmirtes Aug 24, 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'];

/** @var array<string, list<string>> class => methods with documented seam reads */
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 [];
}

$namespace = $scope->getClassReflection()?->getName();
if ($namespace === null) {
return [];
}
$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
54 changes: 15 additions & 39 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ parameters:
count: 3
path: src/Analyser/ExprHandler/AssignHandler.php

-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 3
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
Expand All @@ -36,40 +42,22 @@ 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
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
count: 3
path: src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.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 All @@ -90,6 +78,12 @@ parameters:
count: 1
path: src/Analyser/MutatingScope.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
Expand All @@ -108,24 +102,6 @@ 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
8 changes: 7 additions & 1 deletion src/Analyser/AnalyserResultFinalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use function array_merge;
use function count;
use function get_class;
use function ksort;
use function sprintf;

#[AutowiredService]
Expand Down Expand Up @@ -50,7 +51,12 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
}

$nodeType = CollectedDataNode::class;
$node = new CollectedDataNode($analyserResult->getCollectedData(), $onlyFiles);

// parallel analysis merges collected data in worker-completion order -
// canonicalize so rules never observe a timing-dependent file order
$collectedData = $analyserResult->getCollectedData();
ksort($collectedData);
$node = new CollectedDataNode($collectedData, $onlyFiles);

$file = 'N/A';
$scope = $this->scopeFactory->create(ScopeContext::create($file));
Expand Down
41 changes: 39 additions & 2 deletions src/Analyser/ArgsResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,63 @@

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 = [],
)
{
}

/**
* The already-processed ExpressionResult of a call argument's value expression,
* so callers read its type via the result instead of re-asking the scope.
*/
public function getArgResult(Expr $argValue): ?ExpressionResult
{
return $this->argResults[spl_object_id($argValue)] ?? null;
}

/**
* 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
2 changes: 1 addition & 1 deletion src/Analyser/CalledMethodProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function processCalledMethod(NodeScopeResolver $nodeScopeResolver, Method
$statementResult = $executionEnd->getStatementResult();
$endNode = $executionEnd->getNode();
if ($endNode instanceof Node\Stmt\Expression) {
$exprType = $statementResult->getScope()->getType($endNode->expr);
$exprType = $nodeScopeResolver->readTypeOfMaybeStored($endNode->expr, $statementResult->getScope()->toWalkScope());
if ($exprType instanceof NeverType && $exprType->isExplicit()) {
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Analyser/ConditionalExpressionHolderRecipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function evaluate(MutatingScope $scope): array
// dropped-self-condition complement below
$conditionOriginalTypes = [];
foreach ($this->conditionEntries as [$exprString, $expr, $fromSureTypes, $type]) {
$scopeType = $scope->getType($expr);
$scopeType = $scope->getStateType($expr);
$conditionType = $fromSureTypes
? TypeCombinator::remove($scopeType, $type)
: TypeCombinator::intersect($scopeType, $type);
Expand Down Expand Up @@ -81,7 +81,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
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 @@ -39,8 +41,10 @@ public function __construct(
private $nodeCallback,
private ConstantResolver $constantResolver,
private bool $createsNodeCallbackScopes = false,
?ExpressionResultStorageStack $expressionResultStorageStack = null,
)
{
$this->expressionResultStorageStack = $expressionResultStorageStack ?? new ExpressionResultStorageStack();
}

public function create(
Expand Down Expand Up @@ -78,6 +82,7 @@ public function create(
$this->propertyReflectionFinder,
$this->parser,
$this->constantResolver,
$this->expressionResultStorageStack,
$context,
$this->phpVersion,
$this->attributeReflectionFactory,
Expand All @@ -103,25 +108,15 @@ public function create(

public function toNodeCallbackScopeFactory(): InternalScopeFactory
{
return new self(
$this->container,
$this->reflectionProvider,
$this->initializerExprTypeResolver,
$this->expressionTypeResolverExtensions,
$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 toWalkScopeFactory(): InternalScopeFactory
{
return $this->withFlavor(false);
}

private function withFlavor(bool $createsNodeCallbackScopes): self
{
return new self(
$this->container,
Expand All @@ -137,7 +132,8 @@ public function toWalkScopeFactory(): InternalScopeFactory
$this->configPhpVersion,
$this->nodeCallback,
$this->constantResolver,
false,
$createsNodeCallbackScopes,
$this->expressionResultStorageStack,
);
}

Expand Down
Loading
Loading