mirror of https://github.com/astral-sh/ruff
Check for `Any` in other types for `ANN401` (#5601)
## Summary Check for `Any` in other types for `ANN401`. This reuses the logic from `implicit-optional` rule to resolve the type to `Any`. Following types are supported: * `Union[Any, ...]` * `Any | ...` * `Optional[Any]` * `Annotated[<any of the above variant>, ...]` * Forward references i.e., `"Any | ..."` ## Test Plan Added test cases for various combinations. fixes: #5458
This commit is contained in:
parent
8420008e79
commit
f44acc047a
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Type
|
||||
from typing import Annotated, Any, Optional, Type, Union
|
||||
from typing_extensions import override
|
||||
|
||||
# Error
|
||||
|
|
@ -95,27 +95,27 @@ class Foo:
|
|||
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
|
||||
pass
|
||||
|
||||
# ANN401
|
||||
# OK
|
||||
@override
|
||||
def foo(self: "Foo", a: Any, *params: str, **options: str) -> int:
|
||||
pass
|
||||
|
||||
# ANN401
|
||||
# OK
|
||||
@override
|
||||
def foo(self: "Foo", a: int, *params: str, **options: str) -> Any:
|
||||
pass
|
||||
|
||||
# ANN401
|
||||
# OK
|
||||
@override
|
||||
def foo(self: "Foo", a: int, *params: Any, **options: Any) -> int:
|
||||
pass
|
||||
|
||||
# ANN401
|
||||
# OK
|
||||
@override
|
||||
def foo(self: "Foo", a: int, *params: Any, **options: str) -> int:
|
||||
pass
|
||||
|
||||
# ANN401
|
||||
# OK
|
||||
@override
|
||||
def foo(self: "Foo", a: int, *params: str, **options: Any) -> int:
|
||||
pass
|
||||
|
|
@ -137,3 +137,17 @@ class Foo:
|
|||
|
||||
# OK
|
||||
def f(*args: *tuple[int]) -> None: ...
|
||||
def f(a: object) -> None: ...
|
||||
def f(a: str | bytes) -> None: ...
|
||||
def f(a: Union[str, bytes]) -> None: ...
|
||||
def f(a: Optional[str]) -> None: ...
|
||||
def f(a: Annotated[str, ...]) -> None: ...
|
||||
def f(a: "Union[str, bytes]") -> None: ...
|
||||
|
||||
# ANN401
|
||||
def f(a: Any | int) -> None: ...
|
||||
def f(a: int | Any) -> None: ...
|
||||
def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
def f(a: Optional[Any]) -> None: ...
|
||||
def f(a: Annotated[Any, ...]) -> None: ...
|
||||
def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use rustpython_parser::ast::{ArgWithDefault, Expr, Ranged, Stmt};
|
||||
use rustpython_parser::ast::{self, ArgWithDefault, Constant, Expr, Ranged, Stmt};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
|
|
@ -6,12 +6,14 @@ use ruff_python_ast::cast;
|
|||
use ruff_python_ast::helpers::ReturnStatementVisitor;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::typing::parse_type_annotation;
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
|
||||
use ruff_python_semantic::{Definition, Member, MemberKind};
|
||||
use ruff_python_stdlib::typing::simple_magic_return_type;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::ruff::typing::type_hint_resolves_to_any;
|
||||
|
||||
use super::super::fixes;
|
||||
use super::super::helpers::match_function_def;
|
||||
|
|
@ -432,20 +434,46 @@ fn is_none_returning(body: &[Stmt]) -> bool {
|
|||
|
||||
/// ANN401
|
||||
fn check_dynamically_typed<F>(
|
||||
checker: &Checker,
|
||||
annotation: &Expr,
|
||||
func: F,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
is_overridden: bool,
|
||||
semantic: &SemanticModel,
|
||||
) where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
if !is_overridden && semantic.match_typing_expr(annotation, "Any") {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
range,
|
||||
value: Constant::Str(string),
|
||||
..
|
||||
}) = annotation
|
||||
{
|
||||
// Quoted annotations
|
||||
if let Ok((parsed_annotation, _)) = parse_type_annotation(string, *range, checker.locator) {
|
||||
if type_hint_resolves_to_any(
|
||||
&parsed_annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.settings.target_version.minor(),
|
||||
) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
AnyType { name: func() },
|
||||
annotation.range(),
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if type_hint_resolves_to_any(
|
||||
annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.settings.target_version.minor(),
|
||||
) {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
AnyType { name: func() },
|
||||
annotation.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate flake8-annotation checks for a given `Definition`.
|
||||
|
|
@ -500,13 +528,12 @@ pub(crate) fn definition(
|
|||
// ANN401 for dynamically typed arguments
|
||||
if let Some(annotation) = &def.annotation {
|
||||
has_any_typed_arg = true;
|
||||
if checker.enabled(Rule::AnyType) {
|
||||
if checker.enabled(Rule::AnyType) && !is_overridden {
|
||||
check_dynamically_typed(
|
||||
checker,
|
||||
annotation,
|
||||
|| def.arg.to_string(),
|
||||
&mut diagnostics,
|
||||
is_overridden,
|
||||
checker.semantic(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -530,15 +557,9 @@ pub(crate) fn definition(
|
|||
if let Some(expr) = &arg.annotation {
|
||||
has_any_typed_arg = true;
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.enabled(Rule::AnyType) {
|
||||
if checker.enabled(Rule::AnyType) && !is_overridden {
|
||||
let name = &arg.arg;
|
||||
check_dynamically_typed(
|
||||
expr,
|
||||
|| format!("*{name}"),
|
||||
&mut diagnostics,
|
||||
is_overridden,
|
||||
checker.semantic(),
|
||||
);
|
||||
check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -562,14 +583,13 @@ pub(crate) fn definition(
|
|||
if let Some(expr) = &arg.annotation {
|
||||
has_any_typed_arg = true;
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.enabled(Rule::AnyType) {
|
||||
if checker.enabled(Rule::AnyType) && !is_overridden {
|
||||
let name = &arg.arg;
|
||||
check_dynamically_typed(
|
||||
checker,
|
||||
expr,
|
||||
|| format!("**{name}"),
|
||||
&mut diagnostics,
|
||||
is_overridden,
|
||||
checker.semantic(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -629,14 +649,8 @@ pub(crate) fn definition(
|
|||
// ANN201, ANN202, ANN401
|
||||
if let Some(expr) = &returns {
|
||||
has_typed_return = true;
|
||||
if checker.enabled(Rule::AnyType) {
|
||||
check_dynamically_typed(
|
||||
expr,
|
||||
|| name.to_string(),
|
||||
&mut diagnostics,
|
||||
is_overridden,
|
||||
checker.semantic(),
|
||||
);
|
||||
if checker.enabled(Rule::AnyType) && !is_overridden {
|
||||
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
|
||||
}
|
||||
} else if !(
|
||||
// Allow omission of return annotation if the function only returns `None`
|
||||
|
|
|
|||
|
|
@ -186,4 +186,60 @@ annotation_presence.py:134:13: ANN101 Missing type annotation for `self` in meth
|
|||
135 | pass
|
||||
|
|
||||
|
||||
annotation_presence.py:148:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
147 | # ANN401
|
||||
148 | def f(a: Any | int) -> None: ...
|
||||
| ^^^^^^^^^ ANN401
|
||||
149 | def f(a: int | Any) -> None: ...
|
||||
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
|
|
||||
|
||||
annotation_presence.py:149:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
147 | # ANN401
|
||||
148 | def f(a: Any | int) -> None: ...
|
||||
149 | def f(a: int | Any) -> None: ...
|
||||
| ^^^^^^^^^ ANN401
|
||||
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
151 | def f(a: Optional[Any]) -> None: ...
|
||||
|
|
||||
|
||||
annotation_presence.py:150:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
148 | def f(a: Any | int) -> None: ...
|
||||
149 | def f(a: int | Any) -> None: ...
|
||||
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
||||
151 | def f(a: Optional[Any]) -> None: ...
|
||||
152 | def f(a: Annotated[Any, ...]) -> None: ...
|
||||
|
|
||||
|
||||
annotation_presence.py:151:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
149 | def f(a: int | Any) -> None: ...
|
||||
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
151 | def f(a: Optional[Any]) -> None: ...
|
||||
| ^^^^^^^^^^^^^ ANN401
|
||||
152 | def f(a: Annotated[Any, ...]) -> None: ...
|
||||
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
|
|
||||
|
||||
annotation_presence.py:152:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
150 | def f(a: Union[str, bytes, Any]) -> None: ...
|
||||
151 | def f(a: Optional[Any]) -> None: ...
|
||||
152 | def f(a: Annotated[Any, ...]) -> None: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^ ANN401
|
||||
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
|
|
||||
|
||||
annotation_presence.py:153:10: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|
||||
|
|
||||
151 | def f(a: Optional[Any]) -> None: ...
|
||||
152 | def f(a: Annotated[Any, ...]) -> None: ...
|
||||
153 | def f(a: "Union[str, bytes, Any]") -> None: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
||||
|
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Ruff-specific rules.
|
||||
|
||||
pub(crate) mod rules;
|
||||
pub(crate) mod typing;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
|
|||
|
|
@ -6,18 +6,16 @@ use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Op
|
|||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::helpers::is_const_none;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::typing::parse_type_annotation;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::registry::AsRule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
use super::super::typing::type_hint_explicitly_allows_none;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of implicit `Optional` in type annotations when the
|
||||
/// default parameter value is `None`.
|
||||
|
|
@ -121,231 +119,6 @@ impl From<PythonVersion> for ConversionType {
|
|||
}
|
||||
}
|
||||
|
||||
/// Custom iterator to collect all the `|` separated expressions in a PEP 604
|
||||
/// union type.
|
||||
struct PEP604UnionIterator<'a> {
|
||||
stack: Vec<&'a Expr>,
|
||||
}
|
||||
|
||||
impl<'a> PEP604UnionIterator<'a> {
|
||||
fn new(expr: &'a Expr) -> Self {
|
||||
Self { stack: vec![expr] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PEP604UnionIterator<'a> {
|
||||
type Item = &'a Expr;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(expr) = self.stack.pop() {
|
||||
match expr {
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::BitOr,
|
||||
right,
|
||||
..
|
||||
}) => {
|
||||
self.stack.push(left);
|
||||
self.stack.push(right);
|
||||
}
|
||||
_ => return Some(expr),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given call path is a known type.
|
||||
///
|
||||
/// A known type is either a builtin type, any object from the standard library,
|
||||
/// or a type from the `typing_extensions` module.
|
||||
fn is_known_type(call_path: &CallPath, target_version: PythonVersion) -> bool {
|
||||
match call_path.as_slice() {
|
||||
["" | "typing_extensions", ..] => true,
|
||||
[module, ..] => is_known_standard_library(target_version.minor(), module),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TypingTarget<'a> {
|
||||
None,
|
||||
Any,
|
||||
Object,
|
||||
Optional,
|
||||
ForwardReference(Expr),
|
||||
Union(Vec<&'a Expr>),
|
||||
Literal(Vec<&'a Expr>),
|
||||
Annotated(&'a Expr),
|
||||
}
|
||||
|
||||
impl<'a> TypingTarget<'a> {
|
||||
fn try_from_expr(
|
||||
expr: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
target_version: PythonVersion,
|
||||
) -> Option<Self> {
|
||||
match expr {
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
if semantic.match_typing_expr(value, "Optional") {
|
||||
return Some(TypingTarget::Optional);
|
||||
}
|
||||
let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if semantic.match_typing_expr(value, "Literal") {
|
||||
Some(TypingTarget::Literal(elements.iter().collect()))
|
||||
} else if semantic.match_typing_expr(value, "Union") {
|
||||
Some(TypingTarget::Union(elements.iter().collect()))
|
||||
} else if semantic.match_typing_expr(value, "Annotated") {
|
||||
elements.first().map(TypingTarget::Annotated)
|
||||
} else {
|
||||
semantic.resolve_call_path(value).map_or(
|
||||
// If we can't resolve the call path, it must be defined
|
||||
// in the same file, so we assume it's `Any` as it could
|
||||
// be a type alias.
|
||||
Some(TypingTarget::Any),
|
||||
|call_path| {
|
||||
if is_known_type(&call_path, target_version) {
|
||||
None
|
||||
} else {
|
||||
// If it's not a known type, we assume it's `Any`.
|
||||
Some(TypingTarget::Any)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Expr::BinOp(..) => Some(TypingTarget::Union(
|
||||
PEP604UnionIterator::new(expr).collect(),
|
||||
)),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::None,
|
||||
..
|
||||
}) => Some(TypingTarget::None),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(string),
|
||||
range,
|
||||
..
|
||||
}) => parse_type_annotation(string, *range, locator)
|
||||
// In case of a parse error, we return `Any` to avoid false positives.
|
||||
.map_or(Some(TypingTarget::Any), |(expr, _)| {
|
||||
Some(TypingTarget::ForwardReference(expr))
|
||||
}),
|
||||
_ => semantic.resolve_call_path(expr).map_or(
|
||||
// If we can't resolve the call path, it must be defined in the
|
||||
// same file, so we assume it's `Any` as it could be a type alias.
|
||||
Some(TypingTarget::Any),
|
||||
|call_path| {
|
||||
if semantic.match_typing_call_path(&call_path, "Any") {
|
||||
Some(TypingTarget::Any)
|
||||
} else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) {
|
||||
Some(TypingTarget::Object)
|
||||
} else if !is_known_type(&call_path, target_version) {
|
||||
// If it's not a known type, we assume it's `Any`.
|
||||
Some(TypingTarget::Any)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
||||
fn contains_none(
|
||||
&self,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
target_version: PythonVersion,
|
||||
) -> bool {
|
||||
match self {
|
||||
TypingTarget::None
|
||||
| TypingTarget::Optional
|
||||
| TypingTarget::Any
|
||||
| TypingTarget::Object => true,
|
||||
TypingTarget::Literal(elements) => elements.iter().any(|element| {
|
||||
let Some(new_target) =
|
||||
TypingTarget::try_from_expr(element, semantic, locator, target_version)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
// Literal can only contain `None`, a literal value, other `Literal`
|
||||
// or an enum value.
|
||||
match new_target {
|
||||
TypingTarget::None => true,
|
||||
TypingTarget::Literal(_) => {
|
||||
new_target.contains_none(semantic, locator, target_version)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}),
|
||||
TypingTarget::Union(elements) => elements.iter().any(|element| {
|
||||
let Some(new_target) =
|
||||
TypingTarget::try_from_expr(element, semantic, locator, target_version)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
new_target.contains_none(semantic, locator, target_version)
|
||||
}),
|
||||
TypingTarget::Annotated(element) => {
|
||||
let Some(new_target) =
|
||||
TypingTarget::try_from_expr(element, semantic, locator, target_version)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
new_target.contains_none(semantic, locator, target_version)
|
||||
}
|
||||
TypingTarget::ForwardReference(expr) => {
|
||||
let Some(new_target) =
|
||||
TypingTarget::try_from_expr(expr, semantic, locator, target_version)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
new_target.contains_none(semantic, locator, target_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given annotation [`Expr`] explicitly allows `None`.
|
||||
///
|
||||
/// This function will return `None` if the annotation explicitly allows `None`
|
||||
/// otherwise it will return the annotation itself. If it's a `Annotated` type,
|
||||
/// then the inner type will be checked.
|
||||
///
|
||||
/// This function assumes that the annotation is a valid typing annotation expression.
|
||||
fn type_hint_explicitly_allows_none<'a>(
|
||||
annotation: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
target_version: PythonVersion,
|
||||
) -> Option<&'a Expr> {
|
||||
let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version)
|
||||
else {
|
||||
return Some(annotation);
|
||||
};
|
||||
match target {
|
||||
// Short circuit on top level `None`, `Any` or `Optional`
|
||||
TypingTarget::None | TypingTarget::Optional | TypingTarget::Any => None,
|
||||
// Top-level `Annotated` node should check for the inner type and
|
||||
// return the inner type if it doesn't allow `None`. If `Annotated`
|
||||
// is found nested inside another type, then the outer type should
|
||||
// be returned.
|
||||
TypingTarget::Annotated(expr) => {
|
||||
type_hint_explicitly_allows_none(expr, semantic, locator, target_version)
|
||||
}
|
||||
_ => {
|
||||
if target.contains_none(semantic, locator, target_version) {
|
||||
None
|
||||
} else {
|
||||
Some(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] for the given [`Expr`] as per the [`ConversionType`].
|
||||
fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) -> Result<Fix> {
|
||||
match conversion_type {
|
||||
|
|
@ -423,7 +196,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) {
|
|||
&annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.settings.target_version,
|
||||
checker.settings.target_version.minor(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -444,7 +217,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) {
|
|||
annotation,
|
||||
checker.semantic(),
|
||||
checker.locator,
|
||||
checker.settings.target_version,
|
||||
checker.settings.target_version.minor(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -459,40 +232,3 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
use super::is_known_type;
|
||||
|
||||
#[test]
|
||||
fn test_is_known_type() {
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["", "int"]),
|
||||
PythonVersion::Py311
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["builtins", "int"]),
|
||||
PythonVersion::Py311
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["typing", "Optional"]),
|
||||
PythonVersion::Py311
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["typing_extensions", "Literal"]),
|
||||
PythonVersion::Py311
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["zoneinfo", "ZoneInfo"]),
|
||||
PythonVersion::Py311
|
||||
));
|
||||
assert!(!is_known_type(
|
||||
&CallPath::from_slice(&["zoneinfo", "ZoneInfo"]),
|
||||
PythonVersion::Py38
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,42 +37,6 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
|||
27 27 |
|
||||
28 28 |
|
||||
|
||||
RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
29 | def f(arg: typing.List[str] = None): # RUF013
|
||||
| ^^^^^^^^^^^^^^^^ RUF013
|
||||
30 | pass
|
||||
|
|
||||
= help: Convert to `Optional[T]`
|
||||
|
||||
ℹ Suggested fix
|
||||
26 26 | pass
|
||||
27 27 |
|
||||
28 28 |
|
||||
29 |-def f(arg: typing.List[str] = None): # RUF013
|
||||
29 |+def f(arg: Optional[typing.List[str]] = None): # RUF013
|
||||
30 30 | pass
|
||||
31 31 |
|
||||
32 32 |
|
||||
|
||||
RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
33 | def f(arg: Tuple[str] = None): # RUF013
|
||||
| ^^^^^^^^^^ RUF013
|
||||
34 | pass
|
||||
|
|
||||
= help: Convert to `Optional[T]`
|
||||
|
||||
ℹ Suggested fix
|
||||
30 30 | pass
|
||||
31 31 |
|
||||
32 32 |
|
||||
33 |-def f(arg: Tuple[str] = None): # RUF013
|
||||
33 |+def f(arg: Optional[Tuple[str]] = None): # RUF013
|
||||
34 34 | pass
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
67 | def f(arg: Union = None): # RUF013
|
||||
|
|
|
|||
|
|
@ -37,42 +37,6 @@ RUF013_0.py:25:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
|||
27 27 |
|
||||
28 28 |
|
||||
|
||||
RUF013_0.py:29:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
29 | def f(arg: typing.List[str] = None): # RUF013
|
||||
| ^^^^^^^^^^^^^^^^ RUF013
|
||||
30 | pass
|
||||
|
|
||||
= help: Convert to `T | None`
|
||||
|
||||
ℹ Suggested fix
|
||||
26 26 | pass
|
||||
27 27 |
|
||||
28 28 |
|
||||
29 |-def f(arg: typing.List[str] = None): # RUF013
|
||||
29 |+def f(arg: typing.List[str] | None = None): # RUF013
|
||||
30 30 | pass
|
||||
31 31 |
|
||||
32 32 |
|
||||
|
||||
RUF013_0.py:33:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
33 | def f(arg: Tuple[str] = None): # RUF013
|
||||
| ^^^^^^^^^^ RUF013
|
||||
34 | pass
|
||||
|
|
||||
= help: Convert to `T | None`
|
||||
|
||||
ℹ Suggested fix
|
||||
30 30 | pass
|
||||
31 31 |
|
||||
32 32 |
|
||||
33 |-def f(arg: Tuple[str] = None): # RUF013
|
||||
33 |+def f(arg: Tuple[str] | None = None): # RUF013
|
||||
34 34 | pass
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
RUF013_0.py:67:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||
|
|
||||
67 | def f(arg: Union = None): # RUF013
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Operator};
|
||||
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::typing::parse_type_annotation;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||
|
||||
/// Custom iterator to collect all the `|` separated expressions in a PEP 604
|
||||
/// union type.
|
||||
struct PEP604UnionIterator<'a> {
|
||||
stack: Vec<&'a Expr>,
|
||||
}
|
||||
|
||||
impl<'a> PEP604UnionIterator<'a> {
|
||||
fn new(expr: &'a Expr) -> Self {
|
||||
Self { stack: vec![expr] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PEP604UnionIterator<'a> {
|
||||
type Item = &'a Expr;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(expr) = self.stack.pop() {
|
||||
match expr {
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
left,
|
||||
op: Operator::BitOr,
|
||||
right,
|
||||
..
|
||||
}) => {
|
||||
self.stack.push(left);
|
||||
self.stack.push(right);
|
||||
}
|
||||
_ => return Some(expr),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given call path is a known type.
|
||||
///
|
||||
/// A known type is either a builtin type, any object from the standard library,
|
||||
/// or a type from the `typing_extensions` module.
|
||||
fn is_known_type(call_path: &CallPath, minor_version: u32) -> bool {
|
||||
match call_path.as_slice() {
|
||||
["" | "typing_extensions", ..] => true,
|
||||
[module, ..] => is_known_standard_library(minor_version, module),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TypingTarget<'a> {
|
||||
/// Literal `None` type.
|
||||
None,
|
||||
|
||||
/// A `typing.Any` type.
|
||||
Any,
|
||||
|
||||
/// Literal `object` type.
|
||||
Object,
|
||||
|
||||
/// Forward reference to a type e.g., `"List[str]"`.
|
||||
ForwardReference(Expr),
|
||||
|
||||
/// A `typing.Union` type or `|` separated types e.g., `Union[int, str]`
|
||||
/// or `int | str`.
|
||||
Union(Vec<&'a Expr>),
|
||||
|
||||
/// A `typing.Literal` type e.g., `Literal[1, 2, 3]`.
|
||||
Literal(Vec<&'a Expr>),
|
||||
|
||||
/// A `typing.Optional` type e.g., `Optional[int]`.
|
||||
Optional(&'a Expr),
|
||||
|
||||
/// A `typing.Annotated` type e.g., `Annotated[int, ...]`.
|
||||
Annotated(&'a Expr),
|
||||
|
||||
/// Special type used to represent an unknown type (and not a typing target)
|
||||
/// which could be a type alias.
|
||||
Unknown,
|
||||
|
||||
/// Special type used to represent a known type (and not a typing target).
|
||||
/// A known type is either a builtin type, any object from the standard
|
||||
/// library, or a type from the `typing_extensions` module.
|
||||
Known,
|
||||
}
|
||||
|
||||
impl<'a> TypingTarget<'a> {
|
||||
fn try_from_expr(
|
||||
expr: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> Option<Self> {
|
||||
match expr {
|
||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
if semantic.match_typing_expr(value, "Optional") {
|
||||
return Some(TypingTarget::Optional(slice.as_ref()));
|
||||
}
|
||||
let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if semantic.match_typing_expr(value, "Literal") {
|
||||
Some(TypingTarget::Literal(elements.iter().collect()))
|
||||
} else if semantic.match_typing_expr(value, "Union") {
|
||||
Some(TypingTarget::Union(elements.iter().collect()))
|
||||
} else if semantic.match_typing_expr(value, "Annotated") {
|
||||
elements.first().map(TypingTarget::Annotated)
|
||||
} else {
|
||||
semantic.resolve_call_path(value).map_or(
|
||||
// If we can't resolve the call path, it must be defined
|
||||
// in the same file and could be a type alias.
|
||||
Some(TypingTarget::Unknown),
|
||||
|call_path| {
|
||||
if is_known_type(&call_path, minor_version) {
|
||||
Some(TypingTarget::Known)
|
||||
} else {
|
||||
Some(TypingTarget::Unknown)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Expr::BinOp(..) => Some(TypingTarget::Union(
|
||||
PEP604UnionIterator::new(expr).collect(),
|
||||
)),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::None,
|
||||
..
|
||||
}) => Some(TypingTarget::None),
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(string),
|
||||
range,
|
||||
..
|
||||
}) => parse_type_annotation(string, *range, locator)
|
||||
.map_or(None, |(expr, _)| Some(TypingTarget::ForwardReference(expr))),
|
||||
_ => semantic.resolve_call_path(expr).map_or(
|
||||
// If we can't resolve the call path, it must be defined in the
|
||||
// same file, so we assume it's `Any` as it could be a type alias.
|
||||
Some(TypingTarget::Unknown),
|
||||
|call_path| {
|
||||
if semantic.match_typing_call_path(&call_path, "Any") {
|
||||
Some(TypingTarget::Any)
|
||||
} else if matches!(call_path.as_slice(), ["" | "builtins", "object"]) {
|
||||
Some(TypingTarget::Object)
|
||||
} else if !is_known_type(&call_path, minor_version) {
|
||||
// If it's not a known type, we assume it's `Any`.
|
||||
Some(TypingTarget::Unknown)
|
||||
} else {
|
||||
Some(TypingTarget::Known)
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
||||
fn contains_none(
|
||||
&self,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> bool {
|
||||
match self {
|
||||
TypingTarget::None
|
||||
| TypingTarget::Optional(_)
|
||||
| TypingTarget::Any
|
||||
| TypingTarget::Object
|
||||
| TypingTarget::Unknown => true,
|
||||
TypingTarget::Known => false,
|
||||
TypingTarget::Literal(elements) => elements.iter().any(|element| {
|
||||
// Literal can only contain `None`, a literal value, other `Literal`
|
||||
// or an enum value.
|
||||
match TypingTarget::try_from_expr(element, semantic, locator, minor_version) {
|
||||
None | Some(TypingTarget::None) => true,
|
||||
Some(new_target @ TypingTarget::Literal(_)) => {
|
||||
new_target.contains_none(semantic, locator, minor_version)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}),
|
||||
TypingTarget::Union(elements) => elements.iter().any(|element| {
|
||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(semantic, locator, minor_version)
|
||||
})
|
||||
}),
|
||||
TypingTarget::Annotated(element) => {
|
||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(semantic, locator, minor_version)
|
||||
})
|
||||
}
|
||||
TypingTarget::ForwardReference(expr) => {
|
||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_none(semantic, locator, minor_version)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the [`TypingTarget`] explicitly allows `Any`.
|
||||
fn contains_any(
|
||||
&self,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> bool {
|
||||
match self {
|
||||
TypingTarget::Any => true,
|
||||
// `Literal` cannot contain `Any` as it's a dynamic value.
|
||||
TypingTarget::Literal(_)
|
||||
| TypingTarget::None
|
||||
| TypingTarget::Object
|
||||
| TypingTarget::Known
|
||||
| TypingTarget::Unknown => false,
|
||||
TypingTarget::Union(elements) => elements.iter().any(|element| {
|
||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(semantic, locator, minor_version)
|
||||
})
|
||||
}),
|
||||
TypingTarget::Annotated(element) | TypingTarget::Optional(element) => {
|
||||
TypingTarget::try_from_expr(element, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(semantic, locator, minor_version)
|
||||
})
|
||||
}
|
||||
TypingTarget::ForwardReference(expr) => {
|
||||
TypingTarget::try_from_expr(expr, semantic, locator, minor_version)
|
||||
.map_or(true, |new_target| {
|
||||
new_target.contains_any(semantic, locator, minor_version)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given annotation [`Expr`] explicitly allows `None`.
|
||||
///
|
||||
/// This function will return `None` if the annotation explicitly allows `None`
|
||||
/// otherwise it will return the annotation itself. If it's a `Annotated` type,
|
||||
/// then the inner type will be checked.
|
||||
///
|
||||
/// This function assumes that the annotation is a valid typing annotation expression.
|
||||
pub(crate) fn type_hint_explicitly_allows_none<'a>(
|
||||
annotation: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> Option<&'a Expr> {
|
||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
||||
None |
|
||||
// Short circuit on top level `None`, `Any` or `Optional`
|
||||
Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None,
|
||||
// Top-level `Annotated` node should check for the inner type and
|
||||
// return the inner type if it doesn't allow `None`. If `Annotated`
|
||||
// is found nested inside another type, then the outer type should
|
||||
// be returned.
|
||||
Some(TypingTarget::Annotated(expr)) => {
|
||||
type_hint_explicitly_allows_none(expr, semantic, locator, minor_version)
|
||||
}
|
||||
Some(target) => {
|
||||
if target.contains_none(semantic, locator, minor_version) {
|
||||
None
|
||||
} else {
|
||||
Some(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given annotation [`Expr`] resolves to `Any`.
|
||||
///
|
||||
/// This function assumes that the annotation is a valid typing annotation expression.
|
||||
pub(crate) fn type_hint_resolves_to_any(
|
||||
annotation: &Expr,
|
||||
semantic: &SemanticModel,
|
||||
locator: &Locator,
|
||||
minor_version: u32,
|
||||
) -> bool {
|
||||
match TypingTarget::try_from_expr(annotation, semantic, locator, minor_version) {
|
||||
None |
|
||||
// Short circuit on top level `Any`
|
||||
Some(TypingTarget::Any) => true,
|
||||
// Top-level `Annotated` node should check if the inner type resolves
|
||||
// to `Any`.
|
||||
Some(TypingTarget::Annotated(expr)) => {
|
||||
type_hint_resolves_to_any(expr, semantic, locator, minor_version)
|
||||
}
|
||||
Some(target) => target.contains_any(semantic, locator, minor_version),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::call_path::CallPath;
|
||||
|
||||
use super::is_known_type;
|
||||
|
||||
#[test]
|
||||
fn test_is_known_type() {
|
||||
assert!(is_known_type(&CallPath::from_slice(&["", "int"]), 11));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["builtins", "int"]),
|
||||
11
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["typing", "Optional"]),
|
||||
11
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["typing_extensions", "Literal"]),
|
||||
11
|
||||
));
|
||||
assert!(is_known_type(
|
||||
&CallPath::from_slice(&["zoneinfo", "ZoneInfo"]),
|
||||
11
|
||||
));
|
||||
assert!(!is_known_type(
|
||||
&CallPath::from_slice(&["zoneinfo", "ZoneInfo"]),
|
||||
8
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ ignore = [
|
|||
"T", # flake8-print
|
||||
"FBT", # flake8-boolean-trap
|
||||
"PERF", # perflint
|
||||
"ANN401",
|
||||
]
|
||||
|
||||
[tool.ruff.isort]
|
||||
|
|
|
|||
Loading…
Reference in New Issue