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