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