1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use Closure;
6: use PhpParser\Node\Expr;
7: use PHPStan\Node\Expr\TypeExpr;
8: use PHPStan\Reflection\ClassReflection;
9: use PHPStan\Reflection\FunctionReflection;
10: use PHPStan\Reflection\MethodReflection;
11: use PHPStan\Reflection\ParameterReflection;
12: use PHPStan\TrinaryLogic;
13: use PHPStan\Type\Type;
14: use function array_pop;
15: use function count;
16: use function spl_object_id;
17:
18: final class NodeCallbackScope extends MutatingScope
19: {
20:
21: /**
22: * Scope-deriving calls a rule made on this scope, in call order. Asks are
23: * answered from the asked node's stored before-scope (see doGetType()),
24: * which knows nothing about what the rule derived locally — so every
25: * deriving call is recorded here and replayed onto the before-scope
26: * before it answers (preprocessScope()). Only top-level calls are
27: * recorded: a mutator implemented via other mutators (assignVariable()
28: * calls assignExpression()) resets the list to its own caller's view and
29: * appends itself, and the replay re-runs the composition.
30: *
31: * @var list<Closure(MutatingScope): MutatingScope>
32: */
33: private array $scopeOps = [];
34:
35: private ?MutatingScope $walkScope = null;
36:
37: /**
38: * The storage of the emitting walk, pushed by callNodeCallback() for the
39: * duration of the callback - the same association a suspended fiber's
40: * request had with the frame that would resolve it. Resolved through the
41: * container so the scope never references a storage directly (a direct
42: * reference would cycle with the storage's stored scopes and never free
43: * with the cycle collector disabled).
44: */
45: private function findStoredBeforeScope(Expr $expr): ?MutatingScope
46: {
47: $storage = $this->container->getByType(ExpressionResultStorageStack::class)->getCurrent();
48: if ($storage === null) {
49: return null;
50: }
51:
52: $beforeScope = $storage->findBeforeScope($expr);
53: if ($beforeScope instanceof MutatingScope) {
54: return $beforeScope;
55: }
56:
57: return null;
58: }
59:
60: public function toNodeCallbackScope(): self
61: {
62: return $this;
63: }
64:
65: public function toWalkScope(): MutatingScope
66: {
67: if ($this->walkScope !== null) {
68: return $this->walkScope;
69: }
70:
71: return $this->walkScope = $this->scopeFactory->toWalkScopeFactory()->create(
72: $this->context,
73: $this->isDeclareStrictTypes(),
74: $this->getFunction(),
75: $this->getNamespace(),
76: $this->expressionTypes,
77: $this->nativeExpressionTypes,
78: $this->conditionalExpressions,
79: $this->inClosureBindScopeClasses,
80: $this->getAnonymousFunctionReflection(),
81: $this->isInFirstLevelStatement(),
82: $this->currentlyAssignedExpressions,
83: $this->currentlyAllowedUndefinedExpressions,
84: $this->inFunctionCallsStack,
85: $this->afterExtractCall,
86: $this->getParentScope(),
87: $this->nativeTypesPromoted,
88: );
89: }
90:
91: /**
92: * Asked types memoized by node identity. Rules and collectors re-ask the
93: * same nodes across a callback batch (this scope is shared by every
94: * callback fired at the emission point), and the walk scope's own
95: * resolvedTypes memo answered those in O(1) before this class existed -
96: * without this, every repeat pays the stored-result lookup again. The
97: * entry keeps the node itself: a synthetic node a rule dropped can hand
98: * its object id to the next synthetic, and the identity check rejects
99: * such stale hits.
100: *
101: * @var array<int, array{Expr, Type}>
102: */
103: private array $askedTypes = [];
104:
105: /** @var array<int, array{Expr, Type}> */
106: private array $askedNativeTypes = [];
107:
108: /** @api */
109: public function getType(Expr $node): Type
110: {
111: if ($node instanceof TypeExpr) {
112: // Scope-independent by construction - suspending would park this
113: // fiber until the end of the function because the node is never
114: // visited by NodeScopeResolver, and would resolve to the same type.
115: return $node->getExprType();
116: }
117:
118: $nodeId = spl_object_id($node);
119: if (isset($this->askedTypes[$nodeId]) && $this->askedTypes[$nodeId][0] === $node) {
120: return $this->askedTypes[$nodeId][1];
121: }
122:
123: $type = $this->doGetType($node);
124: $this->askedTypes[$nodeId] = [$node, $type];
125:
126: return $type;
127: }
128:
129: private function doGetType(Expr $node): Type
130: {
131: // post-order emission means the node's own result and every subnode
132: // result are already stored when the callback fires - answer from the
133: // stored before-scope; an unstored ask is a synthetic node or a node
134: // ahead of the walk, answered on demand through the MutatingScope path
135: // (the same answer the fiber flush produced for a never-stored ask)
136: $beforeScope = $this->findStoredBeforeScope($node);
137:
138: if (
139: !$this->nativeTypesPromoted
140: && count($this->scopeOps) === 0
141: ) {
142: if ($beforeScope !== null) {
143: return $beforeScope->getType($node);
144: }
145:
146: return $this->toWalkScope()->getType($node);
147: }
148:
149: $scope = $this->preprocessScope($beforeScope ?? $this->toWalkScope());
150: return $scope->getType($node);
151: }
152:
153: public function getScopeType(Expr $expr): Type
154: {
155: return $this->toWalkScope()->getType($expr);
156: }
157:
158: public function getScopeNativeType(Expr $expr): Type
159: {
160: return $this->toWalkScope()->getNativeType($expr);
161: }
162:
163: /** @api */
164: public function getNativeType(Expr $expr): Type
165: {
166: if ($expr instanceof TypeExpr) {
167: // See getType() - same reasoning
168: return $expr->getExprType();
169: }
170:
171: $nodeId = spl_object_id($expr);
172: if (isset($this->askedNativeTypes[$nodeId]) && $this->askedNativeTypes[$nodeId][0] === $expr) {
173: return $this->askedNativeTypes[$nodeId][1];
174: }
175:
176: $type = $this->doGetNativeType($expr);
177: $this->askedNativeTypes[$nodeId] = [$expr, $type];
178:
179: return $type;
180: }
181:
182: private function doGetNativeType(Expr $expr): Type
183: {
184: $beforeScope = $this->findStoredBeforeScope($expr);
185:
186: if (
187: !$this->nativeTypesPromoted
188: && count($this->scopeOps) === 0
189: ) {
190: if ($beforeScope !== null) {
191: return $beforeScope->getNativeType($expr);
192: }
193:
194: return $this->toWalkScope()->getNativeType($expr);
195: }
196:
197: $scope = $this->preprocessScope($beforeScope ?? $this->toWalkScope());
198: return $scope->getNativeType($expr);
199: }
200:
201: public function getKeepVoidType(Expr $node): Type
202: {
203: $beforeScope = $this->findStoredBeforeScope($node);
204:
205: $scope = $this->preprocessScope($beforeScope ?? $this->toWalkScope());
206:
207: return $scope->getKeepVoidType($node);
208: }
209:
210: public function filterByTruthyValue(Expr $expr): self
211: {
212: /** @var self $scope */
213: $scope = parent::filterByTruthyValue($expr);
214: $scope->scopeOps = $this->scopeOps;
215: $scope->scopeOps[] = static fn (MutatingScope $scope): MutatingScope => $scope->filterByTruthyValue($expr);
216:
217: return $scope;
218: }
219:
220: public function filterByFalseyValue(Expr $expr): self
221: {
222: /** @var self $scope */
223: $scope = parent::filterByFalseyValue($expr);
224: $scope->scopeOps = $this->scopeOps;
225: $scope->scopeOps[] = static fn (MutatingScope $scope): MutatingScope => $scope->filterByFalseyValue($expr);
226:
227: return $scope;
228: }
229:
230: /**
231: * @param list<string> $intertwinedPropagatedFrom
232: */
233: public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self
234: {
235: /** @var self $scope */
236: $scope = parent::assignVariable($variableName, $type, $nativeType, $certainty, $intertwinedPropagatedFrom);
237: $scope->scopeOps = $this->scopeOps;
238: $scope->scopeOps[] = static fn (MutatingScope $scope): MutatingScope => $scope->assignVariable($variableName, $type, $nativeType, $certainty, $intertwinedPropagatedFrom);
239:
240: return $scope;
241: }
242:
243: public function assignExpression(Expr $expr, Type $type, Type $nativeType): self
244: {
245: /** @var self $scope */
246: $scope = parent::assignExpression($expr, $type, $nativeType);
247: $scope->scopeOps = $this->scopeOps;
248: $scope->scopeOps[] = static fn (MutatingScope $scope): MutatingScope => $scope->assignExpression($expr, $type, $nativeType);
249:
250: return $scope;
251: }
252:
253: public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self
254: {
255: /** @var self $scope */
256: $scope = parent::invalidateExpression($expressionToInvalidate, $requireMoreCharacters, $invalidatingClass);
257: $scope->scopeOps = $this->scopeOps;
258: $scope->scopeOps[] = static fn (MutatingScope $scope): MutatingScope => $scope->invalidateExpression($expressionToInvalidate, $requireMoreCharacters, $invalidatingClass);
259:
260: return $scope;
261: }
262:
263: private function preprocessScope(MutatingScope $scope): Scope
264: {
265: // a nested walk a rule started from its NodeCallbackScope may have anchored
266: // results to fiber scopes - re-entering this class's ask paths from
267: // here would derive scopes without end
268: $scope = $scope->toWalkScope();
269: if ($this->nativeTypesPromoted) {
270: $scope = $scope->doNotTreatPhpDocTypesAsCertain();
271: }
272:
273: foreach ($this->scopeOps as $op) {
274: $scope = $op($scope);
275: }
276:
277: return $scope;
278: }
279:
280: /**
281: * @param MethodReflection|FunctionReflection|null $reflection
282: */
283: public function pushInFunctionCall($reflection, ?ParameterReflection $parameter, bool $rememberTypes): self
284: {
285: /** @var self $scope */
286: $scope = parent::pushInFunctionCall($reflection, $parameter, $rememberTypes);
287: $scope->scopeOps = $this->scopeOps;
288:
289: return $scope;
290: }
291:
292: public function popInFunctionCall(): self
293: {
294: $stack = $this->inFunctionCallsStack;
295: array_pop($stack);
296:
297: /** @var self $scope */
298: $scope = parent::popInFunctionCall();
299: $scope->scopeOps = $this->scopeOps;
300:
301: return $scope;
302: }
303:
304: public function getParentScope(): ?MutatingScope
305: {
306: $parent = parent::getParentScope();
307: if ($parent === null) {
308: return null;
309: }
310:
311: return $parent->toNodeCallbackScope();
312: }
313:
314: }
315: