mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
[ruff
] Implement none-not-at-end-of-union
(RUF036
) (#14314)
This commit is contained in:
parent
577de6c599
commit
a40bc6a460
10 changed files with 300 additions and 0 deletions
47
crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py
vendored
Normal file
47
crates/ruff_linter/resources/test/fixtures/ruff/RUF036.py
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from typing import Union as U
|
||||||
|
|
||||||
|
|
||||||
|
def func1(arg: None | int):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def func2() -> None | int:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def func3(arg: None | None | int):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def func4(arg: U[None, int]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def func5() -> U[None, int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def func6(arg: U[None, None, int]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
# Ok
|
||||||
|
def good_func1(arg: int | None):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def good_func2() -> int | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def good_func3(arg: None):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def good_func4(arg: U[None]):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def good_func5(arg: U[int]):
|
||||||
|
...
|
||||||
|
|
25
crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi
vendored
Normal file
25
crates/ruff_linter/resources/test/fixtures/ruff/RUF036.pyi
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from typing import Union as U
|
||||||
|
|
||||||
|
|
||||||
|
def func1(arg: None | int): ...
|
||||||
|
|
||||||
|
def func2() -> None | int: ...
|
||||||
|
|
||||||
|
def func3(arg: None | None | int): ...
|
||||||
|
|
||||||
|
def func4(arg: U[None, int]): ...
|
||||||
|
|
||||||
|
def func5() -> U[None, int]: ...
|
||||||
|
|
||||||
|
def func6(arg: U[None, None, int]): ...
|
||||||
|
|
||||||
|
# Ok
|
||||||
|
def good_func1(arg: int | None): ...
|
||||||
|
|
||||||
|
def good_func2() -> int | None: ...
|
||||||
|
|
||||||
|
def good_func3(arg: None): ...
|
||||||
|
|
||||||
|
def good_func4(arg: U[None]): ...
|
||||||
|
|
||||||
|
def good_func5(arg: U[int]): ...
|
|
@ -80,6 +80,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
Rule::DuplicateUnionMember,
|
Rule::DuplicateUnionMember,
|
||||||
Rule::RedundantLiteralUnion,
|
Rule::RedundantLiteralUnion,
|
||||||
Rule::UnnecessaryTypeUnion,
|
Rule::UnnecessaryTypeUnion,
|
||||||
|
Rule::NoneNotAtEndOfUnion,
|
||||||
]) {
|
]) {
|
||||||
// Avoid duplicate checks if the parent is a union, since these rules already
|
// Avoid duplicate checks if the parent is a union, since these rules already
|
||||||
// traverse nested unions.
|
// traverse nested unions.
|
||||||
|
@ -96,6 +97,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::UnnecessaryTypeUnion) {
|
if checker.enabled(Rule::UnnecessaryTypeUnion) {
|
||||||
flake8_pyi::rules::unnecessary_type_union(checker, expr);
|
flake8_pyi::rules::unnecessary_type_union(checker, expr);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::NoneNotAtEndOfUnion) {
|
||||||
|
ruff::rules::none_not_at_end_of_union(checker, expr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1282,6 +1286,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::RuntimeStringUnion) {
|
if checker.enabled(Rule::RuntimeStringUnion) {
|
||||||
flake8_type_checking::rules::runtime_string_union(checker, expr);
|
flake8_type_checking::rules::runtime_string_union(checker, expr);
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::NoneNotAtEndOfUnion) {
|
||||||
|
ruff::rules::none_not_at_end_of_union(checker, expr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::UnaryOp(
|
Expr::UnaryOp(
|
||||||
|
|
|
@ -968,6 +968,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),
|
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),
|
||||||
(Ruff, "034") => (RuleGroup::Preview, rules::ruff::rules::UselessIfElse),
|
(Ruff, "034") => (RuleGroup::Preview, rules::ruff::rules::UselessIfElse),
|
||||||
(Ruff, "035") => (RuleGroup::Preview, rules::ruff::rules::UnsafeMarkupUse),
|
(Ruff, "035") => (RuleGroup::Preview, rules::ruff::rules::UnsafeMarkupUse),
|
||||||
|
(Ruff, "036") => (RuleGroup::Preview, rules::ruff::rules::NoneNotAtEndOfUnion),
|
||||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ mod tests {
|
||||||
#[test_case(Rule::UselessIfElse, Path::new("RUF034.py"))]
|
#[test_case(Rule::UselessIfElse, Path::new("RUF034.py"))]
|
||||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
||||||
#[test_case(Rule::PostInitDefault, Path::new("RUF033.py"))]
|
#[test_case(Rule::PostInitDefault, Path::new("RUF033.py"))]
|
||||||
|
#[test_case(Rule::NoneNotAtEndOfUnion, Path::new("RUF036.py"))]
|
||||||
|
#[test_case(Rule::NoneNotAtEndOfUnion, Path::new("RUF036.pyi"))]
|
||||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||||
let diagnostics = test_path(
|
let diagnostics = test_path(
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub(crate) use mutable_class_default::*;
|
||||||
pub(crate) use mutable_dataclass_default::*;
|
pub(crate) use mutable_dataclass_default::*;
|
||||||
pub(crate) use mutable_fromkeys_value::*;
|
pub(crate) use mutable_fromkeys_value::*;
|
||||||
pub(crate) use never_union::*;
|
pub(crate) use never_union::*;
|
||||||
|
pub(crate) use none_not_at_end_of_union::*;
|
||||||
pub(crate) use parenthesize_logical_operators::*;
|
pub(crate) use parenthesize_logical_operators::*;
|
||||||
pub(crate) use post_init_default::*;
|
pub(crate) use post_init_default::*;
|
||||||
pub(crate) use quadratic_list_summation::*;
|
pub(crate) use quadratic_list_summation::*;
|
||||||
|
@ -55,6 +56,7 @@ mod mutable_class_default;
|
||||||
mod mutable_dataclass_default;
|
mod mutable_dataclass_default;
|
||||||
mod mutable_fromkeys_value;
|
mod mutable_fromkeys_value;
|
||||||
mod never_union;
|
mod never_union;
|
||||||
|
mod none_not_at_end_of_union;
|
||||||
mod parenthesize_logical_operators;
|
mod parenthesize_logical_operators;
|
||||||
mod post_init_default;
|
mod post_init_default;
|
||||||
mod quadratic_list_summation;
|
mod quadratic_list_summation;
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_python_ast::Expr;
|
||||||
|
use ruff_python_semantic::analyze::typing::traverse_union;
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for type annotations where `None` is not at the end of an union.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// Type annotation unions are associative, meaning that the order of the elements
|
||||||
|
/// does not matter. The `None` literal represents the absence of a value. For
|
||||||
|
/// readability, it's preferred to write the more informative type expressions first.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// def func(arg: None | int): ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// def func(arg: int | None): ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## References
|
||||||
|
/// - [Python documentation: Union type](https://docs.python.org/3/library/stdtypes.html#types-union)
|
||||||
|
/// - [Python documentation: `typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)
|
||||||
|
/// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None)
|
||||||
|
#[violation]
|
||||||
|
pub struct NoneNotAtEndOfUnion;
|
||||||
|
|
||||||
|
impl Violation for NoneNotAtEndOfUnion {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
"`None` not at the end of the type annotation.".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RUF036
|
||||||
|
pub(crate) fn none_not_at_end_of_union<'a>(checker: &mut Checker, union: &'a Expr) {
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
let mut none_exprs: SmallVec<[&Expr; 1]> = SmallVec::new();
|
||||||
|
|
||||||
|
let mut last_expr: Option<&Expr> = None;
|
||||||
|
let mut find_none = |expr: &'a Expr, _parent: &Expr| {
|
||||||
|
if matches!(expr, Expr::NoneLiteral(_)) {
|
||||||
|
none_exprs.push(expr);
|
||||||
|
}
|
||||||
|
last_expr = Some(expr);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walk through all type expressions in the union and keep track of `None` literals.
|
||||||
|
traverse_union(&mut find_none, semantic, union);
|
||||||
|
|
||||||
|
let Some(last_expr) = last_expr else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The must be at least one `None` expression.
|
||||||
|
let Some(last_none) = none_exprs.last() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If any of the `None` literals is last we do not emit.
|
||||||
|
if *last_none == last_expr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for none_expr in none_exprs {
|
||||||
|
checker
|
||||||
|
.diagnostics
|
||||||
|
.push(Diagnostic::new(NoneNotAtEndOfUnion, none_expr.range()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||||
|
---
|
||||||
|
RUF036.py:4:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
4 | def func1(arg: None | int):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
5 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:8:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
8 | def func2() -> None | int:
|
||||||
|
| ^^^^ RUF036
|
||||||
|
9 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:12:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
12 | def func3(arg: None | None | int):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
13 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:12:23: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
12 | def func3(arg: None | None | int):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
13 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:16:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
16 | def func4(arg: U[None, int]):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
17 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:20:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
20 | def func5() -> U[None, int]:
|
||||||
|
| ^^^^ RUF036
|
||||||
|
21 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:24:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
24 | def func6(arg: U[None, None, int]):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
25 | ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.py:24:24: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
24 | def func6(arg: U[None, None, int]):
|
||||||
|
| ^^^^ RUF036
|
||||||
|
25 | ...
|
||||||
|
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||||
|
---
|
||||||
|
RUF036.pyi:4:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
4 | def func1(arg: None | int): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
5 |
|
||||||
|
6 | def func2() -> None | int: ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:6:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
4 | def func1(arg: None | int): ...
|
||||||
|
5 |
|
||||||
|
6 | def func2() -> None | int: ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
7 |
|
||||||
|
8 | def func3(arg: None | None | int): ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:8:16: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
6 | def func2() -> None | int: ...
|
||||||
|
7 |
|
||||||
|
8 | def func3(arg: None | None | int): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
9 |
|
||||||
|
10 | def func4(arg: U[None, int]): ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:8:23: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
6 | def func2() -> None | int: ...
|
||||||
|
7 |
|
||||||
|
8 | def func3(arg: None | None | int): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
9 |
|
||||||
|
10 | def func4(arg: U[None, int]): ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:10:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
8 | def func3(arg: None | None | int): ...
|
||||||
|
9 |
|
||||||
|
10 | def func4(arg: U[None, int]): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
11 |
|
||||||
|
12 | def func5() -> U[None, int]: ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:12:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
10 | def func4(arg: U[None, int]): ...
|
||||||
|
11 |
|
||||||
|
12 | def func5() -> U[None, int]: ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
13 |
|
||||||
|
14 | def func6(arg: U[None, None, int]): ...
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:14:18: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
12 | def func5() -> U[None, int]: ...
|
||||||
|
13 |
|
||||||
|
14 | def func6(arg: U[None, None, int]): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
15 |
|
||||||
|
16 | # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
RUF036.pyi:14:24: RUF036 `None` not at the end of the type annotation.
|
||||||
|
|
|
||||||
|
12 | def func5() -> U[None, int]: ...
|
||||||
|
13 |
|
||||||
|
14 | def func6(arg: U[None, None, int]): ...
|
||||||
|
| ^^^^ RUF036
|
||||||
|
15 |
|
||||||
|
16 | # Ok
|
||||||
|
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3833,6 +3833,7 @@
|
||||||
"RUF033",
|
"RUF033",
|
||||||
"RUF034",
|
"RUF034",
|
||||||
"RUF035",
|
"RUF035",
|
||||||
|
"RUF036",
|
||||||
"RUF1",
|
"RUF1",
|
||||||
"RUF10",
|
"RUF10",
|
||||||
"RUF100",
|
"RUF100",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue