1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Rules;
4:
5: use PHPStan\Analyser\Error;
6: use PHPStan\ShouldNotHappenException;
7: use function array_map;
8: use function class_exists;
9: use function count;
10: use function implode;
11: use function is_file;
12: use function sprintf;
13:
14: /**
15: * @api
16: * @final
17: * @template-covariant T of RuleError
18: */
19: class RuleErrorBuilder
20: {
21:
22: private const TYPE_MESSAGE = 1;
23: private const TYPE_LINE = 2;
24: private const TYPE_FILE = 4;
25: private const TYPE_TIP = 8;
26: private const TYPE_IDENTIFIER = 16;
27: private const TYPE_METADATA = 32;
28: private const TYPE_NON_IGNORABLE = 64;
29:
30: private int $type;
31:
32: /** @var mixed[] */
33: private array $properties;
34:
35: /** @var list<string> */
36: private array $tips = [];
37:
38: private function __construct(string $message)
39: {
40: $this->properties['message'] = $message;
41: $this->type = self::TYPE_MESSAGE;
42: }
43:
44: /**
45: * @return array<int, array{string, array<array{string|null, string|null, string|null}>}>
46: */
47: public static function getRuleErrorTypes(): array
48: {
49: return [
50: self::TYPE_MESSAGE => [
51: RuleError::class,
52: [
53: [
54: 'message',
55: 'string',
56: 'string',
57: ],
58: ],
59: ],
60: self::TYPE_LINE => [
61: LineRuleError::class,
62: [
63: [
64: 'line',
65: 'int',
66: 'int',
67: ],
68: ],
69: ],
70: self::TYPE_FILE => [
71: FileRuleError::class,
72: [
73: [
74: 'file',
75: 'string',
76: 'string',
77: ],
78: [
79: 'fileDescription',
80: 'string',
81: 'string',
82: ],
83: ],
84: ],
85: self::TYPE_TIP => [
86: TipRuleError::class,
87: [
88: [
89: 'tip',
90: 'string',
91: 'string',
92: ],
93: ],
94: ],
95: self::TYPE_IDENTIFIER => [
96: IdentifierRuleError::class,
97: [
98: [
99: 'identifier',
100: 'string',
101: 'string',
102: ],
103: ],
104: ],
105: self::TYPE_METADATA => [
106: MetadataRuleError::class,
107: [
108: [
109: 'metadata',
110: 'array',
111: 'mixed[]',
112: ],
113: ],
114: ],
115: self::TYPE_NON_IGNORABLE => [
116: NonIgnorableRuleError::class,
117: [],
118: ],
119: ];
120: }
121:
122: /**
123: * @return self<RuleError>
124: */
125: public static function message(string $message): self
126: {
127: return new self($message);
128: }
129:
130: /**
131: * @phpstan-this-out self<T&LineRuleError>
132: * @return self<T&LineRuleError>
133: */
134: public function line(int $line): self
135: {
136: $this->properties['line'] = $line;
137: $this->type |= self::TYPE_LINE;
138:
139: return $this;
140: }
141:
142: /**
143: * @phpstan-this-out self<T&FileRuleError>
144: * @return self<T&FileRuleError>
145: */
146: public function file(string $file, ?string $fileDescription = null): self
147: {
148: if (!is_file($file)) {
149: throw new ShouldNotHappenException(sprintf('File %s does not exist.', $file));
150: }
151: $this->properties['file'] = $file;
152: $this->properties['fileDescription'] = $fileDescription ?? $file;
153: $this->type |= self::TYPE_FILE;
154:
155: return $this;
156: }
157:
158: /**
159: * @phpstan-this-out self<T&TipRuleError>
160: * @return self<T&TipRuleError>
161: */
162: public function tip(string $tip): self
163: {
164: $this->tips = [$tip];
165: $this->type |= self::TYPE_TIP;
166:
167: return $this;
168: }
169:
170: /**
171: * @phpstan-this-out self<T&TipRuleError>
172: * @return self<T&TipRuleError>
173: */
174: public function addTip(string $tip): self
175: {
176: $this->tips[] = $tip;
177: $this->type |= self::TYPE_TIP;
178:
179: return $this;
180: }
181:
182: /**
183: * @phpstan-this-out self<T&TipRuleError>
184: * @return self<T&TipRuleError>
185: */
186: public function discoveringSymbolsTip(): self
187: {
188: return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols');
189: }
190:
191: /**
192: * @param list<string> $reasons
193: * @phpstan-this-out self<T&TipRuleError>
194: * @return self<T&TipRuleError>
195: */
196: public function acceptsReasonsTip(array $reasons): self
197: {
198: foreach ($reasons as $reason) {
199: $this->addTip($reason);
200: }
201:
202: return $this;
203: }
204:
205: /**
206: * @phpstan-this-out self<T&TipRuleError>
207: * @return self<T&TipRuleError>
208: */
209: public function treatPhpDocTypesAsCertainTip(): self
210: {
211: return $this->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.');
212: }
213:
214: /**
215: * Sets an error identifier.
216: *
217: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
218: *
219: * @phpstan-this-out self<T&IdentifierRuleError>
220: * @return self<T&IdentifierRuleError>
221: */
222: public function identifier(string $identifier): self
223: {
224: if (!Error::validateIdentifier($identifier)) {
225: throw new ShouldNotHappenException(sprintf('Invalid identifier: %s, error identifiers must match /%s/', $identifier, Error::PATTERN_IDENTIFIER));
226: }
227:
228: $this->properties['identifier'] = $identifier;
229: $this->type |= self::TYPE_IDENTIFIER;
230:
231: return $this;
232: }
233:
234: /**
235: * @param mixed[] $metadata
236: * @phpstan-this-out self<T&MetadataRuleError>
237: * @return self<T&MetadataRuleError>
238: */
239: public function metadata(array $metadata): self
240: {
241: $this->properties['metadata'] = $metadata;
242: $this->type |= self::TYPE_METADATA;
243:
244: return $this;
245: }
246:
247: /**
248: * @phpstan-this-out self<T&NonIgnorableRuleError>
249: * @return self<T&NonIgnorableRuleError>
250: */
251: public function nonIgnorable(): self
252: {
253: $this->type |= self::TYPE_NON_IGNORABLE;
254:
255: return $this;
256: }
257:
258: /**
259: * @return T
260: */
261: public function build(): RuleError
262: {
263: /** @var class-string<T> $className */
264: $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type);
265: if (!class_exists($className)) {
266: throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className));
267: }
268:
269: $ruleError = new $className();
270: foreach ($this->properties as $propertyName => $value) {
271: $ruleError->{$propertyName} = $value;
272: }
273:
274: if (count($this->tips) > 0) {
275: if (count($this->tips) === 1) {
276: $ruleError->tip = $this->tips[0];
277: } else {
278: $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips));
279: }
280: }
281:
282: return $ruleError;
283: }
284:
285: }
286: