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