Skip to content

Commit 851fea0

Browse files
Use ast.parse() work in progress
1 parent cef34cc commit 851fea0

File tree

3 files changed

+43
-54
lines changed

3 files changed

+43
-54
lines changed

pylint/checkers/strings.py

Lines changed: 28 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
"""Checker for string formatting operations.
3636
"""
37-
37+
import ast
3838
import collections
3939
import numbers
4040
import re
@@ -151,7 +151,7 @@
151151
"E1310": (
152152
"Suspicious argument in %s.%s call",
153153
"bad-str-strip-call",
154-
"The argument to a str.{l,r,}strip call contains a duplicate character, ",
154+
"The argument to a str.{l,r,}strip call contains a duplicate character, ", # pylint: disable=possible-forgotten-f-prefix
155155
),
156156
"W1302": (
157157
"Invalid format string",
@@ -189,7 +189,7 @@
189189
"W1307": (
190190
"Using invalid lookup key %r in format specifier %r",
191191
"invalid-format-index",
192-
"Used when a PEP 3101 format string uses a lookup specifier "
192+
"Used when a PEP 3101 format string uses a lookup specifier " # pylint: disable=possible-forgotten-f-prefix
193193
"({a[1]}), but the argument passed for formatting "
194194
"doesn't contain or doesn't have that key as an attribute.",
195195
),
@@ -932,49 +932,31 @@ def _detect_possible_f_string(self, node: astroid.Const):
932932
"""Check whether strings include local/global variables in '{}'
933933
Those should probably be f-strings's
934934
"""
935-
936-
def detect_if_used_in_format(node: astroid.Const, assign_name: str) -> bool:
937-
"""Check if the node is used in a call to format() if so return True"""
938-
# the skip_class is to make sure we don't go into inner scopes, but might not be needed per se
939-
for attr in node.scope().nodes_of_class(
940-
astroid.Attribute, skip_klass=(astroid.FunctionDef,)
941-
):
942-
if isinstance(attr.expr, astroid.Name):
943-
if attr.expr.name == assign_name and attr.attrname == "format":
944-
return True
945-
return False
946-
947-
if node.pytype() == "builtins.str" and not isinstance(
948-
node.parent, astroid.JoinedStr
949-
):
950-
# Find all pairs of '{}' within a string
951-
inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value)
952-
if len(inner_matches) != len(set(inner_matches)):
953-
return
954-
if inner_matches:
955-
for match in inner_matches:
956-
# Check if match is a local or global variable
957-
if not (
958-
node.scope().locals.get(match) or node.root().locals.get(match)
959-
):
960-
return
961-
assign_node = node
962-
while not isinstance(assign_node, astroid.Assign):
963-
assign_node = assign_node.parent
964-
if isinstance(assign_node.value, astroid.Tuple):
965-
node_index = assign_node.value.elts.index(node)
966-
assign_name = assign_node.targets[0].elts[node_index].name
967-
else:
968-
assign_name = assign_node.targets[0].name
969-
if not detect_if_used_in_format(node, assign_name):
970-
self.add_message(
971-
"possible-forgotten-f-prefix",
972-
line=node.lineno,
973-
node=node,
974-
args=(f"{{{match}}}",),
975-
)
976-
else:
977-
return
935+
# Find all pairs of '{}' within a string
936+
inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value)
937+
if len(inner_matches) != len(set(inner_matches)):
938+
return
939+
if inner_matches:
940+
for match in inner_matches:
941+
try:
942+
ast.parse(match, "<fstring>", "eval")
943+
except SyntaxError:
944+
# Not valid python
945+
continue
946+
# if not isinstance(parsed_match, ast.Expression):
947+
# # Not a proper expression, won't work in f-string
948+
# continue
949+
# for ast_node in ast.walk(parsed_match):
950+
# if isinstance(ast_node, ast.Name):
951+
# print(
952+
# f"TODO check that the name {ast_node.id} exists in the scope ?"
953+
# )
954+
self.add_message(
955+
"possible-forgotten-f-prefix",
956+
line=node.lineno,
957+
node=node,
958+
args=(f"'{{{match}}}'",),
959+
)
978960

979961
def _detect_u_string_prefix(self, node: astroid.Const):
980962
"""Check whether strings include a 'u' prefix like u'String'"""

tests/functional/p/possible_forgotten_f_prefix.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pylint: disable=missing-module-docstring, invalid-name, line-too-long
1+
# pylint: disable=missing-docstring, invalid-name, line-too-long, multiple-statements
22
var = "string"
33
var_two = "extra string"
44

@@ -8,13 +8,17 @@
88
x = "This is a {var} and {var_two} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix]
99
x1, x2, x3 = (1, 2, "This is a {var} which should be a f-string") # [possible-forgotten-f-prefix]
1010

11-
y = "This is a {var} used for formatting later"
11+
y = "This is a {var} used for formatting later" # [possible-forgotten-f-prefix]
1212
z = y.format(var="string")
1313

14-
g = "This is a {another_var} used for formatting later"
15-
h = y.format(another_var="string")
14+
g = "This is a {another_var} used for formatting later" # [possible-forgotten-f-prefix]
15+
h = g.format(another_var="string")
1616

1717
i = "This is {invalid /// python /// inside}"
18+
j = "This is {not */ valid python.}"
19+
k = "This is {def function(): return 42} valid python but not an expression"
20+
21+
def function(): return 42
1822

1923
examples = [var, var_two]
2024
x = f"This is an example with a list: {''.join(examples) + 'well...' }"
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
possible-forgotten-f-prefix:7:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH
2-
possible-forgotten-f-prefix:8:4::The {var_two} syntax imply an f-string but the leading 'f' is missing:HIGH
3-
possible-forgotten-f-prefix:8:4::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH
4-
possible-forgotten-f-prefix:9:20::The {var} syntax imply an f-string but the leading 'f' is missing:HIGH
1+
possible-forgotten-f-prefix:7:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH
2+
possible-forgotten-f-prefix:8:4::The '{var_two}' syntax imply an f-string but the leading 'f' is missing:HIGH
3+
possible-forgotten-f-prefix:8:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH
4+
possible-forgotten-f-prefix:9:20::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH
5+
possible-forgotten-f-prefix:11:4::The '{var}' syntax imply an f-string but the leading 'f' is missing:HIGH
6+
possible-forgotten-f-prefix:14:4::The '{another_var}' syntax imply an f-string but the leading 'f' is missing:HIGH
7+
possible-forgotten-f-prefix:25:4::The '{''.join(examples) + 'well...' }' syntax imply an f-string but the leading 'f' is missing:HIGH

0 commit comments

Comments
 (0)