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