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