1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Constant;
4:
5: use Nette\Utils\RegexpException;
6: use Nette\Utils\Strings;
7: use PhpParser\Node\Name;
8: use PHPStan\Analyser\OutOfClassScope;
9: use PHPStan\DependencyInjection\BleedingEdgeToggle;
10: use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode;
11: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
12: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13: use PHPStan\Reflection\Callables\FunctionCallableVariant;
14: use PHPStan\Reflection\ClassMemberAccessAnswerer;
15: use PHPStan\Reflection\ConstantReflection;
16: use PHPStan\Reflection\InaccessibleMethod;
17: use PHPStan\Reflection\PhpVersionStaticAccessor;
18: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
19: use PHPStan\Reflection\TrivialParametersAcceptor;
20: use PHPStan\ShouldNotHappenException;
21: use PHPStan\TrinaryLogic;
22: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
23: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
24: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
25: use PHPStan\Type\ClassStringType;
26: use PHPStan\Type\CompoundType;
27: use PHPStan\Type\ConstantScalarType;
28: use PHPStan\Type\ErrorType;
29: use PHPStan\Type\GeneralizePrecision;
30: use PHPStan\Type\Generic\GenericClassStringType;
31: use PHPStan\Type\Generic\TemplateType;
32: use PHPStan\Type\IntegerRangeType;
33: use PHPStan\Type\IntersectionType;
34: use PHPStan\Type\MixedType;
35: use PHPStan\Type\NullType;
36: use PHPStan\Type\ObjectType;
37: use PHPStan\Type\StaticType;
38: use PHPStan\Type\StringType;
39: use PHPStan\Type\Traits\ConstantScalarTypeTrait;
40: use PHPStan\Type\Type;
41: use PHPStan\Type\TypeCombinator;
42: use PHPStan\Type\VerbosityLevel;
43: use function addcslashes;
44: use function in_array;
45: use function is_float;
46: use function is_int;
47: use function is_numeric;
48: use function key;
49: use function strlen;
50: use function substr;
51: use function substr_count;
52:
53: /** @api */
54: class ConstantStringType extends StringType implements ConstantScalarType
55: {
56:
57: private const DESCRIBE_LIMIT = 20;
58:
59: use ConstantScalarTypeTrait;
60: use ConstantScalarToBooleanTrait;
61:
62: private ?ObjectType $objectType = null;
63:
64: private ?Type $arrayKeyType = null;
65:
66: /** @api */
67: public function __construct(private string $value, private bool $isClassString = false)
68: {
69: parent::__construct();
70: }
71:
72: public function getValue(): string
73: {
74: return $this->value;
75: }
76:
77: public function getConstantStrings(): array
78: {
79: return [$this];
80: }
81:
82: public function isClassStringType(): TrinaryLogic
83: {
84: if ($this->isClassString) {
85: return TrinaryLogic::createYes();
86: }
87:
88: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
89:
90: return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($this->value));
91: }
92:
93: public function getClassStringObjectType(): Type
94: {
95: if ($this->isClassStringType()->yes()) {
96: return new ObjectType($this->value);
97: }
98:
99: return new ErrorType();
100: }
101:
102: public function getObjectTypeOrClassStringObjectType(): Type
103: {
104: return $this->getClassStringObjectType();
105: }
106:
107: /**
108: * @deprecated use isClassStringType() instead
109: */
110: public function isClassString(): bool
111: {
112: return $this->isClassStringType()->yes();
113: }
114:
115: public function describe(VerbosityLevel $level): string
116: {
117: return $level->handle(
118: static fn (): string => 'string',
119: function (): string {
120: $value = $this->value;
121:
122: if (!$this->isClassString) {
123: try {
124: $value = Strings::truncate($value, self::DESCRIBE_LIMIT);
125: } catch (RegexpException) {
126: $value = substr($value, 0, self::DESCRIBE_LIMIT) . "\u{2026}";
127: }
128: }
129:
130: return self::export($value);
131: },
132: fn (): string => self::export($this->value),
133: );
134: }
135:
136: private function export(string $value): string
137: {
138: $escapedValue = addcslashes($value, "\0..\37");
139: if ($escapedValue !== $value) {
140: return '"' . addcslashes($value, "\0..\37\\\"") . '"';
141: }
142:
143: return "'" . addcslashes($value, '\\\'') . "'";
144: }
145:
146: public function isSuperTypeOf(Type $type): TrinaryLogic
147: {
148: if ($type instanceof GenericClassStringType) {
149: $genericType = $type->getGenericType();
150: if ($genericType instanceof MixedType) {
151: return TrinaryLogic::createMaybe();
152: }
153: if ($genericType instanceof StaticType) {
154: $genericType = $genericType->getStaticObjectType();
155: }
156:
157: // We are transforming constant class-string to ObjectType. But we need to filter out
158: // an uncertainty originating in possible ObjectType's class subtypes.
159: $objectType = $this->getObjectType();
160:
161: // Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
162: // uncertainty into account.
163: if ($genericType instanceof TemplateType) {
164: $isSuperType = $genericType->getBound()->isSuperTypeOf($objectType);
165: } else {
166: $isSuperType = $genericType->isSuperTypeOf($objectType);
167: }
168:
169: // Explicitly handle the uncertainty for Yes & Maybe.
170: if ($isSuperType->yes()) {
171: return TrinaryLogic::createMaybe();
172: }
173: return TrinaryLogic::createNo();
174: }
175: if ($type instanceof ClassStringType) {
176: return $this->isClassStringType()->yes() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo();
177: }
178:
179: if ($type instanceof self) {
180: return $this->value === $type->value ? TrinaryLogic::createYes() : TrinaryLogic::createNo();
181: }
182:
183: if ($type instanceof parent) {
184: return TrinaryLogic::createMaybe();
185: }
186:
187: if ($type instanceof CompoundType) {
188: return $type->isSubTypeOf($this);
189: }
190:
191: return TrinaryLogic::createNo();
192: }
193:
194: public function isCallable(): TrinaryLogic
195: {
196: if ($this->value === '') {
197: return TrinaryLogic::createNo();
198: }
199:
200: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
201:
202: // 'my_function'
203: if ($reflectionProvider->hasFunction(new Name($this->value), null)) {
204: return TrinaryLogic::createYes();
205: }
206:
207: // 'MyClass::myStaticFunction'
208: $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#');
209: if ($matches !== null) {
210: if (!$reflectionProvider->hasClass($matches[1])) {
211: return TrinaryLogic::createMaybe();
212: }
213:
214: $phpVersion = PhpVersionStaticAccessor::getInstance();
215: $classRef = $reflectionProvider->getClass($matches[1]);
216: if ($classRef->hasMethod($matches[2])) {
217: $method = $classRef->getMethod($matches[2], new OutOfClassScope());
218: if (
219: BleedingEdgeToggle::isBleedingEdge()
220: && !$phpVersion->supportsCallableInstanceMethods()
221: && !$method->isStatic()
222: ) {
223: return TrinaryLogic::createNo();
224: }
225:
226: return TrinaryLogic::createYes();
227: }
228:
229: if (!$classRef->getNativeReflection()->isFinal()) {
230: return TrinaryLogic::createMaybe();
231: }
232:
233: return TrinaryLogic::createNo();
234: }
235:
236: return TrinaryLogic::createNo();
237: }
238:
239: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
240: {
241: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
242:
243: // 'my_function'
244: $functionName = new Name($this->value);
245: if ($reflectionProvider->hasFunction($functionName, null)) {
246: $function = $reflectionProvider->getFunction($functionName, null);
247: return FunctionCallableVariant::createFromVariants($function, $function->getVariants());
248: }
249:
250: // 'MyClass::myStaticFunction'
251: $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#');
252: if ($matches !== null) {
253: if (!$reflectionProvider->hasClass($matches[1])) {
254: return [new TrivialParametersAcceptor()];
255: }
256:
257: $classReflection = $reflectionProvider->getClass($matches[1]);
258: if ($classReflection->hasMethod($matches[2])) {
259: $method = $classReflection->getMethod($matches[2], $scope);
260: if (!$scope->canCallMethod($method)) {
261: return [new InaccessibleMethod($method)];
262: }
263:
264: return FunctionCallableVariant::createFromVariants($method, $method->getVariants());
265: }
266:
267: if (!$classReflection->getNativeReflection()->isFinal()) {
268: return [new TrivialParametersAcceptor()];
269: }
270: }
271:
272: throw new ShouldNotHappenException();
273: }
274:
275: public function toNumber(): Type
276: {
277: if (is_numeric($this->value)) {
278: $value = $this->value;
279: $value = +$value;
280: if (is_float($value)) {
281: return new ConstantFloatType($value);
282: }
283:
284: return new ConstantIntegerType($value);
285: }
286:
287: return new ErrorType();
288: }
289:
290: public function toInteger(): Type
291: {
292: return new ConstantIntegerType((int) $this->value);
293: }
294:
295: public function toFloat(): Type
296: {
297: return new ConstantFloatType((float) $this->value);
298: }
299:
300: public function toArrayKey(): Type
301: {
302: if ($this->arrayKeyType !== null) {
303: return $this->arrayKeyType;
304: }
305:
306: /** @var int|string $offsetValue */
307: $offsetValue = key([$this->value => null]);
308: return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue);
309: }
310:
311: public function isString(): TrinaryLogic
312: {
313: return TrinaryLogic::createYes();
314: }
315:
316: public function isNumericString(): TrinaryLogic
317: {
318: return TrinaryLogic::createFromBoolean(is_numeric($this->getValue()));
319: }
320:
321: public function isNonEmptyString(): TrinaryLogic
322: {
323: return TrinaryLogic::createFromBoolean($this->getValue() !== '');
324: }
325:
326: public function isNonFalsyString(): TrinaryLogic
327: {
328: return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true));
329: }
330:
331: public function isLiteralString(): TrinaryLogic
332: {
333: return TrinaryLogic::createYes();
334: }
335:
336: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
337: {
338: if ($offsetType instanceof ConstantIntegerType) {
339: return TrinaryLogic::createFromBoolean(
340: $offsetType->getValue() < strlen($this->value),
341: );
342: }
343:
344: return parent::hasOffsetValueType($offsetType);
345: }
346:
347: public function getOffsetValueType(Type $offsetType): Type
348: {
349: if ($offsetType instanceof ConstantIntegerType) {
350: if ($offsetType->getValue() < strlen($this->value)) {
351: return new self($this->value[$offsetType->getValue()]);
352: }
353:
354: return new ErrorType();
355: }
356:
357: return parent::getOffsetValueType($offsetType);
358: }
359:
360: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
361: {
362: $valueStringType = $valueType->toString();
363: if ($valueStringType instanceof ErrorType) {
364: return new ErrorType();
365: }
366: if (
367: $offsetType instanceof ConstantIntegerType
368: && $valueStringType instanceof ConstantStringType
369: ) {
370: $value = $this->value;
371: $offsetValue = $offsetType->getValue();
372: if ($offsetValue < 0) {
373: return new ErrorType();
374: }
375: $stringValue = $valueStringType->getValue();
376: if (strlen($stringValue) !== 1) {
377: return new ErrorType();
378: }
379: $value[$offsetValue] = $stringValue;
380:
381: return new self($value);
382: }
383:
384: return parent::setOffsetValueType($offsetType, $valueType);
385: }
386:
387: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
388: {
389: return parent::setOffsetValueType($offsetType, $valueType);
390: }
391:
392: public function append(self $otherString): self
393: {
394: return new self($this->getValue() . $otherString->getValue());
395: }
396:
397: public function generalize(GeneralizePrecision $precision): Type
398: {
399: if ($this->isClassString) {
400: if ($precision->isMoreSpecific()) {
401: return new ClassStringType();
402: }
403:
404: return new StringType();
405: }
406:
407: if ($this->getValue() !== '' && $precision->isMoreSpecific()) {
408: if ($this->getValue() !== '0') {
409: return new IntersectionType([
410: new StringType(),
411: new AccessoryNonFalsyStringType(),
412: new AccessoryLiteralStringType(),
413: ]);
414: }
415:
416: return new IntersectionType([
417: new StringType(),
418: new AccessoryNonEmptyStringType(),
419: new AccessoryLiteralStringType(),
420: ]);
421: }
422:
423: if ($precision->isMoreSpecific()) {
424: return new IntersectionType([
425: new StringType(),
426: new AccessoryLiteralStringType(),
427: ]);
428: }
429:
430: return new StringType();
431: }
432:
433: public function getSmallerType(): Type
434: {
435: $subtractedTypes = [
436: new ConstantBooleanType(true),
437: IntegerRangeType::createAllGreaterThanOrEqualTo((float) $this->value),
438: ];
439:
440: if ($this->value === '') {
441: $subtractedTypes[] = new NullType();
442: $subtractedTypes[] = new StringType();
443: }
444:
445: if (!(bool) $this->value) {
446: $subtractedTypes[] = new ConstantBooleanType(false);
447: }
448:
449: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
450: }
451:
452: public function getSmallerOrEqualType(): Type
453: {
454: $subtractedTypes = [
455: IntegerRangeType::createAllGreaterThan((float) $this->value),
456: ];
457:
458: if (!(bool) $this->value) {
459: $subtractedTypes[] = new ConstantBooleanType(true);
460: }
461:
462: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
463: }
464:
465: public function getGreaterType(): Type
466: {
467: $subtractedTypes = [
468: new ConstantBooleanType(false),
469: IntegerRangeType::createAllSmallerThanOrEqualTo((float) $this->value),
470: ];
471:
472: if ((bool) $this->value) {
473: $subtractedTypes[] = new ConstantBooleanType(true);
474: }
475:
476: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
477: }
478:
479: public function getGreaterOrEqualType(): Type
480: {
481: $subtractedTypes = [
482: IntegerRangeType::createAllSmallerThan((float) $this->value),
483: ];
484:
485: if ((bool) $this->value) {
486: $subtractedTypes[] = new ConstantBooleanType(false);
487: }
488:
489: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
490: }
491:
492: public function canAccessConstants(): TrinaryLogic
493: {
494: return $this->isClassStringType();
495: }
496:
497: public function hasConstant(string $constantName): TrinaryLogic
498: {
499: return $this->getObjectType()->hasConstant($constantName);
500: }
501:
502: public function getConstant(string $constantName): ConstantReflection
503: {
504: return $this->getObjectType()->getConstant($constantName);
505: }
506:
507: private function getObjectType(): ObjectType
508: {
509: return $this->objectType ??= new ObjectType($this->value);
510: }
511:
512: public function toPhpDocNode(): TypeNode
513: {
514: if (substr_count($this->value, "\n") > 0) {
515: return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode();
516: }
517:
518: return new ConstTypeNode(new QuoteAwareConstExprStringNode($this->value, QuoteAwareConstExprStringNode::SINGLE_QUOTED));
519: }
520:
521: /**
522: * @param mixed[] $properties
523: */
524: public static function __set_state(array $properties): Type
525: {
526: return new self($properties['value'], $properties['isClassString'] ?? false);
527: }
528:
529: }
530: