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