mirror of https://github.com/astral-sh/ruff
Refactor affix removal logic in slice_to_remove_prefix_or_suffix rule
- Replaced `FixSafety` enum with `Applicability` to determine the suitability of affix removal fixes. - Introduced new helper functions to assess expression types and their applicability for affix removal. - Updated diagnostic fixes to utilize the new applicability checks, enhancing the safety of modifications. - Improved code clarity and maintainability by consolidating type-checking logic.
This commit is contained in:
parent
968c9786aa
commit
a5694f1ef0
|
|
@ -1,9 +1,8 @@
|
||||||
use rustc_hash::FxHashSet;
|
use ruff_diagnostics::Applicability;
|
||||||
|
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
use ruff_python_ast::{self as ast, PythonVersion};
|
use ruff_python_ast::{self as ast, PythonVersion};
|
||||||
use ruff_python_semantic::analyze::typing::find_binding_value;
|
use ruff_python_semantic::analyze::typing::{find_binding_value, is_bytes, is_string};
|
||||||
use ruff_python_semantic::{BindingId, SemanticModel};
|
use ruff_python_semantic::{Binding, SemanticModel};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::Locator;
|
use crate::Locator;
|
||||||
|
|
@ -81,8 +80,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI
|
||||||
let kind = removal_data.affix_query.kind;
|
let kind = removal_data.affix_query.kind;
|
||||||
let text = removal_data.text;
|
let text = removal_data.text;
|
||||||
|
|
||||||
let Some(fix_safety) = remove_affix_fix_safety(&removal_data, checker.semantic())
|
let Some(applicability) = affix_applicability(&removal_data, checker.semantic()) else {
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -97,10 +95,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI
|
||||||
generate_removeaffix_expr(text, &removal_data.affix_query, checker.locator());
|
generate_removeaffix_expr(text, &removal_data.affix_query, checker.locator());
|
||||||
|
|
||||||
let edit = Edit::replacement(replacement, if_expr.start(), if_expr.end());
|
let edit = Edit::replacement(replacement, if_expr.start(), if_expr.end());
|
||||||
diagnostic.set_fix(match fix_safety {
|
diagnostic.set_fix(Fix::applicable_edit(edit, applicability));
|
||||||
FixSafety::Safe => Fix::safe_edit(edit),
|
|
||||||
FixSafety::Unsafe => Fix::unsafe_edit(edit),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,8 +110,7 @@ pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtI
|
||||||
let kind = removal_data.affix_query.kind;
|
let kind = removal_data.affix_query.kind;
|
||||||
let text = removal_data.text;
|
let text = removal_data.text;
|
||||||
|
|
||||||
let Some(fix_safety) = remove_affix_fix_safety(&removal_data, checker.semantic())
|
let Some(applicability) = affix_applicability(&removal_data, checker.semantic()) else {
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -135,10 +129,7 @@ pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtI
|
||||||
);
|
);
|
||||||
|
|
||||||
let edit = Edit::replacement(replacement, if_stmt.start(), if_stmt.end());
|
let edit = Edit::replacement(replacement, if_stmt.start(), if_stmt.end());
|
||||||
diagnostic.set_fix(match fix_safety {
|
diagnostic.set_fix(Fix::applicable_edit(edit, applicability));
|
||||||
FixSafety::Safe => Fix::safe_edit(edit),
|
|
||||||
FixSafety::Unsafe => Fix::unsafe_edit(edit),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -514,83 +505,89 @@ struct RemoveAffixData<'a> {
|
||||||
affix_query: AffixQuery<'a>,
|
affix_query: AffixQuery<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
/// Determines the applicability of the affix removal fix based on type information.
|
||||||
enum FixSafety {
|
///
|
||||||
Safe,
|
/// Returns:
|
||||||
Unsafe,
|
/// - `None` if the fix should be suppressed (incompatible types like tuple affixes)
|
||||||
|
/// - `Some(Applicability::Safe)` if both text and affix are deterministically typed as str or bytes
|
||||||
|
/// - `Some(Applicability::Unsafe)` if type information is unknown or uncertain
|
||||||
|
fn affix_applicability(data: &RemoveAffixData, semantic: &SemanticModel) -> Option<Applicability> {
|
||||||
|
// Check for tuple affixes - these should be suppressed
|
||||||
|
if is_tuple_affix(data.affix_query.affix, semantic) {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
let text_is_str = is_expr_string(data.text, semantic);
|
||||||
enum TypeKnowledge {
|
let text_is_bytes = is_expr_bytes(data.text, semantic);
|
||||||
KnownStr,
|
let affix_is_str = is_expr_string(data.affix_query.affix, semantic);
|
||||||
KnownBytes,
|
let affix_is_bytes = is_expr_bytes(data.affix_query.affix, semantic);
|
||||||
KnownNonStr,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_affix_fix_safety(data: &RemoveAffixData, semantic: &SemanticModel) -> Option<FixSafety> {
|
match (text_is_str, text_is_bytes, affix_is_str, affix_is_bytes) {
|
||||||
let text_knowledge = classify_expr_type(data.text, semantic);
|
// Both are deterministically str
|
||||||
let affix_knowledge = classify_expr_type(data.affix_query.affix, semantic);
|
(true, false, true, false) => Some(Applicability::Safe),
|
||||||
|
// Both are deterministically bytes
|
||||||
match (text_knowledge, affix_knowledge) {
|
(false, true, false, true) => Some(Applicability::Safe),
|
||||||
(TypeKnowledge::KnownNonStr, _) | (_, TypeKnowledge::KnownNonStr) => None,
|
// Type mismatch - suppress the fix
|
||||||
(TypeKnowledge::KnownStr, TypeKnowledge::KnownStr) => Some(FixSafety::Safe),
|
(true, false, false, true) | (false, true, true, false) => None,
|
||||||
(TypeKnowledge::KnownBytes, TypeKnowledge::KnownBytes) => Some(FixSafety::Safe),
|
// Unknown or ambiguous types - mark as unsafe
|
||||||
(TypeKnowledge::KnownStr, TypeKnowledge::KnownBytes)
|
_ => Some(Applicability::Unsafe),
|
||||||
| (TypeKnowledge::KnownBytes, TypeKnowledge::KnownStr) => None,
|
|
||||||
_ => Some(FixSafety::Unsafe),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_expr_type(expr: &ast::Expr, semantic: &SemanticModel) -> TypeKnowledge {
|
/// Check if an expression is a tuple or a variable bound to a tuple.
|
||||||
let mut seen = FxHashSet::default();
|
fn is_tuple_affix(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
||||||
classify_expr_type_inner(expr, semantic, &mut seen)
|
if matches!(expr, ast::Expr::Tuple(_)) {
|
||||||
}
|
return true;
|
||||||
|
|
||||||
fn classify_expr_type_inner(
|
|
||||||
expr: &ast::Expr,
|
|
||||||
semantic: &SemanticModel,
|
|
||||||
seen: &mut FxHashSet<BindingId>,
|
|
||||||
) -> TypeKnowledge {
|
|
||||||
use ast::Expr;
|
|
||||||
|
|
||||||
match expr {
|
|
||||||
_ if expr.is_string_literal_expr() || expr.is_f_string_expr() => TypeKnowledge::KnownStr,
|
|
||||||
Expr::BytesLiteral(_) => TypeKnowledge::KnownBytes,
|
|
||||||
Expr::Name(name) => classify_name_expr_type(name, semantic, seen),
|
|
||||||
Expr::NumberLiteral(_)
|
|
||||||
| Expr::BooleanLiteral(_)
|
|
||||||
| Expr::NoneLiteral(_)
|
|
||||||
| Expr::EllipsisLiteral(_)
|
|
||||||
| Expr::Tuple(_)
|
|
||||||
| Expr::List(_)
|
|
||||||
| Expr::Set(_)
|
|
||||||
| Expr::Dict(_)
|
|
||||||
| Expr::ListComp(_)
|
|
||||||
| Expr::SetComp(_)
|
|
||||||
| Expr::DictComp(_)
|
|
||||||
| Expr::Generator(_) => TypeKnowledge::KnownNonStr,
|
|
||||||
_ => TypeKnowledge::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn classify_name_expr_type(
|
|
||||||
name: &ast::ExprName,
|
|
||||||
semantic: &SemanticModel,
|
|
||||||
seen: &mut FxHashSet<BindingId>,
|
|
||||||
) -> TypeKnowledge {
|
|
||||||
let Some(binding_id) = semantic.only_binding(name) else {
|
|
||||||
return TypeKnowledge::Unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !seen.insert(binding_id) {
|
|
||||||
return TypeKnowledge::Unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let ast::Expr::Name(name) = expr {
|
||||||
|
if let Some(binding_id) = semantic.only_binding(name) {
|
||||||
let binding = semantic.binding(binding_id);
|
let binding = semantic.binding(binding_id);
|
||||||
if let Some(value) = find_binding_value(binding, semantic) {
|
if let Some(value) = find_binding_value(binding, semantic) {
|
||||||
return classify_expr_type_inner(value, semantic, seen);
|
return matches!(value, ast::Expr::Tuple(_));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeKnowledge::Unknown
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an expression is deterministically a string literal or a variable bound to a string.
|
||||||
|
fn is_expr_string(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
||||||
|
is_expr_type(
|
||||||
|
expr,
|
||||||
|
semantic,
|
||||||
|
|expr| expr.is_string_literal_expr() || expr.is_f_string_expr(),
|
||||||
|
is_string,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an expression is deterministically a bytes literal or a variable bound to bytes.
|
||||||
|
fn is_expr_bytes(expr: &ast::Expr, semantic: &SemanticModel) -> bool {
|
||||||
|
is_expr_type(
|
||||||
|
expr,
|
||||||
|
semantic,
|
||||||
|
|expr| matches!(expr, ast::Expr::BytesLiteral(_)),
|
||||||
|
is_bytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_expr_type(
|
||||||
|
expr: &ast::Expr,
|
||||||
|
semantic: &SemanticModel,
|
||||||
|
literal_check: impl Fn(&ast::Expr) -> bool,
|
||||||
|
binding_check: impl Fn(&Binding, &SemanticModel) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
if literal_check(expr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ast::Expr::Name(name) = expr {
|
||||||
|
if let Some(binding_id) = semantic.only_binding(name) {
|
||||||
|
let binding = semantic.binding(binding_id);
|
||||||
|
return binding_check(binding, semantic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||||
assertion_line: 62
|
|
||||||
---
|
---
|
||||||
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:7:5
|
--> FURB188.py:7:5
|
||||||
|
|
@ -22,7 +21,6 @@ help: Use removesuffix instead of assignment conditional upon endswith.
|
||||||
8 |
|
8 |
|
||||||
9 | return filename
|
9 | return filename
|
||||||
10 |
|
10 |
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:14:5
|
--> FURB188.py:14:5
|
||||||
|
|
@ -44,7 +42,6 @@ help: Use removesuffix instead of assignment conditional upon endswith.
|
||||||
15 |
|
15 |
|
||||||
16 | return filename
|
16 | return filename
|
||||||
17 |
|
17 |
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:21:12
|
--> FURB188.py:21:12
|
||||||
|
|
@ -62,7 +59,6 @@ help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||||
22 |
|
22 |
|
||||||
23 |
|
23 |
|
||||||
24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:25:12
|
--> FURB188.py:25:12
|
||||||
|
|
@ -80,7 +76,6 @@ help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||||
26 |
|
26 |
|
||||||
27 |
|
27 |
|
||||||
28 | def remove_prefix(filename: str) -> str:
|
28 | def remove_prefix(filename: str) -> str:
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:29:12
|
--> FURB188.py:29:12
|
||||||
|
|
@ -98,7 +93,6 @@ help: Use removeprefix instead of ternary expression conditional upon startswith
|
||||||
30 |
|
30 |
|
||||||
31 |
|
31 |
|
||||||
32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:33:12
|
--> FURB188.py:33:12
|
||||||
|
|
@ -116,7 +110,6 @@ help: Use removeprefix instead of ternary expression conditional upon startswith
|
||||||
34 |
|
34 |
|
||||||
35 |
|
35 |
|
||||||
36 | # these should not
|
36 | # these should not
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:146:9
|
--> FURB188.py:146:9
|
||||||
|
|
@ -177,7 +170,6 @@ help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||||
155 |
|
155 |
|
||||||
156 | def okay_steps():
|
156 | def okay_steps():
|
||||||
157 | text = "!x!y!z"
|
157 | text = "!x!y!z"
|
||||||
note: This is an unsafe fix and may change runtime behavior
|
|
||||||
|
|
||||||
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice.
|
||||||
--> FURB188.py:158:5
|
--> FURB188.py:158:5
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue