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