mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
parser: Tweak parsing during error conditions
When an explicit parsing error is encountered and a CompileError raised, apply the changes to the context state. This way, the rule that catches the exception (e.g. Statement or Until) knows where the error occurred. Also, changed "Expected" errors to be reported at the end of the previous non-whitespace token.
This commit is contained in:
parent
e5d6910626
commit
8f3ae9a626
10 changed files with 69 additions and 37 deletions
|
@ -92,29 +92,38 @@ class CompileError(PrintableError):
|
||||||
def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None:
|
def pretty_print(self, filename: str, code: str, stream=sys.stdout) -> None:
|
||||||
assert self.range is not None
|
assert self.range is not None
|
||||||
|
|
||||||
line_num, col_num = utils.idx_to_pos(self.range.start + 1, code)
|
def format_line(range: Range):
|
||||||
end_line_num, end_col_num = utils.idx_to_pos(self.range.end + 1, code)
|
line_num, col_num = utils.idx_to_pos(range.start, code)
|
||||||
line = code.splitlines(True)[line_num] if code != "" else ""
|
end_line_num, end_col_num = utils.idx_to_pos(range.end, code)
|
||||||
|
line = code.splitlines(True)[line_num] if code != "" else ""
|
||||||
|
|
||||||
# Display 1-based line numbers
|
# Display 1-based line numbers
|
||||||
line_num += 1
|
line_num += 1
|
||||||
end_line_num += 1
|
end_line_num += 1
|
||||||
|
col_num += 1
|
||||||
|
end_col_num += 1
|
||||||
|
|
||||||
n_spaces = col_num - 1
|
n_spaces = col_num - 1
|
||||||
n_carets = (
|
n_carets = (
|
||||||
(end_col_num - col_num)
|
(end_col_num - col_num)
|
||||||
if line_num == end_line_num
|
if line_num == end_line_num
|
||||||
else (len(line) - n_spaces - 1)
|
else (len(line) - n_spaces - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
n_spaces += line.count("\t", 0, col_num)
|
n_spaces += line.count("\t", 0, col_num)
|
||||||
n_carets += line.count("\t", col_num, col_num + n_carets)
|
n_carets += line.count("\t", col_num, col_num + n_carets)
|
||||||
line = line.replace("\t", " ")
|
line = line.replace("\t", " ")
|
||||||
|
|
||||||
|
n_carets = max(n_carets, 1)
|
||||||
|
|
||||||
|
return line_num, col_num, line.rstrip(), (" " * n_spaces) + ("^" * n_carets)
|
||||||
|
|
||||||
|
line_num, col_num, line, carets = format_line(self.range)
|
||||||
|
|
||||||
stream.write(
|
stream.write(
|
||||||
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
|
f"""{self.color}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
|
||||||
at {filename} line {line_num} column {col_num}:
|
at {filename} line {line_num} column {col_num}:
|
||||||
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*n_spaces}{"^"*n_carets}{Colors.CLEAR}\n"""
|
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n"""
|
||||||
)
|
)
|
||||||
|
|
||||||
for hint in self.hints:
|
for hint in self.hints:
|
||||||
|
@ -139,14 +148,12 @@ at {filename} line {line_num} column {col_num}:
|
||||||
)
|
)
|
||||||
|
|
||||||
for ref in self.references:
|
for ref in self.references:
|
||||||
line_num, col_num = utils.idx_to_pos(ref.range.start + 1, code)
|
line_num, col_num, line, carets = format_line(ref.range)
|
||||||
line = code.splitlines(True)[line_num]
|
|
||||||
line_num += 1
|
|
||||||
|
|
||||||
stream.write(
|
stream.write(
|
||||||
f"""{Colors.FAINT}note: {ref.message}:
|
f"""{Colors.FAINT}note: {ref.message}:
|
||||||
at {filename} line {line_num} column {col_num}:
|
at {filename} line {line_num} column {col_num}:
|
||||||
{Colors.FAINT}{line_num :>4} |{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}\n"""
|
{Colors.FAINT}{line_num :>4} |{line}\n {Colors.FAINT}|{carets}{Colors.CLEAR}\n"""
|
||||||
)
|
)
|
||||||
|
|
||||||
stream.write("\n")
|
stream.write("\n")
|
||||||
|
|
|
@ -34,6 +34,7 @@ from ..errors import (
|
||||||
CompileError,
|
CompileError,
|
||||||
CompileWarning,
|
CompileWarning,
|
||||||
DeprecatedWarning,
|
DeprecatedWarning,
|
||||||
|
ErrorReference,
|
||||||
MultipleErrors,
|
MultipleErrors,
|
||||||
UnusedWarning,
|
UnusedWarning,
|
||||||
UpgradeWarning,
|
UpgradeWarning,
|
||||||
|
|
|
@ -48,7 +48,7 @@ class ScopeCtx:
|
||||||
return self.node
|
return self.node
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def objects(self) -> T.Dict[str, Object]:
|
def objects(self) -> T.Dict[str, AstNode]:
|
||||||
return {
|
return {
|
||||||
obj.tokens["id"]: obj
|
obj.tokens["id"]: obj
|
||||||
for obj in self._iter_recursive(self.node)
|
for obj in self._iter_recursive(self.node)
|
||||||
|
@ -58,7 +58,7 @@ class ScopeCtx:
|
||||||
def validate_unique_ids(self) -> None:
|
def validate_unique_ids(self) -> None:
|
||||||
from .gtk_list_item_factory import ExtListItemFactory
|
from .gtk_list_item_factory import ExtListItemFactory
|
||||||
|
|
||||||
passed = {}
|
passed: T.Dict[str, AstNode] = {}
|
||||||
for obj in self._iter_recursive(self.node):
|
for obj in self._iter_recursive(self.node):
|
||||||
if obj.tokens["id"] is None:
|
if obj.tokens["id"] is None:
|
||||||
continue
|
continue
|
||||||
|
@ -71,10 +71,16 @@ class ScopeCtx:
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
f"Duplicate object ID '{obj.tokens['id']}'",
|
f"Duplicate object ID '{obj.tokens['id']}'",
|
||||||
token.range,
|
token.range,
|
||||||
|
references=[
|
||||||
|
ErrorReference(
|
||||||
|
passed[obj.tokens["id"]].group.tokens["id"].range,
|
||||||
|
"previous declaration was here",
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
passed[obj.tokens["id"]] = obj
|
passed[obj.tokens["id"]] = obj
|
||||||
|
|
||||||
def _iter_recursive(self, node: AstNode):
|
def _iter_recursive(self, node: AstNode) -> T.Generator[AstNode, T.Any, None]:
|
||||||
yield node
|
yield node
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
if child.context[ScopeCtx] is self:
|
if child.context[ScopeCtx] is self:
|
||||||
|
|
|
@ -235,7 +235,15 @@ class ParseNode:
|
||||||
start_idx = ctx.index
|
start_idx = ctx.index
|
||||||
inner_ctx = ctx.create_child()
|
inner_ctx = ctx.create_child()
|
||||||
|
|
||||||
if self._parse(inner_ctx):
|
try:
|
||||||
|
result = self._parse(inner_ctx)
|
||||||
|
except Exception as e:
|
||||||
|
# If an exception occurs, there's an explicit error, not just a rule that didn't match. Apply the context
|
||||||
|
# state so that whichever rule handles the exception (e.g. a Statement) knows where the error occurred.
|
||||||
|
ctx.apply_child(inner_ctx)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if result:
|
||||||
ctx.apply_child(inner_ctx)
|
ctx.apply_child(inner_ctx)
|
||||||
if ctx.index == start_idx:
|
if ctx.index == start_idx:
|
||||||
return ParseResult.EMPTY
|
return ParseResult.EMPTY
|
||||||
|
@ -269,11 +277,11 @@ class Err(ParseNode):
|
||||||
if self.child.parse(ctx).failed():
|
if self.child.parse(ctx).failed():
|
||||||
start_idx = ctx.start
|
start_idx = ctx.start
|
||||||
while ctx.tokens[start_idx].type in SKIP_TOKENS:
|
while ctx.tokens[start_idx].type in SKIP_TOKENS:
|
||||||
start_idx += 1
|
start_idx -= 1
|
||||||
start_token = ctx.tokens[start_idx]
|
start_token = ctx.tokens[start_idx]
|
||||||
|
|
||||||
raise CompileError(
|
raise CompileError(
|
||||||
self.message, Range(start_token.start, start_token.start, ctx.text)
|
self.message, Range(start_token.end, start_token.end, ctx.text)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -350,7 +358,20 @@ class Statement(ParseNode):
|
||||||
|
|
||||||
token = ctx.peek_token()
|
token = ctx.peek_token()
|
||||||
if str(token) != self.end:
|
if str(token) != self.end:
|
||||||
ctx.errors.append(CompileError(f"Expected `{self.end}`", token.range))
|
start_idx = ctx.index - 1
|
||||||
|
while ctx.tokens[start_idx].type in SKIP_TOKENS:
|
||||||
|
start_idx -= 1
|
||||||
|
start_token = ctx.tokens[start_idx]
|
||||||
|
|
||||||
|
position = (
|
||||||
|
start_token.start if ctx.index - 1 == start_idx else start_token.end
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.errors.append(
|
||||||
|
CompileError(
|
||||||
|
f"Expected `{self.end}`", Range(position, position, ctx.text)
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ctx.next_token()
|
ctx.next_token()
|
||||||
return True
|
return True
|
||||||
|
@ -411,7 +432,6 @@ class Until(ParseNode):
|
||||||
ctx.skip_unexpected_token()
|
ctx.skip_unexpected_token()
|
||||||
except CompileError as e:
|
except CompileError as e:
|
||||||
ctx.errors.append(e)
|
ctx.errors.append(e)
|
||||||
ctx.next_token()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1,0,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)
|
1,1,0,File must start with a "using Gtk" directive (e.g. `using Gtk 4.0;`)
|
|
@ -1 +1 @@
|
||||||
6,1,1,Expected `;`
|
5,4,0,Expected `;`
|
|
@ -1,2 +1 @@
|
||||||
5,1,0,Expected a signal detail name
|
4,11,0,Expected a signal detail name
|
||||||
4,9,3,Unexpected tokens
|
|
|
@ -1,2 +1 @@
|
||||||
4,5,21,Attributes are not permitted at the top level of a menu
|
4,5,21,Attributes are not permitted at the top level of a menu
|
||||||
4,16,10,Unexpected tokens
|
|
|
@ -1 +1 @@
|
||||||
1,11,0,Expected a version number for GTK
|
1,10,0,Expected a version number for GTK
|
||||||
|
|
|
@ -143,9 +143,9 @@ class TestSamples(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
def error_str(error: CompileError):
|
def error_str(error: CompileError):
|
||||||
line, col = utils.idx_to_pos(error.range.start + 1, blueprint)
|
line, col = utils.idx_to_pos(error.range.start, blueprint)
|
||||||
len = error.range.length
|
len = error.range.length
|
||||||
return ",".join([str(line + 1), str(col), str(len), error.message])
|
return ",".join([str(line + 1), str(col + 1), str(len), error.message])
|
||||||
|
|
||||||
actual = "\n".join([error_str(error) for error in errors])
|
actual = "\n".join([error_str(error) for error in errors])
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue