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: if ($type instanceof ClosureType && !$type->isStaticClosure()->maybe()) {
142: $veryVerbose = true;
143: return $type;
144: }
145:
146: // Keep checking if we need to be very verbose.
147: return $traverse($type);
148: }
149: if ($type->isConstantArray()->yes()) {
150: $moreVerbose = true;
151:
152: // For ConstantArrayType we need to keep checking if we need to be very verbose.
153: return $traverse($type);
154: }
155: if ($type->isConstantValue()->yes() && $type->isNull()->no()) {
156: $moreVerbose = true;
157: if (!$type->isArray()->no()) {
158: return $traverse($type);
159: }
160:
161: return $type;
162: }
163: if (
164: // synced with IntersectionType::describe()
165: $type instanceof AccessoryNonEmptyStringType
166: || $type instanceof AccessoryNonFalsyStringType
167: || $type instanceof AccessoryLiteralStringType
168: || $type instanceof AccessoryNumericStringType
169: || $type instanceof AccessoryDecimalIntegerStringType
170: || $type instanceof NonEmptyArrayType
171: || $type instanceof AccessoryArrayListType
172: ) {
173: $moreVerbose = true;
174: return $type;
175: }
176: if (
177: $type instanceof AccessoryLowercaseStringType
178: || $type instanceof AccessoryUppercaseStringType
179: ) {
180: $moreVerbose = true;
181: $veryVerbose = true;
182: return $type;
183: }
184: if ($type instanceof IntegerRangeType) {
185: $moreVerbose = true;
186: return $type;
187: }
188: return $traverse($type);
189: };
190:
191: TypeTraverser::map($acceptingType, $moreVerboseCallback);
192:
193: if ($veryVerbose) {
194: return self::precise();
195: }
196:
197: if ($moreVerbose) {
198: $verbosity = self::value();
199: }
200:
201: if ($acceptedType === null) {
202: return $verbosity ?? self::typeOnly();
203: }
204:
205: $containsInvariantTemplateType = false;
206: TypeTraverser::map($acceptingType, static function (Type $type, callable $traverse) use (&$containsInvariantTemplateType): Type {
207: // stop deep traversal to not waste resources.
208: if ($containsInvariantTemplateType) {
209: return $type;
210: }
211:
212: if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
213: $reflection = $type->getClassReflection();
214: if ($reflection !== null) {
215: $templateTypeMap = $reflection->getTemplateTypeMap();
216: foreach ($templateTypeMap->getTypes() as $templateType) {
217: if (!$templateType instanceof TemplateType) {
218: continue;
219: }
220:
221: if (!$templateType->getVariance()->invariant()) {
222: continue;
223: }
224:
225: $containsInvariantTemplateType = true;
226: return $type;
227: }
228: }
229: }
230:
231: return $traverse($type);
232: });
233:
234: if (!$containsInvariantTemplateType) {
235: return $verbosity ?? self::typeOnly();
236: }
237:
238: /** @var bool $moreVerbose */
239: $moreVerbose = false;
240: /** @var bool $veryVerbose */
241: $veryVerbose = false;
242: TypeTraverser::map($acceptedType, $moreVerboseCallback);
243:
244: if ($veryVerbose) {
245: return self::precise();
246: }
247:
248: return $moreVerbose ? self::value() : $verbosity ?? self::typeOnly();
249: }
250:
251: /**
252: * @param callable(): string $typeOnlyCallback
253: * @param callable(): string $valueCallback
254: * @param callable(): string|null $preciseCallback
255: * @param callable(): string|null $cacheCallback
256: */
257: public function handle(
258: callable $typeOnlyCallback,
259: callable $valueCallback,
260: ?callable $preciseCallback = null,
261: ?callable $cacheCallback = null,
262: ): string
263: {
264: if ($this->value === self::TYPE_ONLY) {
265: return $typeOnlyCallback();
266: }
267:
268: if ($this->value === self::VALUE) {
269: return $valueCallback();
270: }
271:
272: if ($this->value === self::PRECISE) {
273: if ($preciseCallback !== null) {
274: return $preciseCallback();
275: }
276:
277: return $valueCallback();
278: }
279:
280: if ($cacheCallback !== null) {
281: return $cacheCallback();
282: }
283:
284: if ($preciseCallback !== null) {
285: return $preciseCallback();
286: }
287:
288: return $valueCallback();
289: }
290:
291: }
292: