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:
31: private int $type;
32:
33: /** @var mixed[] */
34: private array $properties;
35:
36: /** @var list<string> */
37: private array $tips = [];
38:
39: private function __construct(string $message)
40: {
41: $this->properties['message'] = $message;
42: $this->type = self::TYPE_MESSAGE;
43: }
44:
45: /**
46: * @return array<int, array{string, array<array{string|null, string|null, string|null}>}>
47: */
48: public static function getRuleErrorTypes(): array
49: {
50: return [
51: self::TYPE_MESSAGE => [
52: RuleError::class,
53: [
54: [
55: 'message', // property name
56: 'string', // native type
57: 'string', // PHPDoc type
58: ],
59: ],
60: ],
61: self::TYPE_LINE => [
62: LineRuleError::class,
63: [
64: [
65: 'line',
66: 'int',
67: 'int',
68: ],
69: ],
70: ],
71: self::TYPE_FILE => [
72: FileRuleError::class,
73: [
74: [
75: 'file',
76: 'string',
77: 'string',
78: ],
79: [
80: 'fileDescription',
81: 'string',
82: 'string',
83: ],
84: ],
85: ],
86: self::TYPE_TIP => [
87: TipRuleError::class,
88: [
89: [
90: 'tip',
91: 'string',
92: 'string',
93: ],
94: ],
95: ],
96: self::TYPE_IDENTIFIER => [
97: IdentifierRuleError::class,
98: [
99: [
100: 'identifier',
101: 'string',
102: 'string',
103: ],
104: ],
105: ],
106: self::TYPE_METADATA => [
107: MetadataRuleError::class,
108: [
109: [
110: 'metadata',
111: 'array',
112: 'mixed[]',
113: ],
114: ],
115: ],
116: self::TYPE_NON_IGNORABLE => [
117: NonIgnorableRuleError::class,
118: [],
119: ],
120: self::TYPE_FIXABLE_NODE => [
121: FixableNodeRuleError::class,
122: [
123: [
124: 'originalNode',
125: '\PhpParser\Node',
126: '\PhpParser\Node',
127: ],
128: [
129: 'newNodeCallable',
130: null,
131: 'callable(\PhpParser\Node): \PhpParser\Node',
132: ],
133: ],
134: ],
135: ];
136: }
137:
138: /**
139: * @return self<RuleError>
140: */
141: public static function message(string $message): self
142: {
143: return new self($message);
144: }
145:
146: /**
147: * @phpstan-this-out self<T&LineRuleError>
148: * @return self<T&LineRuleError>
149: */
150: public function line(int $line): self
151: {
152: $this->properties['line'] = $line;
153: $this->type |= self::TYPE_LINE;
154:
155: return $this;
156: }
157:
158: /**
159: * @phpstan-this-out self<T&FileRuleError>
160: * @return self<T&FileRuleError>
161: */
162: public function file(string $file, ?string $fileDescription = null): self
163: {
164: if (!is_file($file)) {
165: throw new ShouldNotHappenException(sprintf('File %s does not exist.', $file));
166: }
167: $this->properties['file'] = $file;
168: $this->properties['fileDescription'] = $fileDescription ?? $file;
169: $this->type |= self::TYPE_FILE;
170:
171: return $this;
172: }
173:
174: /**
175: * @phpstan-this-out self<T&TipRuleError>
176: * @return self<T&TipRuleError>
177: */
178: public function tip(string $tip): self
179: {
180: $this->tips = [$tip];
181: $this->type |= self::TYPE_TIP;
182:
183: return $this;
184: }
185:
186: /**
187: * @phpstan-this-out self<T&TipRuleError>
188: * @return self<T&TipRuleError>
189: */
190: public function addTip(string $tip): self
191: {
192: $this->tips[] = $tip;
193: $this->type |= self::TYPE_TIP;
194:
195: return $this;
196: }
197:
198: /**
199: * @phpstan-this-out self<T&TipRuleError>
200: * @return self<T&TipRuleError>
201: */
202: public function discoveringSymbolsTip(): self
203: {
204: return $this->tip('Learn more at https://phpstan.org/user-guide/discovering-symbols');
205: }
206:
207: /**
208: * @param list<string> $reasons
209: * @phpstan-this-out self<T&TipRuleError>
210: * @return self<T&TipRuleError>
211: */
212: public function acceptsReasonsTip(array $reasons): self
213: {
214: foreach ($reasons as $reason) {
215: $this->addTip($reason);
216: }
217:
218: return $this;
219: }
220:
221: /**
222: * @phpstan-this-out self<T&TipRuleError>
223: * @return self<T&TipRuleError>
224: */
225: public function treatPhpDocTypesAsCertainTip(): self
226: {
227: 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%</>.');
228: }
229:
230: /**
231: * Sets an error identifier.
232: *
233: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
234: *
235: * @phpstan-this-out self<T&IdentifierRuleError>
236: * @return self<T&IdentifierRuleError>
237: */
238: public function identifier(string $identifier): self
239: {
240: if (!Error::validateIdentifier($identifier)) {
241: throw new ShouldNotHappenException(sprintf('Invalid identifier: %s, error identifiers must match /%s/', $identifier, Error::PATTERN_IDENTIFIER));
242: }
243:
244: $this->properties['identifier'] = $identifier;
245: $this->type |= self::TYPE_IDENTIFIER;
246:
247: return $this;
248: }
249:
250: /**
251: * @param mixed[] $metadata
252: * @phpstan-this-out self<T&MetadataRuleError>
253: * @return self<T&MetadataRuleError>
254: */
255: public function metadata(array $metadata): self
256: {
257: $this->properties['metadata'] = $metadata;
258: $this->type |= self::TYPE_METADATA;
259:
260: return $this;
261: }
262:
263: /**
264: * @phpstan-this-out self<T&NonIgnorableRuleError>
265: * @return self<T&NonIgnorableRuleError>
266: */
267: public function nonIgnorable(): self
268: {
269: $this->type |= self::TYPE_NON_IGNORABLE;
270:
271: return $this;
272: }
273:
274: /**
275: * @internal Experimental
276: * @template TNode of Node
277: * @param TNode $node
278: * @param callable(TNode): Node $cb
279: * @phpstan-this-out self<T&FixableNodeRuleError>
280: * @return self<T&FixableNodeRuleError>
281: */
282: public function fixNode(Node $node, callable $cb): self
283: {
284: $this->properties['originalNode'] = $node;
285: $this->properties['newNodeCallable'] = $cb;
286: $this->type |= self::TYPE_FIXABLE_NODE;
287:
288: return $this;
289: }
290:
291: /**
292: * @return T
293: */
294: public function build(): RuleError
295: {
296: /** @var class-string<T> $className */
297: $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type);
298: if (!class_exists($className)) {
299: throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className));
300: }
301:
302: $ruleError = new $className();
303: foreach ($this->properties as $propertyName => $value) {
304: $ruleError->{$propertyName} = $value;
305: }
306:
307: if (count($this->tips) > 0) {
308: if (count($this->tips) === 1) {
309: $ruleError->tip = $this->tips[0];
310: } else {
311: $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips));
312: }
313: }
314:
315: return $ruleError;
316: }
317:
318: }
319: