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: 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: * Sets an error identifier.
206: *
207: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
208: *
209: * @phpstan-this-out self<T&IdentifierRuleError>
210: * @return self<T&IdentifierRuleError>
211: */
212: public function identifier(string $identifier): self
213: {
214: if (!Error::validateIdentifier($identifier)) {
215: throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $identifier));
216: }
217:
218: $this->properties['identifier'] = $identifier;
219: $this->type |= self::TYPE_IDENTIFIER;
220:
221: return $this;
222: }
223:
224: /**
225: * @param mixed[] $metadata
226: * @phpstan-this-out self<T&MetadataRuleError>
227: * @return self<T&MetadataRuleError>
228: */
229: public function metadata(array $metadata): self
230: {
231: $this->properties['metadata'] = $metadata;
232: $this->type |= self::TYPE_METADATA;
233:
234: return $this;
235: }
236:
237: /**
238: * @phpstan-this-out self<T&NonIgnorableRuleError>
239: * @return self<T&NonIgnorableRuleError>
240: */
241: public function nonIgnorable(): self
242: {
243: $this->type |= self::TYPE_NON_IGNORABLE;
244:
245: return $this;
246: }
247:
248: /**
249: * @return T
250: */
251: public function build(): RuleError
252: {
253: /** @var class-string<T> $className */
254: $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type);
255: if (!class_exists($className)) {
256: throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className));
257: }
258:
259: $ruleError = new $className();
260: foreach ($this->properties as $propertyName => $value) {
261: $ruleError->{$propertyName} = $value;
262: }
263:
264: if (count($this->tips) > 0) {
265: if (count($this->tips) === 1) {
266: $ruleError->tip = $this->tips[0];
267: } else {
268: $ruleError->tip = implode("\n", array_map(static fn (string $tip) => sprintf('• %s', $tip), $this->tips));
269: }
270: }
271:
272: return $ruleError;
273: }
274:
275: }
276: