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