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