1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\Analyser; |
4: | |
5: | use Exception; |
6: | use JsonSerializable; |
7: | use Nette\Utils\Strings; |
8: | use Override; |
9: | use PhpParser\Node; |
10: | use PHPStan\ShouldNotHappenException; |
11: | use ReturnTypeWillChange; |
12: | use Throwable; |
13: | use function is_bool; |
14: | use function sprintf; |
15: | |
16: | |
17: | |
18: | |
19: | final class Error implements JsonSerializable |
20: | { |
21: | |
22: | public const PATTERN_IDENTIFIER = '[a-zA-Z0-9](?:[a-zA-Z0-9\\.]*[a-zA-Z0-9])?'; |
23: | |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | public function __construct( |
31: | private string $message, |
32: | private string $file, |
33: | private ?int $line = null, |
34: | private bool|Throwable $canBeIgnored = true, |
35: | private ?string $filePath = null, |
36: | private ?string $traitFilePath = null, |
37: | private ?string $tip = null, |
38: | private ?int $nodeLine = null, |
39: | private ?string $nodeType = null, |
40: | private ?string $identifier = null, |
41: | private array $metadata = [], |
42: | private ?FixedErrorDiff $fixedErrorDiff = null, |
43: | ) |
44: | { |
45: | if ($this->identifier !== null && !self::validateIdentifier($this->identifier)) { |
46: | throw new ShouldNotHappenException(sprintf('Invalid identifier: %s', $this->identifier)); |
47: | } |
48: | } |
49: | |
50: | public function getMessage(): string |
51: | { |
52: | return $this->message; |
53: | } |
54: | |
55: | public function getFile(): string |
56: | { |
57: | return $this->file; |
58: | } |
59: | |
60: | public function getFilePath(): string |
61: | { |
62: | if ($this->filePath === null) { |
63: | return $this->file; |
64: | } |
65: | |
66: | return $this->filePath; |
67: | } |
68: | |
69: | public function changeFilePath(string $newFilePath): self |
70: | { |
71: | if ($this->traitFilePath !== null) { |
72: | throw new ShouldNotHappenException('Errors in traits not yet supported'); |
73: | } |
74: | |
75: | return new self( |
76: | $this->message, |
77: | $newFilePath, |
78: | $this->line, |
79: | $this->canBeIgnored, |
80: | $newFilePath, |
81: | null, |
82: | $this->tip, |
83: | $this->nodeLine, |
84: | $this->nodeType, |
85: | $this->identifier, |
86: | $this->metadata, |
87: | $this->fixedErrorDiff, |
88: | ); |
89: | } |
90: | |
91: | public function changeTraitFilePath(string $newFilePath): self |
92: | { |
93: | return new self( |
94: | $this->message, |
95: | $this->file, |
96: | $this->line, |
97: | $this->canBeIgnored, |
98: | $this->filePath, |
99: | $newFilePath, |
100: | $this->tip, |
101: | $this->nodeLine, |
102: | $this->nodeType, |
103: | $this->identifier, |
104: | $this->metadata, |
105: | $this->fixedErrorDiff, |
106: | ); |
107: | } |
108: | |
109: | public function getTraitFilePath(): ?string |
110: | { |
111: | return $this->traitFilePath; |
112: | } |
113: | |
114: | public function getLine(): ?int |
115: | { |
116: | return $this->line; |
117: | } |
118: | |
119: | public function canBeIgnored(): bool |
120: | { |
121: | return $this->canBeIgnored === true; |
122: | } |
123: | |
124: | public function hasNonIgnorableException(): bool |
125: | { |
126: | return $this->canBeIgnored instanceof Throwable; |
127: | } |
128: | |
129: | public function getTip(): ?string |
130: | { |
131: | return $this->tip; |
132: | } |
133: | |
134: | public function withoutTip(): self |
135: | { |
136: | if ($this->tip === null) { |
137: | return $this; |
138: | } |
139: | |
140: | return new self( |
141: | $this->message, |
142: | $this->file, |
143: | $this->line, |
144: | $this->canBeIgnored, |
145: | $this->filePath, |
146: | $this->traitFilePath, |
147: | null, |
148: | $this->nodeLine, |
149: | $this->nodeType, |
150: | $this->identifier, |
151: | $this->metadata, |
152: | $this->fixedErrorDiff, |
153: | ); |
154: | } |
155: | |
156: | public function doNotIgnore(): self |
157: | { |
158: | if (!$this->canBeIgnored()) { |
159: | return $this; |
160: | } |
161: | |
162: | return new self( |
163: | $this->message, |
164: | $this->file, |
165: | $this->line, |
166: | false, |
167: | $this->filePath, |
168: | $this->traitFilePath, |
169: | $this->tip, |
170: | $this->nodeLine, |
171: | $this->nodeType, |
172: | $this->identifier, |
173: | $this->metadata, |
174: | $this->fixedErrorDiff, |
175: | ); |
176: | } |
177: | |
178: | public function withIdentifier(string $identifier): self |
179: | { |
180: | if ($this->identifier !== null) { |
181: | throw new ShouldNotHappenException(sprintf('Error already has an identifier: %s', $this->identifier)); |
182: | } |
183: | |
184: | return new self( |
185: | $this->message, |
186: | $this->file, |
187: | $this->line, |
188: | $this->canBeIgnored, |
189: | $this->filePath, |
190: | $this->traitFilePath, |
191: | $this->tip, |
192: | $this->nodeLine, |
193: | $this->nodeType, |
194: | $identifier, |
195: | $this->metadata, |
196: | $this->fixedErrorDiff, |
197: | ); |
198: | } |
199: | |
200: | |
201: | |
202: | |
203: | public function withMetadata(array $metadata): self |
204: | { |
205: | if ($this->metadata !== []) { |
206: | throw new ShouldNotHappenException('Error already has metadata'); |
207: | } |
208: | |
209: | return new self( |
210: | $this->message, |
211: | $this->file, |
212: | $this->line, |
213: | $this->canBeIgnored, |
214: | $this->filePath, |
215: | $this->traitFilePath, |
216: | $this->tip, |
217: | $this->nodeLine, |
218: | $this->nodeType, |
219: | $this->identifier, |
220: | $metadata, |
221: | $this->fixedErrorDiff, |
222: | ); |
223: | } |
224: | |
225: | public function getNodeLine(): ?int |
226: | { |
227: | return $this->nodeLine; |
228: | } |
229: | |
230: | |
231: | |
232: | |
233: | public function getNodeType(): ?string |
234: | { |
235: | return $this->nodeType; |
236: | } |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | |
243: | public function getIdentifier(): ?string |
244: | { |
245: | return $this->identifier; |
246: | } |
247: | |
248: | |
249: | |
250: | |
251: | public function getMetadata(): array |
252: | { |
253: | return $this->metadata; |
254: | } |
255: | |
256: | |
257: | |
258: | |
259: | public function getFixedErrorDiff(): ?FixedErrorDiff |
260: | { |
261: | return $this->fixedErrorDiff; |
262: | } |
263: | |
264: | |
265: | |
266: | |
267: | #[ReturnTypeWillChange] |
268: | #[Override] |
269: | public function jsonSerialize() |
270: | { |
271: | $fixedErrorDiffHash = null; |
272: | $fixedErrorDiffDiff = null; |
273: | if ($this->fixedErrorDiff !== null) { |
274: | $fixedErrorDiffHash = $this->fixedErrorDiff->originalHash; |
275: | $fixedErrorDiffDiff = $this->fixedErrorDiff->diff; |
276: | } |
277: | |
278: | return [ |
279: | 'message' => $this->message, |
280: | 'file' => $this->file, |
281: | 'line' => $this->line, |
282: | 'canBeIgnored' => is_bool($this->canBeIgnored) ? $this->canBeIgnored : 'exception', |
283: | 'filePath' => $this->filePath, |
284: | 'traitFilePath' => $this->traitFilePath, |
285: | 'tip' => $this->tip, |
286: | 'nodeLine' => $this->nodeLine, |
287: | 'nodeType' => $this->nodeType, |
288: | 'identifier' => $this->identifier, |
289: | 'metadata' => $this->metadata, |
290: | 'fixedErrorDiffHash' => $fixedErrorDiffHash, |
291: | 'fixedErrorDiffDiff' => $fixedErrorDiffDiff, |
292: | ]; |
293: | } |
294: | |
295: | |
296: | |
297: | |
298: | public static function decode(array $json): self |
299: | { |
300: | $fixedErrorDiff = null; |
301: | if ($json['fixedErrorDiffHash'] !== null && $json['fixedErrorDiffDiff'] !== null) { |
302: | $fixedErrorDiff = new FixedErrorDiff($json['fixedErrorDiffHash'], $json['fixedErrorDiffDiff']); |
303: | } |
304: | |
305: | return new self( |
306: | $json['message'], |
307: | $json['file'], |
308: | $json['line'], |
309: | $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'], |
310: | $json['filePath'], |
311: | $json['traitFilePath'], |
312: | $json['tip'], |
313: | $json['nodeLine'] ?? null, |
314: | $json['nodeType'] ?? null, |
315: | $json['identifier'] ?? null, |
316: | $json['metadata'] ?? [], |
317: | $fixedErrorDiff, |
318: | ); |
319: | } |
320: | |
321: | |
322: | |
323: | |
324: | public static function __set_state(array $properties): self |
325: | { |
326: | return new self( |
327: | $properties['message'], |
328: | $properties['file'], |
329: | $properties['line'], |
330: | $properties['canBeIgnored'], |
331: | $properties['filePath'], |
332: | $properties['traitFilePath'], |
333: | $properties['tip'], |
334: | $properties['nodeLine'] ?? null, |
335: | $properties['nodeType'] ?? null, |
336: | $properties['identifier'] ?? null, |
337: | $properties['metadata'] ?? [], |
338: | $properties['fixedErrorDiff'] ?? null, |
339: | ); |
340: | } |
341: | |
342: | public static function validateIdentifier(string $identifier): bool |
343: | { |
344: | return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null; |
345: | } |
346: | |
347: | } |
348: | |