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