Skip to content

Commit

Permalink
Added basic parsing error reporting and fixed issue in grammar
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankSpruce committed Feb 13, 2020
1 parent 89357e2 commit 7747c1b
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 31 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [0.1.1] 2020-02-13

### Added
- added missing CHANGELOG
- some basic parsing error reporting

### Fixed
- bracket argument candidate which was not closed with proper ending bracket was interpreted as unquoted argument instead of being treated as parsing error

## [0.1.0] 2020-02-11

- first release of `gersemi` which should do some nice formatting
Expand Down
4 changes: 2 additions & 2 deletions gersemi/cmake.lark
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ quoted_element : /([^\\\"]|\n)+/
QUOTED_CONTINUATION : BACKSLASH NEWLINE

unquoted_argument : _unquoted_element+
_unquoted_element : /[^\$\s\(\)#\"\\]+/
| /[^\s\(\)#\"\\]/
_unquoted_element : /[^\$\s\[\(\)#\"\\]+/
| /[^\s\[\(\)#\"\\]/
| ESCAPE_SEQUENCE
| MAKE_STYLE_REFERENCE
| _unquoted_element DOUBLE_QUOTED_STRING
Expand Down
20 changes: 20 additions & 0 deletions gersemi/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,22 @@
class ASTMismatch(Exception):
pass


class ParsingError(SyntaxError):
description: str = ""

def __str__(self):
context, line, column = self.args # pylint: disable=unpacking-non-sequence
return f":{line}:{column}: {self.description}\n{context}"


class GenericParsingError(ParsingError):
description = "unspecified parsing error"


class UnbalancedParentheses(ParsingError):
description = "unbalanced parentheses"


class UnbalancedBrackets(ParsingError):
description = "unbalanced brackets"
65 changes: 56 additions & 9 deletions gersemi/parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import os
from lark import Lark, Token
from lark import Lark, Token, UnexpectedInput
from lark.visitors import Transformer
from gersemi.exceptions import (
GenericParsingError,
UnbalancedParentheses,
UnbalancedBrackets,
)


class DowncaseIdentifiers(Transformer):
Expand All @@ -16,12 +21,54 @@ def IDENTIFIER(self, token):
)


class Parser: # pylint: disable=too-few-public-methods
examples = {
UnbalancedBrackets: [
"foo(foo [[foo]=]",
"foo([=[foo bar]])",
"foo([=[foo bar ]==])",
"foo(foo [=[foo bar ]==] foo)",
],
UnbalancedParentheses: [
"foo(bar",
"foo(bar\n",
"foo(BAR (BAZ)",
"foo(# )",
"foo(bar))",
"foo)",
"foo(BAR (BAZ)))",
"foo(BAR (BAZ FOO)))",
"foo",
"foo # (",
],
}

def __init__(self):
this_file_dir = os.path.dirname(os.path.realpath(__file__))
self.lark_parser = Lark.open(
grammar_filename=os.path.join(this_file_dir, "cmake.lark"),
parser="lalr",
propagate_positions=True,
maybe_placeholders=False,
transformer=DowncaseIdentifiers(),
)

def _match_parsing_error(self, code, exception):
specific_error = exception.match_examples(self.lark_parser.parse, self.examples)
if not specific_error:
raise GenericParsingError(
exception.get_context(code), exception.line, exception.column
)
raise specific_error(
exception.get_context(code), exception.line, exception.column
)

def parse(self, code):
try:
return self.lark_parser.parse(code)
except UnexpectedInput as u:
self._match_parsing_error(code, u)


def create_parser():
this_file_dir = os.path.dirname(os.path.realpath(__file__))
return Lark.open(
grammar_filename=os.path.join(this_file_dir, "cmake.lark"),
parser="lalr",
propagate_positions=True,
maybe_placeholders=False,
transformer=DowncaseIdentifiers(),
)
return Parser()
38 changes: 21 additions & 17 deletions gersemi/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
import sys
import lark
from gersemi.exceptions import ASTMismatch
from gersemi.exceptions import ASTMismatch, ParsingError


SUCCESS = 0
Expand Down Expand Up @@ -37,28 +37,31 @@ def smart_open(filename, mode, *args, **kwargs):
fh.close()


def fromfile(path):
return "<stdin>" if path == Path("-") else str(path)


def tofile(path):
return "<stdout>" if path == Path("-") else str(path)


class Runner: # pylint: disable=too-few-public-methods
def __init__(self, formatter, args):
self.formatter = formatter
self.args = args

def _check_formatting(self, before, after, filename):
def _check_formatting(self, before, after, path):
if before != after:
if filename == Path("-"):
error("<stdin> would be reformatted")
else:
error(f"{filename} would be reformatted")
error(f"{fromfile(path)} would be reformatted")
return FAIL
return SUCCESS

def _show_diff(self, before, after, filename):
fromfile = "<stdin>" if filename == Path("-") else str(filename)
tofile = "<stdout>" if filename == Path("-") else str(filename)
def _show_diff(self, before, after, path):
diff = unified_diff(
a=f"{before}\n".splitlines(keepends=True),
b=f"{after}\n".splitlines(keepends=True),
fromfile=fromfile,
tofile=tofile,
fromfile=fromfile(path),
tofile=tofile(path),
n=5,
)
self._print("".join(diff), sink=sys.stdout)
Expand All @@ -73,25 +76,26 @@ def _run_on_single_file(self, file_to_format):

try:
formatted_code = self.formatter.format(code_to_format)
except lark.UnexpectedInput as exception:
# TODO detailed error description with match_examples
error(f"Failed to parse {file_to_format}: ", exception)
except ParsingError as exception:
error(f"{fromfile(file_to_format)}{exception}")
return INTERNAL_ERROR
except ASTMismatch:
error(f"Failed to format {file_to_format}: AST mismatch after formatting")
error(
f"Failed to format {fromfile(file_to_format)}: AST mismatch after formatting"
)
return INTERNAL_ERROR
except lark.exceptions.VisitError as exception:
error(f"Runtime error when formatting {file_to_format}: ", exception)
return INTERNAL_ERROR

if self.args.show_diff:
return self._show_diff(
before=code_to_format, after=formatted_code, filename=file_to_format
before=code_to_format, after=formatted_code, path=file_to_format
)

if self.args.check_formatting:
return self._check_formatting(
before=code_to_format, after=formatted_code, filename=file_to_format
before=code_to_format, after=formatted_code, path=file_to_format
)

if self.args.in_place:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="gersemi",
version="0.1.0",
version="0.1.1",
author="Blank Spruce",
author_email="blankspruce@protonmail.com",
description="A formatter to make your CMake code the real treasure",
Expand Down
1 change: 1 addition & 0 deletions stubs/lark/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ class Tree:
def __init__(self, name: str, children: Nodes, meta: Optional[Meta] = ...): ...

class Lark: ...
class UnexpectedInput: ...
31 changes: 29 additions & 2 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import pytest
import lark
from gersemi.exceptions import (
ParsingError,
UnbalancedParentheses,
UnbalancedBrackets,
)
from .tests_generator import generate_input_only_tests


def test_parser(parser, case):
try:
parser.parse(case.content)
except lark.UnexpectedInput:
except ParsingError:
pytest.fail("invalid input to parse")
raise


@pytest.mark.parametrize(
["invalid_code", "expected_exception"],
[
("set(FOO BAR", UnbalancedParentheses),
("message(FOO BAR (BAZ AND FOO)", UnbalancedParentheses),
("set(FOO BAR # )", UnbalancedParentheses),
("bar)", UnbalancedParentheses),
("bar(FOO BAR (BAZ OR FOO)))", UnbalancedParentheses),
("another_command #(", UnbalancedParentheses),
("foo_command", UnbalancedParentheses),
("foo([[foo]=])", UnbalancedBrackets),
("foo([=[bar]])", UnbalancedBrackets),
("foo(arg1 arg2 [==[arg3]===] arg4)", UnbalancedBrackets),
],
)
def test_invalid_code_parsing_error(parser, invalid_code, expected_exception):
try:
parser.parse(invalid_code)
raise AssertionError("Parser should throw an exception")
except ParsingError as e:
assert isinstance(e, expected_exception)


pytest_generate_tests = generate_input_only_tests(
where="parser", input_extension=".cmake",
)

0 comments on commit 7747c1b

Please sign in to comment.