1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use Closure;
6: use PhpParser\Node;
7: use PhpParser\Node\Expr;
8: use PhpParser\Node\Expr\ArrayDimFetch;
9: use PhpParser\Node\Expr\FuncCall;
10: use PhpParser\Node\Expr\List_;
11: use PhpParser\Node\Expr\MethodCall;
12: use PhpParser\Node\Expr\New_;
13: use PhpParser\Node\Expr\PropertyFetch;
14: use PhpParser\Node\Expr\StaticCall;
15: use PhpParser\Node\Expr\StaticPropertyFetch;
16: use PhpParser\Node\Expr\Variable;
17: use PhpParser\Node\Identifier;
18: use PhpParser\Node\Stmt\Class_;
19: use PhpParser\Node\Stmt\Echo_;
20: use PhpParser\Node\Stmt\Foreach_;
21: use PhpParser\Node\Stmt\If_;
22: use PhpParser\Node\Stmt\Return_;
23: use PhpParser\Node\Stmt\Static_;
24: use PhpParser\Node\Stmt\Switch_;
25: use PhpParser\NodeFinder;
26: use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
27: use PHPStan\Analyser\Generics\TemplateArgumentConstraints;
28: use PHPStan\Analyser\Generics\TemplateArgumentFrame;
29: use PHPStan\Analyser\Generics\TemplateArgumentObserver;
30: use PHPStan\Analyser\Generics\TemplateArgumentStats;
31: use PHPStan\DependencyInjection\AutowiredExtensions;
32: use PHPStan\DependencyInjection\AutowiredService;
33: use PHPStan\DependencyInjection\Container;
34: use PHPStan\DependencyInjection\ExtensionsCollection;
35: use PHPStan\File\FileHelper;
36: use PHPStan\Node\FunctionCallableNode;
37: use PHPStan\Node\FunctionCallExpressionNode;
38: use PHPStan\Node\InstantiationCallableNode;
39: use PHPStan\Node\MethodCallableNode;
40: use PHPStan\Node\MethodCallExpressionNode;
41: use PHPStan\Node\StaticMethodCallableNode;
42: use PHPStan\Node\StaticMethodCallExpressionNode;
43: use PHPStan\Reflection\Native\NativeMethodReflection;
44: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
45: use PHPStan\Reflection\Php\PhpMethodReflection;
46: use PHPStan\Reflection\Php\PhpPropertyReflection;
47: use PHPStan\ShouldNotHappenException;
48: use PHPStan\Turbo\ShadowedByTurboExtension;
49: use PHPStan\Type\ErrorType;
50: use PHPStan\Type\MixedType;
51: use PHPStan\Type\Type;
52: use PHPStan\Type\TypeUtils;
53: use function array_merge;
54: use function array_pop;
55: use function get_class;
56: use function getenv;
57: use function is_array;
58: use function is_string;
59: use function spl_object_id;
60: use function sprintf;
61:
62: #[AutowiredService]
63: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/NodeScopeResolver.cpp')]
64: class NodeScopeResolver
65: {
66:
67: public const LOOP_SCOPE_ITERATIONS = 3;
68: public const GENERALIZE_AFTER_ITERATION = 1;
69:
70: /** @var array<string, true> filePath(string) => bool(true) */
71: private array $analysedFiles = [];
72:
73: /**
74: * When processing a synthetic node on demand, real AST
75: * nodes contained in it were already processed and must not be processed again.
76: */
77: protected bool $returnStoredExpressionResults = false;
78:
79: /**
80: * Consume-stored mode: a walk that deliberately re-enters an
81: * already-walked subtree (the nullsafe plain twin re-walking its
82: * receiver) consumes stored results unconditionally instead of
83: * re-processing - node callbacks fired during the original walk.
84: */
85: private bool $consumeStoredExpressionResults = false;
86:
87: private ?NonNullabilityHelper $nonNullabilityHelper = null;
88:
89: /**
90: * Engine-feeding gatherer frames (return statements, execution ends,
91: * impure points, ...), innermost last. callNodeCallback() feeds every
92: * frame the raw walk scope at the emission position - gatherers are
93: * engine code and never ask about types, and their arrays are read as
94: * soon as the enclosing body walk returns.
95: *
96: * @var list<callable(Node, Scope): void>
97: */
98: private array $nodeGatherers = [];
99:
100: /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */
101: public static bool $guardNewWorld = false;
102:
103: /**
104: * spl_object_id => true of every Expr in the file's parsed AST. Populated
105: * only when the PHPSTAN_GUARD_NW diagnostic is enabled, so the guards can
106: * tell a real AST node from a node a rule built during analysis (which
107: * legitimately resolves on demand). Static so MutatingScope can read it.
108: *
109: * @var array<int, true>
110: */
111: public static array $guardRealExprIds = [];
112:
113: /**
114: * spl_object_id => true of every Expr already processed by processExprNode
115: * in the current file. Used by the MutatingScope::getType guard to detect a
116: * real AST node whose type is asked before it was processed.
117: *
118: * @var array<int, true>
119: */
120: public static array $guardProcessedExprIds = [];
121:
122: /**
123: * @param ExtensionsCollection<PerFileAnalysisResettable> $perFileAnalysisResettables
124: */
125: public function __construct(
126: private readonly Container $container,
127: private readonly TemplateArgumentObserver $templateArgumentObserver,
128: private readonly FileHelper $fileHelper,
129: #[AutowiredExtensions(of: PerFileAnalysisResettable::class)]
130: private readonly ExtensionsCollection $perFileAnalysisResettables,
131: private readonly ExpressionResultFactory $expressionResultFactory,
132: private readonly StatementsHandler $statementsHandler,
133: )
134: {
135: self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1';
136: TemplateArgumentStats::enableFromEnvironment();
137: }
138:
139: /**
140: * The lookups (isAnalysedFile()) are keyed by normalized paths, so the
141: * given paths are normalized here - a caller-provided unnormalized path
142: * (mixed directory separators on Windows) must not silently skip the
143: * in-class-context analysis of a trait.
144: *
145: * @api
146: * @param string[] $files
147: */
148: public function setAnalysedFiles(array $files): void
149: {
150: $analysedFiles = [];
151: foreach ($files as $file) {
152: $analysedFiles[$this->fileHelper->normalizePath($file)] = true;
153: }
154: $this->analysedFiles = $analysedFiles;
155: }
156:
157: /**
158: * Releases the previous file's node-keyed captures: the parser cache
159: * retains ASTs, so node-keyed cache entries never die on
160: * their own and would hold that file's whole result graph alive.
161: *
162: * Called at the per-file boundary (FileAnalyser), NOT in processNodes():
163: * extensions start nested processNodes() walks mid-file (phpstan-doctrine
164: * parsing a query-builder method, rule tooling re-analysing a callee) and
165: * wiping the per-file caches there forces the outer file to rebuild them -
166: * closure types re-converge, narrowing memos recompute.
167: */
168: private function getNonNullabilityHelper(): NonNullabilityHelper
169: {
170: return $this->nonNullabilityHelper ??= $this->container->getByType(NonNullabilityHelper::class);
171: }
172:
173: public function resetPerFileAnalysisState(): void
174: {
175: foreach ($this->perFileAnalysisResettables->getAll() as $resettableService) {
176: $resettableService->resetFileAnalysisState();
177: }
178: }
179:
180: /**
181: * @api
182: * @param Node[] $nodes
183: * @param callable(Node $node, Scope $scope): void $nodeCallback
184: */
185: public function processNodes(
186: array $nodes,
187: MutatingScope $scope,
188: callable $nodeCallback,
189: ): void
190: {
191: $scope = $scope->toWalkScope();
192: if (self::$guardNewWorld) {
193: self::$guardRealExprIds = [];
194: self::$guardProcessedExprIds = [];
195: foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) {
196: self::$guardRealExprIds[spl_object_id($realExpr)] = true;
197: }
198: }
199:
200: $expressionResultStorage = new ExpressionResultStorage();
201: $scope->pushExpressionResultStorage($expressionResultStorage);
202: // a fresh walk an extension starts mid-analysis must not feed the
203: // interrupted walk's gatherer frames (see processStmtNodes())
204: $gatherers = $this->suspendNodeGatherers();
205: try {
206: $this->statementsHandler->processNodesWithStorage($this, $nodes, $scope, $expressionResultStorage, $nodeCallback);
207: } finally {
208: $this->restoreNodeGatherers($gatherers);
209: $scope->popExpressionResultStorage();
210: }
211: }
212:
213: public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void
214: {
215: if (self::$guardNewWorld) {
216: self::$guardProcessedExprIds[spl_object_id($expr)] = true;
217: }
218: // handlers are answered from stored results in both worlds
219: $storage->storeExpressionResult($expr, $expressionResult);
220: }
221:
222: /**
223: * Narrows a scope by a (often synthetic) control-flow condition the new-world
224: * way: resolve its narrowing through the scope's on-demand dispatcher and apply
225: * it via applySpecifiedTypes, instead of the old-world filterBy*Value().
226: */
227: public function narrowScopeWithCondition(MutatingScope $scope, Expr $expr, TypeSpecifierContext $context): MutatingScope
228: {
229: $specifiedTypes = $scope->specifyTypesOfNewWorldHandlerNode($expr, $context);
230:
231: return $scope->applySpecifiedTypes($specifiedTypes);
232: }
233:
234: /**
235: * @api
236: * @param Node\Stmt[] $stmts
237: * @param callable(Node $node, Scope $scope): void $nodeCallback
238: */
239: public function processStmtNodes(
240: Node $parentNode,
241: array $stmts,
242: MutatingScope $scope,
243: callable $nodeCallback,
244: StatementContext $context,
245: ): StatementResult
246: {
247: // a rule may pass the scope it was handed - the rule-facing NodeCallbackScope -
248: // as the walk's initial scope; the walk must anchor its results to the
249: // state-identical MutatingScope or their consumption re-enters the
250: // rule-facing ask paths
251: $scope = $scope->toWalkScope();
252: $storage = new ExpressionResultStorage();
253: $scope->pushExpressionResultStorage($storage);
254: // a fresh walk an extension starts mid-analysis must not feed the
255: // interrupted walk's gatherer frames - they describe the body walk
256: // that was interrupted, not the nested one
257: $gatherers = $this->suspendNodeGatherers();
258: try {
259: return $this->processStmtNodesInternal(
260: $parentNode,
261: $stmts,
262: $scope,
263: $storage,
264: $nodeCallback,
265: $context,
266: )->toPublic();
267: } finally {
268: $this->restoreNodeGatherers($gatherers);
269: $scope->popExpressionResultStorage();
270: }
271: }
272:
273: /**
274: * @param Node\Stmt[] $stmts
275: * @param callable(Node $node, Scope $scope): void $nodeCallback
276: */
277: public function processStmtNodesInternal(
278: Node $parentNode,
279: array $stmts,
280: MutatingScope $scope,
281: ExpressionResultStorage $storage,
282: callable $nodeCallback,
283: StatementContext $context,
284: ): InternalStatementResult
285: {
286: // make the storage this walk writes into scope-visible: loop-convergence
287: // passes (including the closure by-ref convergence, which calls this
288: // method directly) thread a throwaway duplicate that would otherwise
289: // never reach the storage stack, so every in-pass ask
290: // (applySpecifiedTypes pricing, rules via Scope::getType) would miss the
291: // pass's own results and re-process real nodes on demand
292: $pushStorage = $scope->getCurrentExpressionResultStorage() !== $storage;
293: if ($pushStorage) {
294: $scope->pushExpressionResultStorage($storage);
295: }
296: try {
297: return $this->statementsHandler->doProcessStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context);
298: } finally {
299: if ($pushStorage) {
300: $scope->popExpressionResultStorage();
301: }
302: }
303: }
304:
305: /**
306: * @param callable(Node $node, Scope $scope): void $nodeCallback
307: */
308: public function processStmtNode(
309: Node\Stmt $stmt,
310: MutatingScope $scope,
311: ExpressionResultStorage $storage,
312: callable $nodeCallback,
313: StatementContext $context,
314: ): InternalStatementResult
315: {
316: $overridingThrowPoints = null;
317: if (
318: !$stmt instanceof Static_
319: && !$stmt instanceof Node\Stmt\Global_
320: && !$stmt instanceof Node\Stmt\Property
321: && !$stmt instanceof Node\Stmt\ClassConst
322: && !$stmt instanceof Node\Stmt\Const_
323: && !$stmt instanceof Node\Stmt\ClassLike
324: && !$stmt instanceof Node\Stmt\Function_
325: && !$stmt instanceof Node\Stmt\ClassMethod
326: ) {
327: if (!$stmt instanceof Foreach_) {
328: $scope = $this->statementsHandler->processStmtVarAnnotation($this, $scope, $storage, $stmt, null, $nodeCallback);
329: }
330: $overridingThrowPoints = $this->statementsHandler->getOverridingThrowPoints($stmt, $scope);
331: }
332:
333: if ($stmt instanceof Node\Stmt\ClassMethod) {
334: // a trait method the using class overrides is not analysed here at all -
335: // decided before the node callback is emitted
336: if (!$scope->isInClass()) {
337: throw new ShouldNotHappenException();
338: }
339: if (
340: $scope->isInTrait()
341: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
342: ) {
343: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
344: if ($methodReflection instanceof NativeMethodReflection) {
345: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
346: }
347: if ($methodReflection instanceof PhpMethodReflection) {
348: $declaringTrait = $methodReflection->getDeclaringTrait();
349: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
350: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
351: }
352: }
353: }
354: }
355:
356: // Statements whose work is processing their expressions emit their node
357: // callback AFTER that processing, inside their branches below, with the
358: // entry scope - a synchronously invoked rule (the plain resolver,
359: // PHP < 8.1) then finds the expressions' results in the storage instead
360: // of re-walking them on demand, mirroring processExprNodeInternal().
361: $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_
362: || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_
363: || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst
364: || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_
365: || $stmt instanceof Node\Stmt\Do_;
366: if (!$deferredStmtCallback) {
367: $this->callNodeCallback($nodeCallback, $stmt, $scope, $storage);
368: }
369:
370: $stmtHandler = StmtHandlerRegistry::resolve($stmt, $this->container);
371: if ($stmtHandler !== null) {
372: $stmtResult = $stmtHandler->processStmt($this, $stmt, $scope, $storage, $nodeCallback, $context);
373: if ($overridingThrowPoints !== null) {
374: // the overriding throw points use the scope before the statement,
375: // so the variable flow throws before the statement does its work
376: $overridingThrowFlows = [];
377: foreach ($overridingThrowPoints as $overridingThrowPoint) {
378: $overridingThrowFlows[] = VariableFlow::throwing($overridingThrowPoint->getType(), true, $overridingThrowPoint->canContainAnyThrowable());
379: }
380:
381: return new InternalStatementResult(
382: $stmtResult->getScope(),
383: hasYield: $stmtResult->hasYield(),
384: isAlwaysTerminating: $stmtResult->isAlwaysTerminating(),
385: exitPoints: $stmtResult->getExitPoints(),
386: throwPoints: $overridingThrowPoints,
387: impurePoints: $stmtResult->getImpurePoints(),
388: endStatements: $stmtResult->getEndStatements(),
389: variableFlow: VariableFlow::sequence(...$overridingThrowFlows, ...[$stmtResult->getVariableFlow()]),
390: );
391: }
392:
393: return $stmtResult;
394: }
395:
396: // statements with no analysis of their own (e.g. HaltCompiler)
397: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []);
398: }
399:
400: public function isAnalysedFile(string $fileName): bool
401: {
402: return isset($this->analysedFiles[$fileName]);
403: }
404:
405: /** Whether an on-demand walk answers already processed real nodes from their stored results (see processExprOnDemand()). */
406: public function isReturningStoredExpressionResults(): bool
407: {
408: return $this->returnStoredExpressionResults;
409: }
410:
411: /** Whether the walk consumes stored results unconditionally (see processExprNodeConsumingStored()). */
412: public function isConsumingStoredExpressionResults(): bool
413: {
414: return $this->consumeStoredExpressionResults;
415: }
416:
417: public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
418: {
419: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
420: }
421:
422: public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
423: {
424: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
425: }
426:
427: /**
428: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
429: */
430: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
431: {
432: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
433: $scope = $callback($scope, $expr);
434: }
435:
436: if ($expr instanceof ArrayDimFetch) {
437: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
438: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) {
439: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
440: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
441: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
442: } elseif ($expr instanceof List_) {
443: foreach ($expr->items as $item) {
444: if ($item === null) {
445: continue;
446: }
447:
448: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
449: }
450: }
451:
452: return $scope;
453: }
454:
455: /**
456: * Processes an expression outside the normal AST traversal - e.g. a synthetic
457: * node a rule or extension asks about. Real AST nodes contained in it return
458: * their already-stored results instead of being processed again. New results
459: * are stored into the given storage - pass a duplicate to keep them isolated.
460: */
461: /**
462: * Processes an expression whose already-walked subtrees must be CONSUMED
463: * from their stored results instead of re-walked: the nullsafe handlers
464: * process the receiver once (real callbacks) and then walk the plain twin,
465: * whose receiver subtree answers from storage, re-anchored to the twin's
466: * (ensured) scope.
467: *
468: * @param callable(Node $node, Scope $scope): void $nodeCallback
469: */
470: public function processExprNodeConsumingStored(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
471: {
472: $previous = $this->consumeStoredExpressionResults;
473: $this->consumeStoredExpressionResults = true;
474: try {
475: return $this->processExprNode($stmt, $expr, $scope, $storage, $nodeCallback, $context);
476: } finally {
477: $this->consumeStoredExpressionResults = $previous;
478: }
479: }
480:
481: public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult
482: {
483: // A node no handler supports - a virtual node (BooleanOrNode, ...) a
484: // rule asked the type of - degrades to mixed, mirroring
485: // MutatingScope::resolveType()'s fallback. The main walk's unhandled
486: // throw stays: real source nodes must have a handler.
487: if (
488: ExprHandlerRegistry::resolve($expr, $this->container) === null
489: && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable())
490: ) {
491: $mixed = new MixedType();
492: return $this->expressionResultFactory->create(
493: $scope,
494: beforeScope: $scope,
495: expr: $expr,
496: hasYield: false,
497: isAlwaysTerminating: false,
498: throwPoints: [],
499: impurePoints: [],
500: typeCallback: null,
501: specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
502: type: $mixed,
503: nativeType: $mixed,
504: );
505: }
506:
507: // save/restore, never reset: on-demand walks nest (a typeCallback
508: // evaluated mid-walk prices another synthetic node) and a hard reset
509: // would turn stored-result consumption off for the rest of the outer
510: // walk - re-processing every remaining subtree and bypassing the
511: // closure-argument consume guards in ArgumentsHandler::processArgs()
512: $previous = $this->returnStoredExpressionResults;
513: $this->returnStoredExpressionResults = true;
514: $scope->pushExpressionResultStorage($storage);
515: try {
516: return $this->processExprNode(
517: new Node\Stmt\Expression($expr),
518: $expr,
519: $scope,
520: $storage,
521: new NoopNodeCallback(),
522: ExpressionContext::createTopLevel(resolveTemplateArguments: false),
523: );
524: } finally {
525: $scope->popExpressionResultStorage();
526: $this->returnStoredExpressionResults = $previous;
527: }
528: }
529:
530: /**
531: * The stored ExpressionResult of a node processExprNode() already processed
532: * into the given storage - the caller asserts the processing order by
533: * holding the very storage it processed the node into (a scope-based lookup
534: * would miss loop-convergence storages, which are never scope-visible).
535: * Throws when the node has no stored result.
536: */
537: public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): ExpressionResult
538: {
539: $result = $storage->findExpressionResult($expr);
540: if ($result === null) {
541: throw new ShouldNotHappenException(sprintf(
542: '%s on line %d has no stored ExpressionResult - it was not processed by processExprNode().',
543: get_class($expr),
544: $expr->getStartLine(),
545: ));
546: }
547:
548: return $result;
549: }
550:
551: /**
552: * The type, on the given scope, of a node that may or may not have a stored
553: * ExpressionResult. Every call site of this method is UNDECIDED about whether
554: * the node was already analysed - each should eventually either consume the
555: * node's ExpressionResult where it was processed or be a synthetic node
556: * (processSyntheticOnDemand()).
557: */
558: public function readTypeOfMaybeStored(Expr $expr, MutatingScope $scope): Type
559: {
560: $storage = $scope->getCurrentExpressionResultStorage();
561: $result = $storage !== null ? $storage->findExpressionResult($expr) : null;
562: if ($result !== null) {
563: return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted);
564: }
565:
566: return $this->readScopeStateOrSyntheticType($expr, $scope);
567: }
568:
569: /**
570: * The type the scope itself knows for the expression, without any node
571: * processing: a string-named variable read is scope state (mirrors
572: * VariableHandler's typeCallback), and a type tracked for the whole
573: * expression answers directly - an on-demand walk would return that very
574: * holder anyway (the fresh result's beforeScope is the asking scope),
575: * after paying the walk. Null when the scope has no answer; the caller
576: * decides whether that means a synthetic walk (processSyntheticOnDemand())
577: * or an invariant violation.
578: */
579: public function findScopeStateType(Expr $expr, MutatingScope $scope): ?Type
580: {
581: if ($expr instanceof Expr\Variable && is_string($expr->name)) {
582: if ($scope->hasVariableType($expr->name)->no()) {
583: return new ErrorType();
584: }
585:
586: return $scope->getVariableType($expr->name);
587: }
588:
589: // a literal is position-independent: the scope prices it without a walk,
590: // so an argument the walk reaches only later (an IIFE's or a pipe's
591: // operand) never has to be processed ahead of its turn
592: if ($expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_) {
593: return $scope->getStateType($expr);
594: }
595:
596: // A variable whose name is an expression ($$name) never reaches the read
597: // above, and the scope tracks it like any other expression - so it belongs
598: // here rather than falling through to a walk.
599: if (
600: !$expr instanceof Expr\Closure
601: && !$expr instanceof Expr\ArrowFunction
602: && $scope->hasExpressionType($expr)->yes()
603: ) {
604: return TypeUtils::resolveLateResolvableTypes($scope->getTrackedExpressionType($expr));
605: }
606:
607: return null;
608: }
609:
610: /**
611: * The type, on the given scope, of a node the caller knows has no stored
612: * ExpressionResult in its walk: scope state (variable read / tracked
613: * holder) answers without a walk, anything else is priced as a synthetic
614: * node.
615: */
616: public function readScopeStateOrSyntheticType(Expr $expr, MutatingScope $scope): Type
617: {
618: return $this->findScopeStateType($expr, $scope) ?? $this->processSyntheticOnDemand($expr, $scope)->getTypeOnScope($scope, $scope->nativeTypesPromoted);
619: }
620:
621: /**
622: * The type the scope knows for an expression the caller has already pinned as
623: * tracked there (hasExpressionType() yes, or a string-named variable). Unlike
624: * readScopeStateOrSyntheticType() this never falls back to a synthetic walk -
625: * the caller decided that the scope answers.
626: */
627: public function requireScopeStateType(Expr $expr, MutatingScope $scope): Type
628: {
629: $type = $this->findScopeStateType($expr, $scope);
630: if ($type === null) {
631: throw new ShouldNotHappenException(sprintf(
632: '%s on line %d is not tracked on the scope it was pinned as tracked on.',
633: get_class($expr),
634: $expr->getStartLine(),
635: ));
636: }
637:
638: return $type;
639: }
640:
641: /**
642: * Fires the PHPSTAN_GUARD_NW diagnostic when a real (non-synthetic) AST node
643: * reaches an on-demand pricing path without having been processed and stored
644: * by processExprNode() first. Mirrors the guard in MutatingScope::getType():
645: * such a node should be answered from its stored ExpressionResult, never
646: * re-priced as if it were synthetic. Dormant unless PHPSTAN_GUARD_NW=1.
647: */
648: private function guardAgainstUnprocessedRealNode(Expr $expr, string $caller): void
649: {
650: if (
651: !self::$guardNewWorld
652: || !isset(self::$guardRealExprIds[spl_object_id($expr)])
653: || isset(self::$guardProcessedExprIds[spl_object_id($expr)])
654: ) {
655: return;
656: }
657:
658: throw new ShouldNotHappenException(sprintf(
659: '%s() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.',
660: $caller,
661: get_class($expr),
662: $expr->getStartLine(),
663: ));
664: }
665:
666: /**
667: * Processes a synthetic node (one an ExprHandler built itself) on a duplicate
668: * of the storage of the analysis currently in progress, mirroring
669: * MutatingScope::resolveTypeOfNewWorldHandlerNode(): the duplicate isolates
670: * the synthetic node's own stored result from the live storage while its real
671: * subnodes still resolve from the fallback.
672: */
673: public function processSyntheticOnDemand(Expr $expr, MutatingScope $scope): ExpressionResult
674: {
675: $this->guardAgainstUnprocessedRealNode($expr, __FUNCTION__);
676: $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage();
677:
678: return $this->processExprOnDemand($expr, $scope, $current->duplicate());
679: }
680:
681: /**
682: * @param callable(Node $node, Scope $scope): void $nodeCallback
683: */
684: public function processExprNode(
685: Node\Stmt $stmt,
686: Expr $expr,
687: MutatingScope $scope,
688: ExpressionResultStorage $storage,
689: callable $nodeCallback,
690: ExpressionContext $context,
691: ): ExpressionResult
692: {
693: if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) {
694: $storedResult = $storage->findExpressionResult($expr);
695: // a stored result only answers when the current scope agrees with its
696: // evaluation position on the variables the expression reads - a
697: // counterfactual walk (an extension re-binding a variable and pricing
698: // a real subtree, e.g. array_filter's per-element callback evaluation)
699: // re-processes the node on its own scope instead. In CONSUME mode the
700: // divergence is intentional (an ensured-non-null device) and the
701: // stored result is consumed unconditionally, re-anchored below.
702: if ($storedResult !== null && ($this->consumeStoredExpressionResults || $storedResult->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted))) {
703: // a foreign-position answer must not thread its original walk
704: // scopes into THIS walk - re-anchor it to the asking position so
705: // subsequent operands keep evaluating on the asking scope
706: if ($storedResult->getBeforeScope() === $scope) {
707: return $storedResult;
708: }
709:
710: $reanchored = $storedResult->atAskPosition($scope);
711: if ($this->consumeStoredExpressionResults) {
712: // the re-anchored view IS this walk's result for the node
713: // (the nullsafe twin's receiver at the ensured position) -
714: // store it so later asks (rules' storage reads) see the same
715: // result the twin walk itself consumed, exactly like the
716: // receiver walked inside the twin used to be stored
717: $this->storeExpressionResult($storage, $expr, $reanchored);
718: }
719:
720: return $reanchored;
721: }
722: }
723:
724: return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context);
725: }
726:
727: /**
728: * @param callable(Node $node, Scope $scope): void $nodeCallback
729: */
730: private function processExprNodeInternal(
731: Node\Stmt $stmt,
732: Expr $expr,
733: MutatingScope $scope,
734: ExpressionResultStorage $storage,
735: callable $nodeCallback,
736: ExpressionContext $context,
737: ): ExpressionResult
738: {
739: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
740: if ($expr instanceof FuncCall) {
741: $newExpr = new FunctionCallableNode($expr->name, $expr);
742: } elseif ($expr instanceof MethodCall) {
743: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
744: } elseif ($expr instanceof StaticCall) {
745: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
746: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
747: $newExpr = new InstantiationCallableNode($expr->class, $expr);
748: } else {
749: throw new ShouldNotHappenException();
750: }
751:
752: $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context);
753: $expressionResult = $this->expressionResultFactory->create(
754: $newExprResult->getScope(),
755: beforeScope: $scope,
756: expr: $expr,
757: hasYield: $newExprResult->hasYield(),
758: isAlwaysTerminating: $newExprResult->isAlwaysTerminating(),
759: throwPoints: $newExprResult->getThrowPoints(),
760: impurePoints: $newExprResult->getImpurePoints(),
761: variableFlow: $newExprResult->getVariableFlow(),
762: // the first-class callable closure type lives on the *CallableNode
763: // result; delegate so getType() of the original CallLike answers from it
764: typeCallback: static fn (bool $nativeTypesPromoted): Type => ($nativeTypesPromoted ? $newExprResult->getNativeType() : $newExprResult->getType()),
765: specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
766: );
767: $this->storeExpressionResult($storage, $expr, $expressionResult);
768: return $expressionResult;
769: }
770:
771: $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container);
772: if ($exprHandler !== null) {
773: $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context);
774: // a chain link an enclosing isset/empty/?? could not device ahead
775: // of its walk (an untracked call) is deviced now, from the type
776: // the walk produced
777: $expressionResult = $this->getNonNullabilityHelper()->applyPendingEnsure($expr, $expressionResult);
778: $this->storeExpressionResult($storage, $expr, $expressionResult);
779: // Force potential producers before collecting the body's constraints.
780: // Type reads only construct markers; they never register sites as a side effect.
781: $frame = $scope->getCurrentTemplateArgumentFrame();
782: if (
783: $frame !== null && $frame->isObserving()
784: && $expr instanceof Expr\CallLike
785: ) {
786: $constraints = $this->templateArgumentObserver->collectSites($expressionResult->getType());
787: $expressionResult = $expressionResult->withScope($expressionResult->getScope()->addTemplateArgumentConstraints($constraints));
788: $this->storeExpressionResult($storage, $expr, $expressionResult);
789: }
790: // The node's own callback fires AFTER its result is stored, with the
791: // scope captured before processing. Rules observe the same (scope,
792: // answer) pair as at a pre-order emission - previously a pre-order
793: // rule parks on its first ask and resumes at this store anyway - but
794: // a synchronously invoked rule (the plain resolver, PHP < 8.1) now
795: // finds the node's and its subtree's results in the storage instead
796: // of re-walking them on demand.
797: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
798: // the call is now processed and stored; emit a virtual node so
799: // impossible-check rules read its specified types from the result
800: // instead of asking the scope before the call node is processed
801: if ($expr instanceof FuncCall) {
802: $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
803: } elseif ($expr instanceof MethodCall) {
804: $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
805: } elseif ($expr instanceof StaticCall) {
806: $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
807: }
808: return $expressionResult;
809: }
810:
811: throw new ShouldNotHappenException(sprintf('Unhandled expr: %s', get_class($expr)));
812: }
813:
814: /**
815: * Unlike a method call, a property read defaults to pure: only a hook we're
816: * certain about and that is certainly side-effecting makes the read impure.
817: *
818: * The reset is assumed pure as reporting those would make accessing them
819: * unreasonably annoying.
820: *
821: * @param 'get'|'set' $hookName
822: * @return ImpurePoint[]
823: */
824: public function getImpurePointsFromPropertyHook(
825: MutatingScope $scope,
826: PropertyFetch $propertyFetch,
827: PhpPropertyReflection $propertyReflection,
828: string $hookName,
829: ): array
830: {
831: if ($this->isPropertyHookBackingValueAccess($scope, $propertyFetch)) {
832: return [];
833: }
834:
835: if (!$propertyReflection->hasHook($hookName)) {
836: return [];
837: }
838:
839: if (!$propertyReflection->getHook($hookName)->hasSideEffects()->yes()) {
840: return [];
841: }
842:
843: return [
844: new ImpurePoint(
845: $scope,
846: $propertyFetch,
847: 'propertyHookCall',
848: sprintf(
849: 'call to %s hook of property %s::$%s',
850: $hookName,
851: $propertyReflection->getDeclaringClass()->getDisplayName(),
852: $propertyReflection->getName(),
853: ),
854: true,
855: ),
856: ];
857: }
858:
859: /**
860: * Inside a hook of the same property, $this->prop is the backing value, not
861: * a re-entrant hook call.
862: */
863: private function isPropertyHookBackingValueAccess(MutatingScope $scope, PropertyFetch $propertyFetch): bool
864: {
865: $scopeFunction = $scope->getFunction();
866:
867: return $scopeFunction instanceof PhpMethodFromParserNodeReflection
868: && $scopeFunction->isPropertyHook()
869: && $propertyFetch->var instanceof Variable
870: && $propertyFetch->var->name === 'this'
871: && $propertyFetch->name instanceof Identifier
872: && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName();
873: }
874:
875: /**
876: * @return string[]
877: */
878: public function getAssignedVariables(Expr $expr): array
879: {
880: if ($expr instanceof Expr\Variable) {
881: if (is_string($expr->name)) {
882: return [$expr->name];
883: }
884:
885: return [];
886: }
887:
888: if ($expr instanceof Expr\List_) {
889: $names = [];
890: foreach ($expr->items as $item) {
891: if ($item === null) {
892: continue;
893: }
894:
895: $names = array_merge($names, $this->getAssignedVariables($item->value));
896: }
897:
898: return $names;
899: }
900:
901: if ($expr instanceof ArrayDimFetch) {
902: return $this->getAssignedVariables($expr->var);
903: }
904:
905: return [];
906: }
907:
908: private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody';
909:
910: /**
911: * Whether a recorded convergence pass over the loop body can replace the
912: * final walk. A pass runs at deep statement context, the final walk at top
913: * level - constructs that analyse differently between the two (nested
914: * loop/label fixpoints run only at top level, statement-level classes are
915: * skipped at deep context) disqualify the body. Closure bodies process
916: * context-independently and are not traversed.
917: *
918: * @param Node\Stmt[] $bodyStmts
919: */
920: public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool
921: {
922: $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE);
923: if ($cached !== null) {
924: return $cached;
925: }
926:
927: $replayable = true;
928: foreach ($bodyStmts as $bodyStmt) {
929: if ($this->hasContextSensitiveConstruct($bodyStmt)) {
930: $replayable = false;
931: break;
932: }
933: }
934: $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable);
935:
936: return $replayable;
937: }
938:
939: private function hasContextSensitiveConstruct(Node $node): bool
940: {
941: if ($node instanceof Expr\Closure) {
942: return false;
943: }
944: if (
945: $node instanceof Node\Stmt\While_
946: || $node instanceof Node\Stmt\Do_
947: || $node instanceof Node\Stmt\For_
948: || $node instanceof Foreach_
949: || $node instanceof Node\Stmt\Label
950: || $node instanceof Node\Stmt\ClassLike
951: ) {
952: return true;
953: }
954:
955: foreach ($node->getSubNodeNames() as $subNodeName) {
956: $subNode = $node->$subNodeName;
957: if ($subNode instanceof Node) {
958: if ($this->hasContextSensitiveConstruct($subNode)) {
959: return true;
960: }
961: } elseif (is_array($subNode)) {
962: foreach ($subNode as $item) {
963: if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) {
964: return true;
965: }
966: }
967: }
968: }
969:
970: return false;
971: }
972:
973: /**
974: * Opens an engine-feeding gatherer frame for the duration of a body walk.
975: * The caller closes it in a finally block via popNodeGatherer().
976: *
977: * @param callable(Node, Scope): void $gatherer
978: */
979: public function pushNodeGatherer(callable $gatherer): void
980: {
981: $this->nodeGatherers[] = $gatherer;
982: }
983:
984: public function popNodeGatherer(): void
985: {
986: array_pop($this->nodeGatherers);
987: }
988:
989: /**
990: * Detaches all gatherer frames for a walk that must not feed them. The
991: * caller reattaches them in a finally block via restoreNodeGatherers().
992: *
993: * @return list<callable(Node, Scope): void>
994: */
995: public function suspendNodeGatherers(): array
996: {
997: $gatherers = $this->nodeGatherers;
998: $this->nodeGatherers = [];
999:
1000: return $gatherers;
1001: }
1002:
1003: /**
1004: * @param list<callable(Node, Scope): void> $gatherers
1005: */
1006: public function restoreNodeGatherers(array $gatherers): void
1007: {
1008: $this->nodeGatherers = $gatherers;
1009: }
1010:
1011: /**
1012: * @param callable(Node $node, Scope $scope): void $nodeCallback
1013: */
1014: public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void
1015: {
1016: $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope);
1017: }
1018:
1019: /**
1020: * Replays the recorded pairs [$from, $to) the way callNodeCallback() would
1021: * have emitted them: gatherer frames observe them exactly like live ones
1022: * (with the raw walk scope), a real callback gets the callback scope - and
1023: * a recording callback records them again (a loop's fixpoint replay running
1024: * inside a body's observation pass).
1025: *
1026: * @param callable(Node $node, Scope $scope): void $nodeCallback
1027: */
1028: public function replayRecordingRange(RecordingNodeCallback $recording, int $from, int $to, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void
1029: {
1030: $pairs = $recording->getPairs();
1031: $scope->pushExpressionResultStorage($storage);
1032: try {
1033: for ($i = $from; $i < $to; $i++) {
1034: [$node, $pairScope] = $pairs[$i];
1035: if (!$pairScope instanceof MutatingScope) {
1036: throw new ShouldNotHappenException();
1037: }
1038: $this->callNodeCallback($nodeCallback, $node, $pairScope, $storage);
1039: }
1040: } finally {
1041: $scope->popExpressionResultStorage();
1042: }
1043: }
1044:
1045: /**
1046: * @param callable(Node $node, Scope $scope): void $nodeCallback
1047: */
1048: public function callNodeCallbackWithExpression(
1049: callable $nodeCallback,
1050: Node $expr,
1051: MutatingScope $scope,
1052: ExpressionResultStorage $storage,
1053: ExpressionContext $context,
1054: ): void
1055: {
1056: if ($context->isDeep()) {
1057: $scope = $scope->exitFirstLevelStatements();
1058: }
1059: $this->callNodeCallback($nodeCallback, $expr, $scope, $storage);
1060: }
1061:
1062: /**
1063: * @param callable(Node $node, Scope $scope): void $nodeCallback
1064: */
1065: public function callNodeCallback(
1066: callable $nodeCallback,
1067: Node $node,
1068: MutatingScope $scope,
1069: ExpressionResultStorage $storage,
1070: ): void
1071: {
1072: // Engine-feeding gatherer frames observe the node at the emission
1073: // position - their arrays are read as soon as the enclosing body walk
1074: // returns. Gatherers are engine code and never ask about types -
1075: // handing them the raw scope skips a NodeCallbackScope construction per
1076: // emission; the scopes they capture (return statements, impure points)
1077: // answer later asks through the storage hub like any MutatingScope.
1078: foreach ($this->nodeGatherers as $gatherer) {
1079: $gatherer($node, $scope);
1080: }
1081:
1082: if ($nodeCallback instanceof NoopNodeCallback) {
1083: return;
1084: }
1085:
1086: if ($nodeCallback instanceof RecordingNodeCallback) {
1087: // recording never asks about types - the pairs are wrapped and
1088: // bound to the storage at replay time instead
1089: $nodeCallback($node, $scope);
1090: return;
1091: }
1092:
1093: // post-order emission means the node's own result and every subnode
1094: // result are already stored when the callback fires - NodeCallbackScope
1095: // answers every ask synchronously from the storage
1096: $nodeCallback($node, $scope->toNodeCallbackScope());
1097: }
1098:
1099: /**
1100: * The template argument frame of the body being walked while it observes
1101: * a body that created unresolved template arguments - null otherwise, so
1102: * every observation hook costs a null check outside the observation pass.
1103: */
1104: public function observingTemplateArgumentFrame(MutatingScope $scope): ?TemplateArgumentFrame
1105: {
1106: $frame = $scope->getCurrentTemplateArgumentFrame();
1107: if ($frame === null || !$frame->isObserving() || $scope->getTemplateArgumentConstraints() === null) {
1108: return null;
1109: }
1110:
1111: return $frame;
1112: }
1113:
1114: /**
1115: * A value leaves the function: the declared return type is a send target for
1116: * the unresolved template arguments it carries.
1117: */
1118: public function collectReturnSend(MutatingScope $scope, ExpressionResult $returnedResult): TemplateArgumentConstraints
1119: {
1120: $frame = $this->observingTemplateArgumentFrame($returnedResult->getScope());
1121: if ($frame === null) {
1122: return TemplateArgumentConstraints::createEmpty();
1123: }
1124: if ($scope->isInAnonymousFunction()) {
1125: $declaredReturnType = $scope->getAnonymousFunctionReturnType();
1126: } else {
1127: $function = $scope->getFunction();
1128: $declaredReturnType = $function !== null ? $function->getReturnType() : null;
1129: }
1130: if ($declaredReturnType === null) {
1131: return TemplateArgumentConstraints::createEmpty();
1132: }
1133:
1134: return $this->templateArgumentObserver->collectSend($declaredReturnType, $returnedResult->getType());
1135: }
1136:
1137: }
1138: