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