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