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