1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Turbo\ShadowedByTurboExtension;
6: use PHPStan\Type\Accessory\AccessoryArrayListType;
7: use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
8: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
9: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
10: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
11: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
12: use PHPStan\Type\Accessory\AccessoryNumericStringType;
13: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
14: use PHPStan\Type\Accessory\NonEmptyArrayType;
15: use PHPStan\Type\Generic\GenericObjectType;
16: use PHPStan\Type\Generic\GenericStaticType;
17: use PHPStan\Type\Generic\TemplateType;
18:
19: /**
20: * Controls the verbosity of type descriptions in error messages.
21: *
22: * When PHPStan describes a type for an error message, it uses VerbosityLevel to
23: * decide how much detail to include. Higher levels include more detail like constant
24: * values and array shapes.
25: *
26: * The four levels (from least to most verbose):
27: * - **typeOnly**: Just the type name, e.g. "string", "array", "Foo"
28: * - **value**: Includes constant values, e.g. "'hello'", "array{foo: int}", "non-empty-string"
29: * - **precise**: Maximum detail — adds subtracted types on object/mixed (e.g. "object~Bar"),
30: * lowercase/uppercase string distinctions, untruncated array shapes, and template type scope
31: * - **cache**: Internal level used for generating cache keys
32: *
33: * Used as a parameter to Type::describe() to control output detail:
34: *
35: * ```php
36: * $type->describe(VerbosityLevel::typeOnly()) // "string"
37: * $type->describe(VerbosityLevel::value()) // "'hello'"
38: * $type->describe(VerbosityLevel::precise()) // "non-empty-lowercase-string"
39: * ```
40: *
41: * The getRecommendedLevelByType() factory method automatically chooses the right level
42: * for error messages based on what types are involved — it picks the minimum verbosity
43: * needed to distinguish the accepting type from the accepted type.
44: */
45: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/VerbosityLevel.cpp')]
46: final class VerbosityLevel
47: {
48:
49: private const TYPE_ONLY = 1;
50: private const VALUE = 2;
51: private const PRECISE = 3;
52: private const CACHE = 4;
53:
54: /** @var self[] */
55: private static array $registry;
56:
57: private static self $TYPE_ONLY;
58:
59: private static self $VALUE;
60:
61: private static self $PRECISE;
62:
63: private static self $CACHE;
64:
65: /**
66: * @param self::* $value
67: */
68: private function __construct(private int $value)
69: {
70: }
71:
72: /** @return self::* */
73: public function getLevelValue(): int
74: {
75: return $this->value;
76: }
77:
78: /** @api */
79: public static function typeOnly(): self
80: {
81: return self::$TYPE_ONLY ??= (self::$registry[self::TYPE_ONLY] ??= new self(self::TYPE_ONLY));
82: }
83:
84: /** @api */
85: public static function value(): self
86: {
87: return self::$VALUE ??= (self::$registry[self::VALUE] ??= new self(self::VALUE));
88: }
89:
90: /** @api */
91: public static function precise(): self
92: {
93: return self::$PRECISE ??= (self::$registry[self::PRECISE] ??= new self(self::PRECISE));
94: }
95:
96: /**
97: * Internal level for generating unique cache keys — not for user-facing messages.
98: *
99: * @api
100: */
101: public static function cache(): self
102: {
103: return self::$CACHE ??= (self::$registry[self::CACHE] ??= new self(self::CACHE));
104: }
105:
106: public function isTypeOnly(): bool
107: {
108: return $this->value === self::TYPE_ONLY;
109: }
110:
111: public function isValue(): bool
112: {
113: return $this->value === self::VALUE;
114: }
115:
116: public function isPrecise(): bool
117: {
118: return $this->value === self::PRECISE;
119: }
120:
121: public function isCache(): bool
122: {
123: return $this->value === self::CACHE;
124: }
125:
126: /**
127: * Chooses the minimum verbosity needed to distinguish the two types in error messages.
128: *
129: * @api
130: */
131: public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self
132: {
133: $moreVerbose = false;
134: $veryVerbose = false;
135: $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type {
136: // stop deep traversal to not waste resources.
137: if ($veryVerbose) {
138: return $type;
139: }
140:
141: if ($type->isCallable()->yes()) {
142: $moreVerbose = true;
143:
144: if ($type instanceof ClosureType && !$type->isStaticClosure()->maybe()) {
145: $veryVerbose = true;
146: return $type;
147: }
148:
149: // Keep checking if we need to be very verbose.
150: return $traverse($type);
151: }
152: if ($type->isConstantArray()->yes()) {
153: $moreVerbose = true;
154:
155: // For ConstantArrayType we need to keep checking if we need to be very verbose.
156: return $traverse($type);
157: }
158: if ($type->isConstantValue()->yes() && $type->isNull()->no()) {
159: $moreVerbose = true;
160: if (!$type->isArray()->no()) {
161: return $traverse($type);
162: }
163:
164: return $type;
165: }
166: if (
167: // synced with IntersectionType::describe()
168: $type instanceof AccessoryNonEmptyStringType
169: || $type instanceof AccessoryNonFalsyStringType
170: || $type instanceof AccessoryLiteralStringType
171: || $type instanceof AccessoryNumericStringType
172: || $type instanceof AccessoryDecimalIntegerStringType
173: || $type instanceof NonEmptyArrayType
174: || $type instanceof AccessoryArrayListType
175: ) {
176: $moreVerbose = true;
177: return $type;
178: }
179: if (
180: $type instanceof AccessoryLowercaseStringType
181: || $type instanceof AccessoryUppercaseStringType
182: ) {
183: $moreVerbose = true;
184: $veryVerbose = true;
185: return $type;
186: }
187: if ($type instanceof IntegerRangeType) {
188: $moreVerbose = true;
189: return $type;
190: }
191: return $traverse($type);
192: };
193:
194: TypeTraverser::map($acceptingType, $moreVerboseCallback);
195:
196: if ($veryVerbose) {
197: return self::precise();
198: }
199:
200: if ($moreVerbose) {
201: $verbosity = self::value();
202: }
203:
204: if ($acceptedType === null) {
205: return $verbosity ?? self::typeOnly();
206: }
207:
208: $containsInvariantTemplateType = false;
209: TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type {
210: // stop deep traversal to not waste resources.
211: if ($containsInvariantTemplateType) {
212: return $type;
213: }
214:
215: if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
216: $reflection = $type->getClassReflection();
217: if ($reflection !== null) {
218: $templateTypeMap = $reflection->getTemplateTypeMap();
219: foreach ($templateTypeMap->getTypes() as $templateType) {
220: if (!$templateType instanceof TemplateType) {
221: continue;
222: }
223:
224: if (!$templateType->getVariance()->invariant()) {
225: continue;
226: }
227:
228: $containsInvariantTemplateType = true;
229: return $type;
230: }
231: }
232: }
233:
234: return $traverse($type);
235: });
236:
237: if (!$containsInvariantTemplateType) {
238: return $verbosity ?? self::typeOnly();
239: }
240:
241: /** @var bool $moreVerbose */
242: $moreVerbose = false;
243: /** @var bool $veryVerbose */
244: $veryVerbose = false;
245: TypeTraverser::map($acceptedType, $moreVerboseCallback);
246:
247: if ($veryVerbose) {
248: return self::precise();
249: }
250:
251: return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly();
252: }
253:
254: /**
255: * @param callable(): string $typeOnlyCallback
256: * @param callable(): string $valueCallback
257: * @param callable(): string|null $preciseCallback
258: * @param callable(): string|null $cacheCallback
259: */
260: public function handle(
261: callable $typeOnlyCallback,
262: callable $valueCallback,
263: ?callable $preciseCallback = null,
264: ?callable $cacheCallback = null,
265: ): string
266: {
267: if ($this->value === self::TYPE_ONLY) {
268: return $typeOnlyCallback();
269: }
270:
271: if ($this->value === self::VALUE) {
272: return $valueCallback();
273: }
274:
275: if ($this->value === self::PRECISE) {
276: if ($preciseCallback !== null) {
277: return $preciseCallback();
278: }
279:
280: return $valueCallback();
281: }
282:
283: if ($cacheCallback !== null) {
284: return $cacheCallback();
285: }
286:
287: if ($preciseCallback !== null) {
288: return $preciseCallback();
289: }
290:
291: return $valueCallback();
292: }
293:
294: }
295: