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\Reflector;
17: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
18: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
19: use PHPStan\BetterReflection\Util\ConstantNodeChecker;
20: use PHPStan\BetterReflection\Util\GetLastDocComment;
21:
22: use function array_map;
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: /** @return array<string, mixed> */
123: public function exportToCache(): array
124: {
125: return [
126: 'locatedSource' => $this->locatedSource->exportToCache(),
127: 'name' => $this->name,
128: 'shortName' => $this->shortName,
129: 'value' => is_array($this->value) ? $this->value : ExprCacheHelper::export($this->value),
130: 'docComment' => $this->docComment,
131: 'attributes' => array_map(
132: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
133: $this->attributes,
134: ),
135: 'startLine' => $this->startLine,
136: 'endLine' => $this->endLine,
137: 'startColumn' => $this->startColumn,
138: 'endColumn' => $this->endColumn,
139: 'namespace' => $this->namespace,
140: ];
141: }
142:
143: /** @param array<string, mixed> $data */
144: public static function importFromCache(Reflector $reflector, array $data): self
145: {
146: $reflection = new CoreReflectionClass(self::class);
147: $ref = $reflection->newInstanceWithoutConstructor();
148: assert($ref instanceof self);
149: $ref->reflector = $reflector;
150: $ref->locatedSource = LocatedSource::importFromCache($data['locatedSource']);
151: $ref->namespace = $data['namespace'];
152: $ref->name = $data['name'];
153: $ref->shortName = $data['shortName'];
154:
155: $ref->value = $data['value'];
156:
157: $ref->docComment = $data['docComment'];
158: $ref->attributes = array_map(
159: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
160: $data['attributes'],
161: );
162: $ref->startLine = $data['startLine'];
163: $ref->endLine = $data['endLine'];
164: $ref->startColumn = $data['startColumn'];
165: $ref->endColumn = $data['endColumn'];
166:
167: return $ref;
168: }
169:
170: /**
171: * Create a reflection of a constant
172: *
173: * @internal
174: *
175: * @param Node\Stmt\Const_|Node\Expr\FuncCall $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
176: * @param non-empty-string|null $namespace
177: */
178: public static function createFromNode(
179: Reflector $reflector,
180: Node $node,
181: LocatedSource $locatedSource,
182: ?string $namespace = null,
183: ?int $positionInNode = null
184: ): self {
185: if ($node instanceof Node\Stmt\Const_) {
186: assert(is_int($positionInNode));
187:
188: return self::createFromConstKeyword($reflector, $node, $locatedSource, $namespace, $positionInNode);
189: }
190:
191: return self::createFromDefineFunctionCall($reflector, $node, $locatedSource);
192: }
193:
194: /** @param non-empty-string|null $namespace */
195: private static function createFromConstKeyword(
196: Reflector $reflector,
197: Node\Stmt\Const_ $node,
198: LocatedSource $locatedSource,
199: ?string $namespace,
200: int $positionInNode
201: ): self {
202: return new self(
203: $reflector,
204: $node,
205: $locatedSource,
206: $namespace,
207: $positionInNode,
208: );
209: }
210:
211: /** @throws InvalidConstantNode */
212: private static function createFromDefineFunctionCall(
213: Reflector $reflector,
214: Node\Expr\FuncCall $node,
215: LocatedSource $locatedSource
216: ): self {
217: ConstantNodeChecker::assertValidDefineFunctionCall($node);
218:
219: return new self(
220: $reflector,
221: $node,
222: $locatedSource,
223: );
224: }
225:
226: /**
227: * Get the "short" name of the constant (e.g. for A\B\FOO, this will return
228: * "FOO").
229: *
230: * @return non-empty-string
231: */
232: public function getShortName(): string
233: {
234: return $this->shortName;
235: }
236:
237: /**
238: * Get the "full" name of the constant (e.g. for A\B\FOO, this will return
239: * "A\B\FOO").
240: *
241: * @return non-empty-string
242: */
243: public function getName(): string
244: {
245: return $this->name;
246: }
247:
248: /**
249: * Get the "namespace" name of the constant (e.g. for A\B\FOO, this will
250: * return "A\B").
251: *
252: * @return non-empty-string|null
253: */
254: public function getNamespaceName(): ?string
255: {
256: return $this->namespace;
257: }
258:
259: /**
260: * Decide if this constant is part of a namespace. Returns false if the constant
261: * is in the global namespace or does not have a specified namespace.
262: */
263: public function inNamespace(): bool
264: {
265: return $this->namespace !== null;
266: }
267:
268: /** @return non-empty-string|null */
269: public function getExtensionName(): ?string
270: {
271: return $this->locatedSource->getExtensionName();
272: }
273:
274: /**
275: * Is this an internal constant?
276: */
277: public function isInternal(): bool
278: {
279: return $this->locatedSource->isInternal();
280: }
281:
282: /**
283: * Is this a user-defined function (will always return the opposite of
284: * whatever isInternal returns).
285: */
286: public function isUserDefined(): bool
287: {
288: return ! $this->isInternal();
289: }
290:
291: public function isDeprecated(): bool
292: {
293: return DeprecatedHelper::isDeprecated($this);
294: }
295:
296: public function getValueExpression(): Node\Expr
297: {
298: if (is_array($this->value)) {
299: $this->value = ExprCacheHelper::import($this->value);
300: }
301:
302: return $this->value;
303: }
304:
305: /**
306: * @return mixed
307: */
308: public function getValue()
309: {
310: if ($this->compiledValue === null) {
311: $this->compiledValue = (new CompileNodeToValue())->__invoke(
312: $this->getValueExpression(),
313: new CompilerContext($this->reflector, $this),
314: );
315: }
316:
317: return $this->compiledValue->value;
318: }
319:
320: /** @return non-empty-string|null */
321: public function getFileName(): ?string
322: {
323: return $this->locatedSource->getFileName();
324: }
325:
326: public function getLocatedSource(): LocatedSource
327: {
328: return $this->locatedSource;
329: }
330:
331: /**
332: * Get the line number that this constant starts on.
333: *
334: * @return positive-int
335: */
336: public function getStartLine(): int
337: {
338: return $this->startLine;
339: }
340:
341: /**
342: * Get the line number that this constant ends on.
343: *
344: * @return positive-int
345: */
346: public function getEndLine(): int
347: {
348: return $this->endLine;
349: }
350:
351: /** @return positive-int */
352: public function getStartColumn(): int
353: {
354: return $this->startColumn;
355: }
356:
357: /** @return positive-int */
358: public function getEndColumn(): int
359: {
360: return $this->endColumn;
361: }
362:
363: /** @return non-empty-string|null */
364: public function getDocComment(): ?string
365: {
366: return $this->docComment;
367: }
368:
369: /** @return non-empty-string */
370: public function __toString(): string
371: {
372: return ReflectionConstantStringCast::toString($this);
373: }
374:
375: /** @return list<ReflectionAttribute> */
376: public function getAttributes(): array
377: {
378: return $this->attributes;
379: }
380:
381: /** @return list<ReflectionAttribute> */
382: public function getAttributesByName(string $name): array
383: {
384: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
385: }
386:
387: /**
388: * @param class-string $className
389: *
390: * @return list<ReflectionAttribute>
391: */
392: public function getAttributesByInstance(string $className): array
393: {
394: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
395: }
396:
397: /**
398: * @param \PhpParser\Node\Stmt\Const_|\PhpParser\Node\Expr\FuncCall $node
399: */
400: private function setNamesFromNode($node, ?int $positionInNode): void
401: {
402: if ($node instanceof Node\Expr\FuncCall) {
403: $name = $this->getNameFromDefineFunctionCall($node);
404:
405: $nameParts = explode('\\', $name);
406: /** @phpstan-ignore assign.readOnlyPropertyByPhpDoc */
407: $this->namespace = implode('\\', array_slice($nameParts, 0, -1)) ?: null;
408:
409: $shortName = $nameParts[count($nameParts) - 1];
410: assert($shortName !== '');
411: } else {
412: /** @psalm-suppress PossiblyNullArrayOffset */
413: $constNode = $node->consts[$positionInNode];
414: $namespacedName = $constNode->namespacedName;
415: assert($namespacedName instanceof Node\Name);
416:
417: $name = $namespacedName->toString();
418: $shortName = $constNode->name->name;
419: }
420:
421: $this->name = $name;
422: $this->shortName = $shortName;
423: }
424:
425: /** @return non-empty-string */
426: private function getNameFromDefineFunctionCall(Node\Expr\FuncCall $node): string
427: {
428: $argumentNameNode = $node->args[0];
429: assert($argumentNameNode instanceof Node\Arg);
430: $nameNode = $argumentNameNode->value;
431: assert($nameNode instanceof Node\Scalar\String_);
432:
433: /** @psalm-var non-empty-string */
434: return $nameNode->value;
435: }
436: }
437: