-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ruff
] Implement unnecessary-nested-literal
(RUF039
)
#14323
base: main
Are you sure you want to change the base?
Changes from all commits
ebbd9f1
1bee19a
069327d
f5c223e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from typing import Literal | ||
import typing as t | ||
import typing_extensions | ||
|
||
|
||
y: Literal[1, print("hello"), 3, Literal[4, 1]] | ||
Literal[1, Literal[1]] | ||
Literal[1, 2, Literal[1, 2]] | ||
Literal[1, Literal[1], Literal[1]] | ||
Literal[1, Literal[2], Literal[2]] | ||
t.Literal[1, t.Literal[2, t.Literal[1]]] | ||
Literal[ | ||
1, # comment 1 | ||
Literal[ # another comment | ||
1 # yet annother comment | ||
] | ||
] # once | ||
|
||
# Ensure issue is only raised once, even on nested literals | ||
MyType = Literal["foo", Literal[True, False, True], "bar"] | ||
|
||
# nested literals, all equivalent to `Literal[1]` | ||
Literal[Literal[1]] | ||
Literal[Literal[Literal[1], Literal[1]]] | ||
Literal[Literal[1], Literal[Literal[Literal[1]]]] | ||
|
||
# OK | ||
x: Literal[True, False, True, False] | ||
z: Literal[{1, 3, 5}, "foobar", {1,3,5}] | ||
typing_extensions.Literal[1, 1, 1] | ||
n: Literal["No", "duplicates", "here", 1, "1"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from typing import Literal | ||
import typing as t | ||
import typing_extensions | ||
|
||
|
||
y: Literal[1, print("hello"), 3, Literal[4, 1]] | ||
Literal[1, Literal[1]] | ||
Literal[1, 2, Literal[1, 2]] | ||
Literal[1, Literal[1], Literal[1]] | ||
Literal[1, Literal[2], Literal[2]] | ||
t.Literal[1, t.Literal[2, t.Literal[1]]] | ||
Literal[ | ||
1, # comment 1 | ||
Literal[ # another comment | ||
1 # yet annother comment | ||
] | ||
] # once | ||
|
||
# Ensure issue is only raised once, even on nested literals | ||
MyType = Literal["foo", Literal[True, False, True], "bar"] | ||
|
||
# nested literals, all equivalent to `Literal[1]` | ||
Literal[Literal[1]] | ||
Literal[Literal[Literal[1], Literal[1]]] | ||
Literal[Literal[1], Literal[Literal[Literal[1]]]] | ||
|
||
# OK | ||
x: Literal[True, False, True, False] | ||
z: Literal[{1, 3, 5}, "foobar", {1,3,5}] | ||
typing_extensions.Literal[1, 1, 1] | ||
n: Literal["No", "duplicates", "here", 1, "1"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::{AnyNodeRef, Expr, ExprContext, ExprSubscript, ExprTuple}; | ||
use ruff_python_semantic::analyze::typing::traverse_literal; | ||
use ruff_text_size::{Ranged, TextRange}; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for unnecessary nested `Literal`. | ||
/// | ||
/// ## Why is this bad? | ||
/// Prefer using a single `Literal`, which is equivalent and more concise. | ||
/// | ||
/// Parameterization of literals by other literals is supported as an ergonomic | ||
/// feature as proposed in [PEP 586], to enable patterns such as: | ||
/// ```python | ||
/// ReadOnlyMode = Literal["r", "r+"] | ||
/// WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"] | ||
/// WriteNoTruncateMode = Literal["r+", "r+t"] | ||
/// AppendMode = Literal["a", "a+", "at", "a+t"] | ||
/// | ||
/// AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode, | ||
/// WriteNoTruncateMode, AppendMode] | ||
/// ``` | ||
/// | ||
/// As a consequence, type checkers also support nesting of literals | ||
/// which is less readable than a flat `Literal`: | ||
/// ```python | ||
/// AllModes = Literal[Literal["r", "r+"], Literal["w", "w+", "wt", "w+t"], | ||
/// Literal["r+", "r+t"], Literal["a", "a+", "at", "a+t"]] | ||
/// ``` | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// AllModes = Literal[ | ||
/// Literal["r", "r+"], | ||
/// Literal["w", "w+", "wt", "w+t"], | ||
/// Literal["r+", "r+t"], | ||
/// Literal["a", "a+", "at", "a+t"], | ||
/// ] | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// AllModes = Literal[ | ||
/// "r", "r+", "w", "w+", "wt", "w+t", "r+", "r+t", "a", "a+", "at", "a+t" | ||
/// ] | ||
/// ``` | ||
/// | ||
/// or assign the literal to a variable as in the first example. | ||
/// | ||
/// ## Fix safety | ||
/// The fix for this rule is marked as unsafe when the `Literal` slice is split | ||
/// across multiple lines and some of the lines have trailing comments. | ||
/// | ||
/// ## 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) | ||
/// | ||
/// [PEP 586](https://peps.python.org/pep-0586/) | ||
#[violation] | ||
pub struct UnnecessaryNestedLiteral; | ||
|
||
impl Violation for UnnecessaryNestedLiteral { | ||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; | ||
|
||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
"Unnecessary nested `Literal`".to_string() | ||
} | ||
|
||
fn fix_title(&self) -> Option<String> { | ||
Some("Replace with flattened `Literal`".to_string()) | ||
} | ||
} | ||
|
||
/// RUF039 | ||
pub(crate) fn unnecessary_nested_literal<'a>(checker: &mut Checker, literal_expr: &'a Expr) { | ||
let mut nodes: Vec<&Expr> = Vec::new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might also be worth to use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
let mut is_nested = false; | ||
|
||
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| { | ||
// If the parent is not equal to the `literal_expr` then we know we are traversing recursively. | ||
if !AnyNodeRef::ptr_eq(parent.into(), literal_expr.into()) { | ||
is_nested = true; | ||
}; | ||
nodes.push(expr); | ||
}; | ||
|
||
// Traverse the type expressions in the `Literal`. | ||
traverse_literal( | ||
&mut check_for_duplicate_members, | ||
checker.semantic(), | ||
literal_expr, | ||
); | ||
|
||
if !is_nested { | ||
return; | ||
} | ||
Comment on lines
+97
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you expand on why we have to check inside the rule whether it is a nested literal, considering that the rule is only called when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only consider the top-level union, so when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, we actually only run the rule when not in a nested literal... |
||
|
||
let mut diagnostic = Diagnostic::new(UnnecessaryNestedLiteral, literal_expr.range()); | ||
|
||
// Create a [`Fix`] that flattens all nodes. | ||
if let Expr::Subscript(subscript) = literal_expr { | ||
let subscript = Expr::Subscript(ExprSubscript { | ||
slice: Box::new(if let [elt] = nodes.as_slice() { | ||
(*elt).clone() | ||
} else { | ||
Expr::Tuple(ExprTuple { | ||
elts: nodes.into_iter().cloned().collect(), | ||
range: TextRange::default(), | ||
ctx: ExprContext::Load, | ||
parenthesized: false, | ||
}) | ||
}), | ||
value: subscript.value.clone(), | ||
range: TextRange::default(), | ||
ctx: ExprContext::Load, | ||
}); | ||
let fix = Fix::applicable_edit( | ||
Edit::range_replacement(checker.generator().expr(&subscript), literal_expr.range()), | ||
if checker.comment_ranges().intersects(literal_expr.range()) { | ||
Applicability::Unsafe | ||
} else { | ||
Applicability::Safe | ||
}, | ||
); | ||
diagnostic.set_fix(fix); | ||
}; | ||
|
||
checker.diagnostics.push(diagnostic); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The direct answer to the question of "Why is this bad?" is "it's less readable than the alternative". I think something to that effect should be the first sentence of this section, as it makes it clear that this rule is about readability and style rather than correctness.