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