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