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