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:
Dhruv Manilawala 2023-07-13 18:19:27 +05:30 committed by GitHub
parent 8420008e79
commit f44acc047a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 458 additions and 378 deletions

View File

@ -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: ...

View File

@ -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") {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
));
};
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`

View File

@ -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
|

View File

@ -1,6 +1,7 @@
//! Ruff-specific rules.
pub(crate) mod rules;
pub(crate) mod typing;
#[cfg(test)]
mod tests {

View File

@ -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
));
}
}

View File

@ -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

View File

@ -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

View File

@ -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
));
}
}

View File

@ -19,6 +19,7 @@ ignore = [
"T", # flake8-print
"FBT", # flake8-boolean-trap
"PERF", # perflint
"ANN401",
]
[tool.ruff.isort]