From d1ad8730e36819f787c720d1917de22b59806a75 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Fri, 14 Aug 2020 03:20:46 +0100 Subject: [PATCH] don't strip brackets before lsqb (#1575) (#1590) if the string contains a PERCENT, it's not safe to remove brackets that follow and operator with the same or higher precedence than PERCENT --- src/black/__init__.py | 45 +++++++++++++++++++++++++++++--- tests/data/percent_precedence.py | 39 +++++++++++++++++++++++++++ tests/test_black.py | 8 ++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/data/percent_precedence.py diff --git a/src/black/__init__.py b/src/black/__init__.py index 36c03946b55..b660e4788b4 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -3249,7 +3249,9 @@ class StringParenStripper(StringTransformer): Requirements: The line contains a string which is surrounded by parentheses and: - The target string is NOT the only argument to a function call). - - The RPAR is NOT followed by an attribute access (i.e. a dot). + - If the target string contains a PERCENT, the brackets are not + preceeded or followed by an operator with higher precedence than + PERCENT. Transformations: The parentheses mentioned in the 'Requirements' section are stripped. @@ -3292,14 +3294,51 @@ def do_match(self, line: Line) -> TMatchResult: string_parser = StringParser() next_idx = string_parser.parse(LL, string_idx) + # if the leaves in the parsed string include a PERCENT, we need to + # make sure the initial LPAR is NOT preceded by an operator with + # higher or equal precedence to PERCENT + if ( + is_valid_index(idx - 2) + and token.PERCENT in {leaf.type for leaf in LL[idx - 1 : next_idx]} + and ( + ( + LL[idx - 2].type + in { + token.STAR, + token.AT, + token.SLASH, + token.DOUBLESLASH, + token.PERCENT, + token.TILDE, + token.DOUBLESTAR, + token.AWAIT, + token.LSQB, + token.LPAR, + } + ) + or ( + # only unary PLUS/MINUS + not is_valid_index(idx - 3) + and (LL[idx - 2].type in {token.PLUS, token.MINUS}) + ) + ) + ): + continue + # Should be followed by a non-empty RPAR... if ( is_valid_index(next_idx) and LL[next_idx].type == token.RPAR and not is_empty_rpar(LL[next_idx]) ): - # That RPAR should NOT be followed by a '.' symbol. - if is_valid_index(next_idx + 1) and LL[next_idx + 1].type == token.DOT: + # That RPAR should NOT be followed by anything with higher + # precedence than PERCENT + if is_valid_index(next_idx + 1) and LL[next_idx + 1].type in { + token.DOUBLESTAR, + token.LSQB, + token.LPAR, + token.DOT, + }: continue return Ok(string_idx) diff --git a/tests/data/percent_precedence.py b/tests/data/percent_precedence.py new file mode 100644 index 00000000000..44d30f16e03 --- /dev/null +++ b/tests/data/percent_precedence.py @@ -0,0 +1,39 @@ +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + ("" % a) +-("" % a) +b - ("" % a) +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) +# output +("" % a) ** 2 +("" % a)[0] +("" % a)() +("" % a).b + +2 * ("" % a) +2 @ ("" % a) +2 / ("" % a) +2 // ("" % a) +2 % ("" % a) ++("" % a) +b + "" % a +-("" % a) +b - "" % a +~("" % a) +2 ** ("" % a) +await ("" % a) +b[("" % a)] +b(("" % a)) diff --git a/tests/test_black.py b/tests/test_black.py index f4055581f86..3c766330a08 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -501,6 +501,14 @@ def test_slices(self) -> None: black.assert_equivalent(source, actual) black.assert_stable(source, actual, black.FileMode()) + @patch("black.dump_to_file", dump_to_stderr) + def test_percent_precedence(self) -> None: + source, expected = read_data("percent_precedence") + actual = fs(source) + self.assertFormatEqual(expected, actual) + black.assert_equivalent(source, actual) + black.assert_stable(source, actual, black.FileMode()) + @patch("black.dump_to_file", dump_to_stderr) def test_comments(self) -> None: source, expected = read_data("comments")