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: * @api
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: * Error constructor.
26: *
27: * @param class-string<Node>|null $nodeType
28: * @param mixed[] $metadata
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 removeTraitContext(): self
110: {
111: if ($this->traitFilePath === null) {
112: throw new ShouldNotHappenException();
113: }
114:
115: return new self(
116: $this->message,
117: $this->traitFilePath,
118: $this->line,
119: $this->canBeIgnored,
120: $this->filePath,
121: null,
122: $this->tip,
123: $this->nodeLine,
124: $this->nodeType,
125: $this->identifier,
126: $this->metadata,
127: $this->fixedErrorDiff,
128: );
129: }
130:
131: public function getTraitFilePath(): ?string
132: {
133: return $this->traitFilePath;
134: }
135:
136: public function getLine(): ?int
137: {
138: return $this->line;
139: }
140:
141: public function canBeIgnored(): bool
142: {
143: return $this->canBeIgnored === true;
144: }
145:
146: public function hasNonIgnorableException(): bool
147: {
148: return $this->canBeIgnored instanceof Throwable;
149: }
150:
151: public function getTip(): ?string
152: {
153: return $this->tip;
154: }
155:
156: public function withoutTip(): self
157: {
158: if ($this->tip === null) {
159: return $this;
160: }
161:
162: return new self(
163: $this->message,
164: $this->file,
165: $this->line,
166: $this->canBeIgnored,
167: $this->filePath,
168: $this->traitFilePath,
169: null,
170: $this->nodeLine,
171: $this->nodeType,
172: $this->identifier,
173: $this->metadata,
174: $this->fixedErrorDiff,
175: );
176: }
177:
178: public function doNotIgnore(): self
179: {
180: if (!$this->canBeIgnored()) {
181: return $this;
182: }
183:
184: return new self(
185: $this->message,
186: $this->file,
187: $this->line,
188: false,
189: $this->filePath,
190: $this->traitFilePath,
191: $this->tip,
192: $this->nodeLine,
193: $this->nodeType,
194: $this->identifier,
195: $this->metadata,
196: $this->fixedErrorDiff,
197: );
198: }
199:
200: public function withIdentifier(string $identifier): self
201: {
202: if ($this->identifier !== null) {
203: throw new ShouldNotHappenException(sprintf('Error already has an identifier: %s', $this->identifier));
204: }
205:
206: return new self(
207: $this->message,
208: $this->file,
209: $this->line,
210: $this->canBeIgnored,
211: $this->filePath,
212: $this->traitFilePath,
213: $this->tip,
214: $this->nodeLine,
215: $this->nodeType,
216: $identifier,
217: $this->metadata,
218: $this->fixedErrorDiff,
219: );
220: }
221:
222: /**
223: * @param mixed[] $metadata
224: */
225: public function withMetadata(array $metadata): self
226: {
227: if ($this->metadata !== []) {
228: throw new ShouldNotHappenException('Error already has metadata');
229: }
230:
231: return new self(
232: $this->message,
233: $this->file,
234: $this->line,
235: $this->canBeIgnored,
236: $this->filePath,
237: $this->traitFilePath,
238: $this->tip,
239: $this->nodeLine,
240: $this->nodeType,
241: $this->identifier,
242: $metadata,
243: $this->fixedErrorDiff,
244: );
245: }
246:
247: public function getNodeLine(): ?int
248: {
249: return $this->nodeLine;
250: }
251:
252: /**
253: * @return class-string<Node>|null
254: */
255: public function getNodeType(): ?string
256: {
257: return $this->nodeType;
258: }
259:
260: /**
261: * Error identifier set via `RuleErrorBuilder::identifier()`.
262: *
263: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
264: */
265: public function getIdentifier(): ?string
266: {
267: return $this->identifier;
268: }
269:
270: /**
271: * @return mixed[]
272: */
273: public function getMetadata(): array
274: {
275: return $this->metadata;
276: }
277:
278: /**
279: * @internal Experimental
280: */
281: public function getFixedErrorDiff(): ?FixedErrorDiff
282: {
283: return $this->fixedErrorDiff;
284: }
285:
286: /**
287: * @return mixed
288: */
289: #[ReturnTypeWillChange]
290: #[Override]
291: public function jsonSerialize()
292: {
293: $fixedErrorDiffHash = null;
294: $fixedErrorDiffDiff = null;
295: if ($this->fixedErrorDiff !== null) {
296: $fixedErrorDiffHash = $this->fixedErrorDiff->originalHash;
297: $fixedErrorDiffDiff = $this->fixedErrorDiff->diff;
298: }
299:
300: return [
301: 'message' => $this->message,
302: 'file' => $this->file,
303: 'line' => $this->line,
304: 'canBeIgnored' => is_bool($this->canBeIgnored) ? $this->canBeIgnored : 'exception',
305: 'filePath' => $this->filePath,
306: 'traitFilePath' => $this->traitFilePath,
307: 'tip' => $this->tip,
308: 'nodeLine' => $this->nodeLine,
309: 'nodeType' => $this->nodeType,
310: 'identifier' => $this->identifier,
311: 'metadata' => $this->metadata,
312: 'fixedErrorDiffHash' => $fixedErrorDiffHash,
313: 'fixedErrorDiffDiff' => $fixedErrorDiffDiff,
314: ];
315: }
316:
317: /**
318: * @param mixed[] $json
319: */
320: public static function decode(array $json): self
321: {
322: $fixedErrorDiff = null;
323: if ($json['fixedErrorDiffHash'] !== null && $json['fixedErrorDiffDiff'] !== null) {
324: $fixedErrorDiff = new FixedErrorDiff($json['fixedErrorDiffHash'], $json['fixedErrorDiffDiff']);
325: }
326:
327: return new self(
328: $json['message'],
329: $json['file'],
330: $json['line'],
331: $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'],
332: $json['filePath'],
333: $json['traitFilePath'],
334: $json['tip'],
335: $json['nodeLine'] ?? null,
336: $json['nodeType'] ?? null,
337: $json['identifier'] ?? null,
338: $json['metadata'] ?? [],
339: $fixedErrorDiff,
340: );
341: }
342:
343: /**
344: * @param mixed[] $properties
345: */
346: public static function __set_state(array $properties): self
347: {
348: return new self(
349: $properties['message'],
350: $properties['file'],
351: $properties['line'],
352: $properties['canBeIgnored'],
353: $properties['filePath'],
354: $properties['traitFilePath'],
355: $properties['tip'],
356: $properties['nodeLine'] ?? null,
357: $properties['nodeType'] ?? null,
358: $properties['identifier'] ?? null,
359: $properties['metadata'] ?? [],
360: $properties['fixedErrorDiff'] ?? null,
361: );
362: }
363:
364: public static function validateIdentifier(string $identifier): bool
365: {
366: return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null;
367: }
368:
369: }
370: