Skip to content

Commit 7915f10

Browse files
committed
[ruff] Classes with mixed type variable style (RUF060)
1 parent fab86de commit 7915f10

File tree

11 files changed

+796
-32
lines changed

11 files changed

+796
-32
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from typing import Generic, ParamSpec, TypeVar, TypeVarTuple, Unpack
2+
3+
4+
_A = TypeVar('_A')
5+
_B = TypeVar('_B', bound=int)
6+
_C = TypeVar('_C', str, bytes)
7+
_D = TypeVar('_D', default=int)
8+
_E = TypeVar('_E', bound=int, default=int)
9+
_F = TypeVar('_F', str, bytes, default=str)
10+
11+
_As = TypeVarTuple('_As')
12+
_Bs = TypeVarTuple('_Bs', bound=tuple[int, str])
13+
_Cs = TypeVarTuple('_Cs', default=tuple[int, str])
14+
15+
16+
_P1 = ParamSpec('_P1')
17+
_P2 = ParamSpec('_P2', bound=[bytes, bool])
18+
_P3 = ParamSpec('_P3', default=[int, str])
19+
20+
21+
### Errors
22+
23+
class C[T](Generic[_A]): ...
24+
class C[T](Generic[_B], str): ...
25+
class C[T](int, Generic[_C]): ...
26+
class C[T](bytes, Generic[_D], bool): ... # TODO: Type parameter defaults
27+
class C[T](Generic[_E], list[_E]): ... # TODO: Type parameter defaults
28+
class C[T](list[_F], Generic[_F]): ... # TODO: Type parameter defaults
29+
30+
class C[*Ts](Generic[*_As]): ...
31+
class C[*Ts](Generic[Unpack[_As]]): ...
32+
class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ...
33+
class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults
34+
35+
36+
class C[**P](Generic[_P1]): ...
37+
class C[**P](Generic[_P2]): ...
38+
class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults
39+
40+
41+
class C[T](Generic[T, _A]): ...
42+
43+
44+
# See `is_existing_param_of_same_class`.
45+
# `expr_name_to_type_var` doesn't handle named expressions,
46+
# only simple assignments, so there is no fix.
47+
class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ...
48+
49+
50+
class C(Generic[_B]):
51+
class D[T](Generic[_B, T]): ...
52+
53+
54+
class C[T]:
55+
class D[U](Generic[T, U]): ...
56+
57+
58+
# In a single run, only the first is reported.
59+
# Others will be reported/fixed in following iterations.
60+
class C[T](Generic[_C], Generic[_D]): ...
61+
class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults
62+
63+
64+
class C[
65+
T # Comment
66+
](Generic[_E]): ... # TODO: Type parameter defaults
67+
68+
69+
class C[T](Generic[Generic[_F]]): ...
70+
class C[T](Generic[Unpack[_A]]): ...
71+
class C[T](Generic[Unpack[_P1]]): ...
72+
class C[T](Generic[Unpack[Unpack[_P2]]]): ...
73+
class C[T](Generic[Unpack[*_As]]): ...
74+
class C[T](Generic[Unpack[_As, _Bs]]): ...
75+
76+
77+
class C[T](Generic[_A, _A]): ...
78+
class C[T](Generic[_A, Unpack[_As]]): ...
79+
class C[T](Generic[*_As, _A]): ...
80+
81+
82+
### No errors
83+
84+
class C(Generic[_A]): ...
85+
class C[_A]: ...
86+
class C[_A](list[_A]): ...
87+
class C[_A](list[Generic[_A]]): ...

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

+3
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
563563
if checker.enabled(Rule::NonPEP695GenericClass) {
564564
pyupgrade::rules::non_pep695_generic_class(checker, class_def);
565565
}
566+
if checker.enabled(Rule::ClassWithMixedTypeVars) {
567+
ruff::rules::class_with_mixed_type_vars(checker, class_def);
568+
}
566569
}
567570
Stmt::Import(ast::StmtImport { names, range: _ }) => {
568571
if checker.enabled(Rule::MultipleImportsOnOneLine) {

crates/ruff_linter/src/codes.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10081008
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
10091009
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
10101010
(Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip),
1011+
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
10111012
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
10121013
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
10131014

crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ mod native_literals;
5757
mod open_alias;
5858
mod os_error_alias;
5959
mod outdated_version_block;
60-
mod pep695;
60+
pub(crate) mod pep695;
6161
mod printf_string_formatting;
6262
mod quoted_annotation;
6363
mod redundant_open_modes;

crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs

+56-13
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ruff_python_ast::{
99
self as ast,
1010
name::Name,
1111
visitor::{self, Visitor},
12-
Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
12+
Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign, TypeParam,
1313
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
1414
};
1515
use ruff_python_semantic::SemanticModel;
@@ -26,7 +26,7 @@ mod non_pep695_generic_function;
2626
mod non_pep695_type_alias;
2727

2828
#[derive(Debug)]
29-
enum TypeVarRestriction<'a> {
29+
pub(crate) enum TypeVarRestriction<'a> {
3030
/// A type variable with a bound, e.g., `TypeVar("T", bound=int)`.
3131
Bound(&'a Expr),
3232
/// A type variable with constraints, e.g., `TypeVar("T", int, str)`.
@@ -37,25 +37,25 @@ enum TypeVarRestriction<'a> {
3737
}
3838

3939
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
40-
enum TypeParamKind {
40+
pub(crate) enum TypeParamKind {
4141
TypeVar,
4242
TypeVarTuple,
4343
ParamSpec,
4444
}
4545

4646
#[derive(Debug)]
47-
struct TypeVar<'a> {
48-
name: &'a str,
49-
restriction: Option<TypeVarRestriction<'a>>,
50-
kind: TypeParamKind,
51-
default: Option<&'a Expr>,
47+
pub(crate) struct TypeVar<'a> {
48+
pub(crate) name: &'a str,
49+
pub(crate) restriction: Option<TypeVarRestriction<'a>>,
50+
pub(crate) kind: TypeParamKind,
51+
pub(crate) default: Option<&'a Expr>,
5252
}
5353

5454
/// Wrapper for formatting a sequence of [`TypeVar`]s for use as a generic type parameter (e.g. `[T,
5555
/// *Ts, **P]`). See [`DisplayTypeVar`] for further details.
56-
struct DisplayTypeVars<'a> {
57-
type_vars: &'a [TypeVar<'a>],
58-
source: &'a str,
56+
pub(crate) struct DisplayTypeVars<'a> {
57+
pub(crate) type_vars: &'a [TypeVar<'a>],
58+
pub(crate) source: &'a str,
5959
}
6060

6161
impl Display for DisplayTypeVars<'_> {
@@ -79,7 +79,7 @@ impl Display for DisplayTypeVars<'_> {
7979

8080
/// Used for displaying `type_var`. `source` is the whole file, which will be sliced to recover the
8181
/// `TypeVarRestriction` values for generic bounds and constraints.
82-
struct DisplayTypeVar<'a> {
82+
pub(crate) struct DisplayTypeVar<'a> {
8383
type_var: &'a TypeVar<'a>,
8484
source: &'a str,
8585
}
@@ -190,6 +190,34 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
190190
}
191191
}
192192

193+
impl<'a> From<&'a TypeParam> for TypeVar<'a> {
194+
fn from(param: &'a TypeParam) -> Self {
195+
let (kind, restriction) = match param {
196+
TypeParam::TypeVarTuple(_) => (TypeParamKind::TypeVarTuple, None),
197+
TypeParam::ParamSpec(_) => (TypeParamKind::ParamSpec, None),
198+
199+
TypeParam::TypeVar(param) => {
200+
let restriction = match param.bound.as_deref() {
201+
None => None,
202+
Some(Expr::Tuple(constraints)) => Some(TypeVarRestriction::Constraint(
203+
constraints.elts.iter().collect::<Vec<_>>(),
204+
)),
205+
Some(bound) => Some(TypeVarRestriction::Bound(bound)),
206+
};
207+
208+
(TypeParamKind::TypeVar, restriction)
209+
}
210+
};
211+
212+
Self {
213+
name: param.name(),
214+
kind,
215+
restriction,
216+
default: param.default(),
217+
}
218+
}
219+
}
220+
193221
struct TypeVarReferenceVisitor<'a> {
194222
vars: Vec<TypeVar<'a>>,
195223
semantic: &'a SemanticModel<'a>,
@@ -240,7 +268,7 @@ impl<'a> Visitor<'a> for TypeVarReferenceVisitor<'a> {
240268
}
241269
}
242270

243-
fn expr_name_to_type_var<'a>(
271+
pub(crate) fn expr_name_to_type_var<'a>(
244272
semantic: &'a SemanticModel,
245273
name: &'a ExprName,
246274
) -> Option<TypeVar<'a>> {
@@ -347,3 +375,18 @@ fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
347375
== vars.len())
348376
.then_some(vars)
349377
}
378+
379+
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
380+
/// any), along with its index in the class's bases tuple.
381+
pub(crate) fn find_generic<'a>(
382+
class_bases: &'a Arguments,
383+
semantic: &SemanticModel,
384+
) -> Option<(usize, &'a ExprSubscript)> {
385+
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
386+
expr.as_subscript_expr().and_then(|sub_expr| {
387+
semantic
388+
.match_typing_expr(&sub_expr.value, "Generic")
389+
.then_some((idx, sub_expr))
390+
})
391+
})
392+
}

crates/ruff_linter/src/rules/pyupgrade/rules/pep695/non_pep695_generic_class.rs

+4-18
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
33
use ruff_python_ast::visitor::Visitor;
4-
use ruff_python_ast::{Arguments, ExprSubscript, StmtClassDef};
5-
use ruff_python_semantic::SemanticModel;
4+
use ruff_python_ast::{ExprSubscript, StmtClassDef};
65
use ruff_text_size::Ranged;
76

87
use crate::checkers::ast::Checker;
98
use crate::fix::edits::{remove_argument, Parentheses};
109
use crate::settings::types::PythonVersion;
1110

12-
use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor};
11+
use super::{
12+
check_type_vars, find_generic, in_nested_context, DisplayTypeVars, TypeVarReferenceVisitor,
13+
};
1314

1415
/// ## What it does
1516
///
@@ -203,18 +204,3 @@ pub(crate) fn non_pep695_generic_class(checker: &mut Checker, class_def: &StmtCl
203204

204205
checker.diagnostics.push(diagnostic);
205206
}
206-
207-
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if
208-
/// any), along with its index in the class's bases tuple.
209-
fn find_generic<'a>(
210-
class_bases: &'a Arguments,
211-
semantic: &SemanticModel,
212-
) -> Option<(usize, &'a ExprSubscript)> {
213-
class_bases.args.iter().enumerate().find_map(|(idx, expr)| {
214-
expr.as_subscript_expr().and_then(|sub_expr| {
215-
semantic
216-
.match_typing_expr(&sub_expr.value, "Generic")
217-
.then_some((idx, sub_expr))
218-
})
219-
})
220-
}

crates/ruff_linter/src/rules/ruff/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ mod tests {
429429
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
430430
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
431431
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
432+
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF060.py"))]
432433
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
433434
let snapshot = format!(
434435
"preview__{}_{}",

0 commit comments

Comments
 (0)