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: /**
54: * @param self::* $value
55: */
56: private function __construct(private int $value)
57: {
58: }
59:
60: /**
61: * @param self::* $value
62: */
63: private static function create(int $value): self
64: {
65: self::$registry[$value] ??= new self($value);
66: return self::$registry[$value];
67: }
68:
69: /** @return self::* */
70: public function getLevelValue(): int
71: {
72: return $this->value;
73: }
74:
75: /** @api */
76: public static function typeOnly(): self
77: {
78: return self::create(self::TYPE_ONLY);
79: }
80:
81: /** @api */
82: public static function value(): self
83: {
84: return self::create(self::VALUE);
85: }
86:
87: /** @api */
88: public static function precise(): self
89: {
90: return self::create(self::PRECISE);
91: }
92:
93: /**
94: * Internal level for generating unique cache keys — not for user-facing messages.
95: *
96: * @api
97: */
98: public static function cache(): self
99: {
100: return self::create(self::CACHE);
101: }
102:
103: public function isTypeOnly(): bool
104: {
105: return $this->value === self::TYPE_ONLY;
106: }
107:
108: public function isValue(): bool
109: {
110: return $this->value === self::VALUE;
111: }
112:
113: public function isPrecise(): bool
114: {
115: return $this->value === self::PRECISE;
116: }
117:
118: public function isCache(): bool
119: {
120: return $this->value === self::CACHE;
121: }
122:
123: /**
124: * Chooses the minimum verbosity needed to distinguish the two types in error messages.
125: *
126: * @api
127: */
128: public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self
129: {
130: $moreVerbose = false;
131: $veryVerbose = false;
132: $moreVerboseCallback = static function (Type $type, callable $traverse) use (&$moreVerbose, &$veryVerbose): Type {
133: // stop deep traversal to not waste resources.
134: if ($veryVerbose) {
135: return $type;
136: }
137:
138: if ($type->isCallable()->yes()) {
139: $moreVerbose = true;
140:
141: // Keep checking if we need to be very verbose.
142: return $traverse($type);
143: }
144: if ($type->isConstantArray()->yes()) {
145: $moreVerbose = true;
146:
147: // For ConstantArrayType we need to keep checking if we need to be very verbose.
148: return $traverse($type);
149: }
150: if ($type->isConstantValue()->yes() && $type->isNull()->no()) {
151: $moreVerbose = true;
152: if (!$type->isArray()->no()) {
153: return $traverse($type);
154: }
155:
156: return $type;
157: }
158: if (
159: // synced with IntersectionType::describe()
160: $type instanceof AccessoryNonEmptyStringType
161: || $type instanceof AccessoryNonFalsyStringType
162: || $type instanceof AccessoryLiteralStringType
163: || $type instanceof AccessoryNumericStringType
164: || $type instanceof AccessoryDecimalIntegerStringType
165: || $type instanceof NonEmptyArrayType
166: || $type instanceof AccessoryArrayListType
167: ) {
168: $moreVerbose = true;
169: return $type;
170: }
171: if (
172: $type instanceof AccessoryLowercaseStringType
173: || $type instanceof AccessoryUppercaseStringType
174: ) {
175: $moreVerbose = true;
176: $veryVerbose = true;
177: return $type;
178: }
179: if ($type instanceof IntegerRangeType) {
180: $moreVerbose = true;
181: return $type;
182: }
183: return $traverse($type);
184: };
185:
186: TypeTraverser::map($acceptingType, $moreVerboseCallback);
187:
188: if ($veryVerbose) {
189: return self::precise();
190: }
191:
192: if ($moreVerbose) {
193: $verbosity = self::value();
194: }
195:
196: if ($acceptedType === null) {
197: return $verbosity ?? self::typeOnly();
198: }
199:
200: $containsInvariantTemplateType = false;
201: TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type {
202: // stop deep traversal to not waste resources.
203: if ($containsInvariantTemplateType) {
204: return $type;
205: }
206:
207: if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
208: $reflection = $type->getClassReflection();
209: if ($reflection !== null) {
210: $templateTypeMap = $reflection->getTemplateTypeMap();
211: foreach ($templateTypeMap->getTypes() as $templateType) {
212: if (!$templateType instanceof TemplateType) {
213: continue;
214: }
215:
216: if (!$templateType->getVariance()->invariant()) {
217: continue;
218: }
219:
220: $containsInvariantTemplateType = true;
221: return $type;
222: }
223: }
224: }
225:
226: return $traverse($type);
227: });
228:
229: if (!$containsInvariantTemplateType) {
230: return $verbosity ?? self::typeOnly();
231: }
232:
233: /** @var bool $moreVerbose */
234: $moreVerbose = false;
235: /** @var bool $veryVerbose */
236: $veryVerbose = false;
237: TypeTraverser::map($acceptedType, $moreVerboseCallback);
238:
239: if ($veryVerbose) {
240: return self::precise();
241: }
242:
243: return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly();
244: }
245:
246: /**
247: * @param callable(): string $typeOnlyCallback
248: * @param callable(): string $valueCallback
249: * @param callable(): string|null $preciseCallback
250: * @param callable(): string|null $cacheCallback
251: */
252: public function handle(
253: callable $typeOnlyCallback,
254: callable $valueCallback,
255: ?callable $preciseCallback = null,
256: ?callable $cacheCallback = null,
257: ): string
258: {
259: if ($this->value === self::TYPE_ONLY) {
260: return $typeOnlyCallback();
261: }
262:
263: if ($this->value === self::VALUE) {
264: return $valueCallback();
265: }
266:
267: if ($this->value === self::PRECISE) {
268: if ($preciseCallback !== null) {
269: return $preciseCallback();
270: }
271:
272: return $valueCallback();
273: }
274:
275: if ($cacheCallback !== null) {
276: return $cacheCallback();
277: }
278:
279: if ($preciseCallback !== null) {
280: return $preciseCallback();
281: }
282:
283: return $valueCallback();
284: }
285:
286: }
287: