From 79aeb2a80187fe06280286ea718baad66a3d4206 Mon Sep 17 00:00:00 2001 From: gousaiyang Date: Wed, 30 Dec 2020 12:59:49 -0800 Subject: [PATCH] Fix various issues regarding starred expressions - Properly check for starred expression deletion - Check for starred expressions not in tuple/list/set (when not in assignment) - Fix a bug that considered starred expression assignment `[*x] = 1` as invalid - Enhance test cases for valid and invalid `del` statements and starred expressions --- parso/python/errors.py | 95 +++++++++++++++++++++++++------------- test/failing_examples.py | 41 ++++++++++++++++ test/test_python_errors.py | 53 +++++++++++++++++++++ 3 files changed, 157 insertions(+), 32 deletions(-) diff --git a/parso/python/errors.py b/parso/python/errors.py index af601f8..422234b 100644 --- a/parso/python/errors.py +++ b/parso/python/errors.py @@ -147,6 +147,18 @@ def _remove_parens(atom): return atom +def _skip_parens_bottom_up(node): + """ + Returns an ancestor node of an expression, skipping all levels of parens + bottom-up. + """ + while node.parent is not None: + node = node.parent + if node.type != 'atom' or node.children[0] != '(': + return node + return None + + def _iter_params(parent_node): return (n for n in parent_node.children if n.type == 'param') @@ -730,51 +742,66 @@ class _FutureImportRule(SyntaxRule): @ErrorFinder.register_rule(type='star_expr') class _StarExprRule(SyntaxRule): message_iterable_unpacking = "iterable unpacking cannot be used in comprehension" - message_assignment = "can use starred expression only as assignment target" def is_issue(self, node): + def check_delete_starred(node): + while node.parent is not None: + node = node.parent + if node.type == 'del_stmt': + return True + if node.type not in (*_STAR_EXPR_PARENTS, 'atom'): + return False + return False + + if check_delete_starred(node): + if self._normalizer.version >= (3, 9): + self.add_issue(node, message="cannot delete starred") + else: + self.add_issue(node, message="can't use starred expression here") + return + if node.parent.type == 'testlist_comp': # [*[] for a in [1]] if node.parent.children[1].type in _COMP_FOR_TYPES: self.add_issue(node, message=self.message_iterable_unpacking) + return + + ancestor = _skip_parens_bottom_up(node) + # starred expression not in tuple/list/set + if ancestor.type not in (*_STAR_EXPR_PARENTS, 'dictorsetmaker', 'atom'): + self.add_issue(node, message="can't use starred expression here") @ErrorFinder.register_rule(types=_STAR_EXPR_PARENTS) class _StarExprParentRule(SyntaxRule): def is_issue(self, node): - if node.parent.type == 'del_stmt': - if self._normalizer.version >= (3, 9): - self.add_issue(node.parent, message="cannot delete starred") - else: - self.add_issue(node.parent, message="can't use starred expression here") - else: - def is_definition(node, ancestor): - if ancestor is None: - return False + def is_definition(node, ancestor): + if ancestor is None: + return False - type_ = ancestor.type - if type_ == 'trailer': - return False + type_ = ancestor.type + if type_ == 'trailer': + return False - if type_ == 'expr_stmt': - return node.start_pos < ancestor.children[-1].start_pos + if type_ == 'expr_stmt': + return node.start_pos < ancestor.children[-1].start_pos - return is_definition(node, ancestor.parent) + return is_definition(node, ancestor.parent) - if is_definition(node, node.parent): - args = [c for c in node.children if c != ','] - starred = [c for c in args if c.type == 'star_expr'] - if len(starred) > 1: - if self._normalizer.version < (3, 9): - message = "two starred expressions in assignment" - else: - message = "multiple starred expressions in assignment" - self.add_issue(starred[1], message=message) - elif starred: - count = args.index(starred[0]) - if count >= 256: - message = "too many expressions in star-unpacking assignment" - self.add_issue(starred[0], message=message) + if is_definition(node, node.parent): + args = [c for c in node.children if c != ','] + starred = [c for c in args if c.type == 'star_expr'] + if len(starred) > 1: + if self._normalizer.version < (3, 9): + message = "two starred expressions in assignment" + else: + message = "multiple starred expressions in assignment" + self.add_issue(starred[1], message=message) + elif starred: + count = args.index(starred[0]) + if count >= 256: + message = "too many expressions in star-unpacking assignment" + self.add_issue(starred[0], message=message) @ErrorFinder.register_rule(type='annassign') @@ -1079,8 +1106,12 @@ class _CheckAssignmentRule(SyntaxRule): error = "starred" else: self.add_issue(node, message="can't use starred expression here") - elif not search_ancestor(node, *_STAR_EXPR_PARENTS) and not is_aug_assign: - self.add_issue(node, message="starred assignment target must be in a list or tuple") + else: + ancestor = _skip_parens_bottom_up(node) + if ancestor.type not in _STAR_EXPR_PARENTS and not is_aug_assign \ + and not (ancestor.type == 'atom' and ancestor.children[0] == '['): + message = "starred assignment target must be in a list or tuple" + self.add_issue(node, message=message) self._check_assignment(node.children[1]) diff --git a/test/failing_examples.py b/test/failing_examples.py index 8f2edca..a26c690 100644 --- a/test/failing_examples.py +++ b/test/failing_examples.py @@ -145,6 +145,44 @@ FAILING_EXAMPLES = [ '([False], a) = x', 'def x(): from math import *', + # invalid del statements + 'del x + y', + 'del x(y)', + 'async def foo(): del await x', + 'def foo(): del (yield x)', + 'del [x for x in range(10)]', + 'del *x', + 'del *x,', + 'del (*x,)', + 'del [*x]', + 'del x, *y', + 'del *x.y,', + 'del *x[y],', + 'del *x[y::], z', + 'del x, (y, *z)', + 'del (x, *[y, z])', + 'del [x, *(y, [*z])]', + 'del {}', + 'del {x}', + 'del {x, y}', + 'del {x, *y}', + + # invalid starred expressions + '*x', + '(*x)', + '((*x))', + '1 + (*x)', + '*x; 1', + '1; *x', + '1\n*x', + 'x = *y', + 'x: int = *y', + 'def foo(): return *x', + 'def foo(): yield *x', + 'f"{*x}"', + 'for *x in 1: pass', + '[1 for *x in 1]', + # str/bytes combinations '"s" b""', '"s" b"" ""', @@ -198,6 +236,9 @@ FAILING_EXAMPLES = [ '[*[] for a in [1]]', 'async def bla():\n def x(): await bla()', 'del None', + 'del True', + 'del False', + 'del ...', # Errors of global / nonlocal dedent(''' diff --git a/test/test_python_errors.py b/test/test_python_errors.py index fa07d66..490b103 100644 --- a/test/test_python_errors.py +++ b/test/test_python_errors.py @@ -415,11 +415,32 @@ def test_unparenthesized_genexp(source, no_errors): ('*x = 2', False), ('(*y) = 1', False), ('((*z)) = 1', False), + ('*a,', True), + ('*a, = 1', True), + ('(*a,)', True), + ('(*a,) = 1', True), + ('[*a]', True), + ('[*a] = 1', True), + ('a, *b', True), ('a, *b = 1', True), + ('a, *b, c', True), ('a, *b, c = 1', True), + ('a, (*b), c', True), ('a, (*b), c = 1', True), + ('a, ((*b)), c', True), ('a, ((*b)), c = 1', True), + ('a, (*b, c), d', True), ('a, (*b, c), d = 1', True), + ('*a.b,', True), + ('*a.b, = 1', True), + ('*a[b],', True), + ('*a[b], = 1', True), + ('*a[b::], c', True), + ('*a[b::], c = 1', True), + ('(a, *[b, c])', True), + ('(a, *[b, c]) = 1', True), + ('[a, *(b, [*c])]', True), + ('[a, *(b, [*c])] = 1', True), ('[*(1,2,3)]', True), ('{*(1,2,3)}', True), ('[*(1,2,3),]', True), @@ -432,3 +453,35 @@ def test_unparenthesized_genexp(source, no_errors): ) def test_starred_expr(source, no_errors): assert bool(_get_error_list(source, version="3")) ^ no_errors + + +@pytest.mark.parametrize( + 'code', [ + '() = ()', + '() = []', + '[] = ()', + '[] = []', + ] +) +def test_valid_empty_assignment(code): + assert not _get_error_list(code) + + +@pytest.mark.parametrize( + 'code', [ + 'del ()', + 'del []', + 'del x', + 'del x,', + 'del x, y', + 'del (x, y)', + 'del [x, y]', + 'del (x, [y, z])', + 'del x.y, x[y]', + 'del f(x)[y::]', + 'del x[[*y]]', + 'del x[[*y]::]', + ] +) +def test_valid_del(code): + assert not _get_error_list(code)