1: <?php declare(strict_types=1);
2:
3: namespace PhpParser;
4:
5: class Error extends \RuntimeException {
6: protected string $rawMessage;
7: /** @var array<string, mixed> */
8: protected array $attributes;
9:
10: /**
11: * Creates an Exception signifying a parse error.
12: *
13: * @param string $message Error message
14: * @param array<string, mixed> $attributes Attributes of node/token where error occurred
15: */
16: public function __construct(string $message, array $attributes = []) {
17: $this->rawMessage = $message;
18: $this->attributes = $attributes;
19: $this->updateMessage();
20: }
21:
22: /**
23: * Gets the error message
24: *
25: * @return string Error message
26: */
27: public function getRawMessage(): string {
28: return $this->rawMessage;
29: }
30:
31: /**
32: * Gets the line the error starts in.
33: *
34: * @return int Error start line
35: * @phpstan-return -1|positive-int
36: */
37: public function getStartLine(): int {
38: return $this->attributes['startLine'] ?? -1;
39: }
40:
41: /**
42: * Gets the line the error ends in.
43: *
44: * @return int Error end line
45: * @phpstan-return -1|positive-int
46: */
47: public function getEndLine(): int {
48: return $this->attributes['endLine'] ?? -1;
49: }
50:
51: /**
52: * Gets the attributes of the node/token the error occurred at.
53: *
54: * @return array<string, mixed>
55: */
56: public function getAttributes(): array {
57: return $this->attributes;
58: }
59:
60: /**
61: * Sets the attributes of the node/token the error occurred at.
62: *
63: * @param array<string, mixed> $attributes
64: */
65: public function setAttributes(array $attributes): void {
66: $this->attributes = $attributes;
67: $this->updateMessage();
68: }
69:
70: /**
71: * Sets the line of the PHP file the error occurred in.
72: *
73: * @param string $message Error message
74: */
75: public function setRawMessage(string $message): void {
76: $this->rawMessage = $message;
77: $this->updateMessage();
78: }
79:
80: /**
81: * Sets the line the error starts in.
82: *
83: * @param int $line Error start line
84: */
85: public function setStartLine(int $line): void {
86: $this->attributes['startLine'] = $line;
87: $this->updateMessage();
88: }
89:
90: /**
91: * Returns whether the error has start and end column information.
92: *
93: * For column information enable the startFilePos and endFilePos in the lexer options.
94: */
95: public function hasColumnInfo(): bool {
96: return isset($this->attributes['startFilePos'], $this->attributes['endFilePos']);
97: }
98:
99: /**
100: * Gets the start column (1-based) into the line where the error started.
101: *
102: * @param string $code Source code of the file
103: */
104: public function getStartColumn(string $code): int {
105: if (!$this->hasColumnInfo()) {
106: throw new \RuntimeException('Error does not have column information');
107: }
108:
109: return $this->toColumn($code, $this->attributes['startFilePos']);
110: }
111:
112: /**
113: * Gets the end column (1-based) into the line where the error ended.
114: *
115: * @param string $code Source code of the file
116: */
117: public function getEndColumn(string $code): int {
118: if (!$this->hasColumnInfo()) {
119: throw new \RuntimeException('Error does not have column information');
120: }
121:
122: return $this->toColumn($code, $this->attributes['endFilePos']);
123: }
124:
125: /**
126: * Formats message including line and column information.
127: *
128: * @param string $code Source code associated with the error, for calculation of the columns
129: *
130: * @return string Formatted message
131: */
132: public function getMessageWithColumnInfo(string $code): string {
133: return sprintf(
134: '%s from %d:%d to %d:%d', $this->getRawMessage(),
135: $this->getStartLine(), $this->getStartColumn($code),
136: $this->getEndLine(), $this->getEndColumn($code)
137: );
138: }
139:
140: /**
141: * Converts a file offset into a column.
142: *
143: * @param string $code Source code that $pos indexes into
144: * @param int $pos 0-based position in $code
145: *
146: * @return int 1-based column (relative to start of line)
147: */
148: private function toColumn(string $code, int $pos): int {
149: if ($pos > strlen($code)) {
150: throw new \RuntimeException('Invalid position information');
151: }
152:
153: $lineStartPos = strrpos($code, "\n", $pos - strlen($code));
154: if (false === $lineStartPos) {
155: $lineStartPos = -1;
156: }
157:
158: return $pos - $lineStartPos;
159: }
160:
161: /**
162: * Updates the exception message after a change to rawMessage or rawLine.
163: */
164: protected function updateMessage(): void {
165: $this->message = $this->rawMessage;
166:
167: if (-1 === $this->getStartLine()) {
168: $this->message .= ' on unknown line';
169: } else {
170: $this->message .= ' on line ' . $this->getStartLine();
171: }
172: }
173: }
174: