mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 05:25:17 +00:00
[ruff
] Do not report when Optional
has no type arguments (RUF013
) (#14181)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz (push) Blocked by required conditions
CI / Fuzz the parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz (push) Blocked by required conditions
CI / Fuzz the parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
## Summary Resolves #13833. ## Test Plan `cargo nextest run` and `cargo insta test`. --------- Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
parent
d3f1c8e536
commit
c9b84e2a85
4 changed files with 102 additions and 44 deletions
21
crates/ruff_linter/resources/test/fixtures/ruff/RUF013_4.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/ruff/RUF013_4.py
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# https://github.com/astral-sh/ruff/issues/13833
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def no_default(arg: Optional): ...
|
||||
|
||||
|
||||
def has_default(arg: Optional = None): ...
|
||||
|
||||
|
||||
def multiple_1(arg1: Optional, arg2: Optional = None): ...
|
||||
|
||||
|
||||
def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ...
|
||||
|
||||
|
||||
def return_type(arg: Optional = None) -> Optional: ...
|
||||
|
||||
|
||||
def has_type_argument(arg: Optional[int] = None): ...
|
|
@ -32,6 +32,7 @@ mod tests {
|
|||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_2.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_3.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_4.py"))]
|
||||
#[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
|
||||
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
RUF013_4.py:15:61: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ...
|
||||
| ^^^ RUF013
|
||||
|
|
||||
= help: Convert to `T | None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | def multiple_1(arg1: Optional, arg2: Optional = None): ...
|
||||
13 13 |
|
||||
14 14 |
|
||||
15 |-def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ...
|
||||
15 |+def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int | None = None): ...
|
||||
16 16 |
|
||||
17 17 |
|
||||
18 18 | def return_type(arg: Optional = None) -> Optional: ...
|
|
@ -42,19 +42,19 @@ enum TypingTarget<'a> {
|
|||
ForwardReference(&'a Expr),
|
||||
|
||||
/// A `typing.Union` type e.g., `Union[int, str]`.
|
||||
Union(&'a Expr),
|
||||
Union(Option<&'a Expr>),
|
||||
|
||||
/// A PEP 604 union type e.g., `int | str`.
|
||||
PEP604Union(&'a Expr, &'a Expr),
|
||||
|
||||
/// A `typing.Literal` type e.g., `Literal[1, 2, 3]`.
|
||||
Literal(&'a Expr),
|
||||
Literal(Option<&'a Expr>),
|
||||
|
||||
/// A `typing.Optional` type e.g., `Optional[int]`.
|
||||
Optional(&'a Expr),
|
||||
Optional(Option<&'a Expr>),
|
||||
|
||||
/// A `typing.Annotated` type e.g., `Annotated[int, ...]`.
|
||||
Annotated(&'a Expr),
|
||||
Annotated(Option<&'a Expr>),
|
||||
|
||||
/// The `typing.Hashable` type.
|
||||
Hashable,
|
||||
|
@ -80,16 +80,16 @@ impl<'a> TypingTarget<'a> {
|
|||
Some(TypingTarget::Unknown),
|
||||
|qualified_name| {
|
||||
if semantic.match_typing_qualified_name(&qualified_name, "Optional") {
|
||||
Some(TypingTarget::Optional(slice.as_ref()))
|
||||
Some(TypingTarget::Optional(Some(slice.as_ref())))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Literal") {
|
||||
Some(TypingTarget::Literal(slice.as_ref()))
|
||||
Some(TypingTarget::Literal(Some(slice.as_ref())))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Union") {
|
||||
Some(TypingTarget::Union(slice.as_ref()))
|
||||
Some(TypingTarget::Union(Some(slice.as_ref())))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Annotated")
|
||||
{
|
||||
resolve_slice_value(slice.as_ref())
|
||||
.next()
|
||||
.map(TypingTarget::Annotated)
|
||||
Some(TypingTarget::Annotated(
|
||||
resolve_slice_value(slice.as_ref()).next(),
|
||||
))
|
||||
} else {
|
||||
if is_known_type(&qualified_name, minor_version) {
|
||||
Some(TypingTarget::Known)
|
||||
|
@ -118,7 +118,15 @@ impl<'a> TypingTarget<'a> {
|
|||
// same file, so we assume it's `Any` as it could be a type alias.
|
||||
Some(TypingTarget::Unknown),
|
||||
|qualified_name| {
|
||||
if semantic.match_typing_qualified_name(&qualified_name, "Any") {
|
||||
if semantic.match_typing_qualified_name(&qualified_name, "Optional") {
|
||||
Some(TypingTarget::Optional(None))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Literal") {
|
||||
Some(TypingTarget::Literal(None))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Union") {
|
||||
Some(TypingTarget::Union(None))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Annotated") {
|
||||
Some(TypingTarget::Annotated(None))
|
||||
} else if semantic.match_typing_qualified_name(&qualified_name, "Any") {
|
||||
Some(TypingTarget::Any)
|
||||
} else if matches!(qualified_name.segments(), ["" | "builtins", "object"]) {
|
||||
Some(TypingTarget::Object)
|
||||
|
@ -150,22 +158,26 @@ impl<'a> TypingTarget<'a> {
|
|||
| TypingTarget::Object
|
||||
| TypingTarget::Unknown => true,
|
||||
TypingTarget::Known => false,
|
||||
TypingTarget::Literal(slice) => resolve_slice_value(slice).any(|element| {
|
||||
// Literal can only contain `None`, a literal value, other `Literal`
|
||||
// or an enum value.
|
||||
match TypingTarget::try_from_expr(element, checker, minor_version) {
|
||||
None | Some(TypingTarget::None) => true,
|
||||
Some(new_target @ TypingTarget::Literal(_)) => {
|
||||
new_target.contains_none(checker, minor_version)
|
||||
TypingTarget::Literal(slice) => slice.is_some_and(|slice| {
|
||||
resolve_slice_value(slice).any(|element| {
|
||||
// Literal can only contain `None`, a literal value, other `Literal`
|
||||
// or an enum value.
|
||||
match TypingTarget::try_from_expr(element, checker, minor_version) {
|
||||
None | Some(TypingTarget::None) => true,
|
||||
Some(new_target @ TypingTarget::Literal(_)) => {
|
||||
new_target.contains_none(checker, minor_version)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}),
|
||||
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(checker, minor_version)
|
||||
})
|
||||
TypingTarget::Union(slice) => slice.is_some_and(|slice| {
|
||||
resolve_slice_value(slice).any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(checker, minor_version)
|
||||
})
|
||||
})
|
||||
}),
|
||||
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
|
@ -173,12 +185,12 @@ impl<'a> TypingTarget<'a> {
|
|||
new_target.contains_none(checker, minor_version)
|
||||
})
|
||||
}),
|
||||
TypingTarget::Annotated(expr) => {
|
||||
TypingTarget::Annotated(expr) => expr.is_some_and(|expr| {
|
||||
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(checker, minor_version)
|
||||
})
|
||||
}
|
||||
}),
|
||||
TypingTarget::ForwardReference(expr) => {
|
||||
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
|
@ -199,11 +211,13 @@ impl<'a> TypingTarget<'a> {
|
|||
| TypingTarget::Object
|
||||
| TypingTarget::Known
|
||||
| TypingTarget::Unknown => false,
|
||||
TypingTarget::Union(slice) => resolve_slice_value(slice).any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(checker, minor_version)
|
||||
})
|
||||
TypingTarget::Union(slice) => slice.is_some_and(|slice| {
|
||||
resolve_slice_value(slice).any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(checker, minor_version)
|
||||
})
|
||||
})
|
||||
}),
|
||||
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
|
||||
TypingTarget::try_from_expr(element, checker, minor_version)
|
||||
|
@ -212,10 +226,12 @@ impl<'a> TypingTarget<'a> {
|
|||
})
|
||||
}),
|
||||
TypingTarget::Annotated(expr) | TypingTarget::Optional(expr) => {
|
||||
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(checker, minor_version)
|
||||
})
|
||||
expr.is_some_and(|expr| {
|
||||
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(checker, minor_version)
|
||||
})
|
||||
})
|
||||
}
|
||||
TypingTarget::ForwardReference(expr) => {
|
||||
TypingTarget::try_from_expr(expr, checker, minor_version)
|
||||
|
@ -248,14 +264,13 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
|||
// is found nested inside another type, then the outer type should
|
||||
// be returned.
|
||||
Some(TypingTarget::Annotated(expr)) => {
|
||||
type_hint_explicitly_allows_none(expr, checker, minor_version)
|
||||
expr.and_then(|expr| type_hint_explicitly_allows_none(expr, checker, minor_version))
|
||||
}
|
||||
Some(target) => {
|
||||
if target.contains_none(checker, minor_version) {
|
||||
None
|
||||
} else {
|
||||
Some(annotation)
|
||||
return None;
|
||||
}
|
||||
Some(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,13 +284,14 @@ pub(crate) fn type_hint_resolves_to_any(
|
|||
minor_version: u8,
|
||||
) -> bool {
|
||||
match TypingTarget::try_from_expr(annotation, checker, minor_version) {
|
||||
None |
|
||||
// Short circuit on top level `Any`
|
||||
Some(TypingTarget::Any) => true,
|
||||
// Short circuit on top level `Any`
|
||||
None | Some(TypingTarget::Any) => true,
|
||||
// `Optional` is `Optional[Any]` which is `Any | None`.
|
||||
Some(TypingTarget::Optional(None)) => true,
|
||||
// Top-level `Annotated` node should check if the inner type resolves
|
||||
// to `Any`.
|
||||
Some(TypingTarget::Annotated(expr)) => {
|
||||
type_hint_resolves_to_any(expr, checker, minor_version)
|
||||
expr.is_some_and(|expr| type_hint_resolves_to_any(expr, checker, minor_version))
|
||||
}
|
||||
Some(target) => target.contains_any(checker, minor_version),
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue