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