1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node\Expr;
6: use PHPStan\Node\Expr\TypeExpr;
7: use PHPStan\Reflection\FunctionReflection;
8: use PHPStan\Reflection\MethodReflection;
9: use PHPStan\Reflection\ParameterReflection;
10: use PHPStan\Type\Type;
11: use WeakReference;
12: use function array_pop;
13: use function count;
14: use function spl_object_id;
15:
16: final class NodeCallbackScope extends MutatingScope
17: {
18:
19: /** @var Expr[] */
20: private array $truthyValueExprs = [];
21:
22: /** @var Expr[] */
23: private array $falseyValueExprs = [];
24:
25: private ?MutatingScope $walkScope = null;
26:
27: /** @var WeakReference<MutatingScope>|null */
28: private ?WeakReference $seededWalkScope = null;
29:
30: public function toNodeCallbackScope(): self
31: {
32: return $this;
33: }
34:
35: /**
36: * Called by MutatingScope::toNodeCallbackScope() with the scope this one was
37: * created from: same state, so it can answer toWalkScope() directly -
38: * keeping its resolvedTypes memo and the identity with stored results'
39: * beforeScope that askScopeVariableStateMatches() short-circuits on.
40: * Weakly referenced: the origin caches this scope in its $nodeCallbackScope, a
41: * strong back-reference would cycle and never free with GC disabled.
42: */
43: public function seedWalkScope(MutatingScope $scope): void
44: {
45: $this->seededWalkScope = WeakReference::create($scope);
46: }
47:
48: public function toWalkScope(): MutatingScope
49: {
50: if ($this->walkScope !== null) {
51: return $this->walkScope;
52: }
53:
54: if ($this->seededWalkScope !== null) {
55: $seeded = $this->seededWalkScope->get();
56: if ($seeded !== null) {
57: return $seeded;
58: }
59: }
60:
61: return $this->walkScope = $this->scopeFactory->toWalkScopeFactory()->create(
62: $this->context,
63: $this->isDeclareStrictTypes(),
64: $this->getFunction(),
65: $this->getNamespace(),
66: $this->expressionTypes,
67: $this->nativeExpressionTypes,
68: $this->conditionalExpressions,
69: $this->inClosureBindScopeClasses,
70: $this->getAnonymousFunctionReflection(),
71: $this->isInFirstLevelStatement(),
72: $this->currentlyAssignedExpressions,
73: $this->currentlyAllowedUndefinedExpressions,
74: $this->inFunctionCallsStack,
75: $this->afterExtractCall,
76: $this->getParentScope(),
77: $this->nativeTypesPromoted,
78: );
79: }
80:
81: /**
82: * Asked types memoized by node identity. Rules and collectors re-ask the
83: * same nodes across a callback batch (this scope is shared by every
84: * callback fired at the emission point), and the walk scope's own
85: * resolvedTypes memo answered those in O(1) before this class existed -
86: * without this, every repeat pays the stored-result guard again. The
87: * entry keeps the node itself: a synthetic node a rule dropped can hand
88: * its object id to the next synthetic, and the identity check rejects
89: * such stale hits.
90: *
91: * @var array<int, array{Expr, Type}>
92: */
93: private array $askedTypes = [];
94:
95: /** @var array<int, array{Expr, Type}> */
96: private array $askedNativeTypes = [];
97:
98: /** @api */
99: public function getType(Expr $node): Type
100: {
101: if ($node instanceof TypeExpr) {
102: // scope-independent by construction
103: return $node->getExprType();
104: }
105:
106: $nodeId = spl_object_id($node);
107: if (isset($this->askedTypes[$nodeId]) && $this->askedTypes[$nodeId][0] === $node) {
108: return $this->askedTypes[$nodeId][1];
109: }
110:
111: $type = $this->doGetType($node);
112: $this->askedTypes[$nodeId] = [$node, $type];
113:
114: return $type;
115: }
116:
117: private function doGetType(Expr $node): Type
118: {
119: if (
120: !$this->nativeTypesPromoted
121: && count($this->truthyValueExprs) === 0
122: && count($this->falseyValueExprs) === 0
123: ) {
124: $storedResult = $this->findSettledStoredResult($node);
125: if ($storedResult !== null) {
126: return $this->getStoredResultTypeOnThisScope($storedResult, $node, false);
127: }
128:
129: // post-order emission means every real subnode is already stored -
130: // an unstored ask is a synthetic node or a node ahead of the walk,
131: // answered on demand through the MutatingScope path
132: return $this->toWalkScope()->getType($node);
133: }
134:
135: $storedResult = $this->findSettledStoredResult($node);
136: if ($storedResult !== null) {
137: $scope = $this->preprocessScope($storedResult->getBeforeScope());
138: return $scope->getType($node);
139: }
140:
141: // the filters/promotion already narrowed this scope's own tables
142: return $this->toWalkScope()->getType($node);
143: }
144:
145: /**
146: * Consumes a stored result guarded by this scope's position instead of
147: * reading the naked walk-position type: a callback may have derived this
148: * scope (e.g. assignExpression() pinning a call-site literal onto a
149: * parameter) and the walk-position memo predates that. On a state match
150: * the result's own read is the answer; a counterfactual ask re-prices on
151: * the MutatingScope, mirroring resolveTypeOfNewWorldHandlerNode().
152: */
153: private function getStoredResultTypeOnThisScope(ExpressionResult $result, Expr $node, bool $useNativeTypes): Type
154: {
155: $scope = $this->toWalkScope();
156: if ($result->canResolveOwnType() && $result->askScopeVariableStateMatches($scope, $useNativeTypes, true)) {
157: return $useNativeTypes ? $result->getNativeType() : $result->getType();
158: }
159:
160: return $useNativeTypes ? $scope->getNativeType($node) : $scope->getType($node);
161: }
162:
163: public function getScopeType(Expr $expr): Type
164: {
165: return $this->toWalkScope()->getType($expr);
166: }
167:
168: public function getScopeNativeType(Expr $expr): Type
169: {
170: return $this->toWalkScope()->getNativeType($expr);
171: }
172:
173: /** @api */
174: public function getNativeType(Expr $expr): Type
175: {
176: if ($expr instanceof TypeExpr) {
177: // See getType() - same reasoning
178: return $expr->getExprType();
179: }
180:
181: $nodeId = spl_object_id($expr);
182: if (isset($this->askedNativeTypes[$nodeId]) && $this->askedNativeTypes[$nodeId][0] === $expr) {
183: return $this->askedNativeTypes[$nodeId][1];
184: }
185:
186: $type = $this->doGetNativeType($expr);
187: $this->askedNativeTypes[$nodeId] = [$expr, $type];
188:
189: return $type;
190: }
191:
192: private function doGetNativeType(Expr $expr): Type
193: {
194: if (
195: !$this->nativeTypesPromoted
196: && count($this->truthyValueExprs) === 0
197: && count($this->falseyValueExprs) === 0
198: ) {
199: $storedResult = $this->findSettledStoredResult($expr);
200: if ($storedResult !== null) {
201: return $this->getStoredResultTypeOnThisScope($storedResult, $expr, true);
202: }
203:
204: return $this->toWalkScope()->getNativeType($expr);
205: }
206:
207: $storedResult = $this->findSettledStoredResult($expr);
208: if ($storedResult !== null) {
209: $scope = $this->preprocessScope($storedResult->getBeforeScope());
210: return $scope->getNativeType($expr);
211: }
212:
213: return $this->toWalkScope()->getNativeType($expr);
214: }
215:
216: public function getKeepVoidType(Expr $node): Type
217: {
218: $storedResult = $this->findSettledStoredResult($node);
219: if ($storedResult !== null) {
220: $scope = $this->preprocessScope($storedResult->getBeforeScope());
221:
222: return $scope->getKeepVoidType($node);
223: }
224:
225: return $this->toWalkScope()->getKeepVoidType($node);
226: }
227:
228: public function filterByTruthyValue(Expr $expr): self
229: {
230: /** @var self $scope */
231: $scope = parent::filterByTruthyValue($expr);
232: $scope->truthyValueExprs = $this->truthyValueExprs;
233: $scope->falseyValueExprs = $this->falseyValueExprs;
234: $scope->truthyValueExprs[] = $expr;
235:
236: return $scope;
237: }
238:
239: public function filterByFalseyValue(Expr $expr): self
240: {
241: /** @var self $scope */
242: $scope = parent::filterByFalseyValue($expr);
243: $scope->truthyValueExprs = $this->truthyValueExprs;
244: $scope->falseyValueExprs = $this->falseyValueExprs;
245: $scope->falseyValueExprs[] = $expr;
246:
247: return $scope;
248: }
249:
250: private function preprocessScope(MutatingScope $scope): Scope
251: {
252: // a nested walk a rule started from its NodeCallbackScope may have
253: // anchored results to callback scopes - re-entering this class's ask
254: // paths from here would derive scopes without end
255: $scope = $scope->toWalkScope();
256: if ($this->nativeTypesPromoted) {
257: $scope = $scope->doNotTreatPhpDocTypesAsCertain();
258: }
259:
260: foreach ($this->truthyValueExprs as $expr) {
261: $scope = $scope->filterByTruthyValue($expr);
262: }
263: foreach ($this->falseyValueExprs as $expr) {
264: $scope = $scope->filterByFalseyValue($expr);
265: }
266:
267: return $scope;
268: }
269:
270: /**
271: * @param MethodReflection|FunctionReflection|null $reflection
272: */
273: public function pushInFunctionCall($reflection, ?ParameterReflection $parameter, bool $rememberTypes): self
274: {
275: /** @var self $scope */
276: $scope = parent::pushInFunctionCall($reflection, $parameter, $rememberTypes);
277: $scope->truthyValueExprs = $this->truthyValueExprs;
278: $scope->falseyValueExprs = $this->falseyValueExprs;
279:
280: return $scope;
281: }
282:
283: public function popInFunctionCall(): self
284: {
285: $stack = $this->inFunctionCallsStack;
286: array_pop($stack);
287:
288: /** @var self $scope */
289: $scope = parent::popInFunctionCall();
290: $scope->truthyValueExprs = $this->truthyValueExprs;
291: $scope->falseyValueExprs = $this->falseyValueExprs;
292:
293: return $scope;
294: }
295:
296: public function getParentScope(): ?MutatingScope
297: {
298: $parent = parent::getParentScope();
299: if ($parent === null) {
300: return null;
301: }
302:
303: return $parent->toNodeCallbackScope();
304: }
305:
306: }
307: