Avoid infinite-propagation of inline comments when force-splitting imports (#4074)

This commit is contained in:
Charlie Marsh 2023-04-23 22:39:51 -06:00 committed by GitHub
parent d64146683e
commit 407af6e0ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 105 deletions

View file

@ -0,0 +1,4 @@
from mypackage.subpackage import ( # long comment that seems to be a problem
a_long_variable_name_that_causes_problems,
items,
)

View file

@ -3,8 +3,6 @@
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use itertools::Either::{Left, Right};
use annotate::annotate_imports;
use categorize::categorize_imports;
pub use categorize::{categorize, ImportSection, ImportType};
@ -16,7 +14,7 @@ use settings::RelativeImportsOrder;
use sorting::cmp_either_import;
use track::{Block, Trailer};
use types::EitherImport::{Import, ImportFrom};
use types::{AliasData, CommentSet, EitherImport, OrderedImportBlock, TrailingComma};
use types::{AliasData, EitherImport, TrailingComma};
use crate::rules::isort::categorize::KnownModules;
use crate::rules::isort::types::ImportBlock;
@ -61,53 +59,6 @@ pub enum AnnotatedImport<'a> {
},
}
fn force_single_line_imports<'a>(
block: OrderedImportBlock<'a>,
single_line_exclusions: &BTreeSet<String>,
) -> OrderedImportBlock<'a> {
OrderedImportBlock {
import: block.import,
import_from: block
.import_from
.into_iter()
.flat_map(|(from_data, comment_set, trailing_comma, alias_data)| {
if from_data
.module
.map_or(false, |module| single_line_exclusions.contains(module))
{
Left(std::iter::once((
from_data,
comment_set,
trailing_comma,
alias_data,
)))
} else {
Right(
alias_data
.into_iter()
.enumerate()
.map(move |(index, alias_data)| {
(
from_data.clone(),
if index == 0 {
comment_set.clone()
} else {
CommentSet {
atop: vec![],
inline: comment_set.inline.clone(),
}
},
TrailingComma::Absent,
vec![alias_data],
)
}),
)
}
})
.collect(),
}
}
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub fn format_imports(
block: &Block,
@ -141,7 +92,12 @@ pub fn format_imports(
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(block, combine_as_imports, force_single_line);
let block = normalize_imports(
block,
combine_as_imports,
force_single_line,
single_line_exclusions,
);
// Categorize imports.
let mut output = String::new();
@ -153,14 +109,12 @@ pub fn format_imports(
stylist,
src,
package,
force_single_line,
force_sort_within_sections,
force_wrap_aliases,
force_to_top,
known_modules,
order_by_type,
relative_imports_order,
single_line_exclusions,
split_on_trailing_comma,
classes,
constants,
@ -211,14 +165,12 @@ fn format_import_block(
stylist: &Stylist,
src: &[PathBuf],
package: Option<&Path>,
force_single_line: bool,
force_sort_within_sections: bool,
force_wrap_aliases: bool,
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
order_by_type: bool,
relative_imports_order: RelativeImportsOrder,
single_line_exclusions: &BTreeSet<String>,
split_on_trailing_comma: bool,
classes: &BTreeSet<String>,
constants: &BTreeSet<String>,
@ -246,7 +198,7 @@ fn format_import_block(
continue;
};
let mut imports = order_imports(
let imports = order_imports(
import_block,
order_by_type,
relative_imports_order,
@ -256,10 +208,6 @@ fn format_import_block(
force_to_top,
);
if force_single_line {
imports = force_single_line_imports(imports, single_line_exclusions);
}
let imports = {
let mut imports = imports
.import
@ -594,6 +542,24 @@ mod tests {
Ok(())
}
#[test_case(Path::new("propagate_inline_comments.py"))]
fn propagate_inline_comments(path: &Path) -> Result<()> {
let snapshot = format!("propagate_inline_comments_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
isort: super::settings::Settings {
force_single_line: true,
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("order_by_type.py"))]
fn order_by_type(path: &Path) -> Result<()> {
let snapshot = format!("order_by_type_false_{}", path.to_string_lossy());

View file

@ -1,13 +1,15 @@
use crate::rules::isort::types::TrailingComma;
use std::collections::BTreeSet;
use super::types::{AliasData, ImportBlock, ImportFromData};
use super::AnnotatedImport;
pub fn normalize_imports(
imports: Vec<AnnotatedImport>,
pub fn normalize_imports<'a>(
imports: Vec<AnnotatedImport<'a>>,
combine_as_imports: bool,
force_single_line: bool,
) -> ImportBlock {
single_line_exclusions: &'a BTreeSet<String>,
) -> ImportBlock<'a> {
let mut block = ImportBlock::default();
for import in imports {
match import {
@ -52,14 +54,46 @@ pub fn normalize_imports(
inline,
trailing_comma,
} => {
// Whether to track each member of the import as a separate entry.
let isolate_aliases = force_single_line
&& module.map_or(true, |module| !single_line_exclusions.contains(module));
// Insert comments on the statement itself.
if isolate_aliases {
let mut first = true;
for alias in &names {
let import_from = block
.import_from_as
.entry((
ImportFromData { module, level },
AliasData {
name: alias.name,
asname: alias.asname,
},
))
.or_default();
// Associate the comments above the import statement with the first alias
// (best effort).
if std::mem::take(&mut first) {
for comment in &atop {
import_from.comments.atop.push(comment.value.clone());
}
}
// Replicate the inline comments onto every member.
for comment in &inline {
import_from.comments.inline.push(comment.value.clone());
}
}
} else {
if let Some(alias) = names.first() {
let import_from = if alias.name == "*" {
block
.import_from_star
.entry(ImportFromData { module, level })
.or_default()
} else if alias.asname.is_none() || combine_as_imports || force_single_line {
} else if alias.asname.is_none() || combine_as_imports {
block
.import_from
.entry(ImportFromData { module, level })
@ -85,15 +119,16 @@ pub fn normalize_imports(
import_from.comments.inline.push(comment.value);
}
}
}
// Create an entry for every alias (import) within the statement.
// Create an entry for every alias (member) within the statement.
for alias in names {
let import_from = if alias.name == "*" {
block
.import_from_star
.entry(ImportFromData { module, level })
.or_default()
} else if alias.asname.is_none() || combine_as_imports || force_single_line {
} else if !isolate_aliases && (alias.asname.is_none() || combine_as_imports) {
block
.import_from
.entry(ImportFromData { module, level })
@ -127,7 +162,7 @@ pub fn normalize_imports(
}
// Propagate trailing commas.
if matches!(trailing_comma, TrailingComma::Present) {
if !isolate_aliases && matches!(trailing_comma, TrailingComma::Present) {
import_from.trailing_comma = TrailingComma::Present;
}
}

View file

@ -41,37 +41,38 @@ force_single_line.py:1:1: I001 [*] Import block is un-sorted or un-formatted
6 6 | from json import load
7 7 | from json import loads as json_loads
8 |-from logging.handlers import StreamHandler, FileHandler
9 |-
10 |-# comment 1
11 |-from third_party import lib1, lib2, \
12 |- lib3, lib7, lib5, lib6
13 |-# comment 2
14 |-from third_party import lib4
8 |+from logging.handlers import FileHandler, StreamHandler
9 |+from os import path, uname
15 10 |
9 10 |
11 |+# comment 6
12 |+from bar import a # comment 7
13 |+from bar import b # comment 8
16 14 | from foo import bar # comment 3
17 15 | from foo2 import bar2 # comment 4
18 |-from foo3 import bar3, baz3 # comment 5
14 |+from foo import bar # comment 3
15 |+from foo2 import bar2 # comment 4
16 |+from foo3 import bar3 # comment 5
17 |+from foo3 import baz3 # comment 5
19 18 |
18 |+
10 19 | # comment 1
11 |-from third_party import lib1, lib2, \
12 |- lib3, lib7, lib5, lib6
20 |+from third_party import lib1
21 |+from third_party import lib2
22 |+from third_party import lib3
23 |+
13 24 | # comment 2
14 25 | from third_party import lib4
15 |-
16 |-from foo import bar # comment 3
17 |-from foo2 import bar2 # comment 4
18 |-from foo3 import bar3, baz3 # comment 5
19 |-
20 |-# comment 6
21 |-from bar import (
22 |- a, # comment 7
23 |- b, # comment 8
24 |-)
19 |+# comment 1
20 |+# comment 2
21 |+from third_party import lib1
22 |+from third_party import lib2
23 |+from third_party import lib3
24 |+from third_party import lib4
25 |+from third_party import lib5
26 |+from third_party import lib6
27 |+from third_party import lib7
26 |+from third_party import lib5
27 |+from third_party import lib6
28 |+from third_party import lib7

View file

@ -0,0 +1,20 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
propagate_inline_comments.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from mypackage.subpackage import ( # long comment that seems to be a problem
2 | | a_long_variable_name_that_causes_problems,
3 | | items,
4 | | )
|
= help: Organize imports
Suggested fix
1 1 | from mypackage.subpackage import ( # long comment that seems to be a problem
2 2 | a_long_variable_name_that_causes_problems,
3 |- items,
4 3 | )
4 |+from mypackage.subpackage import items # long comment that seems to be a problem