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