Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possible-forgotten-f-prefix checker #4787

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
16061e1
Add ``possible-f-string-as-string`` checker
DanielNoord Aug 2, 2021
92b8959
Remove walrus operator
DanielNoord Aug 2, 2021
240b4da
Apply suggestions from code review
DanielNoord Aug 2, 2021
3c2cf24
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2021
f7f4f89
Add review changes
DanielNoord Aug 2, 2021
2259aaf
Merge branch 'main' into f-strings-variable
Pierre-Sassoulas Aug 3, 2021
a75c186
Merge branch 'main' into f-strings-variable
Pierre-Sassoulas Aug 17, 2021
c91a14f
Fix bad conflict resolution
Pierre-Sassoulas Aug 17, 2021
2060037
Add more test cases with invalid python code
Pierre-Sassoulas Aug 17, 2021
8def6c5
Use ast.parse() work in progress
Pierre-Sassoulas Aug 17, 2021
f803caf
Fix pre-commit messages
DanielNoord Aug 17, 2021
971c29d
Update checks and tests
DanielNoord Aug 17, 2021
817bf99
Update tests/functional/p/possible_forgotten_f_prefix.py
DanielNoord Aug 17, 2021
b351fe9
Fix some tests
DanielNoord Aug 18, 2021
e755973
Merge branch 'main' into f-strings-variable
DanielNoord Aug 18, 2021
f042377
Fix tests
DanielNoord Aug 18, 2021
bfd075c
Fix hashing of list
DanielNoord Aug 18, 2021
8e01ff2
Apply suggestions from code review
DanielNoord Aug 18, 2021
5d13efd
Remove non-relevant changes
DanielNoord Aug 18, 2021
c61e834
Merge branch 'f-strings-variable' of https://github.com/DanielNoord/p…
DanielNoord Aug 18, 2021
f64e4fd
Additional tests..
DanielNoord Aug 18, 2021
4851d9e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog
Expand Up @@ -85,6 +85,10 @@ Release date: TBA

Closes #4681

* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}"

Closes #2507

* Fix false positives for ``superfluous-parens`` with walrus operator, ternary operator and inside list comprehension.

Closes #2818
Expand Down
4 changes: 4 additions & 0 deletions doc/whatsnew/2.10.rst
Expand Up @@ -28,6 +28,10 @@ New checkers

Closes #3692

* Added ``possible-forgotten-f-prefix``: Emitted when variables or valid expressions are used in normal strings in between "{}"

Closes #2507

* Added ``use-sequence-for-iteration``: Emitted when iterating over an in-place defined ``set``.


Expand Down
104 changes: 101 additions & 3 deletions pylint/checkers/strings.py
Expand Up @@ -26,19 +26,20 @@
# Copyright (c) 2020 Anthony <tanant@users.noreply.github.com>
# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
# Copyright (c) 2021 Peter Kolbus <peter.kolbus@garmin.com>
# Copyright (c) 2021 Daniel van Noord <13665637+DanielNoord@users.noreply.github.com>
DanielNoord marked this conversation as resolved.
Show resolved Hide resolved


# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE

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

import ast
import collections
import numbers
import re
import tokenize
from typing import TYPE_CHECKING, Iterable
from typing import TYPE_CHECKING, Iterable, Union

import astroid
from astroid import nodes
Expand Down Expand Up @@ -211,6 +212,12 @@
"Used when we detect a string that does not have any interpolation variables, "
"in which case it can be either a normal string without formatting or a bug in the code.",
),
"W1311": (
"The '%s' syntax implies an f-string but the leading 'f' is missing",
"possible-forgotten-f-prefix",
"Used when we detect a string that uses '{}' with a local variable or valid "
"expression inside. This string is probably meant to be an f-string.",
),
}

OTHER_NODES = (
Expand Down Expand Up @@ -913,12 +920,103 @@ def process_non_raw_string_token(
# character can never be the start of a new backslash escape.
index += 2

@check_messages("possible-forgotten-f-prefix")
@check_messages("redundant-u-string-prefix")
def visit_const(self, node: nodes.Const):
if node.pytype() == "builtins.str" and not isinstance(
node.parent, nodes.JoinedStr
):
self._detect_u_string_prefix(node)
self._detect_possible_f_string(node)
self._detect_u_string_prefix(node)

def _detect_possible_f_string(self, node: nodes.Const):
"""Check whether strings include local/global variables in '{}'
Those should probably be f-strings's
"""

def detect_if_used_in_format(node: nodes.Const) -> bool:
"""A helper function that checks if the node is used
in a call to format() if so returns True"""

def get_all_format_calls(node: nodes.Const) -> set:
"""Return a set of all calls to format()"""
calls = set()
# The skip_class is to make sure we don't go into inner scopes
for call in node.scope().nodes_of_class(
nodes.Attribute, skip_klass=(nodes.FunctionDef,)
):
if call.attrname == "format":
if isinstance(call.expr, nodes.Name):
calls.add(call.expr.name)
elif isinstance(call.expr, nodes.Subscript):
slice_repr = [call.expr.value, call.expr.slice.value]
while not isinstance(slice_repr[0], nodes.Name):
slice_repr = [
slice_repr[0].value,
slice_repr[0].slice.value,
] + slice_repr[1:]
calls.add(tuple(slice_repr[0].name) + tuple(slice_repr[1:]))
return calls

def check_match_in_calls(
assignment: Union[nodes.AssignName, nodes.Subscript]
) -> bool:
"""Check if the node to which is being assigned is used in a call to format()"""
format_calls = get_all_format_calls(node)
if isinstance(assignment, nodes.AssignName):
if assignment.name in format_calls:
return True
elif isinstance(assignment, nodes.Subscript):
slice_repr = [assignment.value, assignment.slice.value]
while not isinstance(slice_repr[0], nodes.Name):
slice_repr = [
slice_repr[0].value,
slice_repr[0].slice.value,
] + slice_repr[1:]
if (
tuple(slice_repr[0].name) + tuple(slice_repr[1:])
in format_calls
):
return True
return False

if isinstance(node.parent, nodes.Assign):
return check_match_in_calls(node.parent.targets[0])
if isinstance(node.parent, nodes.Tuple):
node_index = node.parent.elts.index(node)
return check_match_in_calls(
node.parent.parent.targets[0].elts[node_index]
)
return False

# Find all pairs of '{}' within a string
inner_matches = re.findall(r"(?<=\{).*?(?=\})", node.value)

# If a variable is used twice it is probably used for formatting later on
if len(inner_matches) != len(set(inner_matches)):
return

if (
isinstance(node.parent, nodes.Attribute)
and node.parent.attrname == "format"
):
return

if inner_matches:
for match in inner_matches:
try:
ast.parse(match, "<fstring>", "eval")
except SyntaxError:
# Not valid python
continue

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: nodes.Const):
"""Check whether strings include a 'u' prefix like u'String'"""
Expand Down
3 changes: 3 additions & 0 deletions pylint/reporters/text.py
Expand Up @@ -129,6 +129,7 @@ class TextReporter(BaseReporter):
__implements__ = IReporter
name = "text"
extension = "txt"
# pylint: disable-next=possible-forgotten-f-prefix
line_format = "{path}:{line}:{column}: {msg_id}: {msg} ({symbol})"

def __init__(self, output=None):
Expand Down Expand Up @@ -167,6 +168,7 @@ class ParseableTextReporter(TextReporter):
"""

name = "parseable"
# pylint: disable-next=possible-forgotten-f-prefix
line_format = "{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"

def __init__(self, output=None):
Expand All @@ -182,6 +184,7 @@ class VSTextReporter(ParseableTextReporter):
"""Visual studio text reporter"""

name = "msvs"
# pylint: disable-next=possible-forgotten-f-prefix
line_format = "{path}({line}): [{msg_id}({symbol}){obj}] {msg}"


Expand Down
84 changes: 84 additions & 0 deletions tests/functional/p/possible_forgotten_f_prefix.py
@@ -0,0 +1,84 @@
"""Check various forms of strings which could be f-strings without a prefix"""
# pylint: disable=invalid-name, line-too-long, pointless-string-statement, pointless-statement
# pylint: disable=missing-function-docstring, missing-class-docstring, too-few-public-methods
# pylint: disable=useless-object-inheritance

# Check for local variable interpolation
PARAM = "string"
PARAM_TWO = "extra string"

A = f"This is a {PARAM} which should be a f-string"
B = "This is a {PARAM} used twice, see {PARAM}"
Pierre-Sassoulas marked this conversation as resolved.
Show resolved Hide resolved
C = "This is a {PARAM} which should be a f-string" # [possible-forgotten-f-prefix]
D = "This is a {PARAM} and {PARAM_TWO} which should be a f-string" # [possible-forgotten-f-prefix, possible-forgotten-f-prefix]
E1, E2, E3 = (1, 2, "This is a {PARAM} which should be a f-string") # [possible-forgotten-f-prefix]

# Check for use of .format()
F = "This is a {parameter} used for formatting later"
F.format(parameter="string")
G = F.format(parameter="string")
"{0}, {1}".format(1, 2)
H = "{0}, {1}".format(1, 2)
I1, I2, I3 = (1, 2, "This is a {PARAM} which is later formatted")
I3.format(PARAM=PARAM)

J = {"key_one": "", "key_two": {"inner_key": ""}}
J["key_one"] = "This is a {parameter} used for formatting later"
J["key_one"].format(parameter=PARAM)
K = J["key_one"].format(parameter=PARAM)
J["key_two"]["inner_key"] = "This is a {parameter} used for formatting later"
J["key_two"]["inner_key"].format(parameter=PARAM)
L = J["key_two"]["inner_key"].format(parameter=PARAM)

M = "This is a {parameter} used for formatting later"

def func_one():
return "{0}, {1}".format(1, 2)


def func_two():
x = "{0}, {1}"
return x.format(1, 2)


def func_three():
x = M.format(parameter=PARAM)
return x

class Class(object):
attr = 0

def __str__(self):
return "{self.attr}".format(self=self)


# Check for use of variables within functions
PARAM_LIST = [PARAM, PARAM_TWO]
N = f"This is an example with a list: {''.join(PARAM_LIST) + 'well...'}"
O = "This is an example with a list: {''.join(PARAM_LIST) + 'well...'}" # [possible-forgotten-f-prefix]

# Check for calculations without variables
P = f"This is a calculation: {1 + 1}"
Q = "This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix]

# Check invalid Python code
R = "This is {invalid /// python /// inside}"
S = "This is {not */ valid python.}"
T = "This is {def function(): return 42} valid python but not an expression"

# Check strings without assignment
PARAM_THREE = "string"
f"This is a {PARAM_THREE}"
"This is a string"
"This is a {PARAM_THREE}" # [possible-forgotten-f-prefix]

f"This is a calculation: {1 + 1}"
"This is a calculation: 1 + 1"
"This is a calculation: {1 + 1}" # [possible-forgotten-f-prefix]

f"This is a {'nice' + PARAM_THREE}"
"This is a nice string"
"This is a {'nice' + PARAM_THREE}" # [possible-forgotten-f-prefix]

# Check raw strings
U = r"a{1}"
9 changes: 9 additions & 0 deletions tests/functional/p/possible_forgotten_f_prefix.txt
@@ -0,0 +1,9 @@
possible-forgotten-f-prefix:12:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:13:4::The '{PARAM_TWO}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:13:4::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:14:20::The '{PARAM}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:53:4::The '{''.join(PARAM_LIST) + 'well...'}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:57:4::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:68:0::The '{PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:72:0::The '{1 + 1}' syntax implies an f-string but the leading 'f' is missing:HIGH
possible-forgotten-f-prefix:76:0::The '{'nice' + PARAM_THREE}' syntax implies an f-string but the leading 'f' is missing:HIGH