Infer indentation with imports when logical indent is absent (#11608)

## Summary

In an `__init__.py` file, it's not uncommon to lack a logical indent
(since it may just contain imports). In such cases, we were always
falling back to four-space indent. This PR adds detection for indents
within import groups.

Closes https://github.com/astral-sh/ruff/issues/11606.
This commit is contained in:
Charlie Marsh 2024-05-30 00:18:07 -04:00 committed by GitHub
parent a8d1328c1a
commit bd46cd1fcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 571 additions and 498 deletions

View file

@ -0,0 +1,10 @@
# If the file doesn't contain a logical indent token, we should still detect two-space indentation on imports.
from math import (
sin,
tan,
cos,
nan,
pi,
)
del sin, cos, tan, pi, nan

View file

@ -29,6 +29,11 @@ print("%#o" % (123,))
print("brace {} %s" % (1,))
print((
"foo %s "
"bar %s" % (x, y)
))
print(
"%s" % (
"trailing comma",
@ -52,10 +57,6 @@ print("%(ab)s" % {"a" "b": 1})
print("%(a)s" % {"a" : 1})
print((
"foo %s "
"bar %s" % (x, y)
))
print(
"foo %(foo)s "

View file

@ -341,6 +341,7 @@ mod tests {
#[test_case(Path::new("split.py"))]
#[test_case(Path::new("star_before_others.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
#[test_case(Path::new("two_space.py"))]
#[test_case(Path::new("type_comments.py"))]
#[test_case(Path::new("unicode.py"))]
fn default(path: &Path) -> Result<()> {

View file

@ -0,0 +1,32 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
two_space.py:2:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | # If the file doesn't contain a logical indent token, we should still detect two-space indentation on imports.
2 | / from math import (
3 | | sin,
4 | | tan,
5 | | cos,
6 | | nan,
7 | | pi,
8 | | )
9 | |
10 | | del sin, cos, tan, pi, nan
| |_^ I001
|
= help: Organize imports
Safe fix
1 1 | # If the file doesn't contain a logical indent token, we should still detect two-space indentation on imports.
2 2 | from math import (
3 |- sin,
4 |- tan,
5 3 | cos,
6 4 | nan,
7 5 | pi,
6 |+ sin,
7 |+ tan,
8 8 | )
9 9 |
10 10 | del sin, cos, tan, pi, nan

View file

@ -6,7 +6,7 @@ use once_cell::unsync::OnceCell;
use ruff_python_ast::{str::Quote, StringFlags};
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_parser::{Tok, TokenKind};
use ruff_source_file::{find_newline, LineEnding, Locator};
#[derive(Debug, Clone)]
@ -86,6 +86,38 @@ fn detect_indention(tokens: &[LexResult], locator: &Locator) -> Indentation {
Indentation(whitespace.to_string())
} else {
// If we can't find a logical indent token, search for a non-logical indent, to cover cases
// like:
//```python
// from math import (
// sin,
// tan,
// cos,
// )
// ```
let mut depth = 0usize;
for (token, range) in tokens.iter().flatten() {
match token.kind() {
TokenKind::Lpar | TokenKind::Lbrace | TokenKind::Lsqb => {
depth = depth.saturating_add(1);
}
TokenKind::Rpar | TokenKind::Rbrace | TokenKind::Rsqb => {
depth = depth.saturating_sub(1);
}
TokenKind::NonLogicalNewline => {
let line = locator.line(range.end());
let indent_index = line.chars().position(|c| !c.is_whitespace());
if let Some(indent_index) = indent_index {
if indent_index > 0 {
let whitespace = &line[..indent_index];
return Indentation(whitespace.to_string());
}
}
}
_ => {}
}
}
Indentation::default()
}
}
@ -177,7 +209,6 @@ if True:
&Indentation("\t".to_string())
);
// TODO(charlie): Should non-significant whitespace be detected?
let contents = r"
x = (
1,
@ -189,7 +220,7 @@ x = (
let tokens: Vec<_> = lex(contents, Mode::Module).collect();
assert_eq!(
Stylist::from_tokens(&tokens, &locator).indentation(),
&Indentation::default()
&Indentation(" ".to_string())
);
// formfeed indent, see `detect_indention` comment.