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