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