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