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: $this->traitFilePath,
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: /**
137: * Rewrites every path this error carries, for portable storage in the result cache. The caller
138: * owns the transformation - see ResultCachePathTransformer, which passes relativizePath() when
139: * storing and absolutizePath() when loading - so both directions apply exactly the same rules
140: * to the error's paths as to the cache's file-path keys.
141: *
142: * @param callable(string): string $transformPath
143: */
144: public function transformPaths(callable $transformPath): self
145: {
146: return new self(
147: $this->message,
148: $transformPath($this->file),
149: $this->line,
150: $this->canBeIgnored,
151: $this->filePath === null ? null : $transformPath($this->filePath),
152: $this->traitFilePath === null ? null : $transformPath($this->traitFilePath),
153: $this->tip,
154: $this->nodeLine,
155: $this->nodeType,
156: $this->identifier,
157: $this->metadata,
158: $this->fixedErrorDiff,
159: );
160: }
161:
162: public function getLine(): ?int
163: {
164: return $this->line;
165: }
166:
167: public function canBeIgnored(): bool
168: {
169: return $this->canBeIgnored === true;
170: }
171:
172: public function hasNonIgnorableException(): bool
173: {
174: return $this->canBeIgnored instanceof Throwable;
175: }
176:
177: public function getTip(): ?string
178: {
179: return $this->tip;
180: }
181:
182: public function withoutTip(): self
183: {
184: if ($this->tip === null) {
185: return $this;
186: }
187:
188: return new self(
189: $this->message,
190: $this->file,
191: $this->line,
192: $this->canBeIgnored,
193: $this->filePath,
194: $this->traitFilePath,
195: null,
196: $this->nodeLine,
197: $this->nodeType,
198: $this->identifier,
199: $this->metadata,
200: $this->fixedErrorDiff,
201: );
202: }
203:
204: public function doNotIgnore(): self
205: {
206: if (!$this->canBeIgnored()) {
207: return $this;
208: }
209:
210: return new self(
211: $this->message,
212: $this->file,
213: $this->line,
214: false,
215: $this->filePath,
216: $this->traitFilePath,
217: $this->tip,
218: $this->nodeLine,
219: $this->nodeType,
220: $this->identifier,
221: $this->metadata,
222: $this->fixedErrorDiff,
223: );
224: }
225:
226: public function withIdentifier(string $identifier): self
227: {
228: if ($this->identifier !== null) {
229: throw new ShouldNotHappenException(sprintf('Error already has an identifier: %s', $this->identifier));
230: }
231:
232: return new self(
233: $this->message,
234: $this->file,
235: $this->line,
236: $this->canBeIgnored,
237: $this->filePath,
238: $this->traitFilePath,
239: $this->tip,
240: $this->nodeLine,
241: $this->nodeType,
242: $identifier,
243: $this->metadata,
244: $this->fixedErrorDiff,
245: );
246: }
247:
248: /**
249: * @param mixed[] $metadata
250: */
251: public function withMetadata(array $metadata): self
252: {
253: if ($this->metadata !== []) {
254: throw new ShouldNotHappenException('Error already has metadata');
255: }
256:
257: return new self(
258: $this->message,
259: $this->file,
260: $this->line,
261: $this->canBeIgnored,
262: $this->filePath,
263: $this->traitFilePath,
264: $this->tip,
265: $this->nodeLine,
266: $this->nodeType,
267: $this->identifier,
268: $metadata,
269: $this->fixedErrorDiff,
270: );
271: }
272:
273: public function getNodeLine(): ?int
274: {
275: return $this->nodeLine;
276: }
277:
278: /**
279: * @return class-string<Node>|null
280: */
281: public function getNodeType(): ?string
282: {
283: return $this->nodeType;
284: }
285:
286: /**
287: * Error identifier set via `RuleErrorBuilder::identifier()`.
288: *
289: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
290: */
291: public function getIdentifier(): ?string
292: {
293: return $this->identifier;
294: }
295:
296: /**
297: * @return mixed[]
298: */
299: public function getMetadata(): array
300: {
301: return $this->metadata;
302: }
303:
304: /**
305: * @internal Experimental
306: */
307: public function getFixedErrorDiff(): ?FixedErrorDiff
308: {
309: return $this->fixedErrorDiff;
310: }
311:
312: /**
313: * @return mixed
314: */
315: #[ReturnTypeWillChange]
316: #[Override]
317: public function jsonSerialize()
318: {
319: $fixedErrorDiffHash = null;
320: $fixedErrorDiffDiff = null;
321: if ($this->fixedErrorDiff !== null) {
322: $fixedErrorDiffHash = $this->fixedErrorDiff->originalHash;
323: $fixedErrorDiffDiff = $this->fixedErrorDiff->diff;
324: }
325:
326: return [
327: 'message' => $this->message,
328: 'file' => $this->file,
329: 'line' => $this->line,
330: 'canBeIgnored' => is_bool($this->canBeIgnored) ? $this->canBeIgnored : 'exception',
331: 'filePath' => $this->filePath,
332: 'traitFilePath' => $this->traitFilePath,
333: 'tip' => $this->tip,
334: 'nodeLine' => $this->nodeLine,
335: 'nodeType' => $this->nodeType,
336: 'identifier' => $this->identifier,
337: 'metadata' => $this->metadata,
338: 'fixedErrorDiffHash' => $fixedErrorDiffHash,
339: 'fixedErrorDiffDiff' => $fixedErrorDiffDiff,
340: ];
341: }
342:
343: /**
344: * @param mixed[] $json
345: */
346: public static function decode(array $json): self
347: {
348: $fixedErrorDiff = null;
349: if ($json['fixedErrorDiffHash'] !== null && $json['fixedErrorDiffDiff'] !== null) {
350: $fixedErrorDiff = new FixedErrorDiff($json['fixedErrorDiffHash'], $json['fixedErrorDiffDiff']);
351: }
352:
353: return new self(
354: $json['message'],
355: $json['file'],
356: $json['line'],
357: $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'],
358: $json['filePath'],
359: $json['traitFilePath'],
360: $json['tip'],
361: $json['nodeLine'] ?? null,
362: $json['nodeType'] ?? null,
363: $json['identifier'] ?? null,
364: $json['metadata'] ?? [],
365: $fixedErrorDiff,
366: );
367: }
368:
369: /**
370: * @param mixed[] $properties
371: */
372: public static function __set_state(array $properties): self
373: {
374: return new self(
375: $properties['message'],
376: $properties['file'],
377: $properties['line'],
378: $properties['canBeIgnored'],
379: $properties['filePath'],
380: $properties['traitFilePath'],
381: $properties['tip'],
382: $properties['nodeLine'] ?? null,
383: $properties['nodeType'] ?? null,
384: $properties['identifier'] ?? null,
385: $properties['metadata'] ?? [],
386: $properties['fixedErrorDiff'] ?? null,
387: );
388: }
389:
390: public static function validateIdentifier(string $identifier): bool
391: {
392: return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null;
393: }
394:
395: }
396: