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