forked from astral-sh/ruff
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
…sh#14316) ## Summary `Literal[None]` can be simplified into `None` in type annotations. Surprising to see that this is not that rare: - https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/chat_models/base.py#L54 - https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/annotation.py#L69 - https://github.com/jax-ml/jax/blob/main/jax/numpy/__init__.pyi#L961 - https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/inference/_common.py#L179 ## Test Plan `cargo test` Reviewed all ecosystem results, and they are true positives. --------- Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Charlie Marsh <[email protected]>
- Loading branch information
Showing
11 changed files
with
618 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from typing import Literal | ||
|
||
|
||
def func1(arg1: Literal[None]): | ||
... | ||
|
||
|
||
def func2(arg1: Literal[None] | int): | ||
... | ||
|
||
|
||
def func3() -> Literal[None]: | ||
... | ||
|
||
|
||
def func4(arg1: Literal[int, None, float]): | ||
... | ||
|
||
|
||
def func5(arg1: Literal[None, None]): | ||
... | ||
|
||
|
||
def func6(arg1: Literal[ | ||
"hello", | ||
None # Comment 1 | ||
, "world" | ||
]): | ||
... | ||
|
||
|
||
def func7(arg1: Literal[ | ||
None # Comment 1 | ||
]): | ||
... | ||
|
||
|
||
# OK | ||
def good_func(arg1: Literal[int] | None): | ||
... | ||
|
||
|
||
# From flake8-pyi | ||
Literal[None] # Y061 None inside "Literal[]" expression. Replace with "None" | ||
Literal[True, None] # Y061 None inside "Literal[]" expression. Replace with "Literal[True] | None" | ||
|
||
### | ||
# The following rules here are slightly subtle, | ||
# but make sense when it comes to giving the best suggestions to users of flake8-pyi. | ||
### | ||
|
||
# If Y061 and Y062 both apply, but all the duplicate members are None, | ||
# only emit Y061... | ||
Literal[None, None] # Y061 None inside "Literal[]" expression. Replace with "None" | ||
Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replace with "Literal[1, 'foo'] | None" | ||
|
||
# ... but if Y061 and Y062 both apply | ||
# and there are no None members in the Literal[] slice, | ||
# only emit Y062: | ||
Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True" |
37 changes: 37 additions & 0 deletions
37
crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI061.pyi
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from typing import Literal | ||
|
||
|
||
def func1(arg1: Literal[None]): ... | ||
|
||
|
||
def func2(arg1: Literal[None] | int): ... | ||
|
||
|
||
def func3() -> Literal[None]: ... | ||
|
||
|
||
def func4(arg1: Literal[int, None, float]): ... | ||
|
||
|
||
def func5(arg1: Literal[None, None]): ... | ||
|
||
|
||
def func6(arg1: Literal[ | ||
"hello", | ||
None # Comment 1 | ||
, "world" | ||
]): ... | ||
|
||
|
||
def func7(arg1: Literal[ | ||
None # Comment 1 | ||
]): ... | ||
|
||
|
||
# OK | ||
def good_func(arg1: Literal[int] | None): ... | ||
|
||
|
||
# From flake8-pyi | ||
Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None" | ||
Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::{Expr, ExprNoneLiteral}; | ||
use ruff_python_semantic::analyze::typing::traverse_literal; | ||
use ruff_text_size::Ranged; | ||
|
||
use smallvec::SmallVec; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for redundant `Literal[None]` annotations. | ||
/// | ||
/// ## Why is this bad? | ||
/// While `Literal[None]` is a valid type annotation, it is semantically equivalent to `None`. | ||
/// Prefer `None` over `Literal[None]` for both consistency and readability. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// from typing import Literal | ||
/// | ||
/// Literal[None] | ||
/// Literal[1, 2, 3, "foo", 5, None] | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// from typing import Literal | ||
/// | ||
/// None | ||
/// Literal[1, 2, 3, "foo", 5] | None | ||
/// ``` | ||
/// | ||
/// ## References | ||
/// - [Typing documentation: Legal parameters for `Literal` at type check time](https://typing.readthedocs.io/en/latest/spec/literal.html#legal-parameters-for-literal-at-type-check-time) | ||
#[violation] | ||
pub struct RedundantNoneLiteral { | ||
other_literal_elements_seen: bool, | ||
} | ||
|
||
impl Violation for RedundantNoneLiteral { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
|
||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
if self.other_literal_elements_seen { | ||
"`Literal[None, ...]` can be replaced with `Literal[...] | None`".to_string() | ||
} else { | ||
"`Literal[None]` can be replaced with `None`".to_string() | ||
} | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
Some(if self.other_literal_elements_seen { | ||
"Replace with `Literal[...] | None`".to_string() | ||
} else { | ||
"Replace with `None`".to_string() | ||
}) | ||
} | ||
} | ||
|
||
/// RUF037 | ||
pub(crate) fn redundant_none_literal<'a>(checker: &mut Checker, literal_expr: &'a Expr) { | ||
if !checker.semantic().seen_typing() { | ||
return; | ||
} | ||
|
||
let mut none_exprs: SmallVec<[&ExprNoneLiteral; 1]> = SmallVec::new(); | ||
let mut other_literal_elements_seen = false; | ||
|
||
let mut find_none = |expr: &'a Expr, _parent: &'a Expr| { | ||
if let Expr::NoneLiteral(none_expr) = expr { | ||
none_exprs.push(none_expr); | ||
} else { | ||
other_literal_elements_seen = true; | ||
} | ||
}; | ||
|
||
traverse_literal(&mut find_none, checker.semantic(), literal_expr); | ||
|
||
if none_exprs.is_empty() { | ||
return; | ||
} | ||
|
||
// Provide a [`Fix`] when the complete `Literal` can be replaced. Applying the fix | ||
// can leave an unused import to be fixed by the `unused-import` rule. | ||
let fix = if other_literal_elements_seen { | ||
None | ||
} else { | ||
Some(Fix::applicable_edit( | ||
Edit::range_replacement("None".to_string(), literal_expr.range()), | ||
if checker.comment_ranges().intersects(literal_expr.range()) { | ||
Applicability::Unsafe | ||
} else { | ||
Applicability::Safe | ||
}, | ||
)) | ||
}; | ||
|
||
for none_expr in none_exprs { | ||
let mut diagnostic = Diagnostic::new( | ||
RedundantNoneLiteral { | ||
other_literal_elements_seen, | ||
}, | ||
none_expr.range(), | ||
); | ||
if let Some(ref fix) = fix { | ||
diagnostic.set_fix(fix.clone()); | ||
} | ||
checker.diagnostics.push(diagnostic); | ||
} | ||
} |
Oops, something went wrong.