From 4ff14f50946e02671034948ad346d313bb59ea47 Mon Sep 17 00:00:00 2001 From: adam j hartz Date: Fri, 19 Aug 2016 15:11:34 -0400 Subject: [PATCH] first go at new resolution for additional syntax errors --- xonsh/parsers/base.py | 3 ++ xonsh/parsers/context_check.py | 81 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 xonsh/parsers/context_check.py diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py index a2e037430..b56e2ae1b 100644 --- a/xonsh/parsers/base.py +++ b/xonsh/parsers/base.py @@ -17,6 +17,7 @@ from xonsh.lexer import Lexer, LexToken from xonsh.platform import PYTHON_VERSION_INFO from xonsh.tokenize import SearchPath from xonsh.lazyasd import LazyObject +from xonsh.parsers.context_check import check_contexts RE_SEARCHPATH = LazyObject(lambda: re.compile(SearchPath), globals(), 'RE_SEARCHPATH') @@ -309,6 +310,8 @@ class BaseParser(object): while self.parser is None: time.sleep(0.01) # block until the parser is ready tree = self.parser.parse(input=s, lexer=self.lexer, debug=debug_level) + if tree is not None: + check_contexts(tree) # hack for getting modes right if mode == 'single': if isinstance(tree, ast.Expression): diff --git a/xonsh/parsers/context_check.py b/xonsh/parsers/context_check.py new file mode 100644 index 000000000..74b6ed1a3 --- /dev/null +++ b/xonsh/parsers/context_check.py @@ -0,0 +1,81 @@ +import ast +import keyword +import collections + +_all_keywords = frozenset(keyword.kwlist) + +def _not_assignable(x, augassign=False): + """ + If ``x`` represents a value that can be assigned to, return ``None``. + Otherwise, return a string describing the object. For use in generating + meaningful syntax errors. + """ + if augassign and isinstance(x, (ast.Tuple, ast.List)): + return 'literal' + elif isinstance(x, (ast.Tuple, ast.List)): + if len(x.elts) == 0: + return '()' + for i in x.elts: + res = _not_assignable(i) + if res is not None: + return res + elif isinstance(x, (ast.Set, ast.Dict, ast.Num, ast.Str, ast.Bytes)): + return 'literal' + elif isinstance(x, ast.Call): + return 'function call' + elif isinstance(x, ast.Lambda): + return 'lambda' + elif isinstance(x, (ast.BoolOp, ast.BinOp, ast.UnaryOp)): + return 'operator' + elif isinstance(x, ast.IfExp): + return 'conditional expression' + elif isinstance(x, ast.ListComp): + return 'list comprehension' + elif isinstance(x, ast.DictComp): + return 'dictionary comprehension' + elif isinstance(x, ast.SetComp): + return 'set comprehension' + elif isinstance(x, ast.GeneratorExp): + return 'generator expression' + elif isinstance(x, ast.Compare): + return 'comparison' + elif isinstance(x, ast.Name) and x.id in _all_keywords: + return 'keyword' + elif isinstance(x, ast.NameConstant): + return 'keyword' + +_loc = collections.namedtuple('_loc', ['lineno', 'column']) + +def check_contexts(tree): + c = ContextCheckingVisitor() + c.visit(tree) + if c.error is not None: + e = SyntaxError(c.error[0]) + e.loc = _loc(c.error[1], c.error[2]) + raise e + +class ContextCheckingVisitor(ast.NodeVisitor): + def __init__(self): + self.error = None + + def visit_Delete(self, node): + for i in node.targets: + err = _not_assignable(i) + if err is not None: + msg = "can't delete {}".format(err) + self.error = msg, i.lineno, i.col_offset + break + + def visit_Assign(self, node): + for i in node.targets: + err = _not_assignable(i) + if err is not None: + msg = "can't assign to {}".format(err) + self.error = msg, i.lineno, i.col_offset + break + + def visit_AugAssign(self, node): + err = _not_assignable(node.target, True) + if err is not None: + msg = "illegal target for augmented assignment: {}".format(err) + self.error = msg, node.target.lineno, node.target.col_offset