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