diff --git a/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py new file mode 100644 index 0000000000000..267ae9a38b34f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/type_param_name_mismatch.py @@ -0,0 +1,56 @@ +from typing import TypeVar, ParamSpec, NewType, TypeVarTuple + +# Errors. + +X = TypeVar("T") +X = TypeVar(name="T") + +Y = ParamSpec("T") +Y = ParamSpec(name="T") + +Z = NewType("T", int) +Z = NewType(name="T", tp=int) + +Ws = TypeVarTuple("Ts") +Ws = TypeVarTuple(name="Ts") + +# Non-errors. + +T = TypeVar("T") +T = TypeVar(name="T") + +T = ParamSpec("T") +T = ParamSpec(name="T") + +T = NewType("T", int) +T = NewType(name="T", tp=int) + +Ts = TypeVarTuple("Ts") +Ts = TypeVarTuple(name="Ts") + +# Errors, but not covered by this rule. + +# Non-string literal name. +T = TypeVar(some_str) +T = TypeVar(name=some_str) +T = TypeVar(1) +T = TypeVar(name=1) +T = ParamSpec(some_str) +T = ParamSpec(name=some_str) +T = ParamSpec(1) +T = ParamSpec(name=1) +T = NewType(some_str, int) +T = NewType(name=some_str, tp=int) +T = NewType(1, int) +T = NewType(name=1, tp=int) +Ts = TypeVarTuple(some_str) +Ts = TypeVarTuple(name=some_str) +Ts = TypeVarTuple(1) +Ts = TypeVarTuple(name=1) + +# No names provided. +T = TypeVar() +T = ParamSpec() +T = NewType() +T = NewType(tp=int) +Ts = TypeVarTuple() diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 67752031eb2d1..1ed61170dd8ba 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1655,6 +1655,9 @@ where self.diagnostics.push(diagnostic); } } + if self.settings.rules.enabled(Rule::TypeParamNameMismatch) { + pylint::rules::type_param_name_mismatch(self, value, targets); + } if self.is_stub { if self.any_enabled(&[ Rule::UnprefixedTypeParam, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 5e526f599dbdd..f39106a745412 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -156,6 +156,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyflakes, "901") => (RuleGroup::Unspecified, rules::pyflakes::rules::RaiseNotImplemented), // pylint + (Pylint, "C0132") => (RuleGroup::Unspecified, rules::pylint::rules::TypeParamNameMismatch), (Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots), (Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias), (Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString), diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 4f5a5b2f6a20d..c1bb1ae591082 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -85,6 +85,7 @@ mod tests { Path::new("too_many_return_statements.py") )] #[test_case(Rule::TooManyStatements, Path::new("too_many_statements.py"))] + #[test_case(Rule::TypeParamNameMismatch, Path::new("type_param_name_mismatch.py"))] #[test_case( Rule::UnexpectedSpecialMethodSignature, Path::new("unexpected_special_method_signature.py") diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 84251fb0619f8..431c3fd170c02 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -37,6 +37,7 @@ pub(crate) use too_many_arguments::*; pub(crate) use too_many_branches::*; pub(crate) use too_many_return_statements::*; pub(crate) use too_many_statements::*; +pub(crate) use type_param_name_mismatch::*; pub(crate) use unexpected_special_method_signature::*; pub(crate) use unnecessary_direct_lambda_call::*; pub(crate) use useless_else_on_loop::*; @@ -84,6 +85,7 @@ mod too_many_arguments; mod too_many_branches; mod too_many_return_statements; mod too_many_statements; +mod type_param_name_mismatch; mod unexpected_special_method_signature; mod unnecessary_direct_lambda_call; mod useless_else_on_loop; diff --git a/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs new file mode 100644 index 0000000000000..c7020475ffd59 --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -0,0 +1,166 @@ +use std::fmt; + +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `TypeVar`, `TypeVarTuple`, `ParamSpec`, and `NewType` +/// definitions in which the name of the type parameter does not match the name +/// of the variable to which it is assigned. +/// +/// ## Why is this bad? +/// When defining a `TypeVar` or a related type parameter, Python allows you to +/// provide a name for the type parameter. According to [PEP 484], the name +/// provided to the `TypeVar` constructor must be equal to the name of the +/// variable to which it is assigned. +/// +/// ## Example +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("U") +/// ``` +/// +/// Use instead: +/// ```python +/// from typing import TypeVar +/// +/// T = TypeVar("T") +/// ``` +/// +/// ## References +/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) +/// - [PEP 484 – Type Hints: Generics](https://peps.python.org/pep-0484/#generics) +/// +/// [PEP 484]:https://peps.python.org/pep-0484/#generics +#[violation] +pub struct TypeParamNameMismatch { + kind: VarKind, + var_name: String, + param_name: String, +} + +impl Violation for TypeParamNameMismatch { + #[derive_message_formats] + fn message(&self) -> String { + let TypeParamNameMismatch { + kind, + var_name, + param_name, + } = self; + format!("`{kind}` name `{param_name}` does not match assigned variable name `{var_name}`") + } +} + +/// PLC0132 +pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + let [target] = targets else { + return; + }; + + let Expr::Name(ast::ExprName { id: var_name, .. }) = &target else { + return; + }; + + let Some(param_name) = param_name(value) else { + return; + }; + + if var_name == param_name { + return; + } + + let Expr::Call(ast::ExprCall { func, .. }) = value else { + return; + }; + + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "NewType") + { + Some(VarKind::NewType) + } else { + None + } + }) + else { + return; + }; + + checker.diagnostics.push(Diagnostic::new( + TypeParamNameMismatch { + kind, + var_name: var_name.to_string(), + param_name: param_name.to_string(), + }, + value.range(), + )); +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum VarKind { + TypeVar, + ParamSpec, + TypeVarTuple, + NewType, +} + +impl fmt::Display for VarKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + VarKind::TypeVar => fmt.write_str("TypeVar"), + VarKind::ParamSpec => fmt.write_str("ParamSpec"), + VarKind::TypeVarTuple => fmt.write_str("TypeVarTuple"), + VarKind::NewType => fmt.write_str("NewType"), + } + } +} + +/// Returns the value of the `name` parameter to, e.g., a `TypeVar` constructor. +fn param_name(value: &Expr) -> Option<&str> { + // Handle both `TypeVar("T")` and `TypeVar(name="T")`. + let call = value.as_call_expr()?; + let name_param = call + .keywords + .iter() + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |keyword| keyword.as_str() == "name") + }) + .map(|keyword| &keyword.value) + .or_else(|| call.args.get(0))?; + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(name), + .. + }) = &name_param + { + Some(name) + } else { + None + } +} diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap new file mode 100644 index 0000000000000..44084245c2b75 --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0132_type_param_name_mismatch.py.snap @@ -0,0 +1,76 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +type_param_name_mismatch.py:5:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +3 | # Errors. +4 | +5 | X = TypeVar("T") + | ^^^^^^^^^^^^ PLC0132 +6 | X = TypeVar(name="T") + | + +type_param_name_mismatch.py:6:5: PLC0132 `TypeVar` name `T` does not match assigned variable name `X` + | +5 | X = TypeVar("T") +6 | X = TypeVar(name="T") + | ^^^^^^^^^^^^^^^^^ PLC0132 +7 | +8 | Y = ParamSpec("T") + | + +type_param_name_mismatch.py:8:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | +6 | X = TypeVar(name="T") +7 | +8 | Y = ParamSpec("T") + | ^^^^^^^^^^^^^^ PLC0132 +9 | Y = ParamSpec(name="T") + | + +type_param_name_mismatch.py:9:5: PLC0132 `ParamSpec` name `T` does not match assigned variable name `Y` + | + 8 | Y = ParamSpec("T") + 9 | Y = ParamSpec(name="T") + | ^^^^^^^^^^^^^^^^^^^ PLC0132 +10 | +11 | Z = NewType("T", int) + | + +type_param_name_mismatch.py:11:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | + 9 | Y = ParamSpec(name="T") +10 | +11 | Z = NewType("T", int) + | ^^^^^^^^^^^^^^^^^ PLC0132 +12 | Z = NewType(name="T", tp=int) + | + +type_param_name_mismatch.py:12:5: PLC0132 `NewType` name `T` does not match assigned variable name `Z` + | +11 | Z = NewType("T", int) +12 | Z = NewType(name="T", tp=int) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +13 | +14 | Ws = TypeVarTuple("Ts") + | + +type_param_name_mismatch.py:14:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +12 | Z = NewType(name="T", tp=int) +13 | +14 | Ws = TypeVarTuple("Ts") + | ^^^^^^^^^^^^^^^^^^ PLC0132 +15 | Ws = TypeVarTuple(name="Ts") + | + +type_param_name_mismatch.py:15:6: PLC0132 `TypeVarTuple` name `Ts` does not match assigned variable name `Ws` + | +14 | Ws = TypeVarTuple("Ts") +15 | Ws = TypeVarTuple(name="Ts") + | ^^^^^^^^^^^^^^^^^^^^^^^ PLC0132 +16 | +17 | # Non-errors. + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 064c0f7ec9d2d..3b2252d9875de 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2121,6 +2121,9 @@ "PL", "PLC", "PLC0", + "PLC01", + "PLC013", + "PLC0132", "PLC02", "PLC020", "PLC0205",