1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use PhpParser\Node;
8: use ReflectionClass as CoreReflectionClass;
9: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
10: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
11: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
12: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
13: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
14: use PHPStan\BetterReflection\Reflection\Exception\InvalidConstantNode;
15: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionConstantStringCast;
16: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
17: use PHPStan\BetterReflection\Reflector\Reflector;
18: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
19: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
20: use PHPStan\BetterReflection\Util\ConstantNodeChecker;
21: use PHPStan\BetterReflection\Util\GetLastDocComment;
22:
23: use function array_slice;
24: use function assert;
25: use function count;
26: use function explode;
27: use function implode;
28: use function is_int;
29:
30: /** @psalm-immutable */
31: class ReflectionConstant implements Reflection
32: {
33: private Reflector $reflector;
34: private LocatedSource $locatedSource;
35: /**
36: * @var non-empty-string|null
37: */
38: private $namespace = null;
39: /**
40: * @var non-empty-string
41: * @psalm-allow-private-mutation
42: */
43: private string $name;
44:
45: /**
46: * @var non-empty-string
47: * @psalm-allow-private-mutation
48: */
49: private string $shortName;
50:
51: private Node\Expr $value;
52:
53: /** @var non-empty-string|null */
54: private $docComment;
55:
56: /** @var positive-int */
57: private int $startLine;
58:
59: /** @var positive-int */
60: private int $endLine;
61:
62: /** @var positive-int */
63: private int $startColumn;
64:
65: /** @var positive-int */
66: private int $endColumn;
67:
68: /** @var list<ReflectionAttribute> */
69: private array $attributes;
70:
71: /** @psalm-allow-private-mutation
72: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
73: private $compiledValue = null;
74:
75: /** @param non-empty-string|null $namespace
76: * @param \PhpParser\Node\Stmt\Const_|\PhpParser\Node\Expr\FuncCall $node */
77: private function __construct(Reflector $reflector, $node, LocatedSource $locatedSource, ?string $namespace = null, ?int $positionInNode = null)
78: {
79: $this->reflector = $reflector;
80: $this->locatedSource = $locatedSource;
81: /** @psalm-allow-private-mutation */
82: $this->namespace = $namespace;
83: $this->setNamesFromNode($node, $positionInNode);
84: if ($node instanceof Node\Expr\FuncCall) {
85: $argumentValueNode = $node->args[1];
86: assert($argumentValueNode instanceof Node\Arg);
87: $this->value = $argumentValueNode->value;
88: } else {
89: /** @psalm-suppress PossiblyNullArrayOffset */
90: $this->value = $node->consts[$positionInNode]->value;
91: }
92: $this->docComment = GetLastDocComment::forNode($node);
93: $this->attributes = $node instanceof Node\Stmt\Const_
94: ? ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups)
95: : [];
96: $startLine = $node->getStartLine();
97: assert($startLine > 0);
98: $endLine = $node->getEndLine();
99: assert($endLine > 0);
100: $this->startLine = $startLine;
101: $this->endLine = $endLine;
102: $this->startColumn = CalculateReflectionColumn::getStartColumn($this->locatedSource->getSource(), $node);
103: $this->endColumn = CalculateReflectionColumn::getEndColumn($this->locatedSource->getSource(), $node);
104: }
105:
106: /**
107: * @return array<string, mixed>
108: */
109: public function exportToCache(): array
110: {
111: return [
112: 'locatedSource' => $this->locatedSource->exportToCache(),
113: 'name' => $this->name,
114: 'shortName' => $this->shortName,
115: 'value' => ExprCacheHelper::export($this->value),
116: 'docComment' => $this->docComment,
117: 'attributes' => array_map(
118: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
119: $this->attributes,
120: ),
121: 'startLine' => $this->startLine,
122: 'endLine' => $this->endLine,
123: 'startColumn' => $this->startColumn,
124: 'endColumn' => $this->endColumn,
125: 'namespace' => $this->namespace,
126: ];
127: }
128:
129: /**
130: * @param array<string, mixed> $data
131: */
132: public static function importFromCache(Reflector $reflector, array $data): self
133: {
134: $reflection = new CoreReflectionClass(self::class);
135: /** @var self $ref */
136: $ref = $reflection->newInstanceWithoutConstructor();
137: $ref->reflector = $reflector;
138: $ref->locatedSource = LocatedSource::importFromCache($data['locatedSource']);
139: $ref->namespace = $data['namespace'];
140: $ref->name = $data['name'];
141: $ref->shortName = $data['shortName'];
142:
143: $ref->value = ExprCacheHelper::import($data['value']);
144:
145: $ref->docComment = $data['docComment'];
146: $ref->attributes = array_map(
147: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
148: $data['attributes'],
149: );
150: $ref->startLine = $data['startLine'];
151: $ref->endLine = $data['endLine'];
152: $ref->startColumn = $data['startColumn'];
153: $ref->endColumn = $data['endColumn'];
154:
155: return $ref;
156: }
157:
158: /**
159: * Create a reflection of a constant
160: *
161: * @internal
162: *
163: * @param Node\Stmt\Const_|Node\Expr\FuncCall $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
164: * @param non-empty-string|null $namespace
165: */
166: public static function createFromNode(Reflector $reflector, Node $node, LocatedSource $locatedSource, ?string $namespace = null, ?int $positionInNode = null): self
167: {
168: if ($node instanceof Node\Stmt\Const_) {
169: assert(is_int($positionInNode));
170:
171: return self::createFromConstKeyword($reflector, $node, $locatedSource, $namespace, $positionInNode);
172: }
173: return self::createFromDefineFunctionCall($reflector, $node, $locatedSource);
174: }
175:
176: /** @param non-empty-string|null $namespace */
177: private static function createFromConstKeyword(Reflector $reflector, Node\Stmt\Const_ $node, LocatedSource $locatedSource, ?string $namespace, int $positionInNode): self
178: {
179: return new self(
180: $reflector,
181: $node,
182: $locatedSource,
183: $namespace,
184: $positionInNode,
185: );
186: }
187:
188: /** @throws InvalidConstantNode */
189: private static function createFromDefineFunctionCall(Reflector $reflector, Node\Expr\FuncCall $node, LocatedSource $locatedSource): self
190: {
191: ConstantNodeChecker::assertValidDefineFunctionCall($node);
192: return new self(
193: $reflector,
194: $node,
195: $locatedSource,
196: );
197: }
198:
199: /**
200: * Get the "short" name of the constant (e.g. for A\B\FOO, this will return
201: * "FOO").
202: *
203: * @return non-empty-string
204: */
205: public function getShortName(): string
206: {
207: return $this->shortName;
208: }
209:
210: /**
211: * Get the "full" name of the constant (e.g. for A\B\FOO, this will return
212: * "A\B\FOO").
213: *
214: * @return non-empty-string
215: */
216: public function getName(): string
217: {
218: return $this->name;
219: }
220:
221: /**
222: * Get the "namespace" name of the constant (e.g. for A\B\FOO, this will
223: * return "A\B").
224: *
225: * @return non-empty-string|null
226: */
227: public function getNamespaceName(): ?string
228: {
229: return $this->namespace;
230: }
231:
232: /**
233: * Decide if this constant is part of a namespace. Returns false if the constant
234: * is in the global namespace or does not have a specified namespace.
235: */
236: public function inNamespace(): bool
237: {
238: return $this->namespace !== null;
239: }
240:
241: /** @return non-empty-string|null */
242: public function getExtensionName(): ?string
243: {
244: return $this->locatedSource->getExtensionName();
245: }
246:
247: /**
248: * Is this an internal constant?
249: */
250: public function isInternal(): bool
251: {
252: return $this->locatedSource->isInternal();
253: }
254:
255: /**
256: * Is this a user-defined function (will always return the opposite of
257: * whatever isInternal returns).
258: */
259: public function isUserDefined(): bool
260: {
261: return ! $this->isInternal();
262: }
263:
264: public function isDeprecated(): bool
265: {
266: return DeprecatedHelper::isDeprecated($this);
267: }
268:
269: public function getValueExpression(): Node\Expr
270: {
271: return $this->value;
272: }
273:
274: /**
275: * @return mixed
276: */
277: public function getValue()
278: {
279: if ($this->compiledValue === null) {
280: $this->compiledValue = (new CompileNodeToValue())->__invoke(
281: $this->value,
282: new CompilerContext($this->reflector, $this),
283: );
284: }
285:
286: return $this->compiledValue->value;
287: }
288:
289: /** @return non-empty-string|null */
290: public function getFileName(): ?string
291: {
292: return $this->locatedSource->getFileName();
293: }
294:
295: public function getLocatedSource(): LocatedSource
296: {
297: return $this->locatedSource;
298: }
299:
300: /**
301: * Get the line number that this constant starts on.
302: *
303: * @return positive-int
304: */
305: public function getStartLine(): int
306: {
307: return $this->startLine;
308: }
309:
310: /**
311: * Get the line number that this constant ends on.
312: *
313: * @return positive-int
314: */
315: public function getEndLine(): int
316: {
317: return $this->endLine;
318: }
319:
320: /** @return positive-int */
321: public function getStartColumn(): int
322: {
323: return $this->startColumn;
324: }
325:
326: /** @return positive-int */
327: public function getEndColumn(): int
328: {
329: return $this->endColumn;
330: }
331:
332: /** @return non-empty-string|null */
333: public function getDocComment(): ?string
334: {
335: return $this->docComment;
336: }
337:
338: /** @return non-empty-string */
339: public function __toString(): string
340: {
341: return ReflectionConstantStringCast::toString($this);
342: }
343:
344: /** @return list<ReflectionAttribute> */
345: public function getAttributes(): array
346: {
347: return $this->attributes;
348: }
349:
350: /** @return list<ReflectionAttribute> */
351: public function getAttributesByName(string $name): array
352: {
353: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
354: }
355:
356: /**
357: * @param class-string $className
358: *
359: * @return list<ReflectionAttribute>
360: */
361: public function getAttributesByInstance(string $className): array
362: {
363: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
364: }
365:
366: /**
367: * @param \PhpParser\Node\Stmt\Const_|\PhpParser\Node\Expr\FuncCall $node
368: */
369: private function setNamesFromNode($node, ?int $positionInNode): void
370: {
371: if ($node instanceof Node\Expr\FuncCall) {
372: $name = $this->getNameFromDefineFunctionCall($node);
373:
374: $nameParts = explode('\\', $name);
375: /** @phpstan-ignore assign.readOnlyPropertyByPhpDoc */
376: $this->namespace = implode('\\', array_slice($nameParts, 0, -1)) ?: null;
377:
378: $shortName = $nameParts[count($nameParts) - 1];
379: assert($shortName !== '');
380: } else {
381: /** @psalm-suppress PossiblyNullArrayOffset */
382: $constNode = $node->consts[$positionInNode];
383: $namespacedName = $constNode->namespacedName;
384: assert($namespacedName instanceof Node\Name);
385:
386: $name = $namespacedName->toString();
387: $shortName = $constNode->name->name;
388: }
389:
390: /** @phpstan-ignore assign.readOnlyPropertyByPhpDoc */
391: $this->name = $name;
392: /** @phpstan-ignore assign.readOnlyPropertyByPhpDoc */
393: $this->shortName = $shortName;
394: }
395:
396: /** @return non-empty-string */
397: private function getNameFromDefineFunctionCall(Node\Expr\FuncCall $node): string
398: {
399: $argumentNameNode = $node->args[0];
400: assert($argumentNameNode instanceof Node\Arg);
401: $nameNode = $argumentNameNode->value;
402: assert($nameNode instanceof Node\Scalar\String_);
403:
404: /** @psalm-var non-empty-string */
405: return $nameNode->value;
406: }
407: }
408: