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