From 851fea0ff404b87485c26b1cc94ae5038bf6a56d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 17 Aug 2021 09:43:32 +0200 Subject: [PATCH] Use ast.parse() work in progress --- pylint/checkers/strings.py | 74 +++++++------------ .../p/possible_forgotten_f_prefix.py | 12 ++- .../p/possible_forgotten_f_prefix.txt | 11 ++- 3 files changed, 43 insertions(+), 54 deletions(-) diff --git a/pylint/checkers/strings.py b/pylint/checkers/strings.py index 9f0353c12e9..5991804c2d2 100644 --- a/pylint/checkers/strings.py +++ b/pylint/checkers/strings.py @@ -34,7 +34,7 @@ """Checker for string formatting operations. """ - +import ast import collections import numbers import re @@ -151,7 +151,7 @@ "E1310": ( "Suspicious argument in %s.%s call", "bad-str-strip-call", - "The argument to a str.{l,r,}strip call contains a duplicate character, ", + "The argument to a str.{l,r,}strip call contains a duplicate character, ", # pylint: disable=possible-forgotten-f-prefix ), "W1302": ( "Invalid format string", @@ -189,7 +189,7 @@ "W1307": ( "Using invalid lookup key %r in format specifier %r", "invalid-format-index", - "Used when a PEP 3101 format string uses a lookup specifier " + "Used when a PEP 3101 format string uses a lookup specifier " # pylint: disable=possible-forgotten-f-prefix "({a[1]}), but the argument passed for formatting " "doesn't contain or doesn't have that key as an attribute.", ), @@ -932,49 +932,31 @@ def _detect_possible_f_string(self, node: astroid.Const): """Check whether strings include local/global variables in '{}' Those should probably be f-strings's """ - - def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool: - """Check if the node is used in a call to format() if so return True""" - # the skip_class is to make sure we don't go into inner scopes, but might not be needed per se - for attr in node.scope().nodes_of_class( - astroid.Attribute, skip_klass=(astroid.FunctionDef,) - ): - if isinstance(attr.expr, astroid.Name): - if attr.expr.name == assign_name and attr.attrname == "format": - return True - return False - - if node.pytype() == "builtins.str" and not isinstance( - node.parent, astroid.JoinedStr - ): - # Find all pairs of '{}' within a string - inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) - if len(inner_matches) != len(set(inner_matches)): - return - if inner_matches: - for match in inner_matches: - # Check if match is a local or global variable - if not ( - node.scope().locals.get(match) or node.root().locals.get(match) - ): - return - assign_node = node - while not isinstance(assign_node, astroid.Assign): - assign_node = assign_node.parent - if isinstance(assign_node.value, astroid.Tuple): - node_index = assign_node.value.elts.index(node) - assign_name = assign_node.targets[0].elts[node_index].name - else: - assign_name = assign_node.targets[0].name - if not detect_if_used_in_format(node, assign_name): - self.add_message( - "possible-forgotten-f-prefix", - line=node.lineno, - node=node, - args=(f"{{{match}}}",), - ) - else: - return + # Find all pairs of '{}' within a string + inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value) + if len(inner_matches) != len(set(inner_matches)): + return + if inner_matches: + for match in inner_matches: + try: + ast.parse(match, "", "eval") + except SyntaxError: + # Not valid python + continue + # if not isinstance(parsed_match, ast.Expression): + # # Not a proper expression, won't work in f-string + # continue + # for ast_node in ast.walk(parsed_match): + # if isinstance(ast_node, ast.Name): + # print( + # f"TODO check that the name {ast_node.id} exists in the scope ?" + # ) + self.add_message( + "possible-forgotten-f-prefix", + line=node.lineno, + node=node, + args=(f"'{{{match}}}'",), + ) def _detect_u_string_prefix(self, node: astroid.Const): """Check whether strings include a 'u' prefix like u'String'""" diff --git a/tests/functional/p/possible_forgotten_f_prefix.py b/tests/functional/p/possible_forgotten_f_prefix.py index 2e999b599a9..a9ca52c9dfa 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.py +++ b/tests/functional/p/possible_forgotten_f_prefix.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-module-docstring, invalid-name, line-too-long +# pylint: disable=missing-docstring, invalid-name, line-too-long, multiple-statements var = "string" var_two = "extra string" @@ -8,13 +8,17 @@ x = "This is a {var} and {var_two} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix] x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-forgotten-f-prefix] -y = "This is a {var} used for formatting later" +y = "This is a {var} used for formatting later" # [possible-forgotten-f-prefix] z = y.format(var="string") -g = "This is a {another_var} used for formatting later" -h = y.format(another_var="string") +g = "This is a {another_var} used for formatting later" # [possible-forgotten-f-prefix] +h = g.format(another_var="string") i = "This is {invalid /// python /// inside}" +j = "This is {not */ valid python.}" +k = "This is {def function(): return 42} valid python but not an expression" + +def function(): return 42 examples = [var, var_two] x = f"This is an example with a list: {''.join(examples) + 'well...' }" diff --git a/tests/functional/p/possible_forgotten_f_prefix.txt b/tests/functional/p/possible_forgotten_f_prefix.txt index f252badf139..24e56aa1593 100644 --- a/tests/functional/p/possible_forgotten_f_prefix.txt +++ b/tests/functional/p/possible_forgotten_f_prefix.txt @@ -1,4 +1,7 @@ -possible-forgotten-f-prefix:7:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The {var_two} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:8:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH -possible-forgotten-f-prefix:9:20::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:7:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The '{var_two}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:8:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:9:20::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:11:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:14:4::The '{another_var}' syntax imply an f-string but the leading 'f' is missing:HIGH +possible-forgotten-f-prefix:25:4::The '{''.join(examples) + 'well...' }' syntax imply an f-string but the leading 'f' is missing:HIGH