Skip to content

Commit e231ad9

Browse files
committed
[ruff] Implement nested-annotated-type (RUF066)
1 parent 17c7b3c commit e231ad9

File tree

8 files changed

+312
-0
lines changed

8 files changed

+312
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import Annotated, Optional, TypeAlias, Union
2+
3+
# Errors
4+
a: Union[Annotated[int, "An integer"], None] = 5
5+
b: Annotated[Union[int, str], "An integer or string"] | None = "World"
6+
c: Optional[Annotated[float, "A float"]] = 2.71
7+
d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
8+
9+
def f1(x: Union[Annotated[int, "An integer"], None]) -> None:
10+
pass
11+
12+
def f2(y: Annotated[Union[int, str], "An integer or string"] | None) -> None:
13+
pass
14+
15+
def f3(z: Optional[Annotated[float, "A float"]]) -> None:
16+
pass
17+
18+
e: Annotated[Annotated[int, "An integer"], "Another annotation"]
19+
20+
l: TypeAlias = Annotated[int, "An integer"] | None
21+
m: TypeAlias = Union[Annotated[int, "An integer"], str]
22+
n: TypeAlias = Optional[Annotated[float, "A float"]]
23+
o: TypeAlias = "Annotated[str, 'A string'] | None"
24+
25+
p: None | Annotated[int, "An integer"]
26+
27+
q = Annotated[int | str, "An integer or string"] | None
28+
r = Optional[Annotated[float | None, "A float or None"]]
29+
30+
@some_decorator()
31+
def func(x: Annotated[str, "A string"] | None) -> None:
32+
pass
33+
34+
35+
# OK
36+
g: Annotated[Union[int, str], "An integer or string"] = 42
37+
h: Annotated[Union[float, None], "A float or None"] = 3.14
38+
39+
def f4(x: Annotated[Union[int, str], "An integer or string"]) -> None:
40+
pass
41+
42+
def f5(y: Annotated[Union[float, None], "A float or None"]) -> None:
43+
pass
44+
45+
def f6(z: Annotated[str | None, func()]):
46+
pass
47+
48+
i: Union[int, str] = 7
49+
j: Optional[float] = None
50+
k: Annotated[int, "An integer"] = 10

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
199199
if checker.is_rule_enabled(Rule::AccessAnnotationsFromClassDict) {
200200
ruff::rules::access_annotations_from_class_dict_by_key(checker, subscript);
201201
}
202+
if checker.is_rule_enabled(Rule::NestedAnnotatedType) {
203+
ruff::rules::nested_annotated_type(checker, subscript);
204+
}
202205
pandas_vet::rules::subscript(checker, value, expr);
203206
}
204207
Expr::Tuple(ast::ExprTuple {

crates/ruff_linter/src/codes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
10581058
(Ruff, "063") => rules::ruff::rules::AccessAnnotationsFromClassDict,
10591059
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
10601060
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
1061+
(Ruff, "066") => rules::ruff::rules::NestedAnnotatedType,
10611062

10621063
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
10631064
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,

crates/ruff_linter/src/rules/ruff/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ mod tests {
113113
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
114114
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
115115
#[test_case(Rule::LoggingEagerConversion, Path::new("RUF065.py"))]
116+
#[test_case(Rule::NestedAnnotatedType, Path::new("RUF066.py"))]
116117
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
117118
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
118119
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]

crates/ruff_linter/src/rules/ruff/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub(crate) use mutable_class_default::*;
3030
pub(crate) use mutable_dataclass_default::*;
3131
pub(crate) use mutable_fromkeys_value::*;
3232
pub(crate) use needless_else::*;
33+
pub(crate) use nested_annotated_type::*;
3334
pub(crate) use never_union::*;
3435
pub(crate) use non_octal_permissions::*;
3536
pub(crate) use none_not_at_end_of_union::*;
@@ -94,6 +95,7 @@ mod mutable_class_default;
9495
mod mutable_dataclass_default;
9596
mod mutable_fromkeys_value;
9697
mod needless_else;
98+
mod nested_annotated_type;
9799
mod never_union;
98100
mod non_octal_permissions;
99101
mod none_not_at_end_of_union;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use ruff_macros::{ViolationMetadata, derive_message_formats};
2+
use ruff_python_ast::{Expr, ExprSubscript};
3+
use ruff_text_size::Ranged;
4+
5+
use crate::Violation;
6+
use crate::checkers::ast::Checker;
7+
8+
/// ## What it does
9+
/// Checks for `Annotated[]` types nested within other subscript types or union types.
10+
///
11+
/// ## Why is this bad?
12+
/// Consumers of `Annotated` types often only check the top-level type for annotations,
13+
/// and may miss `Annotated` types inside other types, such as `Optional` or `Union`
14+
///
15+
/// ```python
16+
/// from typing import Annotated, get_type_hints
17+
///
18+
/// def f(a: Annotated[str, "test data"]): ...
19+
/// def z(a: Annotated[str, "test data"] | None): ...
20+
/// def b(a: Annotated[str | None, "test data"]): ...
21+
///
22+
/// get_type_hints(f, include_extras=True)
23+
/// # {'a': typing.Annotated[str, 'test data']}
24+
/// get_type_hints(z, include_extras=True)
25+
/// # {'a': typing.Optional[typing.Annotated[str, 'test data']]}
26+
/// get_type_hints(b, include_extras=True)
27+
/// # {'a': typing.Annotated[str | None, 'test data']}
28+
/// ```
29+
///
30+
#[derive(ViolationMetadata)]
31+
#[violation_metadata(preview_since = "0.14.3")]
32+
pub(crate) struct NestedAnnotatedType {
33+
parent_type: ParentType,
34+
}
35+
36+
impl Violation for NestedAnnotatedType {
37+
#[derive_message_formats]
38+
fn message(&self) -> String {
39+
match self.parent_type {
40+
ParentType::Subscript => {
41+
"`Annotated[]` type must not be nested within another type".to_string()
42+
}
43+
ParentType::BinOp => {
44+
"`Annotated[]` type must not be nested within a PEP604 type union (|)".to_string()
45+
}
46+
}
47+
}
48+
}
49+
50+
/// RUF066
51+
pub(crate) fn nested_annotated_type(checker: &Checker, subscript: &ExprSubscript) {
52+
let semantic = checker.semantic();
53+
54+
if !semantic.match_typing_expr(&subscript.value, "Annotated") {
55+
return;
56+
}
57+
58+
let result = semantic
59+
.current_expressions()
60+
.skip(1)
61+
.filter_map(|expr| match expr {
62+
Expr::Subscript(_) => Some((expr, ParentType::Subscript)),
63+
Expr::BinOp(_) => Some((expr, ParentType::BinOp)),
64+
_ => None,
65+
})
66+
.last();
67+
68+
if let Some((parent, parent_type)) = result {
69+
checker.report_diagnostic(NestedAnnotatedType { parent_type }, parent.range());
70+
}
71+
}
72+
73+
enum ParentType {
74+
Subscript,
75+
BinOp,
76+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
source: crates/ruff_linter/src/rules/ruff/mod.rs
3+
---
4+
RUF066 `Annotated[]` type must not be nested within another type
5+
--> RUF066.py:4:4
6+
|
7+
3 | # Errors
8+
4 | a: Union[Annotated[int, "An integer"], None] = 5
9+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10+
5 | b: Annotated[Union[int, str], "An integer or string"] | None = "World"
11+
6 | c: Optional[Annotated[float, "A float"]] = 2.71
12+
|
13+
14+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
15+
--> RUF066.py:5:4
16+
|
17+
3 | # Errors
18+
4 | a: Union[Annotated[int, "An integer"], None] = 5
19+
5 | b: Annotated[Union[int, str], "An integer or string"] | None = "World"
20+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21+
6 | c: Optional[Annotated[float, "A float"]] = 2.71
22+
7 | d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
23+
|
24+
25+
RUF066 `Annotated[]` type must not be nested within another type
26+
--> RUF066.py:6:4
27+
|
28+
4 | a: Union[Annotated[int, "An integer"], None] = 5
29+
5 | b: Annotated[Union[int, str], "An integer or string"] | None = "World"
30+
6 | c: Optional[Annotated[float, "A float"]] = 2.71
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
7 | d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
33+
|
34+
35+
RUF066 `Annotated[]` type must not be nested within another type
36+
--> RUF066.py:7:4
37+
|
38+
5 | b: Annotated[Union[int, str], "An integer or string"] | None = "World"
39+
6 | c: Optional[Annotated[float, "A float"]] = 2.71
40+
7 | d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
41+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42+
8 |
43+
9 | def f1(x: Union[Annotated[int, "An integer"], None]) -> None:
44+
|
45+
46+
RUF066 `Annotated[]` type must not be nested within another type
47+
--> RUF066.py:7:4
48+
|
49+
5 | b: Annotated[Union[int, str], "An integer or string"] | None = "World"
50+
6 | c: Optional[Annotated[float, "A float"]] = 2.71
51+
7 | d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
52+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53+
8 |
54+
9 | def f1(x: Union[Annotated[int, "An integer"], None]) -> None:
55+
|
56+
57+
RUF066 `Annotated[]` type must not be nested within another type
58+
--> RUF066.py:9:11
59+
|
60+
7 | d: Union[Annotated[int, "An integer"], Annotated[str, "A string"]]
61+
8 |
62+
9 | def f1(x: Union[Annotated[int, "An integer"], None]) -> None:
63+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64+
10 | pass
65+
|
66+
67+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
68+
--> RUF066.py:12:11
69+
|
70+
10 | pass
71+
11 |
72+
12 | def f2(y: Annotated[Union[int, str], "An integer or string"] | None) -> None:
73+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74+
13 | pass
75+
|
76+
77+
RUF066 `Annotated[]` type must not be nested within another type
78+
--> RUF066.py:15:11
79+
|
80+
13 | pass
81+
14 |
82+
15 | def f3(z: Optional[Annotated[float, "A float"]]) -> None:
83+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84+
16 | pass
85+
|
86+
87+
RUF066 `Annotated[]` type must not be nested within another type
88+
--> RUF066.py:18:4
89+
|
90+
16 | pass
91+
17 |
92+
18 | e: Annotated[Annotated[int, "An integer"], "Another annotation"]
93+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
94+
19 |
95+
20 | l: TypeAlias = Annotated[int, "An integer"] | None
96+
|
97+
98+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
99+
--> RUF066.py:20:16
100+
|
101+
18 | e: Annotated[Annotated[int, "An integer"], "Another annotation"]
102+
19 |
103+
20 | l: TypeAlias = Annotated[int, "An integer"] | None
104+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
105+
21 | m: TypeAlias = Union[Annotated[int, "An integer"], str]
106+
22 | n: TypeAlias = Optional[Annotated[float, "A float"]]
107+
|
108+
109+
RUF066 `Annotated[]` type must not be nested within another type
110+
--> RUF066.py:21:16
111+
|
112+
20 | l: TypeAlias = Annotated[int, "An integer"] | None
113+
21 | m: TypeAlias = Union[Annotated[int, "An integer"], str]
114+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115+
22 | n: TypeAlias = Optional[Annotated[float, "A float"]]
116+
23 | o: TypeAlias = "Annotated[str, 'A string'] | None"
117+
|
118+
119+
RUF066 `Annotated[]` type must not be nested within another type
120+
--> RUF066.py:22:16
121+
|
122+
20 | l: TypeAlias = Annotated[int, "An integer"] | None
123+
21 | m: TypeAlias = Union[Annotated[int, "An integer"], str]
124+
22 | n: TypeAlias = Optional[Annotated[float, "A float"]]
125+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
126+
23 | o: TypeAlias = "Annotated[str, 'A string'] | None"
127+
|
128+
129+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
130+
--> RUF066.py:23:17
131+
|
132+
21 | m: TypeAlias = Union[Annotated[int, "An integer"], str]
133+
22 | n: TypeAlias = Optional[Annotated[float, "A float"]]
134+
23 | o: TypeAlias = "Annotated[str, 'A string'] | None"
135+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
136+
24 |
137+
25 | p: None | Annotated[int, "An integer"]
138+
|
139+
140+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
141+
--> RUF066.py:25:4
142+
|
143+
23 | o: TypeAlias = "Annotated[str, 'A string'] | None"
144+
24 |
145+
25 | p: None | Annotated[int, "An integer"]
146+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
147+
26 |
148+
27 | q = Annotated[int | str, "An integer or string"] | None
149+
|
150+
151+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
152+
--> RUF066.py:27:5
153+
|
154+
25 | p: None | Annotated[int, "An integer"]
155+
26 |
156+
27 | q = Annotated[int | str, "An integer or string"] | None
157+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
158+
28 | r = Optional[Annotated[float | None, "A float or None"]]
159+
|
160+
161+
RUF066 `Annotated[]` type must not be nested within another type
162+
--> RUF066.py:28:5
163+
|
164+
27 | q = Annotated[int | str, "An integer or string"] | None
165+
28 | r = Optional[Annotated[float | None, "A float or None"]]
166+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
167+
29 |
168+
30 | @some_decorator()
169+
|
170+
171+
RUF066 `Annotated[]` type must not be nested within a PEP604 type union (|)
172+
--> RUF066.py:31:13
173+
|
174+
30 | @some_decorator()
175+
31 | def func(x: Annotated[str, "A string"] | None) -> None:
176+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
177+
32 | pass
178+
|

ruff.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)