Loading rpcc/console/highlighter.py +3 −3 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ class DiagnosticHighlighter(RegexHighlighter): base_style = "diagnostic." highlights = [ r"(?P<location>.*?:\d+:\d+:)\s(?P<error>error:)\s(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"(?P<location>.*?:\d+:\d+:)\s(?P<note>note:)\s(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"Expected:\n\s+’(?P<token>.*?)’" r"(?P<location>.*?:\d+:\d+:)\s(?P<error>error:)\s(?P<message>.*?)(?:’(?P<ident>.*?)’)?(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"(?P<location>.*?:\d+:\d+:)\s(?P<note>note:)\s(?:’(?P<ident>.*?)’)?(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"Expected:(?:\n\s+’(?P<token>.*?)’)+" ] rpcc/console/theme.py +3 −2 Original line number Diff line number Diff line Loading @@ -5,8 +5,9 @@ diagnostics_theme = Theme({ "diagnostic.location": "bold bright_white", "diagnostic.error": "bold bright_red", "diagnostic.note": "bold cyan", "diagnostic.message": "bold bright_white", "diagnostic.message": "bright_white", "diagnostic.context": "default", "diagnostic.caret": "bold bright_green", "diagnostic.token": "bold bright_cyan" "diagnostic.token": "bold bright_cyan", "diagnostic.ident": "bold bright_white" }) rpcc/exceptions.py +55 −25 Original line number Diff line number Diff line import lark from rpcc.meta import FileLocation class RpccError(SyntaxError): label = "syntax error" def __init__(self, location: FileLocation) -> None: self.location = location def __str__(self): context, line, column = self.args return f'{self.filename}:{line}:{column}: error: {self.label}.\n\n{context}' return f"{self.location.filename}:{self.location.line}:{self.location.column}: error: {self.label}" class UnexpectedToken(RpccError): label = "unexpected token" def __init__(self, location: FileLocation, token, expected, terminals): super().__init__(location) self.token = token self.expected_tokens = expected self.terminals = terminals @staticmethod def _format_expected(expected, terminals): d = {t.name: t for t in terminals} expected = [str(d[t_name].pattern).replace('\\', "") if t_name in d else t_name for t_name in expected] expected = [str(d[t_name].pattern).replace('\\', "").replace('\'', '’') if t_name in d else t_name for t_name in expected] if len(expected) > 1: return "Expected one of:\n\t* %s\n" % '\n\t* '.join(expected) Loading @@ -24,10 +32,9 @@ class UnexpectedToken(RpccError): return "Expected:\n\t%s\n" % '\n\t* '.join(expected) def __str__(self): filename, line, column, token, expected, terminals, context = self.args return (f"{filename}:{line}:{column}: error: {self.label} ’{token}’\n\n" f"{context}\n" f"{self._format_expected(expected, terminals)}") return (super().__str__() + f" ’{self.token}’\n" + f"{get_context(self.location)}\n" + f"{self._format_expected(self.expected_tokens, self.terminals)}") class UnexpectedCharacters(RpccError): Loading @@ -37,42 +44,65 @@ class UnexpectedCharacters(RpccError): class UnexpectedEOF(RpccError): label = "unexpected End-of-File." def __init__(self): pass class MissingRpcDefinition(RpccError): label = "missing rpc definition" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class MissingRpcName(RpccError): label = "missing rpc name" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class EmptyRpcDefinition(RpccError): label = "rpc definition is empty" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class RpcRedefinition(Exception): class RpcRedefinition(RpccError): label = "redefinition of rpc" prev_label = "previously defined here" def __init__(self, location: FileLocation, name: str, prev_definition: FileLocation): super().__init__(location) self.name = name self.prev = prev_definition def __str__(self): filename, text, meta, name = self.args return (f"{filename}:{meta.line}:{meta.column}: error: {self.label} ’{name}’\n" f"{get_context(text, meta)}") return (super().__str__() + f" ’{self.name}’\n" + get_context(self.location) + "\n" + f"{self.prev.filename}:{self.prev.line}:{self.prev.column}: note: ’{self.name}’ {self.prev_label}\n" + get_context(self.prev)) def get_context(text: str, meta: lark.tree.Meta, span: int = 40) -> str: def get_context(location: FileLocation, span: int = 40) -> str: """Returns a pretty string pinpointing the error in the text, with span amount of context characters around it. """ pos = meta.start_pos pos = location.pos_in_stream start = max(pos - span, 0) end = pos + span if not isinstance(text, bytes): max_width = 6 before = text[start:pos].rsplit('\n', 1)[-1] after = text[pos:end].split('\n', 1)[0] return f'{meta.line:>{max_width}} | ' + before + after + '\n' + \ ' ' * len(before.expandtabs()) + ' ' * max_width + ' | ^\n' if not isinstance(location.text, bytes): before = location.text[start:pos].rsplit('\n', 1)[-1] after = location.text[pos:end].split('\n', 1)[0] return f'{location.line:>{max_width}} | ' + before + after + '\n' + \ ' ' * max_width + ' | ' + ' ' * len(before.expandtabs()) + '^\n' else: before = text[start:pos].rsplit(b'\n', 1)[-1] after = text[pos:end].split(b'\n', 1)[0] return (before + after + b'\n' + b' ' * len(before.expandtabs()) + b'^\n').decode("ascii", "backslashreplace") before = location.text[start:pos].rsplit(b'\n', 1)[-1] after = location.text[pos:end].split(b'\n', 1)[0] return (before + after + b'\n' + b' ' * len(before.expandtabs()) + b'^\n').decode("ascii", "backslashreplace") rpcc/meta.py 0 → 100644 +21 −0 Original line number Diff line number Diff line import pathlib class FileLocation: """An object representing a specific location in the parsed text""" def __init__(self, filename: pathlib.Path, text: str, line: int, column: int, pos_in_stream: int) -> None: """Initialize a `FileLocation`. :param filename: The filename. :param text: The parsed text. :param line: The line of interest. :param column: The column of interest. :param pos_in_stream: The position of the location of interest in the file stream. """ self.filename = filename self.text = text self.line = line self.column = column self.pos_in_stream = pos_in_stream rpcc/parser.py +16 −12 Original line number Diff line number Diff line Loading @@ -3,8 +3,11 @@ from lark import Lark, Tree import lark.exceptions from loguru import logger from rpcc.exceptions import MissingRpcDefinition, EmptyRpcDefinition, MissingRpcName, UnexpectedToken, \ from rpcc.exceptions import ( EmptyRpcDefinition, MissingRpcDefinition, MissingRpcName, UnexpectedToken, UnexpectedCharacters, UnexpectedEOF ) from rpcc.meta import FileLocation GRAMMAR = r""" start : ( rpc )+ Loading Loading @@ -73,17 +76,18 @@ class Parser: return self.parser.parse(self.text) except lark.exceptions.UnexpectedInput as u: location = FileLocation(self.input_file, self.text, u.line, u.column, u.pos_in_stream) exc_class = u.match_examples(self.parser.parse, INVALID_INPUT_EXAMPLES, use_accepts=True) if exc_class: raise exc_class(self.input_file, u.line, u.column, u.get_context(self.text)) raise exc_class(location) if isinstance(u, lark.exceptions.UnexpectedToken): raise UnexpectedToken(self.input_file, u.line, u.column, u.token, u.expected, self.parser.terminals, u.get_context(self.text)) raise UnexpectedToken(location, u.token, u.expected, self.parser.terminals) if isinstance(u, lark.exceptions.UnexpectedCharacters): raise UnexpectedCharacters(u.line, u.column, u.get_context(self.text), ) raise UnexpectedCharacters(location) if isinstance(u, lark.exceptions.UnexpectedEOF): raise UnexpectedEOF() Loading Loading
rpcc/console/highlighter.py +3 −3 Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ class DiagnosticHighlighter(RegexHighlighter): base_style = "diagnostic." highlights = [ r"(?P<location>.*?:\d+:\d+:)\s(?P<error>error:)\s(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"(?P<location>.*?:\d+:\d+:)\s(?P<note>note:)\s(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"Expected:\n\s+’(?P<token>.*?)’" r"(?P<location>.*?:\d+:\d+:)\s(?P<error>error:)\s(?P<message>.*?)(?:’(?P<ident>.*?)’)?(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"(?P<location>.*?:\d+:\d+:)\s(?P<note>note:)\s(?:’(?P<ident>.*?)’)?(?P<message>.*?)(?P<context>(?:\n.*){2})(?P<caret>\^)\n", r"Expected:(?:\n\s+’(?P<token>.*?)’)+" ]
rpcc/console/theme.py +3 −2 Original line number Diff line number Diff line Loading @@ -5,8 +5,9 @@ diagnostics_theme = Theme({ "diagnostic.location": "bold bright_white", "diagnostic.error": "bold bright_red", "diagnostic.note": "bold cyan", "diagnostic.message": "bold bright_white", "diagnostic.message": "bright_white", "diagnostic.context": "default", "diagnostic.caret": "bold bright_green", "diagnostic.token": "bold bright_cyan" "diagnostic.token": "bold bright_cyan", "diagnostic.ident": "bold bright_white" })
rpcc/exceptions.py +55 −25 Original line number Diff line number Diff line import lark from rpcc.meta import FileLocation class RpccError(SyntaxError): label = "syntax error" def __init__(self, location: FileLocation) -> None: self.location = location def __str__(self): context, line, column = self.args return f'{self.filename}:{line}:{column}: error: {self.label}.\n\n{context}' return f"{self.location.filename}:{self.location.line}:{self.location.column}: error: {self.label}" class UnexpectedToken(RpccError): label = "unexpected token" def __init__(self, location: FileLocation, token, expected, terminals): super().__init__(location) self.token = token self.expected_tokens = expected self.terminals = terminals @staticmethod def _format_expected(expected, terminals): d = {t.name: t for t in terminals} expected = [str(d[t_name].pattern).replace('\\', "") if t_name in d else t_name for t_name in expected] expected = [str(d[t_name].pattern).replace('\\', "").replace('\'', '’') if t_name in d else t_name for t_name in expected] if len(expected) > 1: return "Expected one of:\n\t* %s\n" % '\n\t* '.join(expected) Loading @@ -24,10 +32,9 @@ class UnexpectedToken(RpccError): return "Expected:\n\t%s\n" % '\n\t* '.join(expected) def __str__(self): filename, line, column, token, expected, terminals, context = self.args return (f"{filename}:{line}:{column}: error: {self.label} ’{token}’\n\n" f"{context}\n" f"{self._format_expected(expected, terminals)}") return (super().__str__() + f" ’{self.token}’\n" + f"{get_context(self.location)}\n" + f"{self._format_expected(self.expected_tokens, self.terminals)}") class UnexpectedCharacters(RpccError): Loading @@ -37,42 +44,65 @@ class UnexpectedCharacters(RpccError): class UnexpectedEOF(RpccError): label = "unexpected End-of-File." def __init__(self): pass class MissingRpcDefinition(RpccError): label = "missing rpc definition" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class MissingRpcName(RpccError): label = "missing rpc name" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class EmptyRpcDefinition(RpccError): label = "rpc definition is empty" def __str__(self): return (super().__str__() + "\n" + get_context(self.location)) class RpcRedefinition(Exception): class RpcRedefinition(RpccError): label = "redefinition of rpc" prev_label = "previously defined here" def __init__(self, location: FileLocation, name: str, prev_definition: FileLocation): super().__init__(location) self.name = name self.prev = prev_definition def __str__(self): filename, text, meta, name = self.args return (f"{filename}:{meta.line}:{meta.column}: error: {self.label} ’{name}’\n" f"{get_context(text, meta)}") return (super().__str__() + f" ’{self.name}’\n" + get_context(self.location) + "\n" + f"{self.prev.filename}:{self.prev.line}:{self.prev.column}: note: ’{self.name}’ {self.prev_label}\n" + get_context(self.prev)) def get_context(text: str, meta: lark.tree.Meta, span: int = 40) -> str: def get_context(location: FileLocation, span: int = 40) -> str: """Returns a pretty string pinpointing the error in the text, with span amount of context characters around it. """ pos = meta.start_pos pos = location.pos_in_stream start = max(pos - span, 0) end = pos + span if not isinstance(text, bytes): max_width = 6 before = text[start:pos].rsplit('\n', 1)[-1] after = text[pos:end].split('\n', 1)[0] return f'{meta.line:>{max_width}} | ' + before + after + '\n' + \ ' ' * len(before.expandtabs()) + ' ' * max_width + ' | ^\n' if not isinstance(location.text, bytes): before = location.text[start:pos].rsplit('\n', 1)[-1] after = location.text[pos:end].split('\n', 1)[0] return f'{location.line:>{max_width}} | ' + before + after + '\n' + \ ' ' * max_width + ' | ' + ' ' * len(before.expandtabs()) + '^\n' else: before = text[start:pos].rsplit(b'\n', 1)[-1] after = text[pos:end].split(b'\n', 1)[0] return (before + after + b'\n' + b' ' * len(before.expandtabs()) + b'^\n').decode("ascii", "backslashreplace") before = location.text[start:pos].rsplit(b'\n', 1)[-1] after = location.text[pos:end].split(b'\n', 1)[0] return (before + after + b'\n' + b' ' * len(before.expandtabs()) + b'^\n').decode("ascii", "backslashreplace")
rpcc/meta.py 0 → 100644 +21 −0 Original line number Diff line number Diff line import pathlib class FileLocation: """An object representing a specific location in the parsed text""" def __init__(self, filename: pathlib.Path, text: str, line: int, column: int, pos_in_stream: int) -> None: """Initialize a `FileLocation`. :param filename: The filename. :param text: The parsed text. :param line: The line of interest. :param column: The column of interest. :param pos_in_stream: The position of the location of interest in the file stream. """ self.filename = filename self.text = text self.line = line self.column = column self.pos_in_stream = pos_in_stream
rpcc/parser.py +16 −12 Original line number Diff line number Diff line Loading @@ -3,8 +3,11 @@ from lark import Lark, Tree import lark.exceptions from loguru import logger from rpcc.exceptions import MissingRpcDefinition, EmptyRpcDefinition, MissingRpcName, UnexpectedToken, \ from rpcc.exceptions import ( EmptyRpcDefinition, MissingRpcDefinition, MissingRpcName, UnexpectedToken, UnexpectedCharacters, UnexpectedEOF ) from rpcc.meta import FileLocation GRAMMAR = r""" start : ( rpc )+ Loading Loading @@ -73,17 +76,18 @@ class Parser: return self.parser.parse(self.text) except lark.exceptions.UnexpectedInput as u: location = FileLocation(self.input_file, self.text, u.line, u.column, u.pos_in_stream) exc_class = u.match_examples(self.parser.parse, INVALID_INPUT_EXAMPLES, use_accepts=True) if exc_class: raise exc_class(self.input_file, u.line, u.column, u.get_context(self.text)) raise exc_class(location) if isinstance(u, lark.exceptions.UnexpectedToken): raise UnexpectedToken(self.input_file, u.line, u.column, u.token, u.expected, self.parser.terminals, u.get_context(self.text)) raise UnexpectedToken(location, u.token, u.expected, self.parser.terminals) if isinstance(u, lark.exceptions.UnexpectedCharacters): raise UnexpectedCharacters(u.line, u.column, u.get_context(self.text), ) raise UnexpectedCharacters(location) if isinstance(u, lark.exceptions.UnexpectedEOF): raise UnexpectedEOF() Loading