mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[pyupgrade
] Prevent infinite loop with I002
(UP010
, UP035
) (#19413)
## Summary Fixes #18729 and fixes #16802 ## Test Plan Manually verified via CLI that Ruff no longer enters an infinite loop by running: ```sh echo 1 | ruff --isolated check - --select I002,UP010 --fix ``` with `required-imports = ["from __future__ import generator_stop"]` set in the config, confirming “All checks passed!” and no snapshots were generated. --------- Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
parent
2ab1502e51
commit
b07def07c9
4 changed files with 123 additions and 7 deletions
|
@ -7,16 +7,18 @@ pub(crate) mod types;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ruff_python_ast::PythonVersion;
|
use ruff_python_ast::PythonVersion;
|
||||||
|
use ruff_python_semantic::{MemberNameImport, NameImport};
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
|
|
||||||
use crate::registry::Rule;
|
use crate::registry::Rule;
|
||||||
use crate::rules::pyupgrade;
|
use crate::rules::{isort, pyupgrade};
|
||||||
use crate::settings::types::PreviewMode;
|
use crate::settings::types::PreviewMode;
|
||||||
use crate::test::test_path;
|
use crate::test::{test_path, test_snippet};
|
||||||
use crate::{assert_diagnostics, settings};
|
use crate::{assert_diagnostics, settings};
|
||||||
|
|
||||||
#[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))]
|
#[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))]
|
||||||
|
@ -294,4 +296,63 @@ mod tests {
|
||||||
assert_diagnostics!(diagnostics);
|
assert_diagnostics!(diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn i002_conflict() {
|
||||||
|
let diagnostics = test_snippet(
|
||||||
|
"from pipes import quote, Template",
|
||||||
|
&settings::LinterSettings {
|
||||||
|
isort: isort::settings::Settings {
|
||||||
|
required_imports: BTreeSet::from_iter([
|
||||||
|
// https://github.com/astral-sh/ruff/issues/18729
|
||||||
|
NameImport::ImportFrom(MemberNameImport::member(
|
||||||
|
"__future__".to_string(),
|
||||||
|
"generator_stop".to_string(),
|
||||||
|
)),
|
||||||
|
// https://github.com/astral-sh/ruff/issues/16802
|
||||||
|
NameImport::ImportFrom(MemberNameImport::member(
|
||||||
|
"collections".to_string(),
|
||||||
|
"Sequence".to_string(),
|
||||||
|
)),
|
||||||
|
// Only bail out if _all_ the names in UP035 are required. `pipes.Template`
|
||||||
|
// isn't flagged by UP035, so requiring it shouldn't prevent `pipes.quote`
|
||||||
|
// from getting a diagnostic.
|
||||||
|
NameImport::ImportFrom(MemberNameImport::member(
|
||||||
|
"pipes".to_string(),
|
||||||
|
"Template".to_string(),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..settings::LinterSettings::for_rules([
|
||||||
|
Rule::MissingRequiredImport,
|
||||||
|
Rule::UnnecessaryFutureImport,
|
||||||
|
Rule::DeprecatedImport,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_diagnostics!(diagnostics, @r"
|
||||||
|
<filename>:1:1: UP035 [*] Import from `shlex` instead: `quote`
|
||||||
|
|
|
||||||
|
1 | from pipes import quote, Template
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||||
|
|
|
||||||
|
= help: Import from `shlex`
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
1 |-from pipes import quote, Template
|
||||||
|
1 |+from pipes import Template
|
||||||
|
2 |+from shlex import quote
|
||||||
|
|
||||||
|
<filename>:1:1: I002 [*] Missing required import: `from __future__ import generator_stop`
|
||||||
|
ℹ Safe fix
|
||||||
|
1 |+from __future__ import generator_stop
|
||||||
|
1 2 | from pipes import quote, Template
|
||||||
|
|
||||||
|
<filename>:1:1: I002 [*] Missing required import: `from collections import Sequence`
|
||||||
|
ℹ Safe fix
|
||||||
|
1 |+from collections import Sequence
|
||||||
|
1 2 | from pipes import quote, Template
|
||||||
|
");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use itertools::Itertools;
|
||||||
|
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
use ruff_python_ast::whitespace::indentation;
|
use ruff_python_ast::whitespace::indentation;
|
||||||
use ruff_python_ast::{Alias, StmtImportFrom};
|
use ruff_python_ast::{Alias, StmtImportFrom, StmtRef};
|
||||||
use ruff_python_codegen::Stylist;
|
use ruff_python_codegen::Stylist;
|
||||||
use ruff_python_parser::Tokens;
|
use ruff_python_parser::Tokens;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
@ -10,9 +10,12 @@ use ruff_text_size::Ranged;
|
||||||
use crate::Locator;
|
use crate::Locator;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::rules::pyupgrade::fixes;
|
use crate::rules::pyupgrade::fixes;
|
||||||
|
use crate::rules::pyupgrade::rules::unnecessary_future_import::is_import_required_by_isort;
|
||||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_python_ast::PythonVersion;
|
use ruff_python_ast::PythonVersion;
|
||||||
|
|
||||||
|
use super::RequiredImports;
|
||||||
|
|
||||||
/// An import was moved and renamed as part of a deprecation.
|
/// An import was moved and renamed as part of a deprecation.
|
||||||
/// For example, `typing.AbstractSet` was moved to `collections.abc.Set`.
|
/// For example, `typing.AbstractSet` was moved to `collections.abc.Set`.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
@ -410,6 +413,7 @@ struct ImportReplacer<'a> {
|
||||||
stylist: &'a Stylist<'a>,
|
stylist: &'a Stylist<'a>,
|
||||||
tokens: &'a Tokens,
|
tokens: &'a Tokens,
|
||||||
version: PythonVersion,
|
version: PythonVersion,
|
||||||
|
required_imports: &'a RequiredImports,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ImportReplacer<'a> {
|
impl<'a> ImportReplacer<'a> {
|
||||||
|
@ -420,6 +424,7 @@ impl<'a> ImportReplacer<'a> {
|
||||||
stylist: &'a Stylist<'a>,
|
stylist: &'a Stylist<'a>,
|
||||||
tokens: &'a Tokens,
|
tokens: &'a Tokens,
|
||||||
version: PythonVersion,
|
version: PythonVersion,
|
||||||
|
required_imports: &'a RequiredImports,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
import_from_stmt,
|
import_from_stmt,
|
||||||
|
@ -428,6 +433,7 @@ impl<'a> ImportReplacer<'a> {
|
||||||
stylist,
|
stylist,
|
||||||
tokens,
|
tokens,
|
||||||
version,
|
version,
|
||||||
|
required_imports,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,6 +443,13 @@ impl<'a> ImportReplacer<'a> {
|
||||||
if self.module == "typing" {
|
if self.module == "typing" {
|
||||||
if self.version >= PythonVersion::PY39 {
|
if self.version >= PythonVersion::PY39 {
|
||||||
for member in &self.import_from_stmt.names {
|
for member in &self.import_from_stmt.names {
|
||||||
|
if is_import_required_by_isort(
|
||||||
|
self.required_imports,
|
||||||
|
StmtRef::ImportFrom(self.import_from_stmt),
|
||||||
|
member,
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(target) = TYPING_TO_RENAME_PY39.iter().find_map(|(name, target)| {
|
if let Some(target) = TYPING_TO_RENAME_PY39.iter().find_map(|(name, target)| {
|
||||||
if &member.name == *name {
|
if &member.name == *name {
|
||||||
Some(*target)
|
Some(*target)
|
||||||
|
@ -673,7 +686,13 @@ impl<'a> ImportReplacer<'a> {
|
||||||
let mut matched_names = vec![];
|
let mut matched_names = vec![];
|
||||||
let mut unmatched_names = vec![];
|
let mut unmatched_names = vec![];
|
||||||
for name in &self.import_from_stmt.names {
|
for name in &self.import_from_stmt.names {
|
||||||
if candidates.contains(&name.name.as_str()) {
|
if is_import_required_by_isort(
|
||||||
|
self.required_imports,
|
||||||
|
StmtRef::ImportFrom(self.import_from_stmt),
|
||||||
|
name,
|
||||||
|
) {
|
||||||
|
unmatched_names.push(name);
|
||||||
|
} else if candidates.contains(&name.name.as_str()) {
|
||||||
matched_names.push(name);
|
matched_names.push(name);
|
||||||
} else {
|
} else {
|
||||||
unmatched_names.push(name);
|
unmatched_names.push(name);
|
||||||
|
@ -726,6 +745,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
|
||||||
checker.stylist(),
|
checker.stylist(),
|
||||||
checker.tokens(),
|
checker.tokens(),
|
||||||
checker.target_version(),
|
checker.target_version(),
|
||||||
|
&checker.settings().isort.required_imports,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (operation, fix) in fixer.without_renames() {
|
for (operation, fix) in fixer.without_renames() {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruff_python_ast::{Alias, Stmt};
|
|
||||||
|
|
||||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
|
use ruff_python_ast::{self as ast, Alias, Stmt, StmtRef};
|
||||||
|
use ruff_python_semantic::NameImport;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
|
@ -84,6 +87,29 @@ const PY37_PLUS_REMOVE_FUTURES: &[&str] = &[
|
||||||
"generator_stop",
|
"generator_stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub(crate) type RequiredImports = BTreeSet<NameImport>;
|
||||||
|
|
||||||
|
pub(crate) fn is_import_required_by_isort(
|
||||||
|
required_imports: &RequiredImports,
|
||||||
|
stmt: StmtRef,
|
||||||
|
alias: &Alias,
|
||||||
|
) -> bool {
|
||||||
|
let segments: &[&str] = match stmt {
|
||||||
|
StmtRef::ImportFrom(ast::StmtImportFrom {
|
||||||
|
module: Some(module),
|
||||||
|
..
|
||||||
|
}) => &[module.as_str(), alias.name.as_str()],
|
||||||
|
StmtRef::ImportFrom(ast::StmtImportFrom { module: None, .. }) | StmtRef::Import(_) => {
|
||||||
|
&[alias.name.as_str()]
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
required_imports
|
||||||
|
.iter()
|
||||||
|
.any(|required_import| required_import.qualified_name().segments() == segments)
|
||||||
|
}
|
||||||
|
|
||||||
/// UP010
|
/// UP010
|
||||||
pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: &[Alias]) {
|
pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: &[Alias]) {
|
||||||
let mut unused_imports: Vec<&Alias> = vec![];
|
let mut unused_imports: Vec<&Alias> = vec![];
|
||||||
|
@ -91,6 +117,15 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: &
|
||||||
if alias.asname.is_some() {
|
if alias.asname.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_import_required_by_isort(
|
||||||
|
&checker.settings().isort.required_imports,
|
||||||
|
stmt.into(),
|
||||||
|
alias,
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if PY33_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
|
if PY33_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
|
||||||
|| PY37_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
|
|| PY37_PLUS_REMOVE_FUTURES.contains(&alias.name.as_str())
|
||||||
{
|
{
|
||||||
|
@ -119,7 +154,7 @@ pub(crate) fn unnecessary_future_import(checker: &Checker, stmt: &Stmt, names: &
|
||||||
unused_imports
|
unused_imports
|
||||||
.iter()
|
.iter()
|
||||||
.map(|alias| &alias.name)
|
.map(|alias| &alias.name)
|
||||||
.map(ruff_python_ast::Identifier::as_str),
|
.map(ast::Identifier::as_str),
|
||||||
statement,
|
statement,
|
||||||
parent,
|
parent,
|
||||||
checker.locator(),
|
checker.locator(),
|
||||||
|
|
|
@ -380,7 +380,7 @@ macro_rules! assert_diagnostics {
|
||||||
}};
|
}};
|
||||||
($value:expr, @$snapshot:literal) => {{
|
($value:expr, @$snapshot:literal) => {{
|
||||||
insta::with_settings!({ omit_expression => true }, {
|
insta::with_settings!({ omit_expression => true }, {
|
||||||
insta::assert_snapshot!($crate::test::print_messages(&$value), $snapshot);
|
insta::assert_snapshot!($crate::test::print_messages(&$value), @$snapshot);
|
||||||
});
|
});
|
||||||
}};
|
}};
|
||||||
($name:expr, $value:expr) => {{
|
($name:expr, $value:expr) => {{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue