Skip to content

Commit

Permalink
Use ast.parse() work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre-Sassoulas committed Aug 17, 2021
1 parent cef34cc commit 851fea0
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 54 deletions.
74 changes: 28 additions & 46 deletions pylint/checkers/strings.py
Expand Up @@ -34,7 +34,7 @@

"""Checker for string formatting operations.
"""

import ast
import collections
import numbers
import re
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
),
Expand Down Expand Up @@ -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, "<fstring>", "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'"""
Expand Down
12 changes: 8 additions & 4 deletions 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"

Expand All @@ -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...' }"
Expand Down
11 changes: 7 additions & 4 deletions 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

0 comments on commit 851fea0

Please sign in to comment.