Verified Commit f520d689 authored by Alberto Miranda's avatar Alberto Miranda ♨️
Browse files

Add exceptions for reporting parsing errors

parent 9e55364e
Loading
Loading
Loading
Loading
+16 −7
Original line number Diff line number Diff line
import argparse
import sys

from pathlib import Path
import lark.exceptions

from rpcc.exceptions import RpccError
from rpcc.version import __version__ as rpcc_version
from rpcc.parser import Parser
from rpcc.transformers.cxx import Transformer as CxxTransformer
from pathlib import Path


class PrintVersion(argparse.Action):
@@ -48,13 +51,19 @@ def main(args=None):
    )
    args = parser.parse_args()

    # try:
    ast = Parser(args.rpc_proto_file).parse()
    print(ast.pretty())
    print(CxxTransformer().transform(ast).pretty())
    try:
        parser = Parser()
        ast = parser.parse(args.rpc_proto_file)
    except RpccError as e:
        print(e, file=sys.stderr)
        return 1

    # except Exception:
    #    return 1
    try:
        ast = CxxTransformer(parser).transform(ast)
    except lark.exceptions.VisitError as e:
        exc = e.orig_exc
        print(exc, file=sys.stderr)
    return 0


if __name__ == "__main__":

rpcc/exceptions.py

0 → 100644
+76 −0
Original line number Diff line number Diff line
import lark


class RpccError(SyntaxError):
    label = "syntax error"

    def __str__(self):
        context, line, column = self.args
        return f'{self.filename}:{line}:{column}: error: {self.label}.\n\n{context}'


class UnexpectedToken(RpccError):
    label = "unexpected token"

    @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]

        if len(expected) > 1:
            return "Expected one of: \n\t* %s\n" % '\n\t* '.join(expected)
        else:
            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)}")


class UnexpectedCharacters(RpccError):
    label = "unexpected character"


class UnexpectedEOF(RpccError):
    label = "unexpected End-of-File."


class MissingRpcDefinition(RpccError):
    label = "missing rpc definition"


class MissingRpcName(RpccError):
    label = "missing rpc name"


class EmptyRpcDefinition(RpccError):
    label = "rpc definition is empty"


class RpcRedefinition(Exception):
    label = "redefinition of rpc"

    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.start_pos)}")


def get_context(text: str, start_pos: int, span: int = 40) -> str:
    """Returns a pretty string pinpointing the error in the text,
       with span amount of context characters around it.
    """
    pos = start_pos
    start = max(pos - span, 0)
    end = pos + span
    if not isinstance(text, bytes):
        before = text[start:pos].rsplit('\n', 1)[-1]
        after = text[pos:end].split('\n', 1)[0]
        return before + after + '\n' + ' ' * 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")
+52 −8
Original line number Diff line number Diff line
import pathlib
from lark import Lark, Tree
import lark.exceptions
from loguru import logger

from rpcc.exceptions import MissingRpcDefinition, EmptyRpcDefinition, MissingRpcName, UnexpectedToken, \
    UnexpectedCharacters, UnexpectedEOF

GRAMMAR = r"""
    start           : ( rpc )+
    rpc             : "rpc" rpc_name "{" (rpc_args rpc_return | rpc_args | rpc_return) "}"
@@ -25,18 +29,38 @@ GRAMMAR = r"""
    %ignore ";"
"""

INVALID_INPUT_EXAMPLES = {
    EmptyRpcDefinition: ['rpc foo {}',
                             'rpc foo { arguments {} returns {} } rpc bar {}'],
    MissingRpcDefinition: ['',
                               'foo',
                               'foo {',
                               'foo {}',
                               '{',
                               'arguments {',
                               'returns {'
                           ],
    MissingRpcName: ['rpc {']
}


class Parser:
    """The main parser class
    """The main parser class"""

    def __init__(self) -> None:
        self.input_file = None
        self.text = None
        self.parser = Lark(GRAMMAR, propagate_positions=True, parser='lalr')

    Parameters:
        input_file: A pathlib.Path containing the path to the RPC description file to parse.
    def parse(self, input_file: pathlib.Path) -> Tree:
        """Parse an input file.

        :param input_file: A `pathlib.Path` containing the path to the RPC description file to parse.
        :return: An AST tree with a representation of the parsed text.
        """
    def __init__(self, input_file: pathlib.Path) -> None:

        self.input_file = input_file
        self.parser = Lark(GRAMMAR, parser='lalr')

    def parse(self) -> Tree:
        try:
            file = open(self.input_file, "r", encoding="utf-8")
        except (FileNotFoundError, EnvironmentError) as err:
@@ -44,4 +68,24 @@ class Parser:
            raise
        else:
            with file:
                return self.parser.parse(file.read())
                self.text = file.read()
                try:
                    return self.parser.parse(self.text)
                except lark.exceptions.UnexpectedInput as u:

                    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))

                    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))

                    if isinstance(u, lark.exceptions.UnexpectedCharacters):
                        raise UnexpectedCharacters(u.line, u.column, u.get_context(self.text), )

                    if isinstance(u, lark.exceptions.UnexpectedEOF):
                        raise UnexpectedEOF()

                    raise u
+18 −4
Original line number Diff line number Diff line
from typing import List, Tuple, Any

import lark
from lark import v_args, Discard
from loguru import logger
from lark import v_args

from rpcc.parser import Parser
from rpcc.exceptions import RpcRedefinition
from rpcc.rpc import RemoteProcedure, Argument, ReturnVariable

fwdecls_template = (
@@ -23,6 +24,11 @@ _cxx_type_map = {


class Transformer(lark.Transformer):

    def __init__(self, parser: Parser):
        self.rpcs = set()
        self.parser = parser

    # variable names can be coverted directly into strings
    NAME = str

@@ -50,16 +56,24 @@ class Transformer(lark.Transformer):
        (typename, varname) = t
        return ReturnVariable(varname, typename)

    @v_args(inline=True)
    def rpc(self, name: str, args: List[Argument], retval: ReturnVariable) -> RemoteProcedure:
    @v_args(inline=True, meta=True)
    def rpc(self, meta: lark.tree.Meta, name: str, args: List[Argument] = None,
            retval: ReturnVariable = None) -> RemoteProcedure:
        """Transform a `rpc` node from the AST into a `RemoteProcedure` object from a name, a list of `Argument`s and a
        `ReturnVariable`.

        :param meta: Metainformation about the current Token such as line, column, start_pos, etc.
        :param name: A name for the remote procedure.
        :param args: A list of `Argument` describing the input arguments for the remote procedure.
        :param retval: A `ReturnValue` describing the remote procedure's return value.
        :return: A `RemoteProcedure` object describing the remote procedure.
        """

        if name in self.rpcs:
            raise RpcRedefinition(self.parser.input_file, self.parser.text, meta, name)
        else:
            self.rpcs.add(name)

        return RemoteProcedure(name, args, retval)

    @v_args(inline=True)