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