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\Php\PhpVersion;
10: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
11: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
12: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13: use PHPStan\Reflection\Callables\FunctionCallableVariant;
14: use PHPStan\Reflection\ClassConstantReflection;
15: use PHPStan\Reflection\ClassMemberAccessAnswerer;
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\AccessoryLowercaseStringType;
24: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
25: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
26: use PHPStan\Type\Accessory\AccessoryNumericStringType;
27: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
28: use PHPStan\Type\ClassNameToObjectTypeResult;
29: use PHPStan\Type\ClassStringType;
30: use PHPStan\Type\CompoundType;
31: use PHPStan\Type\ConstantScalarType;
32: use PHPStan\Type\ErrorType;
33: use PHPStan\Type\GeneralizePrecision;
34: use PHPStan\Type\Generic\GenericClassStringType;
35: use PHPStan\Type\Generic\TemplateType;
36: use PHPStan\Type\InstanceofDeprecated;
37: use PHPStan\Type\IntegerRangeType;
38: use PHPStan\Type\IntersectionType;
39: use PHPStan\Type\IsSuperTypeOfResult;
40: use PHPStan\Type\MixedType;
41: use PHPStan\Type\NeverType;
42: use PHPStan\Type\NullType;
43: use PHPStan\Type\ObjectType;
44: use PHPStan\Type\StaticType;
45: use PHPStan\Type\StringType;
46: use PHPStan\Type\Traits\ConstantScalarTypeTrait;
47: use PHPStan\Type\Type;
48: use PHPStan\Type\TypeCombinator;
49: use PHPStan\Type\UnionType;
50: use PHPStan\Type\VerbosityLevel;
51: use function addcslashes;
52: use function array_unique;
53: use function array_values;
54: use function in_array;
55: use function is_float;
56: use function is_int;
57: use function is_numeric;
58: use function key;
59: use function strlen;
60: use function strtolower;
61: use function strtoupper;
62: use function substr;
63: use function substr_count;
64:
65: /** @api */
66: #[InstanceofDeprecated(insteadUse: 'Type::getConstantStrings()')]
67: class ConstantStringType extends StringType implements ConstantScalarType
68: {
69:
70: private const DESCRIBE_LIMIT = 20;
71:
72: use ConstantScalarTypeTrait;
73: use ConstantScalarToBooleanTrait;
74:
75: private ?ObjectType $objectType = null;
76:
77: private ?Type $arrayKeyType = null;
78:
79: /** @var array<int, string> */
80: private array $cachedDescriptions = [];
81:
82: /** @api */
83: public function __construct(private string $value, private bool $isClassString = false)
84: {
85: parent::__construct();
86: }
87:
88: public function getValue(): string
89: {
90: return $this->value;
91: }
92:
93: public function getConstantStrings(): array
94: {
95: return [$this];
96: }
97:
98: public function isClassString(): TrinaryLogic
99: {
100: if ($this->isClassString) {
101: return TrinaryLogic::createYes();
102: }
103:
104: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
105:
106: return TrinaryLogic::createFromBoolean($reflectionProvider->hasClass($this->value));
107: }
108:
109: public function getClassStringObjectType(): Type
110: {
111: if ($this->isClassString()->yes()) {
112: return new ObjectType($this->value);
113: }
114:
115: return new ErrorType();
116: }
117:
118: public function getObjectTypeOrClassStringObjectType(): Type
119: {
120: return $this->getClassStringObjectType();
121: }
122:
123: public function describe(VerbosityLevel $level): string
124: {
125: $levelValue = $level->getLevelValue();
126:
127: if (isset($this->cachedDescriptions[$levelValue])) {
128: return $this->cachedDescriptions[$levelValue];
129: }
130:
131: return $level->handle(
132: static fn (): string => 'string',
133: function () use ($levelValue): string {
134: $value = $this->value;
135:
136: if (!$this->isClassString) {
137: try {
138: $value = Strings::truncate($value, self::DESCRIBE_LIMIT);
139: } catch (RegexpException) {
140: $value = substr($value, 0, self::DESCRIBE_LIMIT) . "\u{2026}";
141: }
142: }
143:
144: return $this->cachedDescriptions[$levelValue] = self::export($value);
145: },
146: fn (): string => $this->cachedDescriptions[$levelValue] = self::export($this->value),
147: );
148: }
149:
150: private function export(string $value): string
151: {
152: $escapedValue = addcslashes($value, "\0..\37");
153: if ($escapedValue !== $value) {
154: return '"' . addcslashes($value, "\0..\37\\\"") . '"';
155: }
156:
157: return "'" . addcslashes($value, '\\\'') . "'";
158: }
159:
160: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
161: {
162: if ($type instanceof GenericClassStringType) {
163: $genericType = $type->getGenericType();
164: if ($genericType instanceof MixedType) {
165: return IsSuperTypeOfResult::createMaybe();
166: }
167: if ($genericType instanceof StaticType) {
168: $genericType = $genericType->getStaticObjectType();
169: }
170:
171: // We are transforming constant class-string to ObjectType. But we need to filter out
172: // an uncertainty originating in possible ObjectType's class subtypes.
173: $objectType = $this->getObjectType();
174:
175: // Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
176: // uncertainty into account.
177: if ($genericType instanceof TemplateType) {
178: $isSuperType = $genericType->getBound()->isSuperTypeOf($objectType);
179: } else {
180: $isSuperType = $genericType->isSuperTypeOf($objectType);
181: }
182:
183: // Explicitly handle the uncertainty for Yes & Maybe.
184: if ($isSuperType->yes()) {
185: return IsSuperTypeOfResult::createMaybe();
186: }
187: return IsSuperTypeOfResult::createNo();
188: }
189: if ($type instanceof ClassStringType) {
190: return $this->isClassString()->yes() ? IsSuperTypeOfResult::createMaybe() : IsSuperTypeOfResult::createNo();
191: }
192:
193: if ($type instanceof self) {
194: return $this->value === $type->value ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createNo();
195: }
196:
197: if ($type instanceof parent) {
198: return IsSuperTypeOfResult::createMaybe();
199: }
200:
201: if ($type instanceof CompoundType) {
202: return $type->isSubTypeOf($this);
203: }
204:
205: return IsSuperTypeOfResult::createNo();
206: }
207:
208: public function isCallable(): TrinaryLogic
209: {
210: if ($this->value === '') {
211: return TrinaryLogic::createNo();
212: }
213:
214: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
215:
216: // 'my_function'
217: if ($reflectionProvider->hasFunction(new Name($this->value), null)) {
218: return TrinaryLogic::createYes();
219: }
220:
221: // 'MyClass::myStaticFunction'
222: $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#');
223: if ($matches !== null) {
224: if (!$reflectionProvider->hasClass($matches[1])) {
225: return TrinaryLogic::createMaybe();
226: }
227:
228: $classRef = $reflectionProvider->getClass($matches[1]);
229: if ($classRef->hasMethod($matches[2])) {
230: $phpVersion = PhpVersionStaticAccessor::getInstance();
231: if (!$phpVersion->supportsCallableInstanceMethods()) {
232: $method = $classRef->getMethod($matches[2], new OutOfClassScope());
233:
234: if (!$method->isStatic()) {
235: return TrinaryLogic::createNo();
236: }
237: }
238:
239: return TrinaryLogic::createYes();
240: }
241:
242: if (!$classRef->isFinalByKeyword()) {
243: return TrinaryLogic::createMaybe();
244: }
245:
246: return TrinaryLogic::createNo();
247: }
248:
249: return TrinaryLogic::createNo();
250: }
251:
252: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
253: {
254: if ($this->value === '') {
255: return [];
256: }
257:
258: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
259:
260: // 'my_function'
261: $functionName = new Name($this->value);
262: if ($reflectionProvider->hasFunction($functionName, null)) {
263: $function = $reflectionProvider->getFunction($functionName, null);
264: return FunctionCallableVariant::createFromVariants($function, $function->getVariants());
265: }
266:
267: // 'MyClass::myStaticFunction'
268: $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#');
269: if ($matches !== null) {
270: if (!$reflectionProvider->hasClass($matches[1])) {
271: return [new TrivialParametersAcceptor()];
272: }
273:
274: $classReflection = $reflectionProvider->getClass($matches[1]);
275: if ($classReflection->hasMethod($matches[2])) {
276: $method = $classReflection->getMethod($matches[2], $scope);
277: if (!$scope->canCallMethod($method)) {
278: return [new InaccessibleMethod($method)];
279: }
280:
281: return FunctionCallableVariant::createFromVariants($method, $method->getVariants());
282: }
283:
284: if (!$classReflection->isFinalByKeyword()) {
285: return [new TrivialParametersAcceptor()];
286: }
287: }
288:
289: throw new ShouldNotHappenException();
290: }
291:
292: public function toNumber(): Type
293: {
294: if (is_numeric($this->value)) {
295: $value = $this->value;
296: $value = +$value;
297: if (is_float($value)) {
298: return new ConstantFloatType($value);
299: }
300:
301: return new ConstantIntegerType($value);
302: }
303:
304: return new ErrorType();
305: }
306:
307: public function toBitwiseNotType(): Type
308: {
309: return new ConstantStringType(~$this->value);
310: }
311:
312: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
313: {
314: return new ClassNameToObjectTypeResult(new ObjectType($this->value), false);
315: }
316:
317: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
318: {
319: $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames();
320: if ($allowString) {
321: foreach ($objectOrClassType->getConstantStrings() as $constantString) {
322: $objectOrClassTypeClassNames[] = $constantString->getValue();
323: }
324: $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames));
325: }
326:
327: $uncertainty = false;
328: if (!$allowSameClass) {
329: if ($objectOrClassTypeClassNames === [$this->value]) {
330: $isSameClass = true;
331: foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) {
332: if (!$classReflection->isFinal()) {
333: $isSameClass = false;
334: break;
335: }
336: }
337:
338: if ($isSameClass) {
339: return new ClassNameToObjectTypeResult(new NeverType(), false);
340: }
341: }
342:
343: if (
344: // For object, as soon as the exact same type is provided
345: // in the list we cannot be sure of the result
346: in_array($this->value, $objectOrClassTypeClassNames, true)
347: // This also occurs for generic class string
348: || ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($this)->yes())
349: ) {
350: $uncertainty = true;
351: }
352: }
353:
354: if ($allowString) {
355: return new ClassNameToObjectTypeResult(
356: new UnionType([
357: new ObjectType($this->value),
358: new GenericClassStringType(new ObjectType($this->value)),
359: ]),
360: $uncertainty,
361: );
362: }
363:
364: return new ClassNameToObjectTypeResult(new ObjectType($this->value), $uncertainty);
365: }
366:
367: public function toAbsoluteNumber(): Type
368: {
369: return $this->toNumber()->toAbsoluteNumber();
370: }
371:
372: public function toInteger(): Type
373: {
374: return new ConstantIntegerType((int) $this->value);
375: }
376:
377: public function toFloat(): Type
378: {
379: return new ConstantFloatType((float) $this->value);
380: }
381:
382: public function toArrayKey(): Type
383: {
384: if ($this->arrayKeyType !== null) {
385: return $this->arrayKeyType;
386: }
387:
388: /** @var int|string $offsetValue */
389: $offsetValue = key([$this->value => null]);
390:
391: if ($offsetValue === $this->value) {
392: return $this;
393: }
394:
395: return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue);
396: }
397:
398: public function isString(): TrinaryLogic
399: {
400: return TrinaryLogic::createYes();
401: }
402:
403: public function isNumericString(): TrinaryLogic
404: {
405: return TrinaryLogic::createFromBoolean(is_numeric($this->getValue()));
406: }
407:
408: public function isDecimalIntegerString(): TrinaryLogic
409: {
410: return TrinaryLogic::createFromBoolean((string) (int) $this->value === $this->value);
411: }
412:
413: public function isNonEmptyString(): TrinaryLogic
414: {
415: return TrinaryLogic::createFromBoolean($this->getValue() !== '');
416: }
417:
418: public function isNonFalsyString(): TrinaryLogic
419: {
420: return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true));
421: }
422:
423: public function isLiteralString(): TrinaryLogic
424: {
425: return TrinaryLogic::createYes();
426: }
427:
428: public function isLowercaseString(): TrinaryLogic
429: {
430: return TrinaryLogic::createFromBoolean(strtolower($this->value) === $this->value);
431: }
432:
433: public function isUppercaseString(): TrinaryLogic
434: {
435: return TrinaryLogic::createFromBoolean(strtoupper($this->value) === $this->value);
436: }
437:
438: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
439: {
440: if ($offsetType->isInteger()->yes()) {
441: $strlen = strlen($this->value);
442: $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1);
443: return $strLenType->isSuperTypeOf($offsetType)->result;
444: }
445:
446: return parent::hasOffsetValueType($offsetType);
447: }
448:
449: public function getOffsetValueType(Type $offsetType): Type
450: {
451: if ($offsetType->isInteger()->yes()) {
452: $strlen = strlen($this->value);
453: $strLenType = IntegerRangeType::fromInterval(-$strlen, $strlen - 1);
454:
455: if ($offsetType instanceof ConstantIntegerType) {
456: if ($strLenType->isSuperTypeOf($offsetType)->yes()) {
457: return new self($this->value[$offsetType->getValue()]);
458: }
459:
460: return new ErrorType();
461: }
462:
463: $intersected = TypeCombinator::intersect($strLenType, $offsetType);
464: if ($intersected instanceof IntegerRangeType) {
465: $finiteTypes = $intersected->getFiniteTypes();
466: if ($finiteTypes === []) {
467: return parent::getOffsetValueType($offsetType);
468: }
469:
470: $chars = [];
471: foreach ($finiteTypes as $constantInteger) {
472: $chars[] = new self($this->value[$constantInteger->getValue()]);
473: }
474: if (!$strLenType->isSuperTypeOf($offsetType)->yes()) {
475: $chars[] = new self('');
476: }
477:
478: return TypeCombinator::union(...$chars);
479: }
480: }
481:
482: return parent::getOffsetValueType($offsetType);
483: }
484:
485: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
486: {
487: $valueStringType = $valueType->toString();
488: if ($valueStringType instanceof ErrorType) {
489: return new ErrorType();
490: }
491: if (
492: $offsetType instanceof ConstantIntegerType
493: && $valueStringType instanceof ConstantStringType
494: ) {
495: $value = $this->value;
496: $offsetValue = $offsetType->getValue();
497: if ($offsetValue < 0) {
498: return new ErrorType();
499: }
500: $stringValue = $valueStringType->getValue();
501: if (strlen($stringValue) !== 1) {
502: return new ErrorType();
503: }
504: $value[$offsetValue] = $stringValue;
505:
506: return new self($value);
507: }
508:
509: return parent::setOffsetValueType($offsetType, $valueType);
510: }
511:
512: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
513: {
514: return parent::setOffsetValueType($offsetType, $valueType);
515: }
516:
517: public function append(self $otherString): self
518: {
519: return new self($this->getValue() . $otherString->getValue());
520: }
521:
522: public function generalize(GeneralizePrecision $precision): Type
523: {
524: if ($this->isClassString) {
525: if ($precision->isMoreSpecific()) {
526: return new ClassStringType();
527: }
528:
529: return new StringType();
530: }
531:
532: if ($this->getValue() !== '' && $precision->isMoreSpecific()) {
533: $accessories = [
534: new StringType(),
535: new AccessoryLiteralStringType(),
536: ];
537:
538: if (is_numeric($this->getValue())) {
539: $accessories[] = new AccessoryNumericStringType();
540: }
541:
542: if ($this->getValue() !== '0') {
543: $accessories[] = new AccessoryNonFalsyStringType();
544: } else {
545: $accessories[] = new AccessoryNonEmptyStringType();
546: }
547:
548: if (strtolower($this->getValue()) === $this->getValue()) {
549: $accessories[] = new AccessoryLowercaseStringType();
550: }
551:
552: if (strtoupper($this->getValue()) === $this->getValue()) {
553: $accessories[] = new AccessoryUppercaseStringType();
554: }
555:
556: return new IntersectionType($accessories);
557: }
558:
559: if ($precision->isMoreSpecific()) {
560: return new IntersectionType([
561: new StringType(),
562: new AccessoryLiteralStringType(),
563: ]);
564: }
565:
566: return new StringType();
567: }
568:
569: public function getSmallerType(PhpVersion $phpVersion): Type
570: {
571: $subtractedTypes = [
572: new ConstantBooleanType(true),
573: IntegerRangeType::createAllGreaterThanOrEqualTo((float) $this->value),
574: ];
575:
576: if ($this->value === '') {
577: $subtractedTypes[] = new NullType();
578: $subtractedTypes[] = new StringType();
579: }
580:
581: if (!(bool) $this->value) {
582: $subtractedTypes[] = new ConstantBooleanType(false);
583: }
584:
585: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
586: }
587:
588: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
589: {
590: $subtractedTypes = [
591: IntegerRangeType::createAllGreaterThan((float) $this->value),
592: ];
593:
594: if (!(bool) $this->value) {
595: $subtractedTypes[] = new ConstantBooleanType(true);
596: }
597:
598: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
599: }
600:
601: public function getGreaterType(PhpVersion $phpVersion): Type
602: {
603: $subtractedTypes = [
604: new ConstantBooleanType(false),
605: IntegerRangeType::createAllSmallerThanOrEqualTo((float) $this->value),
606: ];
607:
608: if ((bool) $this->value) {
609: $subtractedTypes[] = new ConstantBooleanType(true);
610: }
611:
612: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
613: }
614:
615: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
616: {
617: $subtractedTypes = [
618: IntegerRangeType::createAllSmallerThan((float) $this->value),
619: ];
620:
621: if ((bool) $this->value) {
622: $subtractedTypes[] = new ConstantBooleanType(false);
623: }
624:
625: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
626: }
627:
628: public function canAccessConstants(): TrinaryLogic
629: {
630: return $this->isClassString();
631: }
632:
633: public function hasConstant(string $constantName): TrinaryLogic
634: {
635: return $this->getObjectType()->hasConstant($constantName);
636: }
637:
638: public function getConstant(string $constantName): ClassConstantReflection
639: {
640: return $this->getObjectType()->getConstant($constantName);
641: }
642:
643: private function getObjectType(): ObjectType
644: {
645: return $this->objectType ??= new ObjectType($this->value);
646: }
647:
648: public function toPhpDocNode(): TypeNode
649: {
650: if (substr_count($this->value, "\n") > 0) {
651: return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode();
652: }
653:
654: return new ConstTypeNode(new ConstExprStringNode($this->value, ConstExprStringNode::SINGLE_QUOTED));
655: }
656:
657: }
658: