1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
|
from dataclasses import dataclass
from mypy.nodes import CallExpr, Expression, MemberExpr, NameExpr, OpExpr, UnaryExpr, Var
from refurb.checks.common import extract_binary_oper
from refurb.error import Error
@dataclass
class ErrorInfo(Error):
"""
`startswith()` and `endswith()` both take a tuple, so instead of calling
`startswith()` multiple times on the same string, you can check them all
at once:
Bad:
```
name = "bob"
if name.startswith("b") or name.startswith("B"):
pass
```
Good:
```
name = "bob"
if name.startswith(("b", "B")):
pass
```
"""
name = "use-startswith-endswith-tuple"
code = 102
categories = ("string",)
def are_startswith_or_endswith_calls(
lhs: Expression, rhs: Expression
) -> tuple[str, Expression] | None:
match lhs, rhs:
case (
CallExpr(
callee=MemberExpr(expr=NameExpr(node=Var(type=ty)) as lhs, name=lhs_func),
args=args,
),
CallExpr(callee=MemberExpr(expr=NameExpr() as rhs, name=rhs_func)),
) if (
lhs.fullname == rhs.fullname
and str(ty) in {"builtins.str", "builtins.bytes"}
and lhs_func == rhs_func
and lhs_func in {"startswith", "endswith"}
and args
):
return lhs_func, args[0]
return None
def check(node: OpExpr, errors: list[Error]) -> None:
match extract_binary_oper("or", node):
case (lhs, rhs) if data := are_startswith_or_endswith_calls(lhs, rhs):
func, arg = data
old = f"x.{func}(y) or x.{func}(z)"
new = f"x.{func}((y, z))"
errors.append(ErrorInfo.from_node(arg, msg=f"Replace `{old}` with `{new}`"))
match extract_binary_oper("and", node):
case (
UnaryExpr(op="not", expr=lhs),
UnaryExpr(op="not", expr=rhs),
) if data := are_startswith_or_endswith_calls(lhs, rhs):
func, arg = data
old = f"not x.{func}(y) and not x.{func}(z)"
new = f"not x.{func}((y, z))"
errors.append(
ErrorInfo.from_node(
arg,
msg=f"Replace `{old}` with `{new}`",
)
)
|