1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection\Php;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Expr\Variable;
7: use PhpParser\Node\FunctionLike;
8: use PhpParser\Node\Stmt\ClassMethod;
9: use PhpParser\Node\Stmt\Function_;
10: use PHPStan\Node\NodeScanner;
11: use PHPStan\Reflection\Assertions;
12: use PHPStan\Reflection\AttributeReflection;
13: use PHPStan\Reflection\ExtendedFunctionVariant;
14: use PHPStan\Reflection\ExtendedParameterReflection;
15: use PHPStan\Reflection\ExtendedParametersAcceptor;
16: use PHPStan\Reflection\FunctionReflection;
17: use PHPStan\Reflection\PassedByReference;
18: use PHPStan\ShouldNotHappenException;
19: use PHPStan\TrinaryLogic;
20: use PHPStan\Turbo\ReferencedByTurboExtension;
21: use PHPStan\Type\Generic\TemplateTypeMap;
22: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
23: use PHPStan\Type\MixedType;
24: use PHPStan\Type\Type;
25: use PHPStan\Type\TypehintHelper;
26: use function array_map;
27: use function array_reverse;
28: use function is_string;
29: use function strtolower;
30:
31: /**
32: * @api
33: */
34: #[ReferencedByTurboExtension(key: 'phpFunctionFromParserNodeReflection')]
35: class PhpFunctionFromParserNodeReflection implements FunctionReflection, ExtendedParametersAcceptor
36: {
37:
38: /** @var Function_|ClassMethod|Node\PropertyHook */
39: private Node\FunctionLike $functionLike;
40:
41: /** @var list<ExtendedFunctionVariant>|null */
42: private ?array $variants = null;
43:
44: /**
45: * @param Function_|ClassMethod|Node\PropertyHook $functionLike
46: * @param Type[] $realParameterTypes
47: * @param Type[] $phpDocParameterTypes
48: * @param Type[] $realParameterDefaultValues
49: * @param array<string, list<AttributeReflection>> $parameterAttributes
50: * @param Type[] $parameterOutTypes
51: * @param array<string, bool> $immediatelyInvokedCallableParameters
52: * @param array<string, Type> $phpDocClosureThisTypeParameters
53: * @param list<AttributeReflection> $attributes
54: * @param array<string, bool> $pureUnlessCallableIsImpureParameters
55: * @param array<string, bool> $pureUnlessParameterPassedParameters
56: */
57: public function __construct(
58: FunctionLike $functionLike,
59: private string $fileName,
60: private TemplateTypeMap $templateTypeMap,
61: private array $realParameterTypes,
62: private array $phpDocParameterTypes,
63: private array $realParameterDefaultValues,
64: private array $parameterAttributes,
65: private Type $realReturnType,
66: private ?Type $phpDocReturnType,
67: private ?Type $throwType,
68: private ?string $deprecatedDescription,
69: private bool $isDeprecated,
70: private bool $isInternal,
71: protected ?bool $isPure,
72: private bool $acceptsNamedArguments,
73: private Assertions $assertions,
74: private ?string $phpDocComment,
75: private array $parameterOutTypes,
76: private array $immediatelyInvokedCallableParameters,
77: private array $phpDocClosureThisTypeParameters,
78: private array $attributes,
79: private array $pureUnlessCallableIsImpureParameters,
80: private array $pureUnlessParameterPassedParameters,
81: )
82: {
83: $this->functionLike = $functionLike;
84: }
85:
86: /**
87: * @phpstan-assert-if-true PhpMethodFromParserNodeReflection $this
88: */
89: public function isMethodOrPropertyHook(): bool
90: {
91: return $this instanceof PhpMethodFromParserNodeReflection;
92: }
93:
94: protected function getFunctionLike(): FunctionLike
95: {
96: return $this->functionLike;
97: }
98:
99: public function getFileName(): string
100: {
101: return $this->fileName;
102: }
103:
104: public function getName(): string
105: {
106: if ($this->functionLike instanceof ClassMethod) {
107: return $this->functionLike->name->name;
108: }
109:
110: if (!$this->functionLike instanceof Function_) {
111: // PropertyHook is handled in PhpMethodFromParserNodeReflection subclass
112: throw new ShouldNotHappenException();
113: }
114:
115: if ($this->functionLike->namespacedName === null) {
116: throw new ShouldNotHappenException();
117: }
118:
119: return (string) $this->functionLike->namespacedName;
120: }
121:
122: public function getVariants(): array
123: {
124: return $this->variants ??= [
125: new ExtendedFunctionVariant(
126: $this->getTemplateTypeMap(),
127: $this->getResolvedTemplateTypeMap(),
128: $this->getParameters(),
129: $this->isVariadic(),
130: $this->getReturnType(),
131: $this->getPhpDocReturnType(),
132: $this->getNativeReturnType(),
133: ),
134: ];
135: }
136:
137: public function getOnlyVariant(): ExtendedParametersAcceptor
138: {
139: return $this;
140: }
141:
142: public function getNamedArgumentsVariants(): ?array
143: {
144: return null;
145: }
146:
147: public function getTemplateTypeMap(): TemplateTypeMap
148: {
149: return $this->templateTypeMap;
150: }
151:
152: public function getResolvedTemplateTypeMap(): TemplateTypeMap
153: {
154: return TemplateTypeMap::createEmpty();
155: }
156:
157: /**
158: * @return list<ExtendedParameterReflection>
159: */
160: public function getParameters(): array
161: {
162: $parameters = [];
163: $isOptional = true;
164:
165: /** @var Node\Param $parameter */
166: foreach (array_reverse($this->functionLike->getParams()) as $parameter) {
167: if ($parameter->default === null && !$parameter->variadic) {
168: $isOptional = false;
169: }
170:
171: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
172: throw new ShouldNotHappenException();
173: }
174:
175: if (isset($this->immediatelyInvokedCallableParameters[$parameter->var->name])) {
176: $immediatelyInvokedCallable = TrinaryLogic::createFromBoolean($this->immediatelyInvokedCallableParameters[$parameter->var->name]);
177: } else {
178: $immediatelyInvokedCallable = TrinaryLogic::createMaybe();
179: }
180:
181: if (isset($this->phpDocClosureThisTypeParameters[$parameter->var->name])) {
182: $closureThisType = $this->phpDocClosureThisTypeParameters[$parameter->var->name];
183: } else {
184: $closureThisType = null;
185: }
186:
187: $pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$parameter->var->name] ?? false);
188: $pureUnlessParameterPassedParameter = TrinaryLogic::createFromBoolean($this->pureUnlessParameterPassedParameters[$parameter->var->name] ?? false);
189:
190: $parameters[] = new PhpParameterFromParserNodeReflection(
191: $parameter->var->name,
192: $isOptional,
193: $this->realParameterTypes[$parameter->var->name],
194: $this->phpDocParameterTypes[$parameter->var->name] ?? null,
195: $parameter->byRef
196: ? PassedByReference::createCreatesNewVariable()
197: : PassedByReference::createNo(),
198: $this->realParameterDefaultValues[$parameter->var->name] ?? null,
199: $parameter->variadic,
200: $this->parameterOutTypes[$parameter->var->name] ?? null,
201: $immediatelyInvokedCallable,
202: $closureThisType,
203: $this->parameterAttributes[$parameter->var->name] ?? [],
204: $pureUnlessCallableIsImpureParameter,
205: $pureUnlessParameterPassedParameter,
206: );
207: }
208:
209: return array_reverse($parameters);
210: }
211:
212: public function isVariadic(): bool
213: {
214: foreach ($this->functionLike->getParams() as $parameter) {
215: if ($parameter->variadic) {
216: return true;
217: }
218: }
219:
220: return false;
221: }
222:
223: public function getReturnType(): Type
224: {
225: return TypehintHelper::decideType($this->realReturnType, $this->phpDocReturnType);
226: }
227:
228: public function getPhpDocReturnType(): Type
229: {
230: return $this->phpDocReturnType ?? new MixedType();
231: }
232:
233: public function getNativeReturnType(): Type
234: {
235: return $this->realReturnType;
236: }
237:
238: public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
239: {
240: return TemplateTypeVarianceMap::createEmpty();
241: }
242:
243: public function getDeprecatedDescription(): ?string
244: {
245: if ($this->isDeprecated) {
246: return $this->deprecatedDescription;
247: }
248:
249: return null;
250: }
251:
252: public function isDeprecated(): TrinaryLogic
253: {
254: return TrinaryLogic::createFromBoolean($this->isDeprecated);
255: }
256:
257: public function isInternal(): TrinaryLogic
258: {
259: return TrinaryLogic::createFromBoolean($this->isInternal);
260: }
261:
262: public function getThrowType(): ?Type
263: {
264: return $this->throwType;
265: }
266:
267: public function hasSideEffects(): TrinaryLogic
268: {
269: if ($this->getReturnType()->isVoid()->yes()) {
270: return TrinaryLogic::createYes();
271: }
272: if ($this->isPure !== null) {
273: return TrinaryLogic::createFromBoolean(!$this->isPure);
274: }
275:
276: return TrinaryLogic::createMaybe();
277: }
278:
279: public function isBuiltin(): bool
280: {
281: return false;
282: }
283:
284: public function isGenerator(): bool
285: {
286: // the yield scan walks the whole body; reflections for the same node are
287: // recreated per ask, so the answer is memoized on the AST node itself
288: // and lives and dies with the parser-cached AST
289: $cached = $this->functionLike->getAttribute('phpstanIsGenerator');
290: if ($cached !== null) {
291: return $cached;
292: }
293:
294: $isGenerator = NodeScanner::nodeIsOrContainsYield($this->functionLike);
295: $this->functionLike->setAttribute('phpstanIsGenerator', $isGenerator);
296:
297: return $isGenerator;
298: }
299:
300: public function acceptsNamedArguments(): TrinaryLogic
301: {
302: return TrinaryLogic::createFromBoolean($this->acceptsNamedArguments);
303: }
304:
305: public function getAsserts(): Assertions
306: {
307: return $this->assertions;
308: }
309:
310: public function getDocComment(): ?string
311: {
312: return $this->phpDocComment;
313: }
314:
315: public function returnsByReference(): TrinaryLogic
316: {
317: return TrinaryLogic::createFromBoolean($this->functionLike->returnsByRef());
318: }
319:
320: public function isPure(): TrinaryLogic
321: {
322: if ($this->isPure === null) {
323: return TrinaryLogic::createMaybe();
324: }
325:
326: return TrinaryLogic::createFromBoolean($this->isPure);
327: }
328:
329: /**
330: * @return array<string, TrinaryLogic>
331: */
332: public function getPureUnlessCallableIsImpureParameters(): array
333: {
334: return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessCallableIsImpureParameters);
335: }
336:
337: /**
338: * @return array<string, TrinaryLogic>
339: */
340: public function getPureUnlessParameterPassedParameters(): array
341: {
342: return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessParameterPassedParameters);
343: }
344:
345: public function getAttributes(): array
346: {
347: return $this->attributes;
348: }
349:
350: public function mustUseReturnValue(): TrinaryLogic
351: {
352: foreach ($this->attributes as $attrib) {
353: if (strtolower($attrib->getName()) === 'nodiscard') {
354: return TrinaryLogic::createYes();
355: }
356: }
357: return TrinaryLogic::createNo();
358: }
359:
360: }
361: