Skip to content

Commit

Permalink
Add use-str-func check
Browse files Browse the repository at this point in the history
  • Loading branch information
dosisod committed Nov 20, 2023
1 parent a25b5d6 commit 0c22af3
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 4 deletions.
68 changes: 68 additions & 0 deletions refurb/checks/readability/use_str_func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from dataclasses import dataclass

from mypy.nodes import CallExpr, ListExpr, MemberExpr, NameExpr, StrExpr

from refurb.checks.common import stringify
from refurb.error import Error
from refurb.checks.string.use_fstring_fmt import CONVERSIONS as FURB_119_FUNCS


@dataclass
class ErrorInfo(Error):
"""
If you want to stringify a single value without concatenating anything, use
the `str()` function instead.
Bad:
```
nums = [123, 456]
num = f"{num[0]}")
```
Good:
```
nums = [123, 456]
num = str(num[0])
```
"""

name = "use-str-func"
code = 183
categories = ("readability",)


ignore = set[int]()


def check(node: CallExpr, errors: list[Error]) -> None:
if id(node) in ignore:
return

match node:
case CallExpr(
callee=MemberExpr(
expr=StrExpr(value=""),
name="join",
),
args=[ListExpr(items=items)],
):
ignore.update(id(item) for item in items)

case CallExpr(
callee=MemberExpr(
expr=StrExpr(value="{:{}}"),
name="format",
),
args=[arg, StrExpr(value="")],
):
match arg:
case CallExpr(callee=NameExpr(fullname=fn)) if fn in FURB_119_FUNCS:
return

x = stringify(arg)

msg = f'Replace `f"{{{x}}}"` with `str({x})`'

errors.append(ErrorInfo.from_node(node, msg))
6 changes: 3 additions & 3 deletions refurb/visitor/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ def __init__(self, checks: defaultdict[type[Node], list[Check]], settings: Setti
setattr(self, name, func.__get__(self))

def visit_call_expr(self, o: CallExpr) -> None:
for check in self.checks[CallExpr]:
self.run_check(o, check)

for arg in o.args:
self.accept(arg)

self.accept(o.callee)

for check in self.checks[CallExpr]:
self.run_check(o, check)

def run_check(self, node: Node, check: Check) -> None:
# Hack: use the type annotations to check if the function takes 2 or
# 3 arguments. There is an extra field for return types, hence why we
Expand Down
2 changes: 1 addition & 1 deletion test/data/err_119.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

# these will not

f"{123}"
f"{123}" # noqa: FURB183

f"{0b1010:b}"

Expand Down
12 changes: 12 additions & 0 deletions test/data/err_183.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
x = " "

# these should match

f"{x}"
f"{123}"


# these should not

f"hello{x}world"
f"{x} {x}"
2 changes: 2 additions & 0 deletions test/data/err_183.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test/data/err_183.py:5:1 [FURB183]: Replace `f"{x}"` with `str(x)`
test/data/err_183.py:6:1 [FURB183]: Replace `f"{123}"` with `str(123)`

0 comments on commit 0c22af3

Please sign in to comment.