mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:35 +00:00
Add support for nested quoted annotations in RUF013 (#5254)
## Summary This is a follow up on #5235 to add support for nested quoted annotations for RUF013. ## Test Plan `cargo test`
This commit is contained in:
parent
f9ffb3d50d
commit
c792c10eaa
4 changed files with 77 additions and 27 deletions
|
@ -206,9 +206,18 @@ def f(arg: "Optional[int]" = None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def f(arg: Union["int", "str"] = None): # False negative
|
def f(arg: Union["int", "str"] = None): # RUF013
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def f(arg: Union["int", "None"] = None):
|
def f(arg: Union["int", "None"] = None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def f(arg: Union["No" "ne", "int"] = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Avoid flagging when there's a parse error in the forward reference
|
||||||
|
def f(arg: Union["<>", "int"] = None):
|
||||||
|
pass
|
||||||
|
|
|
@ -7,6 +7,7 @@ use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Constant, Expr, Op
|
||||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
use ruff_python_ast::helpers::is_const_none;
|
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_ast::typing::parse_type_annotation;
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::SemanticModel;
|
||||||
|
|
||||||
|
@ -145,15 +146,15 @@ enum TypingTarget<'a> {
|
||||||
None,
|
None,
|
||||||
Any,
|
Any,
|
||||||
Object,
|
Object,
|
||||||
ForwardReference,
|
|
||||||
Optional,
|
Optional,
|
||||||
|
ForwardReference(Expr),
|
||||||
Union(Vec<&'a Expr>),
|
Union(Vec<&'a Expr>),
|
||||||
Literal(Vec<&'a Expr>),
|
Literal(Vec<&'a Expr>),
|
||||||
Annotated(&'a Expr),
|
Annotated(&'a Expr),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TypingTarget<'a> {
|
impl<'a> TypingTarget<'a> {
|
||||||
fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel) -> Option<Self> {
|
fn try_from_expr(expr: &'a Expr, semantic: &SemanticModel, locator: &Locator) -> Option<Self> {
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||||
if semantic.match_typing_expr(value, "Optional") {
|
if semantic.match_typing_expr(value, "Optional") {
|
||||||
|
@ -180,9 +181,14 @@ impl<'a> TypingTarget<'a> {
|
||||||
..
|
..
|
||||||
}) => Some(TypingTarget::None),
|
}) => Some(TypingTarget::None),
|
||||||
Expr::Constant(ast::ExprConstant {
|
Expr::Constant(ast::ExprConstant {
|
||||||
value: Constant::Str(_),
|
value: Constant::Str(string),
|
||||||
|
range,
|
||||||
..
|
..
|
||||||
}) => Some(TypingTarget::ForwardReference),
|
}) => 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).and_then(|call_path| {
|
_ => semantic.resolve_call_path(expr).and_then(|call_path| {
|
||||||
if semantic.match_typing_call_path(&call_path, "Any") {
|
if semantic.match_typing_call_path(&call_path, "Any") {
|
||||||
Some(TypingTarget::Any)
|
Some(TypingTarget::Any)
|
||||||
|
@ -196,44 +202,42 @@ impl<'a> TypingTarget<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
/// Check if the [`TypingTarget`] explicitly allows `None`.
|
||||||
fn contains_none(&self, semantic: &SemanticModel) -> bool {
|
fn contains_none(&self, semantic: &SemanticModel, locator: &Locator) -> bool {
|
||||||
match self {
|
match self {
|
||||||
TypingTarget::None
|
TypingTarget::None
|
||||||
| TypingTarget::Optional
|
| TypingTarget::Optional
|
||||||
| TypingTarget::Any
|
| TypingTarget::Any
|
||||||
| TypingTarget::Object => true,
|
| TypingTarget::Object => true,
|
||||||
TypingTarget::Literal(elements) => elements.iter().any(|element| {
|
TypingTarget::Literal(elements) => elements.iter().any(|element| {
|
||||||
let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else {
|
let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
// Literal can only contain `None`, a literal value, other `Literal`
|
// Literal can only contain `None`, a literal value, other `Literal`
|
||||||
// or an enum value.
|
// or an enum value.
|
||||||
match new_target {
|
match new_target {
|
||||||
TypingTarget::None => true,
|
TypingTarget::None => true,
|
||||||
TypingTarget::Literal(_) => new_target.contains_none(semantic),
|
TypingTarget::Literal(_) => new_target.contains_none(semantic, locator),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
TypingTarget::Union(elements) => elements.iter().any(|element| {
|
TypingTarget::Union(elements) => elements.iter().any(|element| {
|
||||||
let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else {
|
let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
match new_target {
|
new_target.contains_none(semantic, locator)
|
||||||
TypingTarget::None => true,
|
|
||||||
_ => new_target.contains_none(semantic),
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
TypingTarget::Annotated(element) => {
|
TypingTarget::Annotated(element) => {
|
||||||
let Some(new_target) = TypingTarget::try_from_expr(element, semantic) else {
|
let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
match new_target {
|
new_target.contains_none(semantic, locator)
|
||||||
TypingTarget::None => true,
|
}
|
||||||
_ => new_target.contains_none(semantic),
|
TypingTarget::ForwardReference(expr) => {
|
||||||
}
|
let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
new_target.contains_none(semantic, locator)
|
||||||
}
|
}
|
||||||
// TODO(charlie): Add support for nested forward references (e.g., `Union["A", "B"]`).
|
|
||||||
TypingTarget::ForwardReference => true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,8 +252,9 @@ impl<'a> TypingTarget<'a> {
|
||||||
fn type_hint_explicitly_allows_none<'a>(
|
fn type_hint_explicitly_allows_none<'a>(
|
||||||
annotation: &'a Expr,
|
annotation: &'a Expr,
|
||||||
semantic: &SemanticModel,
|
semantic: &SemanticModel,
|
||||||
|
locator: &Locator,
|
||||||
) -> Option<&'a Expr> {
|
) -> Option<&'a Expr> {
|
||||||
let Some(target) = TypingTarget::try_from_expr(annotation, semantic) else {
|
let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator) else {
|
||||||
return Some(annotation);
|
return Some(annotation);
|
||||||
};
|
};
|
||||||
match target {
|
match target {
|
||||||
|
@ -259,9 +264,9 @@ fn type_hint_explicitly_allows_none<'a>(
|
||||||
// return the inner type if it doesn't allow `None`. If `Annotated`
|
// return the inner type if it doesn't allow `None`. If `Annotated`
|
||||||
// is found nested inside another type, then the outer type should
|
// is found nested inside another type, then the outer type should
|
||||||
// be returned.
|
// be returned.
|
||||||
TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic),
|
TypingTarget::Annotated(expr) => type_hint_explicitly_allows_none(expr, semantic, locator),
|
||||||
_ => {
|
_ => {
|
||||||
if target.contains_none(semantic) {
|
if target.contains_none(semantic, locator) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(annotation)
|
Some(annotation)
|
||||||
|
@ -339,13 +344,13 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) {
|
||||||
|
|
||||||
if let Expr::Constant(ast::ExprConstant {
|
if let Expr::Constant(ast::ExprConstant {
|
||||||
range,
|
range,
|
||||||
value: Constant::Str(value),
|
value: Constant::Str(string),
|
||||||
..
|
..
|
||||||
}) = annotation.as_ref()
|
}) = annotation.as_ref()
|
||||||
{
|
{
|
||||||
// Quoted annotation.
|
// Quoted annotation.
|
||||||
if let Ok((annotation, kind)) = parse_type_annotation(value, *range, checker.locator) {
|
if let Ok((annotation, kind)) = parse_type_annotation(string, *range, checker.locator) {
|
||||||
let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic()) else {
|
let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic(), checker.locator) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let conversion_type = checker.settings.target_version.into();
|
let conversion_type = checker.settings.target_version.into();
|
||||||
|
@ -361,7 +366,7 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unquoted annotation.
|
// Unquoted annotation.
|
||||||
let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic()) else {
|
let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic(), checker.locator) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let conversion_type = checker.settings.target_version.into();
|
let conversion_type = checker.settings.target_version.into();
|
||||||
|
|
|
@ -359,4 +359,22 @@ RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional`
|
||||||
|
|
|
|
||||||
= help: Convert to `Optional[T]`
|
= help: Convert to `Optional[T]`
|
||||||
|
|
||||||
|
RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||||
|
|
|
||||||
|
209 | def f(arg: Union["int", "str"] = None): # RUF013
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ RUF013
|
||||||
|
210 | pass
|
||||||
|
|
|
||||||
|
= help: Convert to `Optional[T]`
|
||||||
|
|
||||||
|
ℹ Suggested fix
|
||||||
|
206 206 | pass
|
||||||
|
207 207 |
|
||||||
|
208 208 |
|
||||||
|
209 |-def f(arg: Union["int", "str"] = None): # RUF013
|
||||||
|
209 |+def f(arg: Optional[Union["int", "str"]] = None): # RUF013
|
||||||
|
210 210 | pass
|
||||||
|
211 211 |
|
||||||
|
212 212 |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -359,4 +359,22 @@ RUF013_0.py:201:12: RUF013 PEP 484 prohibits implicit `Optional`
|
||||||
|
|
|
|
||||||
= help: Convert to `T | None`
|
= help: Convert to `T | None`
|
||||||
|
|
||||||
|
RUF013_0.py:209:12: RUF013 [*] PEP 484 prohibits implicit `Optional`
|
||||||
|
|
|
||||||
|
209 | def f(arg: Union["int", "str"] = None): # RUF013
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ RUF013
|
||||||
|
210 | pass
|
||||||
|
|
|
||||||
|
= help: Convert to `T | None`
|
||||||
|
|
||||||
|
ℹ Suggested fix
|
||||||
|
206 206 | pass
|
||||||
|
207 207 |
|
||||||
|
208 208 |
|
||||||
|
209 |-def f(arg: Union["int", "str"] = None): # RUF013
|
||||||
|
209 |+def f(arg: Union["int", "str"] | None = None): # RUF013
|
||||||
|
210 210 | pass
|
||||||
|
211 211 |
|
||||||
|
212 212 |
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue