Compare commits
2 Commits
0.13.3
...
alex/non-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ef117ed95 | ||
|
|
f9688bd05c |
21
crates/ruff_linter/resources/test/fixtures/flake8_annotations/shadowed_builtins.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/flake8_annotations/shadowed_builtins.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
from collections import UserString as str
|
||||
from typing import override
|
||||
|
||||
def foo():
|
||||
return "!"
|
||||
|
||||
def _foo():
|
||||
return "!"
|
||||
|
||||
class C:
|
||||
@override
|
||||
def __str__(self):
|
||||
return "!"
|
||||
|
||||
@staticmethod
|
||||
def foo():
|
||||
return "!"
|
||||
|
||||
@classmethod
|
||||
def bar(cls):
|
||||
return "!"
|
||||
@@ -142,23 +142,25 @@ impl AutoPythonType {
|
||||
});
|
||||
Some((expr, vec![no_return_edit]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => {
|
||||
let expr = type_expr(python_type)?;
|
||||
Some((expr, vec![]))
|
||||
}
|
||||
AutoPythonType::Atom(python_type) => type_expr(python_type, checker, at),
|
||||
AutoPythonType::Union(python_types) => {
|
||||
if target_version >= PythonVersion::PY310 {
|
||||
// Aggregate all the individual types (e.g., `int`, `float`).
|
||||
let mut all_edits = Vec::new();
|
||||
let names = python_types
|
||||
.iter()
|
||||
.sorted_unstable()
|
||||
.map(|python_type| type_expr(*python_type))
|
||||
.map(|python_type| {
|
||||
let (expr, mut edits) = type_expr(*python_type, checker, at)?;
|
||||
all_edits.append(&mut edits);
|
||||
Some(expr)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
// Wrap in a bitwise union (e.g., `int | float`).
|
||||
let expr = pep_604_union(&names);
|
||||
|
||||
Some((expr, vec![]))
|
||||
Some((expr, all_edits))
|
||||
} else {
|
||||
let python_types = python_types
|
||||
.into_iter()
|
||||
@@ -167,7 +169,7 @@ impl AutoPythonType {
|
||||
|
||||
match python_types.as_slice() {
|
||||
[python_type, PythonType::None] | [PythonType::None, python_type] => {
|
||||
let element = type_expr(*python_type)?;
|
||||
let (element, mut edits) = type_expr(*python_type, checker, at)?;
|
||||
|
||||
// Ex) `Optional[int]`
|
||||
let (optional_edit, binding) = checker
|
||||
@@ -175,12 +177,18 @@ impl AutoPythonType {
|
||||
.import(at)
|
||||
.ok()?;
|
||||
let expr = typing_optional(element, Name::from(binding));
|
||||
Some((expr, vec![optional_edit]))
|
||||
edits.push(optional_edit);
|
||||
Some((expr, edits))
|
||||
}
|
||||
_ => {
|
||||
let mut all_edits = Vec::new();
|
||||
let elements = python_types
|
||||
.into_iter()
|
||||
.map(type_expr)
|
||||
.map(|python_type| {
|
||||
let (expr, mut edits) = type_expr(python_type, checker, at)?;
|
||||
all_edits.append(&mut edits);
|
||||
Some(expr)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
|
||||
// Ex) `Union[int, str]`
|
||||
@@ -189,7 +197,8 @@ impl AutoPythonType {
|
||||
.import(at)
|
||||
.ok()?;
|
||||
let expr = typing_union(&elements, Name::from(binding));
|
||||
Some((expr, vec![union_edit]))
|
||||
all_edits.push(union_edit);
|
||||
Some((expr, all_edits))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,26 +208,44 @@ impl AutoPythonType {
|
||||
}
|
||||
|
||||
/// Given a [`PythonType`], return an [`Expr`] that resolves to that type.
|
||||
fn type_expr(python_type: PythonType) -> Option<Expr> {
|
||||
fn name(name: &str) -> Expr {
|
||||
Expr::Name(ast::ExprName {
|
||||
id: name.into(),
|
||||
///
|
||||
/// If the [`Expr`] relies on importing any external symbols, those imports will be returned as
|
||||
/// additional edits.
|
||||
pub(crate) fn type_expr(
|
||||
python_type: PythonType,
|
||||
checker: &Checker,
|
||||
at: TextSize,
|
||||
) -> Option<(Expr, Vec<Edit>)> {
|
||||
fn name(name: &str, checker: &Checker, at: TextSize) -> Option<(Expr, Vec<Edit>)> {
|
||||
let (edit, binding) = checker
|
||||
.importer()
|
||||
.get_or_import_builtin_symbol(name, at, checker.semantic())
|
||||
.ok()?;
|
||||
let expr = Expr::Name(ast::ExprName {
|
||||
id: binding.into(),
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
ctx: ExprContext::Load,
|
||||
})
|
||||
});
|
||||
Some((expr, edit.map_or_else(Vec::new, |edit| vec![edit])))
|
||||
}
|
||||
|
||||
match python_type {
|
||||
PythonType::String => Some(name("str")),
|
||||
PythonType::Bytes => Some(name("bytes")),
|
||||
PythonType::Number(number) => match number {
|
||||
NumberLike::Integer => Some(name("int")),
|
||||
NumberLike::Float => Some(name("float")),
|
||||
NumberLike::Complex => Some(name("complex")),
|
||||
NumberLike::Bool => Some(name("bool")),
|
||||
},
|
||||
PythonType::None => Some(name("None")),
|
||||
PythonType::String => name("str", checker, at),
|
||||
PythonType::Bytes => name("bytes", checker, at),
|
||||
PythonType::Number(number) => {
|
||||
let symbol = match number {
|
||||
NumberLike::Integer => "int",
|
||||
NumberLike::Float => "float",
|
||||
NumberLike::Complex => "complex",
|
||||
NumberLike::Bool => "bool",
|
||||
};
|
||||
name(symbol, checker, at)
|
||||
}
|
||||
PythonType::None => {
|
||||
let expr = Expr::NoneLiteral(ast::ExprNoneLiteral::default());
|
||||
Some((expr, vec![]))
|
||||
}
|
||||
PythonType::Ellipsis => None,
|
||||
PythonType::Dict => None,
|
||||
PythonType::List => None,
|
||||
|
||||
@@ -229,4 +229,20 @@ mod tests {
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadowed_builtins() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_annotations/shadowed_builtins.py"),
|
||||
&LinterSettings::for_rules(vec![
|
||||
Rule::MissingReturnTypeUndocumentedPublicFunction,
|
||||
Rule::MissingReturnTypePrivateFunction,
|
||||
Rule::MissingReturnTypeSpecialMethod,
|
||||
Rule::MissingReturnTypeStaticMethod,
|
||||
Rule::MissingReturnTypeClassMethod,
|
||||
]),
|
||||
)?;
|
||||
assert_diagnostics!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Definition;
|
||||
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType};
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::{Checker, DiagnosticGuard};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::flake8_annotations::helpers::auto_return_type;
|
||||
use crate::rules::flake8_annotations::helpers::{auto_return_type, type_expr};
|
||||
use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
@@ -825,11 +826,31 @@ pub(crate) fn definition(
|
||||
},
|
||||
function.identifier(),
|
||||
);
|
||||
if let Some(return_type) = return_type {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
|
||||
format!(" -> {return_type}"),
|
||||
function.parameters.end(),
|
||||
)));
|
||||
if let Some(return_type_str) = return_type {
|
||||
// Convert the simple return type to a proper expression that handles shadowed builtins
|
||||
let python_type = match return_type_str {
|
||||
"str" => PythonType::String,
|
||||
"bytes" => PythonType::Bytes,
|
||||
"int" => PythonType::Number(NumberLike::Integer),
|
||||
"float" => PythonType::Number(NumberLike::Float),
|
||||
"complex" => PythonType::Number(NumberLike::Complex),
|
||||
"bool" => PythonType::Number(NumberLike::Bool),
|
||||
"None" => PythonType::None,
|
||||
_ => return, // Unknown type, skip
|
||||
};
|
||||
|
||||
if let Some((expr, edits)) =
|
||||
type_expr(python_type, checker, function.parameters.start())
|
||||
{
|
||||
let return_type_expr = checker.generator().expr(&expr);
|
||||
diagnostic.set_fix(Fix::unsafe_edits(
|
||||
Edit::insertion(
|
||||
format!(" -> {return_type_expr}"),
|
||||
function.parameters.end(),
|
||||
),
|
||||
edits,
|
||||
));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
|
||||
---
|
||||
ANN201 [*] Missing return type annotation for public function `foo`
|
||||
--> shadowed_builtins.py:4:5
|
||||
|
|
||||
2 | from typing import override
|
||||
3 |
|
||||
4 | def foo():
|
||||
| ^^^
|
||||
5 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
- def foo():
|
||||
5 + def foo() -> builtins.str:
|
||||
6 | return "!"
|
||||
7 |
|
||||
8 | def _foo():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN202 [*] Missing return type annotation for private function `_foo`
|
||||
--> shadowed_builtins.py:7:5
|
||||
|
|
||||
5 | return "!"
|
||||
6 |
|
||||
7 | def _foo():
|
||||
| ^^^^
|
||||
8 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
7 |
|
||||
- def _foo():
|
||||
8 + def _foo() -> builtins.str:
|
||||
9 | return "!"
|
||||
10 |
|
||||
11 | class C:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN204 [*] Missing return type annotation for special method `__str__`
|
||||
--> shadowed_builtins.py:12:9
|
||||
|
|
||||
10 | class C:
|
||||
11 | @override
|
||||
12 | def __str__(self):
|
||||
| ^^^^^^^
|
||||
13 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
10 |
|
||||
11 | class C:
|
||||
12 | @override
|
||||
- def __str__(self):
|
||||
13 + def __str__(self) -> builtins.str:
|
||||
14 | return "!"
|
||||
15 |
|
||||
16 | @staticmethod
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN205 [*] Missing return type annotation for staticmethod `foo`
|
||||
--> shadowed_builtins.py:16:9
|
||||
|
|
||||
15 | @staticmethod
|
||||
16 | def foo():
|
||||
| ^^^
|
||||
17 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
14 | return "!"
|
||||
15 |
|
||||
16 | @staticmethod
|
||||
- def foo():
|
||||
17 + def foo() -> builtins.str:
|
||||
18 | return "!"
|
||||
19 |
|
||||
20 | @classmethod
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
ANN206 [*] Missing return type annotation for classmethod `bar`
|
||||
--> shadowed_builtins.py:20:9
|
||||
|
|
||||
19 | @classmethod
|
||||
20 | def bar(cls):
|
||||
| ^^^
|
||||
21 | return "!"
|
||||
|
|
||||
help: Add return type annotation: `builtins.str`
|
||||
1 | from collections import UserString as str
|
||||
2 | from typing import override
|
||||
3 + import builtins
|
||||
4 |
|
||||
5 | def foo():
|
||||
6 | return "!"
|
||||
--------------------------------------------------------------------------------
|
||||
18 | return "!"
|
||||
19 |
|
||||
20 | @classmethod
|
||||
- def bar(cls):
|
||||
21 + def bar(cls) -> builtins.str:
|
||||
22 | return "!"
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -907,6 +907,11 @@ def unknown(
|
||||
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
|
||||
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, Not, Unknown
|
||||
@@ -924,6 +929,50 @@ def unknown(
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Unknown
|
||||
reveal_type(i2) # revealed: Unknown
|
||||
|
||||
class Covariant[T]:
|
||||
def get(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def covariant(
|
||||
i1: Intersection[Covariant[Any], Not[Covariant[Any]]],
|
||||
i2: Intersection[Not[Covariant[Any]], Covariant[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Covariant[Any]
|
||||
reveal_type(i2) # revealed: Covariant[Any]
|
||||
|
||||
class Contravariant[T]:
|
||||
def receive(self, input: T): ...
|
||||
|
||||
def contravariant(
|
||||
i1: Intersection[Contravariant[Any], Not[Contravariant[Any]]],
|
||||
i2: Intersection[Not[Contravariant[Any]], Contravariant[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Contravariant[Any]
|
||||
reveal_type(i2) # revealed: Contravariant[Any]
|
||||
|
||||
class Invariant[T]:
|
||||
mutable_attribute: T
|
||||
|
||||
def invariant(
|
||||
i1: Intersection[Invariant[Any], Not[Invariant[Any]]],
|
||||
i2: Intersection[Not[Invariant[Any]], Invariant[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Invariant[Any]
|
||||
reveal_type(i2) # revealed: Invariant[Any]
|
||||
|
||||
class Bivariant[T]: ...
|
||||
|
||||
# Because of bivariance, the specialisation here is meaningless;
|
||||
# `Bivariant[Any]` is arguably still a fully static type, even
|
||||
# though it is specialised with a gradual form! Thus self-cancellation
|
||||
# here is fine:
|
||||
def bivariant(
|
||||
i1: Intersection[Bivariant[Any], Not[Bivariant[Any]]],
|
||||
i2: Intersection[Not[Bivariant[Any]], Bivariant[Any]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Never
|
||||
reveal_type(i2) # revealed: Never
|
||||
```
|
||||
|
||||
### Mixed dynamic types
|
||||
|
||||
@@ -958,6 +958,12 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// `T & ~T` = `T` if `T` is not a subtype of `T`
|
||||
// (this can only be true if `T` is a non-fully-static type)
|
||||
if existing_negative.is_equivalent_to(db, new_positive) {
|
||||
to_remove.push(index);
|
||||
continue;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_negative.is_disjoint_from(db, new_positive) {
|
||||
to_remove.push(index);
|
||||
@@ -1048,6 +1054,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.positive.insert(Type::Never);
|
||||
return;
|
||||
}
|
||||
// `T & ~T` = `T` if `T` is not a subtype of `T`
|
||||
// (this can only be true if `T` is a non-fully-static type)
|
||||
if existing_positive.is_equivalent_to(db, new_negative) {
|
||||
return;
|
||||
}
|
||||
// A & ~B = A if A and B are disjoint
|
||||
if existing_positive.is_disjoint_from(db, new_negative) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user