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 toAbsoluteNumber(): Type
296: {
297: return $this->toNumber()->toAbsoluteNumber();
298: }
299:
300: public function toInteger(): Type
301: {
302: return new ConstantIntegerType((int) $this->value);
303: }
304:
305: public function toFloat(): Type
306: {
307: return new ConstantFloatType((float) $this->value);
308: }
309:
310: public function toArrayKey(): Type
311: {
312: if ($this->arrayKeyType !== null) {
313: return $this->arrayKeyType;
314: }
315:
316: /** @var int|string $offsetValue */
317: $offsetValue = key([$this->value => null]);
318: return $this->arrayKeyType = is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue);
319: }
320:
321: public function isString(): TrinaryLogic
322: {
323: return TrinaryLogic::createYes();
324: }
325:
326: public function isNumericString(): TrinaryLogic
327: {
328: return TrinaryLogic::createFromBoolean(is_numeric($this->getValue()));
329: }
330:
331: public function isNonEmptyString(): TrinaryLogic
332: {
333: return TrinaryLogic::createFromBoolean($this->getValue() !== '');
334: }
335:
336: public function isNonFalsyString(): TrinaryLogic
337: {
338: return TrinaryLogic::createFromBoolean(!in_array($this->getValue(), ['', '0'], true));
339: }
340:
341: public function isLiteralString(): TrinaryLogic
342: {
343: return TrinaryLogic::createYes();
344: }
345:
346: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
347: {
348: if ($offsetType instanceof ConstantIntegerType) {
349: return TrinaryLogic::createFromBoolean(
350: $offsetType->getValue() < strlen($this->value),
351: );
352: }
353:
354: return parent::hasOffsetValueType($offsetType);
355: }
356:
357: public function getOffsetValueType(Type $offsetType): Type
358: {
359: if ($offsetType instanceof ConstantIntegerType) {
360: if ($offsetType->getValue() < strlen($this->value)) {
361: return new self($this->value[$offsetType->getValue()]);
362: }
363:
364: return new ErrorType();
365: }
366:
367: return parent::getOffsetValueType($offsetType);
368: }
369:
370: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
371: {
372: $valueStringType = $valueType->toString();
373: if ($valueStringType instanceof ErrorType) {
374: return new ErrorType();
375: }
376: if (
377: $offsetType instanceof ConstantIntegerType
378: && $valueStringType instanceof ConstantStringType
379: ) {
380: $value = $this->value;
381: $offsetValue = $offsetType->getValue();
382: if ($offsetValue < 0) {
383: return new ErrorType();
384: }
385: $stringValue = $valueStringType->getValue();
386: if (strlen($stringValue) !== 1) {
387: return new ErrorType();
388: }
389: $value[$offsetValue] = $stringValue;
390:
391: return new self($value);
392: }
393:
394: return parent::setOffsetValueType($offsetType, $valueType);
395: }
396:
397: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
398: {
399: return parent::setOffsetValueType($offsetType, $valueType);
400: }
401:
402: public function append(self $otherString): self
403: {
404: return new self($this->getValue() . $otherString->getValue());
405: }
406:
407: public function generalize(GeneralizePrecision $precision): Type
408: {
409: if ($this->isClassString) {
410: if ($precision->isMoreSpecific()) {
411: return new ClassStringType();
412: }
413:
414: return new StringType();
415: }
416:
417: if ($this->getValue() !== '' && $precision->isMoreSpecific()) {
418: $accessories = [
419: new StringType(),
420: new AccessoryLiteralStringType(),
421: ];
422:
423: if (is_numeric($this->getValue())) {
424: $accessories[] = new AccessoryNumericStringType();
425: }
426:
427: if ($this->getValue() !== '0') {
428: $accessories[] = new AccessoryNonFalsyStringType();
429: } else {
430: $accessories[] = new AccessoryNonEmptyStringType();
431: }
432:
433: return new IntersectionType($accessories);
434: }
435:
436: if ($precision->isMoreSpecific()) {
437: return new IntersectionType([
438: new StringType(),
439: new AccessoryLiteralStringType(),
440: ]);
441: }
442:
443: return new StringType();
444: }
445:
446: public function getSmallerType(): Type
447: {
448: $subtractedTypes = [
449: new ConstantBooleanType(true),
450: IntegerRangeType::createAllGreaterThanOrEqualTo((float) $this->value),
451: ];
452:
453: if ($this->value === '') {
454: $subtractedTypes[] = new NullType();
455: $subtractedTypes[] = new StringType();
456: }
457:
458: if (!(bool) $this->value) {
459: $subtractedTypes[] = new ConstantBooleanType(false);
460: }
461:
462: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
463: }
464:
465: public function getSmallerOrEqualType(): Type
466: {
467: $subtractedTypes = [
468: IntegerRangeType::createAllGreaterThan((float) $this->value),
469: ];
470:
471: if (!(bool) $this->value) {
472: $subtractedTypes[] = new ConstantBooleanType(true);
473: }
474:
475: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
476: }
477:
478: public function getGreaterType(): Type
479: {
480: $subtractedTypes = [
481: new ConstantBooleanType(false),
482: IntegerRangeType::createAllSmallerThanOrEqualTo((float) $this->value),
483: ];
484:
485: if ((bool) $this->value) {
486: $subtractedTypes[] = new ConstantBooleanType(true);
487: }
488:
489: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
490: }
491:
492: public function getGreaterOrEqualType(): Type
493: {
494: $subtractedTypes = [
495: IntegerRangeType::createAllSmallerThan((float) $this->value),
496: ];
497:
498: if ((bool) $this->value) {
499: $subtractedTypes[] = new ConstantBooleanType(false);
500: }
501:
502: return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
503: }
504:
505: public function canAccessConstants(): TrinaryLogic
506: {
507: return $this->isClassStringType();
508: }
509:
510: public function hasConstant(string $constantName): TrinaryLogic
511: {
512: return $this->getObjectType()->hasConstant($constantName);
513: }
514:
515: public function getConstant(string $constantName): ConstantReflection
516: {
517: return $this->getObjectType()->getConstant($constantName);
518: }
519:
520: private function getObjectType(): ObjectType
521: {
522: return $this->objectType ??= new ObjectType($this->value);
523: }
524:
525: public function toPhpDocNode(): TypeNode
526: {
527: if (substr_count($this->value, "\n") > 0) {
528: return $this->generalize(GeneralizePrecision::moreSpecific())->toPhpDocNode();
529: }
530:
531: return new ConstTypeNode(new QuoteAwareConstExprStringNode($this->value, QuoteAwareConstExprStringNode::SINGLE_QUOTED));
532: }
533:
534: /**
535: * @param mixed[] $properties
536: */
537: public static function __set_state(array $properties): Type
538: {
539: return new self($properties['value'], $properties['isClassString'] ?? false);
540: }
541:
542: }
543: