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