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: public function getNodeLine(): ?int
188: {
189: return $this->nodeLine;
190: }
191:
192: /**
193: * @return class-string<Node>|null
194: */
195: public function getNodeType(): ?string
196: {
197: return $this->nodeType;
198: }
199:
200: /**
201: * Error identifier set via `RuleErrorBuilder::identifier()`.
202: *
203: * List of all current error identifiers in PHPStan: https://phpstan.org/error-identifiers
204: */
205: public function getIdentifier(): ?string
206: {
207: return $this->identifier;
208: }
209:
210: /**
211: * @return mixed[]
212: */
213: public function getMetadata(): array
214: {
215: return $this->metadata;
216: }
217:
218: /**
219: * @return mixed
220: */
221: #[ReturnTypeWillChange]
222: public function jsonSerialize()
223: {
224: return [
225: 'message' => $this->message,
226: 'file' => $this->file,
227: 'line' => $this->line,
228: 'canBeIgnored' => is_bool($this->canBeIgnored) ? $this->canBeIgnored : 'exception',
229: 'filePath' => $this->filePath,
230: 'traitFilePath' => $this->traitFilePath,
231: 'tip' => $this->tip,
232: 'nodeLine' => $this->nodeLine,
233: 'nodeType' => $this->nodeType,
234: 'identifier' => $this->identifier,
235: 'metadata' => $this->metadata,
236: ];
237: }
238:
239: /**
240: * @param mixed[] $json
241: */
242: public static function decode(array $json): self
243: {
244: return new self(
245: $json['message'],
246: $json['file'],
247: $json['line'],
248: $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'],
249: $json['filePath'],
250: $json['traitFilePath'],
251: $json['tip'],
252: $json['nodeLine'] ?? null,
253: $json['nodeType'] ?? null,
254: $json['identifier'] ?? null,
255: $json['metadata'] ?? [],
256: );
257: }
258:
259: /**
260: * @param mixed[] $properties
261: */
262: public static function __set_state(array $properties): self
263: {
264: return new self(
265: $properties['message'],
266: $properties['file'],
267: $properties['line'],
268: $properties['canBeIgnored'],
269: $properties['filePath'],
270: $properties['traitFilePath'],
271: $properties['tip'],
272: $properties['nodeLine'] ?? null,
273: $properties['nodeType'] ?? null,
274: $properties['identifier'] ?? null,
275: $properties['metadata'] ?? [],
276: );
277: }
278:
279: public static function validateIdentifier(string $identifier): bool
280: {
281: return Strings::match($identifier, '~^' . self::PATTERN_IDENTIFIER . '$~') !== null;
282: }
283:
284: }
285: