-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Summary Implement Pylint `typevar-double-variance` (`C0131`) as `type-bivariance` (`PLC0131`). Includes documentation. Related to #970. Renamed the rule to be more clear (it's not immediately obvious what 'double' means, IMO). The Pylint implementation checks only `TypeVar`, but this PR checks `ParamSpec` as well. ## Test Plan Added tests. `cargo test`
- Loading branch information
Showing
10 changed files
with
270 additions
and
33 deletions.
There are no files selected for viewing
37 changes: 37 additions & 0 deletions
37
crates/ruff/resources/test/fixtures/pylint/type_bivariance.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from typing import ParamSpec, TypeVar | ||
|
||
# Errors. | ||
|
||
T = TypeVar("T", covariant=True, contravariant=True) | ||
T = TypeVar(name="T", covariant=True, contravariant=True) | ||
|
||
T = ParamSpec("T", covariant=True, contravariant=True) | ||
T = ParamSpec(name="T", covariant=True, contravariant=True) | ||
|
||
# Non-errors. | ||
|
||
T = TypeVar("T") | ||
T = TypeVar("T", covariant=False) | ||
T = TypeVar("T", contravariant=False) | ||
T = TypeVar("T", covariant=False, contravariant=False) | ||
T = TypeVar("T", covariant=True) | ||
T = TypeVar("T", covariant=True, contravariant=False) | ||
T = TypeVar(name="T", covariant=True, contravariant=False) | ||
T = TypeVar(name="T", covariant=True) | ||
T = TypeVar("T", contravariant=True) | ||
T = TypeVar("T", covariant=False, contravariant=True) | ||
T = TypeVar(name="T", covariant=False, contravariant=True) | ||
T = TypeVar(name="T", contravariant=True) | ||
|
||
T = ParamSpec("T") | ||
T = ParamSpec("T", covariant=False) | ||
T = ParamSpec("T", contravariant=False) | ||
T = ParamSpec("T", covariant=False, contravariant=False) | ||
T = ParamSpec("T", covariant=True) | ||
T = ParamSpec("T", covariant=True, contravariant=False) | ||
T = ParamSpec(name="T", covariant=True, contravariant=False) | ||
T = ParamSpec(name="T", covariant=True) | ||
T = ParamSpec("T", contravariant=True) | ||
T = ParamSpec("T", covariant=False, contravariant=True) | ||
T = ParamSpec(name="T", covariant=False, contravariant=True) | ||
T = ParamSpec(name="T", contravariant=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
use std::fmt; | ||
|
||
use rustpython_parser::ast::{self, Expr, Ranged}; | ||
|
||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::helpers::is_const_true; | ||
|
||
use crate::checkers::ast::Checker; | ||
use crate::rules::pylint::helpers::type_param_name; | ||
|
||
/// ## What it does | ||
/// Checks for `TypeVar` and `ParamSpec` definitions in which the type is | ||
/// both covariant and contravariant. | ||
/// | ||
/// ## Why is this bad? | ||
/// By default, Python's generic types are invariant, but can be marked as | ||
/// either covariant or contravariant via the `covariant` and `contravariant` | ||
/// keyword arguments. While the API does allow you to mark a type as both | ||
/// covariant and contravariant, this is not supported by the type system, | ||
/// and should be avoided. | ||
/// | ||
/// Instead, change the variance of the type to be either covariant, | ||
/// contravariant, or invariant. If you want to describe both covariance and | ||
/// contravariance, consider using two separate type parameters. | ||
/// | ||
/// For context: an "invariant" generic type only accepts values that exactly | ||
/// match the type parameter; for example, `list[Dog]` accepts only `list[Dog]`, | ||
/// not `list[Animal]` (superclass) or `list[Bulldog]` (subclass). This is | ||
/// the default behavior for Python's generic types. | ||
/// | ||
/// A "covariant" generic type accepts subclasses of the type parameter; for | ||
/// example, `Sequence[Animal]` accepts `Sequence[Dog]`. A "contravariant" | ||
/// generic type accepts superclasses of the type parameter; for example, | ||
/// `Callable[Dog]` accepts `Callable[Animal]`. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// from typing import TypeVar | ||
/// | ||
/// T = TypeVar("T", covariant=True, contravariant=True) | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// from typing import TypeVar | ||
/// | ||
/// T_co = TypeVar("T_co", covariant=True) | ||
/// T_contra = TypeVar("T_contra", contravariant=True) | ||
/// ``` | ||
/// | ||
/// ## References | ||
/// - [Python documentation: `typing` — Support for type hints](https://docs.python.org/3/library/typing.html) | ||
/// - [PEP 483 – The Theory of Type Hints: Covariance and Contravariance](https://peps.python.org/pep-0483/#covariance-and-contravariance) | ||
/// - [PEP 484 – Type Hints: Covariance and contravariance](https://peps.python.org/pep-0484/#covariance-and-contravariance) | ||
#[violation] | ||
pub struct TypeBivariance { | ||
kind: VarKind, | ||
param_name: Option<String>, | ||
} | ||
|
||
impl Violation for TypeBivariance { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let TypeBivariance { kind, param_name } = self; | ||
match param_name { | ||
None => format!("`{kind}` cannot be both covariant and contravariant"), | ||
Some(param_name) => { | ||
format!("`{kind}` \"{param_name}\" cannot be both covariant and contravariant",) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// PLC0131 | ||
pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { | ||
let Expr::Call(ast::ExprCall { func,args, keywords, .. }) = value else { | ||
return; | ||
}; | ||
|
||
let Some(covariant) = keywords | ||
.iter() | ||
.find(|keyword| { | ||
keyword | ||
.arg | ||
.as_ref() | ||
.map_or(false, |keyword| keyword.as_str() == "covariant") | ||
}) | ||
.map(|keyword| &keyword.value) | ||
else { | ||
return; | ||
}; | ||
|
||
let Some(contravariant) = keywords | ||
.iter() | ||
.find(|keyword| { | ||
keyword | ||
.arg | ||
.as_ref() | ||
.map_or(false, |keyword| keyword.as_str() == "contravariant") | ||
}) | ||
.map(|keyword| &keyword.value) | ||
else { | ||
return; | ||
}; | ||
|
||
if is_const_true(covariant) && is_const_true(contravariant) { | ||
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 { | ||
None | ||
} | ||
}) | ||
else { | ||
return; | ||
}; | ||
|
||
checker.diagnostics.push(Diagnostic::new( | ||
TypeBivariance { | ||
kind, | ||
param_name: type_param_name(args, keywords).map(ToString::to_string), | ||
}, | ||
func.range(), | ||
)); | ||
} | ||
} | ||
|
||
#[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
enum VarKind { | ||
TypeVar, | ||
ParamSpec, | ||
} | ||
|
||
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"), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
...ff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLC0131_type_bivariance.py.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--- | ||
source: crates/ruff/src/rules/pylint/mod.rs | ||
--- | ||
type_bivariance.py:5:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant | ||
| | ||
3 | # Errors. | ||
4 | | ||
5 | T = TypeVar("T", covariant=True, contravariant=True) | ||
| ^^^^^^^ PLC0131 | ||
6 | T = TypeVar(name="T", covariant=True, contravariant=True) | ||
| | ||
|
||
type_bivariance.py:6:5: PLC0131 `TypeVar` "T" cannot be both covariant and contravariant | ||
| | ||
5 | T = TypeVar("T", covariant=True, contravariant=True) | ||
6 | T = TypeVar(name="T", covariant=True, contravariant=True) | ||
| ^^^^^^^ PLC0131 | ||
7 | | ||
8 | T = ParamSpec("T", covariant=True, contravariant=True) | ||
| | ||
|
||
type_bivariance.py:8:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant | ||
| | ||
6 | T = TypeVar(name="T", covariant=True, contravariant=True) | ||
7 | | ||
8 | T = ParamSpec("T", covariant=True, contravariant=True) | ||
| ^^^^^^^^^ PLC0131 | ||
9 | T = ParamSpec(name="T", covariant=True, contravariant=True) | ||
| | ||
|
||
type_bivariance.py:9:5: PLC0131 `ParamSpec` "T" cannot be both covariant and contravariant | ||
| | ||
8 | T = ParamSpec("T", covariant=True, contravariant=True) | ||
9 | T = ParamSpec(name="T", covariant=True, contravariant=True) | ||
| ^^^^^^^^^ PLC0131 | ||
10 | | ||
11 | # Non-errors. | ||
| | ||
|
||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.