1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Rules;
4:
5: use PhpParser\Node;
6: use PHPStan\Analyser\Error;
7: use PHPStan\ShouldNotHappenException;
8: use function array_map;
9: use function class_exists;
10: use function count;
11: use function implode;
12: use function is_file;
13: use function sprintf;
14:
15: /**
16: * @api
17: * @template-covariant T of RuleError
18: */
19: final 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: private const TYPE_FIXABLE_NODE = 128;
30: private const TYPE_FILE_DEPENDENCIES = 256;
31:
32: private int $type;
33:
34: /** @var mixed[] */
35: private array $properties;
36:
37: /** @var list<string> */
38: private array $tips = [];
39:
40: private function __construct(string $message)
41: {
42: $this->properties['message'] = $message;
43: $this->type = self::TYPE_MESSAGE;
44: }
45:
46: /**
47: * @return array<int, array{string, array<array{string, string|null, string}>}>
48: */
49: public static function getRuleErrorTypes(): array
50: {
51: return [
52: self::TYPE_MESSAGE => [
53: RuleError::class,
54: [
55: [
56: 'message', // property name
57: 'string', // native type
58: 'string', // PHPDoc type
59: ],
60: ],
61: ],
62: self::TYPE_LINE => [
63: LineRuleError::class,
64: [
65: [
66: 'line',
67: 'int',
68: 'int',
69: ],
70: ],
71: ],
72: self::TYPE_FILE => [
73: FileRuleError::class,
74: [
75: [
76: 'file',
77: 'string',
78: 'string',
79: ],
80: [
81: 'fileDescription',
82: 'string',
83: 'string',
84: ],
85: ],
86: ],
87: self::TYPE_TIP => [
88: TipRuleError::class,
89: [
90: [
91: 'tip',
92: 'string',
93: 'string',
94: ],
95: ],
96: ],
97: self::TYPE_IDENTIFIER => [
98: IdentifierRuleError::class,
99: [
100: [
101: 'identifier',
102: 'string',
103: 'string',
104: ],
105: ],
106: ],
107: self::TYPE_METADATA => [
108: MetadataRuleError::class,
109: [
110: [
111: 'metadata',
112: 'array',
113: 'mixed[]',
114: ],
115: ],
116: ],
117: self::TYPE_NON_IGNORABLE => [
118: NonIgnorableRuleError::class,
119: [],
120: ],
121: self::TYPE_FIXABLE_NODE => [
122: FixableNodeRuleError::class,
123: [
124: [
125: 'originalNode',
126: '\PhpParser\Node',
127: '\PhpParser\Node',
128: ],
129: [
130: 'newNodeCallable',
131: null,
132: 'callable(\PhpParser\Node): \PhpParser\Node',
133: ],
134: ],
135: ],
136: self::TYPE_FILE_DEPENDENCIES => [
137: FileDependenciesRuleError::class,
138: [
139: [
140: 'fileDependencies',
141: 'array',
142: 'list<string>',
143: ],
144: ],
145: ],
146: ];
147: }
148:
149: /**
150: * @return self<RuleError>
151: */
152: public static function message(string $message): self
153: {
154: return new self($message);
155: }
156:
157: /**
158: * @phpstan-this-out self<T&LineRuleError>
159: * @return self<T&LineRuleError>
160: */
161: public function line(int $line): self
162: {
163: $this->properties['line'] = $line;
164: $this->type |= self::TYPE_LINE;
165:
166: return $this;
167: }
168:
169: /**
170: * @phpstan-this-out self<T&FileRuleError>
171: * @return self<T&FileRuleError>
172: */
173: public function file(string $file, ?string $fileDescription = null): self
174: {
175: if (!is_file($file)) {
176: throw new ShouldNotHappenException(sprintf('File %s does not exist.', $file));
177: }
178: $this->properties['file'] = $file;
179: $this->properties['fileDescription'] = $fileDescription ?? $file;
180: $this->type |= self::TYPE_FILE;
181:
182: return $this;
183: }
184:
185: /**
186: * Declares that this error depends on the contents of another file, so that the result cache
187: * re-analyses the file the error is reported in when that file appears, changes or is deleted.
188: * The path must be absolute; it does not have to exist.
189: *
190: * @deprecated Call trackFileDependency() on the Scope instead (Scope&DependencyTracker) - it declares
191: * the dependency whether the rule reports an error or not.
192: * @api
193: * @phpstan-this-out self<T&FileDependenciesRuleError>
194: * @return self<T&FileDependenciesRuleError>
195: */
196: public function fileDependency(string $file): self
197: {
198: /** @var list<string> $fileDependencies */
199: $fileDependencies = $this->properties['fileDependencies'] ?? [];
200: $fileDependencies[] = $file;
201: $this->properties['fileDependencies'] = $fileDependencies;
202: $this->type |= self::TYPE_FILE_DEPENDENCIES;
203:
204: return $this;
205: }
206:
207: /**
208: * @phpstan-this-out self<T&TipRuleError>
209: * @return self<T&TipRuleError>
210: */
211: public function tip(string $tip): self
212: {
213: $this->tips = [$tip];
214: $this->type |= self::TYPE_TIP;
215:
216: return $this;
217: }
218:
219: /**
220: * @phpstan-this-out self<T&TipRuleError>
221: * @return self<T&TipRuleError>
222: */
223: public function addTip(string $tip): self
224: {
225: $this->tips[] = $tip;
226: $this->type |= self::TYPE_TIP;
227:
228: return $this;
229: }
230:
231: /**
232: * @phpstan-this-out self<T&TipRuleError>
233: * @return self<T&TipRuleError>
234: */
235: public function discoveringSymbolsTip(): self
236: {
237: return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols');
238: }
239:
240: /**
241: * @param list<string> $reasons
242: * @phpstan-this-out self<T&TipRuleError>
243: * @return self<T&TipRuleError>
244: */
245: public function acceptsReasonsTip(array $reasons): self
246: {
247: foreach ($reasons as $reason) {
248: $this->addTip($reason);
249: }
250:
251: return $this;
252: }
253:
254: /**
255: * @phpstan-this-out self<T&TipRuleError>
256: * @return self<T&TipRuleError>
257: */
258: public function treatPhpDocTypesAsCertainTip(): self
259: {
260: 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%</>.');
261: }
262:
263: /**
264: * @param list<string> $callDescriptions
265: * @phpstan-this-out self<T&TipRuleError>
266: * @return self<T&TipRuleError>
267: */
268: public function possiblyImpureTip(array $callDescriptions): self
269: {
270: foreach ($callDescriptions as $callDescription) {
271: $this->addTip(sprintf('If %s is impure, add <fg=cyan>@phpstan-impure</> PHPDoc tag above its declaration. Learn more: <fg=cyan>https://phpstan.org/blog/remembering-and-forgetting-returned-values</>', $callDescription));
272: }
273:
274: return $this;
275: }
276:
277: /**
278: * Sets an error identifier.
279: *
280: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
281: *
282: * @phpstan-this-out self<T&IdentifierRuleError>
283: * @return self<T&IdentifierRuleError>
284: */
285: public function identifier(string $identifier): self
286: {
287: if (!Error::validateIdentifier($identifier)) {
288: throw new ShouldNotHappenException(sprintf('Invalid identifier: %s, error identifiers must match /%s/', $identifier, Error::PATTERN_IDENTIFIER));
289: }
290:
291: $this->properties['identifier'] = $identifier;
292: $this->type |= self::TYPE_IDENTIFIER;
293:
294: return $this;
295: }
296:
297: /**
298: * @param mixed[] $metadata
299: * @phpstan-this-out self<T&MetadataRuleError>
300: * @return self<T&MetadataRuleError>
301: */
302: public function metadata(array $metadata): self
303: {
304: $this->properties['metadata'] = $metadata;
305: $this->type |= self::TYPE_METADATA;
306:
307: return $this;
308: }
309:
310: /**
311: * @phpstan-this-out self<T&NonIgnorableRuleError>
312: * @return self<T&NonIgnorableRuleError>
313: */
314: public function nonIgnorable(): self
315: {
316: $this->type |= self::TYPE_NON_IGNORABLE;
317:
318: return $this;
319: }
320:
321: /**
322: * @internal Experimental
323: * @template TNode of Node
324: * @param TNode $node
325: * @param callable(TNode): Node $cb
326: * @phpstan-this-out self<T&FixableNodeRuleError>
327: * @return self<T&FixableNodeRuleError>
328: */
329: public function fixNode(Node $node, callable $cb): self
330: {
331: $this->properties['originalNode'] = $node;
332: $this->properties['newNodeCallable'] = $cb;
333: $this->type |= self::TYPE_FIXABLE_NODE;
334:
335: return $this;
336: }
337:
338: /**
339: * @return T
340: */
341: public function build(): RuleError
342: {
343: /** @var class-string<T> $className */
344: $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type);
345: if (!class_exists($className)) {
346: throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className));
347: }
348:
349: $ruleError = new $className();
350: foreach ($this->properties as $propertyName => $value) {
351: $ruleError->{$propertyName} = $value;
352: }
353:
354: if (count($this->tips) > 0) {
355: if (count($this->tips) === 1) {
356: $ruleError->tip = $this->tips[0];
357: } else {
358: $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips));
359: }
360: }
361:
362: return $ruleError;
363: }
364:
365: }
366: