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 2060037 commit 8def6c5
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 45 deletions.
73 changes: 31 additions & 42 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 @@ -933,48 +933,37 @@ def _detect_possible_f_string(self, node: astroid.Const):
Those should probably be f-strings's
"""

def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool:
def detect_if_used_in_format(node: astroid.Const) -> 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
print(node)
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 ?"
# )
if not detect_if_used_in_format(node):
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
10 changes: 7 additions & 3 deletions tests/functional/p/possible_forgotten_f_prefix.py
Expand Up @@ -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

0 comments on commit 8def6c5

Please sign in to comment.