mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
Break up autofix/fixes.rs (#449)
This commit is contained in:
parent
206e6463be
commit
583149a472
13 changed files with 400 additions and 384 deletions
|
@ -1,6 +1,6 @@
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
|
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, StmtKind};
|
||||||
|
|
||||||
use crate::python::typing;
|
use crate::python::typing;
|
||||||
|
|
||||||
|
@ -131,3 +131,15 @@ pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||||
|
pub fn to_absolute(relative: &Location, base: &Location) -> Location {
|
||||||
|
if relative.row() == 1 {
|
||||||
|
Location::new(
|
||||||
|
relative.row() + base.row() - 1,
|
||||||
|
relative.column() + base.column() - 1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Location::new(relative.row() + base.row() - 1, relative.column())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,371 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use libcst_native::ImportNames::Aliases;
|
|
||||||
use libcst_native::NameOrAttribute::N;
|
|
||||||
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
|
||||||
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
|
|
||||||
use rustpython_parser::lexer;
|
|
||||||
use rustpython_parser::token::Tok;
|
|
||||||
|
|
||||||
use crate::ast::operations::SourceCodeLocator;
|
|
||||||
use crate::ast::types::Range;
|
|
||||||
use crate::autofix::Fix;
|
|
||||||
|
|
||||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
|
||||||
fn to_absolute(relative: &Location, base: &Location) -> Location {
|
|
||||||
if relative.row() == 1 {
|
|
||||||
Location::new(
|
|
||||||
relative.row() + base.row() - 1,
|
|
||||||
relative.column() + base.column() - 1,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Location::new(relative.row() + base.row() - 1, relative.column())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a fix to remove a base from a ClassDef statement.
|
|
||||||
pub fn remove_class_def_base(
|
|
||||||
locator: &mut SourceCodeLocator,
|
|
||||||
stmt_at: &Location,
|
|
||||||
expr_at: Location,
|
|
||||||
bases: &[Expr],
|
|
||||||
keywords: &[Keyword],
|
|
||||||
) -> Option<Fix> {
|
|
||||||
let content = locator.slice_source_code_at(stmt_at);
|
|
||||||
|
|
||||||
// Case 1: `object` is the only base.
|
|
||||||
if bases.len() == 1 && keywords.is_empty() {
|
|
||||||
let mut fix_start = None;
|
|
||||||
let mut fix_end = None;
|
|
||||||
let mut count: usize = 0;
|
|
||||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
|
||||||
if matches!(tok, Tok::Lpar) {
|
|
||||||
if count == 0 {
|
|
||||||
fix_start = Some(to_absolute(&start, stmt_at));
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(tok, Tok::Rpar) {
|
|
||||||
count -= 1;
|
|
||||||
if count == 0 {
|
|
||||||
fix_end = Some(to_absolute(&end, stmt_at));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return match (fix_start, fix_end) {
|
|
||||||
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if bases
|
|
||||||
.iter()
|
|
||||||
.map(|node| node.location)
|
|
||||||
.chain(keywords.iter().map(|node| node.location))
|
|
||||||
.any(|location| location > expr_at)
|
|
||||||
{
|
|
||||||
// Case 2: `object` is _not_ the last node.
|
|
||||||
let mut fix_start: Option<Location> = None;
|
|
||||||
let mut fix_end: Option<Location> = None;
|
|
||||||
let mut seen_comma = false;
|
|
||||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
|
||||||
let start = to_absolute(&start, stmt_at);
|
|
||||||
if seen_comma {
|
|
||||||
if matches!(tok, Tok::Newline) {
|
|
||||||
fix_end = Some(end);
|
|
||||||
} else {
|
|
||||||
fix_end = Some(start);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if start == expr_at {
|
|
||||||
fix_start = Some(start);
|
|
||||||
}
|
|
||||||
if fix_start.is_some() && matches!(tok, Tok::Comma) {
|
|
||||||
seen_comma = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (fix_start, fix_end) {
|
|
||||||
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
|
|
||||||
let mut fix_start: Option<Location> = None;
|
|
||||||
let mut fix_end: Option<Location> = None;
|
|
||||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
|
||||||
let start = to_absolute(&start, stmt_at);
|
|
||||||
let end = to_absolute(&end, stmt_at);
|
|
||||||
if start == expr_at {
|
|
||||||
fix_end = Some(end);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if matches!(tok, Tok::Comma) {
|
|
||||||
fix_start = Some(start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (fix_start, fix_end) {
|
|
||||||
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
|
||||||
let range = Range::from_located(expr);
|
|
||||||
let contents = locator.slice_source_code_range(&range);
|
|
||||||
|
|
||||||
let mut tree = match libcst_native::parse_module(contents, None) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
|
||||||
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
|
|
||||||
if let Expression::Call(body) = &mut body.value {
|
|
||||||
body.args = vec![];
|
|
||||||
body.whitespace_before_args = Default::default();
|
|
||||||
body.whitespace_after_func = Default::default();
|
|
||||||
|
|
||||||
let mut state = Default::default();
|
|
||||||
tree.codegen(&mut state);
|
|
||||||
|
|
||||||
return Some(Fix::replacement(
|
|
||||||
state.to_string(),
|
|
||||||
range.location,
|
|
||||||
range.end_location,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 child is the only statement in its body.
|
|
||||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
|
||||||
match &parent.node {
|
|
||||||
StmtKind::FunctionDef { body, .. }
|
|
||||||
| StmtKind::AsyncFunctionDef { body, .. }
|
|
||||||
| StmtKind::ClassDef { body, .. }
|
|
||||||
| StmtKind::With { body, .. }
|
|
||||||
| StmtKind::AsyncWith { body, .. } => {
|
|
||||||
if body.iter().contains(child) {
|
|
||||||
Ok(has_single_child(body, deleted))
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StmtKind::For { body, orelse, .. }
|
|
||||||
| StmtKind::AsyncFor { body, orelse, .. }
|
|
||||||
| StmtKind::While { body, orelse, .. }
|
|
||||||
| StmtKind::If { 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 {
|
|
||||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StmtKind::Try {
|
|
||||||
body,
|
|
||||||
handlers,
|
|
||||||
orelse,
|
|
||||||
finalbody,
|
|
||||||
} => {
|
|
||||||
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.node {
|
|
||||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
|
||||||
if body.iter().contains(child) {
|
|
||||||
Some(body)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Ok(has_single_child(body, deleted))
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
|
||||||
if parent
|
|
||||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
|
||||||
.map_or(Ok(None), |v| v.map(Some))?
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
// If removing this node would lead to an invalid syntax tree, replace
|
|
||||||
// it with a `pass`.
|
|
||||||
Ok(Fix::replacement(
|
|
||||||
"pass".to_string(),
|
|
||||||
stmt.location,
|
|
||||||
stmt.end_location.unwrap(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
// Otherwise, nuke the entire line.
|
|
||||||
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
|
||||||
Ok(Fix::deletion(
|
|
||||||
Location::new(stmt.location.row(), 1),
|
|
||||||
Location::new(stmt.end_location.unwrap().row() + 1, 1),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a Fix to remove any unused imports from an `import` statement.
|
|
||||||
pub fn remove_unused_imports(
|
|
||||||
locator: &mut SourceCodeLocator,
|
|
||||||
full_names: &[&str],
|
|
||||||
stmt: &Stmt,
|
|
||||||
parent: Option<&Stmt>,
|
|
||||||
deleted: &[&Stmt],
|
|
||||||
) -> Result<Fix> {
|
|
||||||
let mut tree = match libcst_native::parse_module(
|
|
||||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
|
||||||
None,
|
|
||||||
) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
|
||||||
body
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
|
||||||
};
|
|
||||||
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
|
|
||||||
body
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Expected node to be: SmallStatement::ImportFrom."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let aliases = &mut body.names;
|
|
||||||
|
|
||||||
// Preserve the trailing comma (or not) from the last entry.
|
|
||||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
|
||||||
|
|
||||||
// Identify unused imports from within the `import from`.
|
|
||||||
let mut removable = vec![];
|
|
||||||
for (index, alias) in aliases.iter().enumerate() {
|
|
||||||
if let N(import_name) = &alias.name {
|
|
||||||
if full_names.contains(&import_name.value) {
|
|
||||||
removable.push(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO(charlie): This is quadratic.
|
|
||||||
for index in removable.iter().rev() {
|
|
||||||
aliases.remove(*index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(alias) = aliases.last_mut() {
|
|
||||||
alias.comma = trailing_comma;
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliases.is_empty() {
|
|
||||||
remove_stmt(stmt, parent, deleted)
|
|
||||||
} else {
|
|
||||||
let mut state = Default::default();
|
|
||||||
tree.codegen(&mut state);
|
|
||||||
|
|
||||||
Ok(Fix::replacement(
|
|
||||||
state.to_string(),
|
|
||||||
stmt.location,
|
|
||||||
stmt.end_location.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
|
||||||
pub fn remove_unused_import_froms(
|
|
||||||
locator: &mut SourceCodeLocator,
|
|
||||||
full_names: &[&str],
|
|
||||||
stmt: &Stmt,
|
|
||||||
parent: Option<&Stmt>,
|
|
||||||
deleted: &[&Stmt],
|
|
||||||
) -> Result<Fix> {
|
|
||||||
let mut tree = match libcst_native::parse_module(
|
|
||||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
|
||||||
None,
|
|
||||||
) {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
|
||||||
body
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
|
||||||
};
|
|
||||||
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
|
|
||||||
body
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Expected node to be: SmallStatement::ImportFrom."
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let aliases = if let Aliases(aliases) = &mut body.names {
|
|
||||||
aliases
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preserve the trailing comma (or not) from the last entry.
|
|
||||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
|
||||||
|
|
||||||
// Identify unused imports from within the `import from`.
|
|
||||||
let mut removable = vec![];
|
|
||||||
for (index, alias) in aliases.iter().enumerate() {
|
|
||||||
if let N(name) = &alias.name {
|
|
||||||
let import_name = if let Some(N(module_name)) = &body.module {
|
|
||||||
format!("{}.{}", module_name.value, name.value)
|
|
||||||
} else {
|
|
||||||
name.value.to_string()
|
|
||||||
};
|
|
||||||
if full_names.contains(&import_name.as_str()) {
|
|
||||||
removable.push(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO(charlie): This is quadratic.
|
|
||||||
for index in removable.iter().rev() {
|
|
||||||
aliases.remove(*index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(alias) = aliases.last_mut() {
|
|
||||||
alias.comma = trailing_comma;
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliases.is_empty() {
|
|
||||||
remove_stmt(stmt, parent, deleted)
|
|
||||||
} else {
|
|
||||||
let mut state = Default::default();
|
|
||||||
tree.codegen(&mut state);
|
|
||||||
|
|
||||||
Ok(Fix::replacement(
|
|
||||||
state.to_string(),
|
|
||||||
stmt.location,
|
|
||||||
stmt.end_location.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
89
src/autofix/helpers.rs
Normal file
89
src/autofix/helpers.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rustpython_parser::ast::{ExcepthandlerKind, Location, Stmt, StmtKind};
|
||||||
|
|
||||||
|
use crate::autofix::Fix;
|
||||||
|
|
||||||
|
/// 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 child is the only statement in its body.
|
||||||
|
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||||
|
match &parent.node {
|
||||||
|
StmtKind::FunctionDef { body, .. }
|
||||||
|
| StmtKind::AsyncFunctionDef { body, .. }
|
||||||
|
| StmtKind::ClassDef { body, .. }
|
||||||
|
| StmtKind::With { body, .. }
|
||||||
|
| StmtKind::AsyncWith { body, .. } => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StmtKind::For { body, orelse, .. }
|
||||||
|
| StmtKind::AsyncFor { body, orelse, .. }
|
||||||
|
| StmtKind::While { body, orelse, .. }
|
||||||
|
| StmtKind::If { 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 {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StmtKind::Try {
|
||||||
|
body,
|
||||||
|
handlers,
|
||||||
|
orelse,
|
||||||
|
finalbody,
|
||||||
|
} => {
|
||||||
|
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.node {
|
||||||
|
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||||
|
if body.iter().contains(child) {
|
||||||
|
Some(body)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(has_single_child(body, deleted))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||||
|
if parent
|
||||||
|
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||||
|
.map_or(Ok(None), |v| v.map(Some))?
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
// If removing this node would lead to an invalid syntax tree, replace
|
||||||
|
// it with a `pass`.
|
||||||
|
Ok(Fix::replacement(
|
||||||
|
"pass".to_string(),
|
||||||
|
stmt.location,
|
||||||
|
stmt.end_location.unwrap(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// Otherwise, nuke the entire line.
|
||||||
|
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||||
|
Ok(Fix::deletion(
|
||||||
|
Location::new(stmt.location.row(), 1),
|
||||||
|
Location::new(stmt.end_location.unwrap().row() + 1, 1),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ use rustpython_ast::Location;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod fixer;
|
pub mod fixer;
|
||||||
pub mod fixes;
|
pub mod helpers;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub struct Patch {
|
pub struct Patch {
|
||||||
|
|
|
@ -18,7 +18,7 @@ use crate::ast::types::{
|
||||||
};
|
};
|
||||||
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||||
use crate::ast::{helpers, operations, visitor};
|
use crate::ast::{helpers, operations, visitor};
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::fixer;
|
||||||
use crate::checks::{Check, CheckCode, CheckKind};
|
use crate::checks::{Check, CheckCode, CheckKind};
|
||||||
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
||||||
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||||
|
@ -1955,8 +1955,8 @@ impl<'a> Checker<'a> {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let removal_fn = match kind {
|
let removal_fn = match kind {
|
||||||
ImportKind::Import => fixes::remove_unused_imports,
|
ImportKind::Import => pyflakes::fixes::remove_unused_imports,
|
||||||
ImportKind::ImportFrom => fixes::remove_unused_import_froms,
|
ImportKind::ImportFrom => pyflakes::fixes::remove_unused_import_froms,
|
||||||
};
|
};
|
||||||
|
|
||||||
match removal_fn(&mut self.locator, &full_names, child, parent, &deleted) {
|
match removal_fn(&mut self.locator, &full_names, child, parent, &deleted) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use log::error;
|
use log::error;
|
||||||
use rustpython_ast::{Expr, Stmt, StmtKind};
|
use rustpython_ast::{Expr, Stmt, StmtKind};
|
||||||
|
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::{fixer, helpers};
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
use crate::checks::CheckCode;
|
use crate::checks::CheckCode;
|
||||||
use crate::flake8_print::checks;
|
use crate::flake8_print::checks;
|
||||||
|
@ -25,7 +25,7 @@ pub fn print_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
|
||||||
.map(|index| checker.parents[*index])
|
.map(|index| checker.parents[*index])
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
match fixes::remove_stmt(
|
match helpers::remove_stmt(
|
||||||
checker.parents[context.defined_by],
|
checker.parents[context.defined_by],
|
||||||
context.defined_in.map(|index| checker.parents[index]),
|
context.defined_in.map(|index| checker.parents[index]),
|
||||||
&deleted,
|
&deleted,
|
||||||
|
|
147
src/pyflakes/fixes.rs
Normal file
147
src/pyflakes/fixes.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
use libcst_native::ImportNames::Aliases;
|
||||||
|
use libcst_native::NameOrAttribute::N;
|
||||||
|
use libcst_native::{Codegen, SmallStatement, Statement};
|
||||||
|
use rustpython_ast::Stmt;
|
||||||
|
|
||||||
|
use crate::ast::operations::SourceCodeLocator;
|
||||||
|
use crate::ast::types::Range;
|
||||||
|
use crate::autofix::{helpers, Fix};
|
||||||
|
|
||||||
|
/// Generate a Fix to remove any unused imports from an `import` statement.
|
||||||
|
pub fn remove_unused_imports(
|
||||||
|
locator: &mut SourceCodeLocator,
|
||||||
|
full_names: &[&str],
|
||||||
|
stmt: &Stmt,
|
||||||
|
parent: Option<&Stmt>,
|
||||||
|
deleted: &[&Stmt],
|
||||||
|
) -> anyhow::Result<Fix> {
|
||||||
|
let mut tree = match libcst_native::parse_module(
|
||||||
|
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||||
|
};
|
||||||
|
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Expected node to be: SmallStatement::ImportFrom."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let aliases = &mut body.names;
|
||||||
|
|
||||||
|
// Preserve the trailing comma (or not) from the last entry.
|
||||||
|
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||||
|
|
||||||
|
// Identify unused imports from within the `import from`.
|
||||||
|
let mut removable = vec![];
|
||||||
|
for (index, alias) in aliases.iter().enumerate() {
|
||||||
|
if let N(import_name) = &alias.name {
|
||||||
|
if full_names.contains(&import_name.value) {
|
||||||
|
removable.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(charlie): This is quadratic.
|
||||||
|
for index in removable.iter().rev() {
|
||||||
|
aliases.remove(*index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alias) = aliases.last_mut() {
|
||||||
|
alias.comma = trailing_comma;
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliases.is_empty() {
|
||||||
|
helpers::remove_stmt(stmt, parent, deleted)
|
||||||
|
} else {
|
||||||
|
let mut state = Default::default();
|
||||||
|
tree.codegen(&mut state);
|
||||||
|
|
||||||
|
Ok(Fix::replacement(
|
||||||
|
state.to_string(),
|
||||||
|
stmt.location,
|
||||||
|
stmt.end_location.unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
||||||
|
pub fn remove_unused_import_froms(
|
||||||
|
locator: &mut SourceCodeLocator,
|
||||||
|
full_names: &[&str],
|
||||||
|
stmt: &Stmt,
|
||||||
|
parent: Option<&Stmt>,
|
||||||
|
deleted: &[&Stmt],
|
||||||
|
) -> anyhow::Result<Fix> {
|
||||||
|
let mut tree = match libcst_native::parse_module(
|
||||||
|
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||||
|
};
|
||||||
|
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Expected node to be: SmallStatement::ImportFrom."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let aliases = if let Aliases(aliases) = &mut body.names {
|
||||||
|
aliases
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the trailing comma (or not) from the last entry.
|
||||||
|
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||||
|
|
||||||
|
// Identify unused imports from within the `import from`.
|
||||||
|
let mut removable = vec![];
|
||||||
|
for (index, alias) in aliases.iter().enumerate() {
|
||||||
|
if let N(name) = &alias.name {
|
||||||
|
let import_name = if let Some(N(module_name)) = &body.module {
|
||||||
|
format!("{}.{}", module_name.value, name.value)
|
||||||
|
} else {
|
||||||
|
name.value.to_string()
|
||||||
|
};
|
||||||
|
if full_names.contains(&import_name.as_str()) {
|
||||||
|
removable.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(charlie): This is quadratic.
|
||||||
|
for index in removable.iter().rev() {
|
||||||
|
aliases.remove(*index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(alias) = aliases.last_mut() {
|
||||||
|
alias.comma = trailing_comma;
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliases.is_empty() {
|
||||||
|
helpers::remove_stmt(stmt, parent, deleted)
|
||||||
|
} else {
|
||||||
|
let mut state = Default::default();
|
||||||
|
tree.codegen(&mut state);
|
||||||
|
|
||||||
|
Ok(Fix::replacement(
|
||||||
|
state.to_string(),
|
||||||
|
stmt.location,
|
||||||
|
stmt.end_location.unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod checks;
|
pub mod checks;
|
||||||
|
pub mod fixes;
|
||||||
pub mod plugins;
|
pub mod plugins;
|
||||||
|
|
133
src/pyupgrade/fixes.rs
Normal file
133
src/pyupgrade/fixes.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
||||||
|
use rustpython_ast::{Expr, Keyword, Location};
|
||||||
|
use rustpython_parser::lexer;
|
||||||
|
use rustpython_parser::lexer::Tok;
|
||||||
|
|
||||||
|
use crate::ast::helpers;
|
||||||
|
use crate::ast::operations::SourceCodeLocator;
|
||||||
|
use crate::ast::types::Range;
|
||||||
|
use crate::autofix::Fix;
|
||||||
|
|
||||||
|
/// Generate a fix to remove a base from a ClassDef statement.
|
||||||
|
pub fn remove_class_def_base(
|
||||||
|
locator: &mut SourceCodeLocator,
|
||||||
|
stmt_at: &Location,
|
||||||
|
expr_at: Location,
|
||||||
|
bases: &[Expr],
|
||||||
|
keywords: &[Keyword],
|
||||||
|
) -> Option<Fix> {
|
||||||
|
let content = locator.slice_source_code_at(stmt_at);
|
||||||
|
|
||||||
|
// Case 1: `object` is the only base.
|
||||||
|
if bases.len() == 1 && keywords.is_empty() {
|
||||||
|
let mut fix_start = None;
|
||||||
|
let mut fix_end = None;
|
||||||
|
let mut count: usize = 0;
|
||||||
|
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||||
|
if matches!(tok, Tok::Lpar) {
|
||||||
|
if count == 0 {
|
||||||
|
fix_start = Some(helpers::to_absolute(&start, stmt_at));
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(tok, Tok::Rpar) {
|
||||||
|
count -= 1;
|
||||||
|
if count == 0 {
|
||||||
|
fix_end = Some(helpers::to_absolute(&end, stmt_at));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match (fix_start, fix_end) {
|
||||||
|
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if bases
|
||||||
|
.iter()
|
||||||
|
.map(|node| node.location)
|
||||||
|
.chain(keywords.iter().map(|node| node.location))
|
||||||
|
.any(|location| location > expr_at)
|
||||||
|
{
|
||||||
|
// Case 2: `object` is _not_ the last node.
|
||||||
|
let mut fix_start: Option<Location> = None;
|
||||||
|
let mut fix_end: Option<Location> = None;
|
||||||
|
let mut seen_comma = false;
|
||||||
|
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||||
|
let start = helpers::to_absolute(&start, stmt_at);
|
||||||
|
if seen_comma {
|
||||||
|
if matches!(tok, Tok::Newline) {
|
||||||
|
fix_end = Some(end);
|
||||||
|
} else {
|
||||||
|
fix_end = Some(start);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if start == expr_at {
|
||||||
|
fix_start = Some(start);
|
||||||
|
}
|
||||||
|
if fix_start.is_some() && matches!(tok, Tok::Comma) {
|
||||||
|
seen_comma = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (fix_start, fix_end) {
|
||||||
|
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
|
||||||
|
let mut fix_start: Option<Location> = None;
|
||||||
|
let mut fix_end: Option<Location> = None;
|
||||||
|
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||||
|
let start = helpers::to_absolute(&start, stmt_at);
|
||||||
|
let end = helpers::to_absolute(&end, stmt_at);
|
||||||
|
if start == expr_at {
|
||||||
|
fix_end = Some(end);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if matches!(tok, Tok::Comma) {
|
||||||
|
fix_start = Some(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (fix_start, fix_end) {
|
||||||
|
(Some(start), Some(end)) => Some(Fix::replacement("".to_string(), start, end)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||||
|
let range = Range::from_located(expr);
|
||||||
|
let contents = locator.slice_source_code_range(&range);
|
||||||
|
|
||||||
|
let mut tree = match libcst_native::parse_module(contents, None) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||||
|
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
|
||||||
|
if let Expression::Call(body) = &mut body.value {
|
||||||
|
body.args = vec![];
|
||||||
|
body.whitespace_before_args = Default::default();
|
||||||
|
body.whitespace_after_func = Default::default();
|
||||||
|
|
||||||
|
let mut state = Default::default();
|
||||||
|
tree.codegen(&mut state);
|
||||||
|
|
||||||
|
return Some(Fix::replacement(
|
||||||
|
state.to_string(),
|
||||||
|
range.location,
|
||||||
|
range.end_location,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
mod checks;
|
mod checks;
|
||||||
|
pub mod fixes;
|
||||||
pub mod plugins;
|
pub mod plugins;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use rustpython_ast::{Expr, Stmt};
|
use rustpython_ast::{Expr, Stmt};
|
||||||
|
|
||||||
use crate::ast::helpers;
|
use crate::ast::helpers;
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::fixer;
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
|
use crate::pyupgrade;
|
||||||
use crate::pyupgrade::checks;
|
use crate::pyupgrade::checks;
|
||||||
|
|
||||||
pub fn super_call_with_parameters(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
pub fn super_call_with_parameters(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
|
||||||
|
@ -17,7 +18,9 @@ pub fn super_call_with_parameters(checker: &mut Checker, expr: &Expr, func: &Exp
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(mut check) = checks::super_args(scope, &parents, expr, func, args) {
|
if let Some(mut check) = checks::super_args(scope, &parents, expr, func, args) {
|
||||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||||
if let Some(fix) = fixes::remove_super_arguments(&mut checker.locator, expr) {
|
if let Some(fix) =
|
||||||
|
pyupgrade::fixes::remove_super_arguments(&mut checker.locator, expr)
|
||||||
|
{
|
||||||
check.amend(fix);
|
check.amend(fix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use log::error;
|
||||||
use rustpython_ast::{Expr, Stmt};
|
use rustpython_ast::{Expr, Stmt};
|
||||||
|
|
||||||
use crate::ast::types::{CheckLocator, Range};
|
use crate::ast::types::{CheckLocator, Range};
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::{fixer, helpers};
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
use crate::pyupgrade::checks;
|
use crate::pyupgrade::checks;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ pub fn useless_metaclass_type(checker: &mut Checker, stmt: &Stmt, value: &Expr,
|
||||||
.map(|index| checker.parents[*index])
|
.map(|index| checker.parents[*index])
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
match fixes::remove_stmt(
|
match helpers::remove_stmt(
|
||||||
checker.parents[context.defined_by],
|
checker.parents[context.defined_by],
|
||||||
context.defined_in.map(|index| checker.parents[index]),
|
context.defined_in.map(|index| checker.parents[index]),
|
||||||
&deleted,
|
&deleted,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use rustpython_ast::{Expr, Keyword, Stmt};
|
use rustpython_ast::{Expr, Keyword, Stmt};
|
||||||
|
|
||||||
use crate::autofix::{fixer, fixes};
|
use crate::autofix::fixer;
|
||||||
use crate::check_ast::Checker;
|
use crate::check_ast::Checker;
|
||||||
|
use crate::pyupgrade;
|
||||||
use crate::pyupgrade::checks;
|
use crate::pyupgrade::checks;
|
||||||
|
|
||||||
pub fn useless_object_inheritance(
|
pub fn useless_object_inheritance(
|
||||||
|
@ -14,7 +15,7 @@ pub fn useless_object_inheritance(
|
||||||
let scope = checker.current_scope();
|
let scope = checker.current_scope();
|
||||||
if let Some(mut check) = checks::useless_object_inheritance(name, bases, scope) {
|
if let Some(mut check) = checks::useless_object_inheritance(name, bases, scope) {
|
||||||
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
if matches!(checker.autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||||
if let Some(fix) = fixes::remove_class_def_base(
|
if let Some(fix) = pyupgrade::fixes::remove_class_def_base(
|
||||||
&mut checker.locator,
|
&mut checker.locator,
|
||||||
&stmt.location,
|
&stmt.location,
|
||||||
check.location,
|
check.location,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue