Skip to content

Commit

Permalink
[ruff] Teach autofix for used-dummy-variable about TypeVars etc. …
Browse files Browse the repository at this point in the history
…(`RUF052`)
  • Loading branch information
AlexWaygood committed Dec 6, 2024
1 parent 4fdd4dd commit 633fb54
Show file tree
Hide file tree
Showing 5 changed files with 580 additions and 0 deletions.
16 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/ruff/RUF052.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,19 @@ def nested():
# unfixable because the rename would shadow a variable from the outer function
_local = "local4"
print(_local)

def special_calls():
from typing import TypeVar, ParamSpec, NamedTuple
from enum import Enum
from collections import namedtuple

_P = ParamSpec("_P")
_T = TypeVar(name="_T", covariant=True, bound=int|str)
_NT = NamedTuple("_NT", [("foo", int)])
_E = Enum("_E", ["a", "b", "c"])
_NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
_NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
_DynamicClass = type("_DynamicClass", (), {})
_NotADynamicClass = type("_NotADynamicClass")

print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
92 changes: 92 additions & 0 deletions crates/ruff_linter/src/renamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;

use ruff_diagnostics::Edit;
use ruff_python_ast as ast;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
use ruff_text_size::Ranged;
Expand Down Expand Up @@ -69,6 +70,11 @@ impl Renamer {
/// example, to rename `pandas` to `pd`, we may need to rewrite `import pandas` to
/// `import pandas as pd`, rather than `import pd`.
///
/// 1. Check to see if the binding is assigned to a known special call where the first argument
/// must be a string that is the same as the binding's name. For example,
/// `T = TypeVar("_T")` will be rejected by a type checker; only `T = TypeVar("T")` will do.
/// If it *is* one of these calls, we rename the relevant argument as well.
///
/// 1. Rename every reference to the [`Binding`]. For example, renaming the references to the
/// `x = 1` binding above would give us:
///
Expand Down Expand Up @@ -198,6 +204,12 @@ impl Renamer {
if let Some(edit) = Renamer::rename_binding(binding, name, target) {
edits.push(edit);

if let Some(edit) =
Renamer::fixup_assigned_value(binding, semantic, stylist, name, target)
{
edits.push(edit);
}

// Rename any delayed annotations.
if let Some(annotations) = semantic.delayed_annotations(binding_id) {
edits.extend(annotations.iter().filter_map(|annotation_id| {
Expand Down Expand Up @@ -231,6 +243,86 @@ impl Renamer {
edits
}

/// If the r.h.s. of a call expression is a call expression,
/// we may need to fixup some arguments passed to that call expression.
///
/// It's impossible to do this entirely rigorously;
/// we only special-case some common standard-library constructors here.
///
/// For example, in this `TypeVar` definition:
/// ```py
/// from typing import TypeVar
///
/// _T = TypeVar("_T")
/// ```
///
/// If we're renaming it from `_T` to `T`, we want this to be the end result:
/// ```py
/// from typing import TypeVar
///
/// T = TypeVar("T")
/// ```
///
/// Not this, which a type checker will reject:
/// ```py
/// from typing import TypeVar
///
/// T = TypeVar("_T")
/// ```
fn fixup_assigned_value(
binding: &Binding,
semantic: &SemanticModel,
stylist: &Stylist,
name: &str,
target: &str,
) -> Option<Edit> {
let statement = binding.statement(semantic)?;

let (ast::Stmt::Assign(ast::StmtAssign { value, .. })
| ast::Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
})) = statement
else {
return None;
};

let ast::ExprCall {
func, arguments, ..
} = value.as_call_expr()?;

let qualified_name = semantic.resolve_qualified_name(func)?;

let name_argument = match qualified_name.segments() {
["collections", "namedtuple"] => arguments.find_argument("typename", 0),

["typing" | "typing_extensions", "TypeVar" | "ParamSpec" | "TypeVarTuple" | "NewType" | "TypeAliasType"] => {
arguments.find_argument("name", 0)
}

["enum", "Enum" | "IntEnum" | "StrEnum" | "ReprEnum" | "Flag" | "IntFlag"]
| ["typing" | "typing_extensions", "NamedTuple" | "TypedDict"] => {
arguments.find_positional(0)
}

["builtins" | "", "type"] if arguments.len() == 3 => arguments.find_positional(0),

_ => None,
}?;

let name_argument = name_argument.as_string_literal_expr()?;

if name_argument.value.to_str() != name {
return None;
}

let quote = stylist.quote();

Some(Edit::range_replacement(
format!("{quote}{target}{quote}"),
name_argument.range(),
))
}

/// Rename a [`Binding`] reference.
fn rename_binding(binding: &Binding, name: &str, target: &str) -> Option<Edit> {
match &binding.kind {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,196 @@ RUF052.py:130:9: RUF052 Local dummy variable `_local` is accessed
131 | print(_local)
|
= help: Prefer using trailing underscores to avoid shadowing a variable

RUF052.py:138:5: RUF052 [*] Local dummy variable `_P` is accessed
|
136 | from collections import namedtuple
137 |
138 | _P = ParamSpec("_P")
| ^^ RUF052
139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 | _NT = NamedTuple("_NT", [("foo", int)])
|
= help: Remove leading underscores

Safe fix
135 135 | from enum import Enum
136 136 | from collections import namedtuple
137 137 |
138 |- _P = ParamSpec("_P")
138 |+ P = ParamSpec("P")
139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 140 | _NT = NamedTuple("_NT", [("foo", int)])
141 141 | _E = Enum("_E", ["a", "b", "c"])
--------------------------------------------------------------------------------
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:139:5: RUF052 [*] Local dummy variable `_T` is accessed
|
138 | _P = ParamSpec("_P")
139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
| ^^ RUF052
140 | _NT = NamedTuple("_NT", [("foo", int)])
141 | _E = Enum("_E", ["a", "b", "c"])
|
= help: Remove leading underscores

Safe fix
136 136 | from collections import namedtuple
137 137 |
138 138 | _P = ParamSpec("_P")
139 |- _T = TypeVar(name="_T", covariant=True, bound=int|str)
139 |+ T = TypeVar(name="T", covariant=True, bound=int|str)
140 140 | _NT = NamedTuple("_NT", [("foo", int)])
141 141 | _E = Enum("_E", ["a", "b", "c"])
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
--------------------------------------------------------------------------------
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:140:5: RUF052 [*] Local dummy variable `_NT` is accessed
|
138 | _P = ParamSpec("_P")
139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 | _NT = NamedTuple("_NT", [("foo", int)])
| ^^^ RUF052
141 | _E = Enum("_E", ["a", "b", "c"])
142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
|
= help: Remove leading underscores

Safe fix
137 137 |
138 138 | _P = ParamSpec("_P")
139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 |- _NT = NamedTuple("_NT", [("foo", int)])
140 |+ NT = NamedTuple("NT", [("foo", int)])
141 141 | _E = Enum("_E", ["a", "b", "c"])
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:141:5: RUF052 [*] Local dummy variable `_E` is accessed
|
139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 | _NT = NamedTuple("_NT", [("foo", int)])
141 | _E = Enum("_E", ["a", "b", "c"])
| ^^ RUF052
142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
|
= help: Remove leading underscores

Safe fix
138 138 | _P = ParamSpec("_P")
139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 140 | _NT = NamedTuple("_NT", [("foo", int)])
141 |- _E = Enum("_E", ["a", "b", "c"])
141 |+ E = Enum("E", ["a", "b", "c"])
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, _NT, E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:142:5: RUF052 [*] Local dummy variable `_NT2` is accessed
|
140 | _NT = NamedTuple("_NT", [("foo", int)])
141 | _E = Enum("_E", ["a", "b", "c"])
142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
| ^^^^ RUF052
143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 | _DynamicClass = type("_DynamicClass", (), {})
|
= help: Remove leading underscores

Safe fix
139 139 | _T = TypeVar(name="_T", covariant=True, bound=int|str)
140 140 | _NT = NamedTuple("_NT", [("foo", int)])
141 141 | _E = Enum("_E", ["a", "b", "c"])
142 |- _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
142 |+ NT2 = namedtuple("NT2", ['x', 'y', 'z'])
143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, _NT, _E, NT2, _NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:143:5: RUF052 [*] Local dummy variable `_NT3` is accessed
|
141 | _E = Enum("_E", ["a", "b", "c"])
142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
| ^^^^ RUF052
144 | _DynamicClass = type("_DynamicClass", (), {})
145 | _NotADynamicClass = type("_NotADynamicClass")
|
= help: Remove leading underscores

Safe fix
140 140 | _NT = NamedTuple("_NT", [("foo", int)])
141 141 | _E = Enum("_E", ["a", "b", "c"])
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 |- _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
143 |+ NT3 = namedtuple(typename="NT3", field_names=['x', 'y', 'z'])
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, _NT, _E, _NT2, NT3, _DynamicClass, _NotADynamicClass)

RUF052.py:144:5: RUF052 [*] Local dummy variable `_DynamicClass` is accessed
|
142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 | _DynamicClass = type("_DynamicClass", (), {})
| ^^^^^^^^^^^^^ RUF052
145 | _NotADynamicClass = type("_NotADynamicClass")
|
= help: Remove leading underscores

Safe fix
141 141 | _E = Enum("_E", ["a", "b", "c"])
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 |- _DynamicClass = type("_DynamicClass", (), {})
144 |+ DynamicClass = type("DynamicClass", (), {})
145 145 | _NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, _NT, _E, _NT2, _NT3, DynamicClass, _NotADynamicClass)

RUF052.py:145:5: RUF052 [*] Local dummy variable `_NotADynamicClass` is accessed
|
143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 | _DynamicClass = type("_DynamicClass", (), {})
145 | _NotADynamicClass = type("_NotADynamicClass")
| ^^^^^^^^^^^^^^^^^ RUF052
146 |
147 | print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
|
= help: Remove leading underscores

Safe fix
142 142 | _NT2 = namedtuple("_NT2", ['x', 'y', 'z'])
143 143 | _NT3 = namedtuple(typename="_NT3", field_names=['x', 'y', 'z'])
144 144 | _DynamicClass = type("_DynamicClass", (), {})
145 |- _NotADynamicClass = type("_NotADynamicClass")
145 |+ NotADynamicClass = type("_NotADynamicClass")
146 146 |
147 |- print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, _NotADynamicClass)
147 |+ print(_T, _P, _NT, _E, _NT2, _NT3, _DynamicClass, NotADynamicClass)
Loading

0 comments on commit 633fb54

Please sign in to comment.