Replace deletion-tracking with enforced isolation levels (#4766)

This commit is contained in:
Charlie Marsh 2023-06-01 22:45:56 -04:00 committed by GitHub
parent fcbf5c3fae
commit 621718784a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 471 additions and 620 deletions

1
Cargo.lock generated
View file

@ -1908,6 +1908,7 @@ name = "ruff_diagnostics"
version = "0.0.0"
dependencies = [
"anyhow",
"is-macro",
"log",
"ruff_text_size",
"serde",

View file

@ -1,6 +1,5 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use itertools::Itertools;
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::{self, Excepthandler, Expr, Keyword, Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
@ -28,21 +27,19 @@ use crate::autofix::codemods;
pub(crate) fn delete_stmt(
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Edit> {
) -> Edit {
if parent
.map(|parent| is_lone_child(stmt, parent, deleted))
.map_or(Ok(None), |v| v.map(Some))?
.map(|parent| is_lone_child(stmt, parent))
.unwrap_or_default()
{
// If removing this node would lead to an invalid syntax tree, replace
// it with a `pass`.
Ok(Edit::range_replacement("pass".to_string(), stmt.range()))
Edit::range_replacement("pass".to_string(), stmt.range())
} else {
Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) {
if let Some(semicolon) = trailing_semicolon(stmt, locator) {
let next = next_stmt_break(semicolon, locator);
Edit::deletion(stmt.start(), next)
} else if helpers::has_leading_content(stmt, locator) {
@ -57,7 +54,7 @@ pub(crate) fn delete_stmt(
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
})
}
}
}
@ -66,13 +63,12 @@ pub(crate) fn remove_unused_imports<'a>(
unused_imports: impl Iterator<Item = &'a str>,
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Edit> {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
None => delete_stmt(stmt, parent, deleted, locator, indexer, stylist),
None => Ok(delete_stmt(stmt, parent, locator, indexer, stylist)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
}
}
@ -179,36 +175,29 @@ pub(crate) fn remove_argument(
}
}
/// Determine if a body contains only a single statement, taking into account
/// deleted.
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
body.iter().filter(|child| !deleted.contains(child)).count() == 1
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value
}
/// Determine if a child is the only statement in its body.
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. })
| Stmt::If(ast::StmtIf { body, orelse, .. }) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else {
bail!("Unable to find child in parent body")
if is_only(body, child) || is_only(orelse, child) {
return true;
}
}
Stmt::Try(ast::StmtTry {
@ -225,41 +214,26 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
finalbody,
range: _,
}) => {
if body.iter().contains(child) {
Ok(has_single_child(body, deleted))
} else if orelse.iter().contains(child) {
Ok(has_single_child(orelse, deleted))
} else if finalbody.iter().contains(child) {
Ok(has_single_child(finalbody, deleted))
} else if let Some(body) = handlers.iter().find_map(|handler| match handler {
Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler { body, .. }) => {
if body.iter().contains(child) {
Some(body)
} else {
None
}
}
}) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
if is_only(body, child)
|| is_only(orelse, child)
|| is_only(finalbody, child)
|| handlers.iter().any(|handler| match handler {
Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler {
body, ..
}) => is_only(body, child),
})
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if let Some(body) = cases.iter().find_map(|case| {
if case.body.iter().contains(child) {
Some(&case.body)
} else {
None
}
}) {
Ok(has_single_child(body, deleted))
} else {
bail!("Unable to find child in parent body")
if cases.iter().any(|case| is_only(&case.body, child)) {
return true;
}
}
_ => bail!("Unable to find child in parent body"),
_ => {}
}
false
}
/// Return the location of a trailing semicolon following a `Stmt`, if it's part

View file

@ -37,6 +37,7 @@ fn apply_fixes<'a>(
) -> (String, FixTable) {
let mut output = String::with_capacity(locator.len());
let mut last_pos: Option<TextSize> = None;
let mut isolation = false;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut fixed = FxHashMap::default();
@ -65,6 +66,15 @@ fn apply_fixes<'a>(
continue;
}
// If this fix requires isolation, and we've already applied another fix that
// requires isolation, skip it. We apply at most one isolated fix per run.
if fix.isolation().is_isolated() {
if isolation {
continue;
}
isolation = true;
}
for edit in fix
.edits()
.iter()

View file

@ -3,19 +3,19 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::{FxHashMap, FxHashSet};
use rustc_hash::FxHashMap;
use rustpython_format::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
self, Arg, Arguments, Comprehension, Constant, Excepthandler, Expr, ExprContext, Keyword,
Operator, Pattern, Ranged, Stmt, Suite, Unaryop,
};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_diagnostics::{Diagnostic, Fix, IsolationLevel};
use ruff_python_ast::all::{extract_all_names, AllNamesFlags};
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::source_code::{Generator, Indexer, Locator, Quote, Stylist};
use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::types::{Node, RefEquality};
use ruff_python_ast::types::Node;
use ruff_python_ast::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_ast::visitor::{walk_excepthandler, walk_pattern, Visitor};
use ruff_python_ast::{cast, helpers, str, visitor};
@ -75,9 +75,8 @@ pub(crate) struct Checker<'a> {
pub(crate) importer: Importer<'a>,
// Stateful fields.
semantic_model: SemanticModel<'a>,
pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) deletions: FxHashSet<RefEquality<'a, Stmt>>,
deferred: Deferred<'a>,
pub(crate) diagnostics: Vec<Diagnostic>,
// Check-specific state.
pub(crate) flake8_bugbear_seen: Vec<&'a Expr>,
}
@ -111,7 +110,6 @@ impl<'a> Checker<'a> {
semantic_model: SemanticModel::new(&settings.typing_modules, path, module),
deferred: Deferred::default(),
diagnostics: Vec::default(),
deletions: FxHashSet::default(),
flake8_bugbear_seen: Vec::default(),
}
}
@ -2124,7 +2122,7 @@ where
}
if self.enabled(Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, stmt, body);
flake8_type_checking::rules::empty_type_checking_block(self, stmt_if);
}
self.visit_type_checking_block(body);
@ -5245,14 +5243,14 @@ impl<'a> Checker<'a> {
_ => continue,
};
let child_id = binding.source.unwrap();
let parent_id = self.semantic_model.stmts.parent_id(child_id);
let stmt_id = binding.source.unwrap();
let parent_id = self.semantic_model.stmts.parent_id(stmt_id);
let exceptions = binding.exceptions;
let diagnostic_offset = binding.range.start();
let child = &self.semantic_model.stmts[child_id];
let parent_offset = if matches!(child, Stmt::ImportFrom(_)) {
Some(child.start())
let stmt = &self.semantic_model.stmts[stmt_id];
let parent_offset = if matches!(stmt, Stmt::ImportFrom(_)) {
Some(stmt.start())
} else {
None
};
@ -5263,12 +5261,12 @@ impl<'a> Checker<'a> {
})
{
ignored
.entry((child_id, parent_id, exceptions))
.entry((stmt_id, parent_id, exceptions))
.or_default()
.push((full_name, &binding.range));
} else {
unused
.entry((child_id, parent_id, exceptions))
.entry((stmt_id, parent_id, exceptions))
.or_default()
.push((full_name, &binding.range));
}
@ -5276,38 +5274,26 @@ impl<'a> Checker<'a> {
let in_init =
self.settings.ignore_init_module_imports && self.path.ends_with("__init__.py");
for ((defined_by, defined_in, exceptions), unused_imports) in unused
for ((stmt_id, parent_id, exceptions), unused_imports) in unused
.into_iter()
.sorted_by_key(|((defined_by, ..), ..)| *defined_by)
{
let child = self.semantic_model.stmts[defined_by];
let parent = defined_in.map(|defined_in| self.semantic_model.stmts[defined_in]);
let stmt = self.semantic_model.stmts[stmt_id];
let parent = parent_id.map(|parent_id| self.semantic_model.stmts[parent_id]);
let multiple = unused_imports.len() > 1;
let in_except_handler = exceptions
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
let fix = if !in_init && !in_except_handler && self.patch(Rule::UnusedImport) {
let deleted: Vec<&Stmt> = self.deletions.iter().map(Into::into).collect();
match autofix::edits::remove_unused_imports(
autofix::edits::remove_unused_imports(
unused_imports.iter().map(|(full_name, _)| *full_name),
child,
stmt,
parent,
&deleted,
self.locator,
self.indexer,
self.stylist,
) {
Ok(fix) => {
if fix.is_deletion() || fix.content() == Some("pass") {
self.deletions.insert(RefEquality(child));
}
Some(fix)
}
Err(e) => {
error!("Failed to remove unused imports: {e}");
None
}
}
)
.ok()
} else {
None
};
@ -5327,22 +5313,26 @@ impl<'a> Checker<'a> {
},
*range,
);
if matches!(child, Stmt::ImportFrom(_)) {
diagnostic.set_parent(child.start());
if matches!(stmt, Stmt::ImportFrom(_)) {
diagnostic.set_parent(stmt.start());
}
if let Some(edit) = &fix {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit.clone()));
if let Some(edit) = fix.as_ref() {
diagnostic.set_fix(Fix::automatic(edit.clone()).isolate(
if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
},
));
}
diagnostics.push(diagnostic);
}
}
for ((child, .., exceptions), unused_imports) in ignored
for ((stmt_id, .., exceptions), unused_imports) in ignored
.into_iter()
.sorted_by_key(|((defined_by, ..), ..)| *defined_by)
.sorted_by_key(|((stmt_id, ..), ..)| *stmt_id)
{
let child = self.semantic_model.stmts[child];
let stmt = self.semantic_model.stmts[stmt_id];
let multiple = unused_imports.len() > 1;
let in_except_handler = exceptions
.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
@ -5361,8 +5351,8 @@ impl<'a> Checker<'a> {
},
*range,
);
if matches!(child, Stmt::ImportFrom(_)) {
diagnostic.set_parent(child.start());
if matches!(stmt, Stmt::ImportFrom(_)) {
diagnostic.set_parent(stmt.start());
}
diagnostics.push(diagnostic);
}

View file

@ -1,15 +1,11 @@
use log::error;
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
use ruff_diagnostics::AlwaysAutofixableViolation;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -35,17 +31,19 @@ use crate::registry::AsRule;
/// ...
/// ```
#[violation]
pub struct DuplicateClassFieldDefinition(pub String);
pub struct DuplicateClassFieldDefinition {
name: String,
}
impl AlwaysAutofixableViolation for DuplicateClassFieldDefinition {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateClassFieldDefinition(name) = self;
let DuplicateClassFieldDefinition { name } = self;
format!("Class field `{name}` is defined multiple times")
}
fn autofix_title(&self) -> String {
let DuplicateClassFieldDefinition(name) = self;
let DuplicateClassFieldDefinition { name } = self;
format!("Remove duplicate field definition for `{name}`")
}
}
@ -84,29 +82,20 @@ pub(crate) fn duplicate_class_field_definition<'a, 'b>(
if !seen_targets.insert(target) {
let mut diagnostic = Diagnostic::new(
DuplicateClassFieldDefinition(target.to_string()),
DuplicateClassFieldDefinition {
name: target.to_string(),
},
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let locator = checker.locator;
match delete_stmt(
let edit = autofix::edits::delete_stmt(
stmt,
Some(parent),
&deleted,
locator,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
checker.deletions.insert(RefEquality(stmt));
#[allow(deprecated)]
diagnostic.set_fix_from_edit(fix);
}
Err(err) => {
error!("Failed to remove duplicate class definition: {}", err);
}
}
);
diagnostic.set_fix(Fix::suggested(edit).isolate(IsolationLevel::Isolated));
}
checker.diagnostics.push(diagnostic);
}

View file

@ -1,12 +1,11 @@
use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt};
use rustpython_parser::ast::{Ranged, Stmt};
use ruff_diagnostics::AlwaysAutofixableViolation;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_docstring_stmt, trailing_comment_start_offset};
use ruff_python_ast::helpers::trailing_comment_start_offset;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -53,41 +52,31 @@ pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
if body.len() > 1 {
// This only catches the case in which a docstring makes a `pass` statement
// redundant. Consider removing all `pass` statements instead.
let docstring_stmt = &body[0];
let pass_stmt = &body[1];
let Stmt::Expr(ast::StmtExpr { value, range: _ } )= docstring_stmt else {
if !is_docstring_stmt(&body[0]) {
return;
};
if matches!(
value.as_ref(),
Expr::Constant(ast::ExprConstant {
value: Constant::Str(..),
..
})
) {
if pass_stmt.is_pass_stmt() {
let mut diagnostic = Diagnostic::new(UnnecessaryPass, pass_stmt.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(index) = trailing_comment_start_offset(pass_stmt, checker.locator) {
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(
pass_stmt.range().add_end(index),
)));
} else {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
delete_stmt(
pass_stmt,
None,
&[],
checker.locator,
checker.indexer,
checker.stylist,
)
});
}
}
checker.diagnostics.push(diagnostic);
}
}
// The second statement must be a `pass` statement.
let stmt = &body[1];
if !stmt.is_pass_stmt() {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryPass, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.locator) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
autofix::edits::delete_stmt(
stmt,
None,
checker.locator,
checker.indexer,
checker.stylist,
)
};
diagnostic.set_fix(Fix::automatic(edit));
}
checker.diagnostics.push(diagnostic);
}
}

View file

@ -10,7 +10,7 @@ PIE790.py:4:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
1 1 | class Foo:
2 2 | """buzz"""
3 3 |
@ -28,7 +28,7 @@ PIE790.py:9:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
6 6 |
7 7 | if foo:
8 8 | """foo"""
@ -46,7 +46,7 @@ PIE790.py:14:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
11 11 |
12 12 | def multi_statement() -> None:
13 13 | """This is a function."""
@ -65,7 +65,7 @@ PIE790.py:21:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
18 18 | pass
19 19 | else:
20 20 | """bar"""
@ -83,7 +83,7 @@ PIE790.py:28:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
25 25 | pass
26 26 | else:
27 27 | """bar"""
@ -101,7 +101,7 @@ PIE790.py:35:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
32 32 | pass
33 33 | else:
34 34 | """bar"""
@ -119,7 +119,7 @@ PIE790.py:42:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
39 39 | pass
40 40 | else:
41 41 | """bar"""
@ -137,7 +137,7 @@ PIE790.py:50:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
47 47 | buzz
48 48 | """
49 49 |
@ -155,7 +155,7 @@ PIE790.py:58:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
55 55 | buzz
56 56 | """
57 57 |
@ -175,7 +175,7 @@ PIE790.py:65:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
62 62 | """
63 63 | buzz
64 64 | """
@ -193,7 +193,7 @@ PIE790.py:74:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
71 71 | bar()
72 72 | except ValueError:
73 73 | """bar"""
@ -213,7 +213,7 @@ PIE790.py:79:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
76 76 |
77 77 | for _ in range(10):
78 78 | """buzz"""
@ -233,7 +233,7 @@ PIE790.py:83:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
80 80 |
81 81 | async for _ in range(10):
82 82 | """buzz"""
@ -251,7 +251,7 @@ PIE790.py:87:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
84 84 |
85 85 | while cond:
86 86 | """buzz"""
@ -271,7 +271,7 @@ PIE790.py:92:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
89 89 |
90 90 | with bar:
91 91 | """buzz"""
@ -289,7 +289,7 @@ PIE790.py:96:5: PIE790 [*] Unnecessary `pass` statement
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
93 93 |
94 94 | async with bar:
95 95 | """buzz"""

View file

@ -1,10 +1,9 @@
use rustpython_parser::ast::{Expr, ExprConstant, Ranged, Stmt, StmtExpr};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -56,38 +55,29 @@ pub(crate) fn ellipsis_in_non_empty_class_body<'a>(
}
for stmt in body {
if let Stmt::Expr(StmtExpr { value, .. }) = &stmt {
if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() {
if value.is_ellipsis() {
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else {
continue;
};
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let deleted: Vec<&Stmt> =
checker.deletions.iter().map(Into::into).collect();
let edit = delete_stmt(
stmt,
Some(parent),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
)?;
let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() else {
continue;
};
// In the unlikely event the class body consists solely of several
// consecutive ellipses, `delete_stmt` can actually result in a
// `pass`.
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(stmt));
}
Ok(Fix::automatic(edit))
});
}
checker.diagnostics.push(diagnostic);
}
}
if !value.is_ellipsis() {
continue;
}
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = autofix::edits::delete_stmt(
stmt,
Some(parent),
checker.locator,
checker.indexer,
checker.stylist,
);
diagnostic.set_fix(Fix::automatic(edit).isolate(IsolationLevel::Isolated));
}
checker.diagnostics.push(diagnostic);
}
}

View file

@ -1,11 +1,9 @@
use log::error;
use rustpython_parser::ast::{Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -35,33 +33,21 @@ pub(crate) fn pass_in_class_body<'a>(
}
for stmt in body {
if stmt.is_pass_stmt() {
let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
stmt,
Some(parent),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
if fix.is_deletion() || fix.content() == Some("pass") {
checker.deletions.insert(RefEquality(stmt));
}
#[allow(deprecated)]
diagnostic.set_fix_from_edit(fix);
}
Err(e) => {
error!("Failed to delete `pass` statement: {}", e);
}
};
};
checker.diagnostics.push(diagnostic);
if !stmt.is_pass_stmt() {
continue;
}
let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = autofix::edits::delete_stmt(
stmt,
Some(parent),
checker.locator,
checker.indexer,
checker.stylist,
);
diagnostic.set_fix(Fix::automatic(edit).isolate(IsolationLevel::Isolated));
}
checker.diagnostics.push(diagnostic);
}
}

View file

@ -12,7 +12,7 @@ PYI012.pyi:5:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
2 2 |
3 3 | class OneAttributeClass:
4 4 | value: int
@ -30,7 +30,7 @@ PYI012.pyi:8:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
5 5 | pass # PYI012 Class body must not contain `pass`
6 6 |
7 7 | class OneAttributeClassRev:
@ -50,7 +50,7 @@ PYI012.pyi:16:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
13 13 | My body only contains pass.
14 14 | """
15 15 |
@ -70,7 +70,7 @@ PYI012.pyi:20:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
17 17 |
18 18 | class NonEmptyChild(Exception):
19 19 | value: int
@ -88,7 +88,7 @@ PYI012.pyi:23:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
20 20 | pass # PYI012 Class body must not contain `pass`
21 21 |
22 22 | class NonEmptyChild2(Exception):
@ -108,7 +108,7 @@ PYI012.pyi:28:5: PYI012 [*] Class body must not contain `pass`
|
= help: Remove unnecessary `pass`
Suggested fix
Fix
25 25 |
26 26 | class NonEmptyWithInit:
27 27 | value: int

View file

@ -91,10 +91,9 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
15 15 | class TwoEllipsesClass:
16 16 | ...
17 |- ... # Error
17 |+ pass # Error
18 18 |
19 19 | class DocstringClass:
20 20 | """
18 17 |
19 18 | class DocstringClass:
20 19 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|

View file

@ -1,11 +1,10 @@
use log::error;
use rustpython_parser::ast::{Ranged, Stmt};
use rustpython_parser::ast;
use rustpython_parser::ast::Ranged;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -48,39 +47,33 @@ impl AlwaysAutofixableViolation for EmptyTypeCheckingBlock {
}
/// TCH005
pub(crate) fn empty_type_checking_block<'a, 'b>(
checker: &mut Checker<'a>,
stmt: &'a Stmt,
body: &'a [Stmt],
) where
'b: 'a,
{
if body.len() == 1 && body[0].is_pass_stmt() {
let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, body[0].range());
// Delete the entire type-checking block.
if checker.patch(diagnostic.kind.rule()) {
let parent = checker.semantic_model().stmts.parent(stmt);
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
stmt,
parent,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(edit) => {
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(stmt));
}
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit));
}
Err(e) => error!("Failed to remove empty type-checking block: {e}"),
}
}
checker.diagnostics.push(diagnostic);
pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtIf) {
if stmt.body.len() != 1 {
return;
}
let stmt = &stmt.body[0];
if !stmt.is_pass_stmt() {
return;
}
let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
// Delete the entire type-checking block.
let stmt = checker.semantic_model().stmt();
let parent = checker.semantic_model().stmt_parent();
let edit = autofix::edits::delete_stmt(
stmt,
parent,
checker.locator,
checker.indexer,
checker.stylist,
);
diagnostic.set_fix(Fix::automatic(edit).isolate(if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
}));
}
checker.diagnostics.push(diagnostic);
}

View file

@ -1,6 +1,4 @@
use rustpython_parser::ast::Stmt;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::binding::{
Binding, BindingKind, FromImportation, Importation, SubmoduleImportation,
@ -98,21 +96,15 @@ pub(crate) fn runtime_import_in_type_checking_block(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
// Step 1) Remove the import from the type-checking block.
// Step 1) Remove the import.
// SAFETY: All non-builtin bindings have a source.
let source = binding.source.unwrap();
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let stmt = checker.semantic_model().stmts[source];
let parent = checker
.semantic_model()
.stmts
.parent_id(source)
.map(|id| checker.semantic_model().stmts[id]);
let parent = checker.semantic_model().stmts.parent(stmt);
let remove_import_edit = autofix::edits::remove_unused_imports(
std::iter::once(full_name),
stmt,
parent,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
@ -125,10 +117,15 @@ pub(crate) fn runtime_import_in_type_checking_block(
reference.range().start(),
)?;
Ok(Fix::suggested_edits(
remove_import_edit,
add_import_edit.into_edits(),
))
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
},
),
)
});
}

View file

@ -1,6 +1,4 @@
use rustpython_parser::ast::Stmt;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::binding::{
Binding, BindingKind, FromImportation, Importation, SubmoduleImportation,
@ -373,18 +371,12 @@ pub(crate) fn typing_only_runtime_import(
// Step 1) Remove the import.
// SAFETY: All non-builtin bindings have a source.
let source = binding.source.unwrap();
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let stmt = checker.semantic_model().stmts[source];
let parent = checker
.semantic_model()
.stmts
.parent_id(source)
.map(|id| checker.semantic_model().stmts[id]);
let parent = checker.semantic_model().stmts.parent(stmt);
let remove_import_edit = autofix::edits::remove_unused_imports(
std::iter::once(full_name),
stmt,
parent,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
@ -398,10 +390,15 @@ pub(crate) fn typing_only_runtime_import(
checker.semantic_model(),
)?;
Ok(Fix::suggested_edits(
remove_import_edit,
add_import_edit.into_edits(),
))
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
},
),
)
});
}

View file

@ -9,7 +9,7 @@ TCH005.py:4:5: TCH005 [*] Found empty type-checking block
|
= help: Delete empty type-checking block
Suggested fix
Fix
1 1 | from typing import TYPE_CHECKING, List
2 2 |
3 |-if TYPE_CHECKING:
@ -28,7 +28,7 @@ TCH005.py:8:5: TCH005 [*] Found empty type-checking block
|
= help: Delete empty type-checking block
Suggested fix
Fix
4 4 | pass # TCH005
5 5 |
6 6 |
@ -46,7 +46,7 @@ TCH005.py:11:5: TCH005 [*] Found empty type-checking block
|
= help: Delete empty type-checking block
Suggested fix
Fix
7 7 | if False:
8 8 | pass # TCH005
9 9 |
@ -66,7 +66,7 @@ TCH005.py:16:9: TCH005 [*] Found empty type-checking block
|
= help: Delete empty type-checking block
Suggested fix
Fix
12 12 |
13 13 |
14 14 | def example():
@ -86,7 +86,7 @@ TCH005.py:22:9: TCH005 [*] Found empty type-checking block
|
= help: Delete empty type-checking block
Suggested fix
Fix
18 18 |
19 19 |
20 20 | class Test:

View file

@ -1,14 +1,12 @@
use itertools::Itertools;
use log::error;
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, IsolationLevel, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::types::RefEquality;
use ruff_python_semantic::scope::{ScopeId, ScopeKind};
use crate::autofix::edits::delete_stmt;
@ -186,19 +184,14 @@ where
unreachable!("No token after matched");
}
#[derive(Copy, Clone)]
enum DeletionKind {
Whole,
Partial,
}
/// Generate a [`Edit`] to remove an unused variable assignment, given the
/// enclosing [`Stmt`] and the [`TextRange`] of the variable binding.
fn remove_unused_variable(
stmt: &Stmt,
parent: Option<&Stmt>,
range: TextRange,
checker: &Checker,
) -> Option<(DeletionKind, Fix)> {
) -> Option<Fix> {
// First case: simple assignment (`x = 1`)
if let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = stmt {
if let Some(target) = targets.iter().find(|target| range == target.range()) {
@ -208,34 +201,25 @@ fn remove_unused_variable(
{
// If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side.
#[allow(deprecated)]
Some((
DeletionKind::Partial,
Fix::unspecified(Edit::deletion(
target.start(),
match_token_after(target, checker.locator, |tok| tok == Tok::Equal)
.start(),
)),
))
let edit = Edit::deletion(
target.start(),
match_token_after(target, checker.locator, |tok| tok == Tok::Equal).start(),
);
Some(Fix::suggested(edit))
} else {
// If (e.g.) assigning to a constant (`x = 1`), delete the entire statement.
let parent = checker.semantic_model().stmts.parent(stmt);
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
let edit = delete_stmt(
stmt,
parent,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
#[allow(deprecated)]
Ok(fix) => Some((DeletionKind::Whole, Fix::unspecified(fix))),
Err(err) => {
error!("Failed to delete unused variable: {}", err);
None
}
}
);
Some(Fix::suggested(edit).isolate(if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
}))
};
}
}
@ -252,33 +236,25 @@ fn remove_unused_variable(
return if contains_effect(value, |id| checker.semantic_model().is_builtin(id)) {
// If the expression is complex (`x = foo()`), remove the assignment,
// but preserve the right-hand side.
#[allow(deprecated)]
Some((
DeletionKind::Partial,
Fix::unspecified(Edit::deletion(
stmt.start(),
match_token_after(stmt, checker.locator, |tok| tok == Tok::Equal).start(),
)),
))
let edit = Edit::deletion(
stmt.start(),
match_token_after(stmt, checker.locator, |tok| tok == Tok::Equal).start(),
);
Some(Fix::suggested(edit))
} else {
// If assigning to a constant (`x = 1`), delete the entire statement.
let parent = checker.semantic_model().stmts.parent(stmt);
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
// If (e.g.) assigning to a constant (`x = 1`), delete the entire statement.
let edit = delete_stmt(
stmt,
parent,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
#[allow(deprecated)]
Ok(edit) => Some((DeletionKind::Whole, Fix::unspecified(edit))),
Err(err) => {
error!("Failed to delete unused variable: {}", err);
None
}
}
);
Some(Fix::suggested(edit).isolate(if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
}))
};
}
}
@ -290,19 +266,16 @@ fn remove_unused_variable(
for item in items {
if let Some(optional_vars) = &item.optional_vars {
if optional_vars.range() == range {
#[allow(deprecated)]
return Some((
DeletionKind::Partial,
Fix::unspecified(Edit::deletion(
item.context_expr.end(),
// The end of the `Withitem` is the colon, comma, or closing
// parenthesis following the `optional_vars`.
match_token(&item.context_expr, checker.locator, |tok| {
tok == Tok::Colon || tok == Tok::Comma || tok == Tok::Rpar
})
.start(),
)),
));
let edit = Edit::deletion(
item.context_expr.end(),
// The end of the `Withitem` is the colon, comma, or closing
// parenthesis following the `optional_vars`.
match_token(&item.context_expr, checker.locator, |tok| {
tok == Tok::Colon || tok == Tok::Comma || tok == Tok::Rpar
})
.start(),
);
return Some(Fix::suggested(edit));
}
}
}
@ -339,19 +312,15 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) {
for (name, range, source) in bindings {
let mut diagnostic = Diagnostic::new(UnusedVariable { name }, range);
if checker.patch(diagnostic.kind.rule()) {
if let Some(source) = source {
let stmt = checker.semantic_model().stmts[source];
if let Some((kind, fix)) = remove_unused_variable(stmt, range, checker) {
if matches!(kind, DeletionKind::Whole) {
checker.deletions.insert(RefEquality(stmt));
}
let parent = checker.semantic_model().stmts.parent(stmt);
if let Some(fix) = remove_unused_variable(stmt, parent, range, checker) {
diagnostic.set_fix(fix);
}
}
}
checker.diagnostics.push(diagnostic);
}
}

View file

@ -11,7 +11,7 @@ F401_0.py:2:8: F401 [*] `functools` imported but unused
|
= help: Remove unused import: `functools`
Suggested fix
Fix
1 1 | from __future__ import all_feature_names
2 |-import functools, os
2 |+import os
@ -30,7 +30,7 @@ F401_0.py:6:5: F401 [*] `collections.OrderedDict` imported but unused
|
= help: Remove unused import: `collections.OrderedDict`
Suggested fix
Fix
3 3 | from datetime import datetime
4 4 | from collections import (
5 5 | Counter,
@ -50,7 +50,7 @@ F401_0.py:12:8: F401 [*] `logging.handlers` imported but unused
|
= help: Remove unused import: `logging.handlers`
Suggested fix
Fix
9 9 | import multiprocessing.pool
10 10 | import multiprocessing.process
11 11 | import logging.config
@ -68,7 +68,7 @@ F401_0.py:32:12: F401 [*] `shelve` imported but unused
|
= help: Remove unused import: `shelve`
Suggested fix
Fix
29 29 | from models import Fruit, Nut, Vegetable
30 30 |
31 31 | if TYPE_CHECKING:
@ -88,15 +88,14 @@ F401_0.py:33:12: F401 [*] `importlib` imported but unused
|
= help: Remove unused import: `importlib`
Suggested fix
Fix
30 30 |
31 31 | if TYPE_CHECKING:
32 32 | import shelve
33 |- import importlib
33 |+ pass
34 34 |
35 35 | if TYPE_CHECKING:
36 36 | """Hello, world!"""
34 33 |
35 34 | if TYPE_CHECKING:
36 35 | """Hello, world!"""
F401_0.py:37:12: F401 [*] `pathlib` imported but unused
|
@ -109,7 +108,7 @@ F401_0.py:37:12: F401 [*] `pathlib` imported but unused
|
= help: Remove unused import: `pathlib`
Suggested fix
Fix
34 34 |
35 35 | if TYPE_CHECKING:
36 36 | """Hello, world!"""
@ -126,7 +125,7 @@ F401_0.py:52:16: F401 [*] `pickle` imported but unused
|
= help: Remove unused import: `pickle`
Suggested fix
Fix
49 49 | z = multiprocessing.pool.ThreadPool()
50 50 |
51 51 | def b(self) -> None:
@ -146,7 +145,7 @@ F401_0.py:93:16: F401 [*] `x` imported but unused
|
= help: Remove unused import: `x`
Suggested fix
Fix
90 90 | # Test: match statements.
91 91 | match *0, 1, *2:
92 92 | case 0,:
@ -162,11 +161,10 @@ F401_0.py:94:16: F401 [*] `y` imported but unused
|
= help: Remove unused import: `y`
Suggested fix
Fix
91 91 | match *0, 1, *2:
92 92 | case 0,:
93 93 | import x
94 |- import y
94 |+ pass

View file

@ -9,7 +9,7 @@ F401_11.py:4:27: F401 [*] `pathlib.PurePath` imported but unused
|
= help: Remove unused import: `pathlib.PurePath`
Suggested fix
Fix
1 1 | """Test: parsing of nested string annotations."""
2 2 |
3 3 | from typing import List

View file

@ -9,7 +9,7 @@ F401_15.py:5:25: F401 [*] `pathlib.Path` imported but unused
|
= help: Remove unused import: `pathlib.Path`
Suggested fix
Fix
2 2 | from django.db.models import ForeignKey
3 3 |
4 4 | if TYPE_CHECKING:

View file

@ -11,7 +11,7 @@ F401_5.py:2:17: F401 [*] `a.b.c` imported but unused
|
= help: Remove unused import: `a.b.c`
Suggested fix
Fix
1 1 | """Test: removal of multi-segment and aliases imports."""
2 |-from a.b import c
3 2 | from d.e import f as g
@ -29,7 +29,7 @@ F401_5.py:3:17: F401 [*] `d.e.f` imported but unused
|
= help: Remove unused import: `d.e.f`
Suggested fix
Fix
1 1 | """Test: removal of multi-segment and aliases imports."""
2 2 | from a.b import c
3 |-from d.e import f as g
@ -46,7 +46,7 @@ F401_5.py:4:8: F401 [*] `h.i` imported but unused
|
= help: Remove unused import: `h.i`
Suggested fix
Fix
1 1 | """Test: removal of multi-segment and aliases imports."""
2 2 | from a.b import c
3 3 | from d.e import f as g
@ -62,7 +62,7 @@ F401_5.py:5:8: F401 [*] `j.k` imported but unused
|
= help: Remove unused import: `j.k`
Suggested fix
Fix
2 2 | from a.b import c
3 3 | from d.e import f as g
4 4 | import h.i

View file

@ -11,7 +11,7 @@ F401_6.py:7:25: F401 [*] `.background.BackgroundTasks` imported but unused
|
= help: Remove unused import: `.background.BackgroundTasks`
Suggested fix
Fix
4 4 | from .applications import FastAPI as FastAPI
5 5 |
6 6 | # F401 `background.BackgroundTasks` imported but unused
@ -30,7 +30,7 @@ F401_6.py:10:29: F401 [*] `.datastructures.UploadFile` imported but unused
|
= help: Remove unused import: `.datastructures.UploadFile`
Suggested fix
Fix
7 7 | from .background import BackgroundTasks
8 8 |
9 9 | # F401 `datastructures.UploadFile` imported but unused
@ -49,7 +49,7 @@ F401_6.py:16:8: F401 [*] `background` imported but unused
|
= help: Remove unused import: `background`
Suggested fix
Fix
13 13 | import applications as applications
14 14 |
15 15 | # F401 `background` imported but unused
@ -66,7 +66,7 @@ F401_6.py:19:8: F401 [*] `datastructures` imported but unused
|
= help: Remove unused import: `datastructures`
Suggested fix
Fix
16 16 | import background
17 17 |
18 18 | # F401 `datastructures` imported but unused

View file

@ -11,7 +11,7 @@ F401_7.py:30:5: F401 [*] `typing.Union` imported but unused
|
= help: Remove unused import: `typing.Union`
Suggested fix
Fix
27 27 | # This should ignore the first error.
28 28 | from typing import (
29 29 | Mapping, # noqa: F401
@ -30,7 +30,7 @@ F401_7.py:66:20: F401 [*] `typing.Awaitable` imported but unused
|
= help: Remove unused import
Suggested fix
Fix
63 63 | from typing import AsyncIterable, AsyncGenerator # noqa
64 64 |
65 65 | # This should mark F501 as unused.
@ -44,7 +44,7 @@ F401_7.py:66:31: F401 [*] `typing.AwaitableGenerator` imported but unused
|
= help: Remove unused import
Suggested fix
Fix
63 63 | from typing import AsyncIterable, AsyncGenerator # noqa
64 64 |
65 65 | # This should mark F501 as unused.

View file

@ -9,7 +9,7 @@ F401_9.py:4:22: F401 [*] `foo.baz` imported but unused
|
= help: Remove unused import: `foo.baz`
Suggested fix
Fix
1 1 | """Test: late-binding of `__all__`."""
2 2 |
3 3 | __all__ = ("bar",)

View file

@ -315,10 +315,9 @@ F841_3.py:61:5: F841 [*] Local variable `x` is assigned to but never used
63 |- if a is not None
64 |- else b
65 |- )
61 |+ pass
66 62 |
67 63 | y = \
68 64 | a if a is not None else b
66 61 |
67 62 | y = \
68 63 | a if a is not None else b
F841_3.py:67:5: F841 [*] Local variable `y` is assigned to but never used
|

View file

@ -131,10 +131,9 @@ F841_0.py:37:5: F841 [*] Local variable `_discarded` is assigned to but never us
35 35 | _ = 1
36 36 | __ = 1
37 |- _discarded = 1
37 |+ pass
38 38 |
39 39 |
40 40 | a = 1
38 37 |
39 38 |
40 39 | a = 1
F841_0.py:51:9: F841 [*] Local variable `b` is assigned to but never used
|

View file

@ -11,7 +11,7 @@ future_annotations.py:8:5: F401 [*] `models.Nut` imported but unused
|
= help: Remove unused import: `models.Nut`
Suggested fix
Fix
5 5 |
6 6 | from models import (
7 7 | Fruit,

View file

@ -10,7 +10,7 @@ multi_statement_lines.py:3:12: F401 [*] `foo1` imported but unused
|
= help: Remove unused import: `foo1`
Suggested fix
Fix
1 1 |
2 2 | if True:
3 |- import foo1; x = 1
@ -30,7 +30,7 @@ multi_statement_lines.py:4:12: F401 [*] `foo2` imported but unused
|
= help: Remove unused import: `foo2`
Suggested fix
Fix
1 1 |
2 2 | if True:
3 3 | import foo1; x = 1
@ -49,7 +49,7 @@ multi_statement_lines.py:7:12: F401 [*] `foo3` imported but unused
|
= help: Remove unused import: `foo3`
Suggested fix
Fix
4 4 | import foo2; x = 1
5 5 |
6 6 | if True:
@ -69,7 +69,7 @@ multi_statement_lines.py:11:12: F401 [*] `foo4` imported but unused
|
= help: Remove unused import: `foo4`
Suggested fix
Fix
8 8 | x = 1
9 9 |
10 10 | if True:
@ -88,7 +88,7 @@ multi_statement_lines.py:16:19: F401 [*] `foo5` imported but unused
|
= help: Remove unused import: `foo5`
Suggested fix
Fix
13 13 |
14 14 |
15 15 | if True:
@ -107,7 +107,7 @@ multi_statement_lines.py:21:17: F401 [*] `foo6` imported but unused
|
= help: Remove unused import: `foo6`
Suggested fix
Fix
18 18 |
19 19 | if True:
20 20 | x = 1; \
@ -126,7 +126,7 @@ multi_statement_lines.py:26:18: F401 [*] `foo7` imported but unused
|
= help: Remove unused import: `foo7`
Suggested fix
Fix
23 23 |
24 24 | if True:
25 25 | x = 1 \
@ -145,7 +145,7 @@ multi_statement_lines.py:30:19: F401 [*] `foo8` imported but unused
|
= help: Remove unused import: `foo8`
Suggested fix
Fix
27 27 |
28 28 |
29 29 | if True:
@ -166,7 +166,7 @@ multi_statement_lines.py:31:23: F401 [*] `foo9` imported but unused
|
= help: Remove unused import: `foo9`
Suggested fix
Fix
28 28 |
29 29 | if True:
30 30 | x = 1; import foo8; x = 1
@ -186,7 +186,7 @@ multi_statement_lines.py:35:16: F401 [*] `foo10` imported but unused
|
= help: Remove unused import: `foo10`
Suggested fix
Fix
32 32 |
33 33 | if True:
34 34 | x = 1; \
@ -207,7 +207,7 @@ multi_statement_lines.py:40:17: F401 [*] `foo11` imported but unused
|
= help: Remove unused import: `foo11`
Suggested fix
Fix
37 37 |
38 38 | if True:
39 39 | x = 1 \
@ -227,7 +227,7 @@ multi_statement_lines.py:46:8: F401 [*] `foo12` imported but unused
|
= help: Remove unused import: `foo12`
Suggested fix
Fix
43 43 |
44 44 | # Continuation, but not as the last content in the file.
45 45 | x = 1; \
@ -247,7 +247,7 @@ multi_statement_lines.py:51:8: F401 [*] `foo13` imported but unused
|
= help: Remove unused import: `foo13`
Suggested fix
Fix
48 48 | # Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax
49 49 | # error.)
50 50 | x = 1; \

View file

@ -1,13 +1,11 @@
use log::error;
use rustpython_parser::ast::{self, Constant, Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_none, ReturnStatementVisitor};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits::delete_stmt;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -57,14 +55,12 @@ pub(crate) fn useless_return<'a>(
return;
}
// Skip empty functions.
if body.is_empty() {
return;
}
// Find the last statement in the function.
let last_stmt = body.last().unwrap();
if !matches!(last_stmt, Stmt::Return(_)) {
let Some(last_stmt) = body.last() else {
// Skip empty functions.
return;
};
if !last_stmt.is_return_stmt() {
return;
}
@ -107,26 +103,14 @@ pub(crate) fn useless_return<'a>(
let mut diagnostic = Diagnostic::new(UselessReturn, last_stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
match delete_stmt(
let edit = autofix::edits::delete_stmt(
last_stmt,
Some(stmt),
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(edit) => {
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(last_stmt));
}
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit));
}
Err(e) => {
error!("Failed to delete `return` statement: {}", e);
}
};
);
diagnostic.set_fix(Fix::automatic(edit).isolate(IsolationLevel::Isolated));
}
checker.diagnostics.push(diagnostic);
}

View file

@ -10,7 +10,7 @@ useless_return.py:6:5: PLR1711 [*] Useless `return` statement at end of function
|
= help: Remove useless `return` statement
Suggested fix
Fix
3 3 |
4 4 | def print_python_version():
5 5 | print(sys.version)
@ -28,7 +28,7 @@ useless_return.py:11:5: PLR1711 [*] Useless `return` statement at end of functio
|
= help: Remove useless `return` statement
Suggested fix
Fix
8 8 |
9 9 | def print_python_version():
10 10 | print(sys.version)
@ -46,7 +46,7 @@ useless_return.py:16:5: PLR1711 [*] Useless `return` statement at end of functio
|
= help: Remove useless `return` statement
Suggested fix
Fix
13 13 |
14 14 | def print_python_version():
15 15 | print(sys.version)
@ -64,7 +64,7 @@ useless_return.py:22:9: PLR1711 [*] Useless `return` statement at end of functio
|
= help: Remove useless `return` statement
Suggested fix
Fix
19 19 | class SomeClass:
20 20 | def print_python_version(self):
21 21 | print(sys.version)
@ -82,7 +82,7 @@ useless_return.py:50:5: PLR1711 [*] Useless `return` statement at end of functio
|
= help: Remove useless `return` statement
Suggested fix
Fix
47 47 | def print_python_version():
48 48 | """This function returns None."""
49 49 | print(sys.version)
@ -100,7 +100,7 @@ useless_return.py:60:9: PLR1711 [*] Useless `return` statement at end of functio
|
= help: Remove useless `return` statement
Suggested fix
Fix
57 57 |
58 58 | def get(self, key: str) -> None:
59 59 | print(f"{key} not found")

View file

@ -1,6 +1,5 @@
use std::cmp::Ordering;
use log::error;
use num_bigint::{BigInt, Sign};
use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{self, Cmpop, Constant, Expr, Ranged, Stmt};
@ -9,7 +8,6 @@ use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::types::RefEquality;
use ruff_python_ast::whitespace::indentation;
use crate::autofix::edits::delete_stmt;
@ -180,7 +178,7 @@ fn compare_version(if_version: &[u32], py_version: PythonVersion, or_equal: bool
/// Convert a [`Stmt::If`], retaining the `else`.
fn fix_py2_block(
checker: &mut Checker,
checker: &Checker,
stmt: &Stmt,
orelse: &[Stmt],
block: &BlockMetadata,
@ -190,31 +188,16 @@ fn fix_py2_block(
// Delete the entire statement. If this is an `elif`, know it's the only child
// of its parent, so avoid passing in the parent at all. Otherwise,
// `delete_stmt` will erroneously include a `pass`.
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let defined_by = checker.semantic_model().stmt();
let defined_in = checker.semantic_model().stmt_parent();
return match delete_stmt(
defined_by,
if matches!(block.leading_token.tok, StartTok::If) {
defined_in
} else {
None
},
&deleted,
let stmt = checker.semantic_model().stmt();
let parent = checker.semantic_model().stmt_parent();
let edit = delete_stmt(
stmt,
if matches!(block.leading_token.tok, StartTok::If) { parent } else { None },
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(edit) => {
checker.deletions.insert(RefEquality(defined_by));
#[allow(deprecated)]
Some(Fix::unspecified(edit))
}
Err(err) => {
error!("Failed to remove block: {}", err);
None
}
};
);
return Some(Fix::suggested(edit));
};
match (&leading_token.tok, &trailing_token.tok) {

View file

@ -1,10 +1,8 @@
use itertools::Itertools;
use log::error;
use rustpython_parser::ast::{Alias, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix;
use crate::checkers::ast::Checker;
@ -80,6 +78,7 @@ pub(crate) fn unnecessary_builtin_import(
_ => return,
};
// Do this with a filter?
let mut unused_imports: Vec<&Alias> = vec![];
for alias in names {
if alias.asname.is_some() {
@ -93,6 +92,7 @@ pub(crate) fn unnecessary_builtin_import(
if unused_imports.is_empty() {
return;
}
let mut diagnostic = Diagnostic::new(
UnnecessaryBuiltinImport {
names: unused_imports
@ -103,33 +103,28 @@ pub(crate) fn unnecessary_builtin_import(
},
stmt.range(),
);
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let defined_by = checker.semantic_model().stmt();
let defined_in = checker.semantic_model().stmt_parent();
let unused_imports: Vec<String> = unused_imports
.iter()
.map(|alias| format!("{module}.{}", alias.name))
.collect();
match autofix::edits::remove_unused_imports(
unused_imports.iter().map(String::as_str),
defined_by,
defined_in,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(edit) => {
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(defined_by));
}
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit));
}
Err(e) => error!("Failed to remove builtin import: {e}"),
}
diagnostic.try_set_fix(|| {
let stmt = checker.semantic_model().stmt();
let parent = checker.semantic_model().stmt_parent();
let unused_imports: Vec<String> = unused_imports
.iter()
.map(|alias| format!("{module}.{}", alias.name))
.collect();
let edit = autofix::edits::remove_unused_imports(
unused_imports.iter().map(String::as_str),
stmt,
parent,
checker.locator,
checker.indexer,
checker.stylist,
)?;
Ok(Fix::suggested(edit).isolate(if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
}))
});
}
checker.diagnostics.push(diagnostic);
}

View file

@ -1,10 +1,8 @@
use itertools::Itertools;
use log::error;
use rustpython_parser::ast::{Alias, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix;
use crate::checkers::ast::Checker;
@ -85,31 +83,27 @@ pub(crate) fn unnecessary_future_import(checker: &mut Checker, stmt: &Stmt, name
);
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let defined_by = checker.semantic_model().stmt();
let defined_in = checker.semantic_model().stmt_parent();
let unused_imports: Vec<String> = unused_imports
.iter()
.map(|alias| format!("__future__.{}", alias.name))
.collect();
match autofix::edits::remove_unused_imports(
unused_imports.iter().map(String::as_str),
defined_by,
defined_in,
&deleted,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(fix) => {
if fix.is_deletion() || fix.content() == Some("pass") {
checker.deletions.insert(RefEquality(defined_by));
}
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(fix));
}
Err(e) => error!("Failed to remove `__future__` import: {e}"),
}
diagnostic.try_set_fix(|| {
let unused_imports: Vec<String> = unused_imports
.iter()
.map(|alias| format!("__future__.{}", alias.name))
.collect();
let stmt = checker.semantic_model().stmt();
let parent = checker.semantic_model().stmt_parent();
let edit = autofix::edits::remove_unused_imports(
unused_imports.iter().map(String::as_str),
stmt,
parent,
checker.locator,
checker.indexer,
checker.stylist,
)?;
Ok(Fix::suggested(edit).isolate(if parent.is_some() {
IsolationLevel::Isolated
} else {
IsolationLevel::NonOverlapping
}))
});
}
checker.diagnostics.push(diagnostic);
}

View file

@ -1,12 +1,9 @@
use log::error;
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, IsolationLevel};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::types::RefEquality;
use crate::autofix::edits;
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@ -24,25 +21,6 @@ impl AlwaysAutofixableViolation for UselessMetaclassType {
}
}
fn rule(targets: &[Expr], value: &Expr, location: TextRange) -> Option<Diagnostic> {
if targets.len() != 1 {
return None;
}
let Expr::Name(ast::ExprName { id, .. }) = targets.first().unwrap() else {
return None;
};
if id != "__metaclass__" {
return None;
}
let Expr::Name(ast::ExprName { id, .. }) = value else {
return None;
};
if id != "type" {
return None;
}
Some(Diagnostic::new(UselessMetaclassType, location))
}
/// UP001
pub(crate) fn useless_metaclass_type(
checker: &mut Checker,
@ -50,31 +28,34 @@ pub(crate) fn useless_metaclass_type(
value: &Expr,
targets: &[Expr],
) {
let Some(mut diagnostic) =
rule(targets, value, stmt.range()) else {
return;
};
if targets.len() != 1 {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = targets.first().unwrap() else {
return ;
};
if id != "__metaclass__" {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = value else {
return ;
};
if id != "type" {
return;
}
let mut diagnostic = Diagnostic::new(UselessMetaclassType, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect();
let defined_by = checker.semantic_model().stmt();
let defined_in = checker.semantic_model().stmt_parent();
match edits::delete_stmt(
defined_by,
defined_in,
&deleted,
let stmt = checker.semantic_model().stmt();
let parent = checker.semantic_model().stmt_parent();
let edit = autofix::edits::delete_stmt(
stmt,
parent,
checker.locator,
checker.indexer,
checker.stylist,
) {
Ok(edit) => {
if edit.is_deletion() || edit.content() == Some("pass") {
checker.deletions.insert(RefEquality(defined_by));
}
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(edit));
}
Err(e) => error!("Failed to fix remove metaclass type: {e}"),
}
);
diagnostic.set_fix(Fix::automatic(edit).isolate(IsolationLevel::Isolated));
}
checker.diagnostics.push(diagnostic);
}

View file

@ -9,7 +9,7 @@ UP001.py:2:5: UP001 [*] `__metaclass__ = type` is implied
|
= help: Remove `metaclass = type`
Suggested fix
Fix
1 1 | class A:
2 |- __metaclass__ = type
2 |+ pass
@ -27,7 +27,7 @@ UP001.py:6:5: UP001 [*] `__metaclass__ = type` is implied
|
= help: Remove `metaclass = type`
Suggested fix
Fix
3 3 |
4 4 |
5 5 | class B:

View file

@ -146,10 +146,9 @@ UP010.py:10:5: UP010 [*] Unnecessary `__future__` import `generators` for target
8 8 | if True:
9 9 | from __future__ import generator_stop
10 |- from __future__ import generators
10 |+ pass
11 11 |
12 12 | if True:
13 13 | from __future__ import generator_stop
11 10 |
12 11 | if True:
13 12 | from __future__ import generator_stop
UP010.py:13:5: UP010 [*] Unnecessary `__future__` import `generator_stop` for target Python version
|

View file

@ -228,7 +228,7 @@ RUF100_0.py:85:8: F401 [*] `shelve` imported but unused
|
= help: Remove unused import: `shelve`
Suggested fix
Fix
82 82 |
83 83 | import collections # noqa
84 84 | import os # noqa: F401, RUF100

View file

@ -11,7 +11,7 @@ RUF100_1.py:37:9: F401 [*] `typing.Union` imported but unused
|
= help: Remove unused import: `typing.Union`
Suggested fix
Fix
34 34 | # This should ignore the first error.
35 35 | from typing import (
36 36 | Mapping, # noqa: F401
@ -112,7 +112,7 @@ RUF100_1.py:89:24: F401 [*] `typing.Awaitable` imported but unused
|
= help: Remove unused import
Suggested fix
Fix
86 86 |
87 87 | def f():
88 88 | # This should mark F501 as unused.
@ -128,7 +128,7 @@ RUF100_1.py:89:35: F401 [*] `typing.AwaitableGenerator` imported but unused
|
= help: Remove unused import
Suggested fix
Fix
86 86 |
87 87 | def f():
88 88 | # This should mark F501 as unused.

View file

@ -40,9 +40,9 @@ pub(crate) fn test_snippet(contents: &str, settings: &Settings) -> Vec<Message>
}
/// A convenient wrapper around [`check_path`], that additionally
/// asserts that autofixes converge after 10 iterations.
/// asserts that autofixes converge after a fixed number of iterations.
fn test_contents(contents: &str, path: &Path, settings: &Settings) -> Vec<Message> {
static MAX_ITERATIONS: usize = 10;
static MAX_ITERATIONS: usize = 20;
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
let locator = Locator::new(contents);

View file

@ -93,7 +93,7 @@ fn stdin_json() -> Result<()> {
"code": "F401",
"message": "`os` imported but unused",
"fix": {{
"applicability": "Unspecified",
"applicability": "Automatic",
"message": "Remove unused import: `os`",
"edits": [
{{

View file

@ -8,7 +8,9 @@ rust-version = { workspace = true }
[lib]
[dependencies]
anyhow = { workspace = true }
log = { workspace = true }
ruff_text_size = { workspace = true }
anyhow = { workspace = true }
is-macro = { workspace = true }
log = { workspace = true }
serde = { workspace = true, optional = true, features = [] }

View file

@ -27,12 +27,24 @@ pub enum Applicability {
Unspecified,
}
/// Indicates the level of isolation required to apply a fix.
#[derive(Default, Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, is_macro::Is)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum IsolationLevel {
/// The fix should be applied in isolation.
Isolated,
/// The fix should be applied as long as it does not overlap with any other fixes.
#[default]
NonOverlapping,
}
/// A collection of [`Edit`] elements to be applied to a source file.
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fix {
edits: Vec<Edit>,
applicability: Applicability,
isolation_level: IsolationLevel,
}
impl Fix {
@ -44,6 +56,7 @@ impl Fix {
Self {
edits: vec![edit],
applicability: Applicability::Unspecified,
isolation_level: IsolationLevel::default(),
}
}
@ -55,6 +68,7 @@ impl Fix {
Self {
edits: std::iter::once(edit).chain(rest.into_iter()).collect(),
applicability: Applicability::Unspecified,
isolation_level: IsolationLevel::default(),
}
}
@ -63,6 +77,7 @@ impl Fix {
Self {
edits: vec![edit],
applicability: Applicability::Automatic,
isolation_level: IsolationLevel::default(),
}
}
@ -71,6 +86,7 @@ impl Fix {
Self {
edits: std::iter::once(edit).chain(rest.into_iter()).collect(),
applicability: Applicability::Automatic,
isolation_level: IsolationLevel::default(),
}
}
@ -79,6 +95,7 @@ impl Fix {
Self {
edits: vec![edit],
applicability: Applicability::Suggested,
isolation_level: IsolationLevel::default(),
}
}
@ -87,6 +104,7 @@ impl Fix {
Self {
edits: std::iter::once(edit).chain(rest.into_iter()).collect(),
applicability: Applicability::Suggested,
isolation_level: IsolationLevel::default(),
}
}
@ -95,6 +113,7 @@ impl Fix {
Self {
edits: vec![edit],
applicability: Applicability::Manual,
isolation_level: IsolationLevel::default(),
}
}
@ -103,6 +122,7 @@ impl Fix {
Self {
edits: std::iter::once(edit).chain(rest.into_iter()).collect(),
applicability: Applicability::Manual,
isolation_level: IsolationLevel::default(),
}
}
@ -120,7 +140,20 @@ impl Fix {
self.edits
}
/// Return the [`Applicability`] of the [`Fix`].
pub fn applicability(&self) -> Applicability {
self.applicability
}
/// Return the [`IsolationLevel`] of the [`Fix`].
pub fn isolation(&self) -> IsolationLevel {
self.isolation_level
}
/// Create a new [`Fix`] with the given [`IsolationLevel`].
#[must_use]
pub fn isolate(mut self, isolation: IsolationLevel) -> Self {
self.isolation_level = isolation;
self
}
}

View file

@ -1,6 +1,6 @@
pub use diagnostic::{Diagnostic, DiagnosticKind};
pub use edit::Edit;
pub use fix::{Applicability, Fix};
pub use fix::{Applicability, Fix, IsolationLevel};
pub use violation::{AlwaysAutofixableViolation, AutofixKind, Violation};
mod diagnostic;