Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't raise D208 when last line is non-empty #13372

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pydocstyle/D208.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,36 @@ def memory_test():
"""
   参数含义:precision:精确到小数点后几位
"""



class Platform:
"""Over indented last line
Args:
Returns:
"""


class Platform:
"""All lines are over indented including the last containing the closing quotes
Args:
Returns:
"""

class Platform:
"""All lines are over indented including the last
Args:
Returns"""

# OK: This doesn't get flagged because it is accepted when the closing quotes are on a separate line (see next test). Raises D209
class Platform:
"""Over indented last line with content
Args:
Some content on the last line"""

# OK:
class Platform:
"""Over indented last line with content
Args:
Some content on the last line
"""
39 changes: 39 additions & 0 deletions crates/ruff_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,42 @@ pub mod upstream_categories;
pub mod test;

pub const RUFF_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

#[cfg(test)]
mod tests {
use std::path::Path;

use ruff_python_ast::PySourceType;

use crate::codes::Rule;
use crate::settings::LinterSettings;
use crate::source_kind::SourceKind;
use crate::test::{print_messages, test_contents};

/// Test for ad-hoc debugging.
#[test]
#[ignore]
fn linter_quick_test() {
let code = r#"class Platform:
""" Remove sampler
Args:
    Returns:
"""
"#;
let source_type = PySourceType::Python;
let rule = Rule::OverIndentation;

let source_kind = SourceKind::from_source_code(code.to_string(), source_type)
.expect("Source code should be valid")
.expect("Notebook to contain python code");

let (diagnostics, fixed) = test_contents(
&source_kind,
Path::new("ruff_linter/rules/quick_test"),
&LinterSettings::for_rule(rule),
);

assert_eq!(print_messages(&diagnostics), "");
assert_eq!(fixed.source_code(), code);
}
}
88 changes: 60 additions & 28 deletions crates/ruff_linter/src/rules/pydocstyle/rules/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::docstrings::{clean_space, leading_space};
use ruff_source_file::NewlineWithTrailingNewline;
use ruff_source_file::{Line, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::{TextLen, TextRange};

Expand Down Expand Up @@ -169,32 +169,49 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
let body = docstring.body();

// Split the docstring into lines.
let lines: Vec<_> = NewlineWithTrailingNewline::with_offset(&body, body.start()).collect();
if lines.len() <= 1 {
let mut lines = NewlineWithTrailingNewline::with_offset(&body, body.start()).peekable();

// The current line being processed
let mut current: Option<Line> = lines.next();

if lines.peek().is_none() {
return;
}

let mut has_seen_tab = docstring.indentation.contains('\t');
let mut is_over_indented = true;
let docstring_indent_size = docstring.indentation.chars().count();

// Lines, other than the last, that are over indented.
let mut over_indented_lines = vec![];
let mut over_indented_size = usize::MAX;
// The smallest over indent that all docstring lines have in common. None if any line isn't over indented.
let mut smallest_over_indent_size = Some(usize::MAX);
// The last processed line
let mut last = None;

let docstring_indent_size = docstring.indentation.chars().count();
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
while let Some(line) = current {
// First lines and continuations don't need any indentation.
if last.is_none()
|| last
.as_deref()
.is_some_and(|last: &str| last.ends_with('\\'))
{
last = Some(line);
current = lines.next();
continue;
}

let line = &lines[i];
let is_last = lines.peek().is_none();

// Omit empty lines, except for the last line, which is non-empty by way of
// containing the closing quotation marks.
let is_blank = line.trim().is_empty();
if i < lines.len() - 1 && is_blank {
if !is_last && is_blank {
last = Some(line);
current = lines.next();
continue;
}

let line_indent = leading_space(line);
let line_indent = leading_space(&line);
let line_indent_size = line_indent.chars().count();

// We only report tab indentation once, so only check if we haven't seen a tab
Expand All @@ -204,7 +221,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
if checker.enabled(Rule::UnderIndentation) {
// We report under-indentation on every line. This isn't great, but enables
// fix.
if (i == lines.len() - 1 || !is_blank) && line_indent_size < docstring_indent_size {
if (is_last || !is_blank) && line_indent_size < docstring_indent_size {
let mut diagnostic =
Diagnostic::new(UnderIndentation, TextRange::empty(line.start()));
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
Expand All @@ -215,23 +232,35 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
}
}

// Only true when the last line is indentation only followed by the closing quotes.
// False when it is not the last line or the last line contains content other than the closing quotes.
// The last line only has special handling when it contains no other content.
let is_last_closing_quotes_only = is_last && is_blank;

// Like pydocstyle, we only report over-indentation if either: (1) every line
// (except, optionally, the last line) is over-indented, or (2) the last line
// (which contains the closing quotation marks) is
// over-indented. We can't know if we've achieved that condition
// until we've viewed all the lines, so for now, just track
// the over-indentation status of every line.
if i < lines.len() - 1 {
if line_indent_size > docstring_indent_size {
over_indented_lines.push(line);
if !is_last_closing_quotes_only {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main fix where we only exclude the last line if it is all empty

smallest_over_indent_size =
smallest_over_indent_size.and_then(|smallest_over_indent_size| {
let over_indent_size = line_indent_size.saturating_sub(docstring_indent_size);

// Track the _smallest_ offset we see, in terms of characters.
over_indented_size =
std::cmp::min(line_indent_size - docstring_indent_size, over_indented_size);
} else {
is_over_indented = false;
}
// `docstring_indent_size < line_indent_size`
if over_indent_size > 0 {
over_indented_lines.push(line.clone());
// Track the _smallest_ offset we see, in terms of characters.
Some(smallest_over_indent_size.min(over_indent_size))
} else {
None
}
});
}

last = Some(line);
current = lines.next();
}

if checker.enabled(Rule::IndentWithSpaces) {
Expand All @@ -244,9 +273,9 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {

if checker.enabled(Rule::OverIndentation) {
// If every line (except the last) is over-indented...
if is_over_indented {
if let Some(smallest_over_indent_size) = smallest_over_indent_size {
for line in over_indented_lines {
let line_indent = leading_space(line);
let line_indent = leading_space(&line);
let indent = clean_space(docstring.indentation);

// We report over-indentation on every line. This isn't great, but
Expand Down Expand Up @@ -275,7 +304,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
.locator()
.after(line.start())
.chars()
.take(docstring.indentation.chars().count() + over_indented_size)
.take(docstring_indent_size + smallest_over_indent_size)
.map(TextLen::text_len)
.sum::<TextSize>();
let range = TextRange::at(line.start(), offset);
Expand All @@ -287,10 +316,13 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
}

// If the last line is over-indented...
if let Some(last) = lines.last() {
let line_indent = leading_space(last);
if let Some(last) = last {
let line_indent = leading_space(&last);
let line_indent_size = line_indent.chars().count();
if line_indent_size > docstring_indent_size {
let last_line_over_indent = line_indent_size.saturating_sub(docstring_indent_size);

let is_indent_only = line_indent.len() == last.len();
if last_line_over_indent > 0 && is_indent_only {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires the same logic. Only apply the last line logic when it is the last line and it only contains the quotes

let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(last.start()));
let indent = clean_space(docstring.indentation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,124 @@ D208.py:10:1: D208 [*] Docstring is over-indented
12 12 |
13 13 | def memory_test():

D208.py:24:1: D208 [*] Docstring is over-indented
|
22 | Args:
23 | Returns:
24 | """
| D208
|
= help: Remove over-indentation

ℹ Safe fix
21 21 | """Over indented last line
22 22 | Args:
23 23 | Returns:
24 |- """
24 |+ """
25 25 |
26 26 |
27 27 | class Platform:

D208.py:29:1: D208 [*] Docstring is over-indented
|
27 | class Platform:
28 | """All lines are over indented including the last containing the closing quotes
29 | Args:
| D208
30 | Returns:
31 | """
|
= help: Remove over-indentation

ℹ Safe fix
26 26 |
27 27 | class Platform:
28 28 | """All lines are over indented including the last containing the closing quotes
29 |- Args:
29 |+ Args:
30 30 | Returns:
31 31 | """
32 32 |

D208.py:30:1: D208 [*] Docstring is over-indented
|
28 | """All lines are over indented including the last containing the closing quotes
29 | Args:
30 | Returns:
| D208
31 | """
|
= help: Remove over-indentation

ℹ Safe fix
27 27 | class Platform:
28 28 | """All lines are over indented including the last containing the closing quotes
29 29 | Args:
30 |- Returns:
30 |+ Returns:
31 31 | """
32 32 |
33 33 | class Platform:

D208.py:31:1: D208 [*] Docstring is over-indented
|
29 | Args:
30 | Returns:
31 | """
| D208
32 |
33 | class Platform:
|
= help: Remove over-indentation

ℹ Safe fix
28 28 | """All lines are over indented including the last containing the closing quotes
29 29 | Args:
30 30 | Returns:
31 |- """
31 |+ """
32 32 |
33 33 | class Platform:
34 34 | """All lines are over indented including the last

D208.py:35:1: D208 [*] Docstring is over-indented
|
33 | class Platform:
34 | """All lines are over indented including the last
35 | Args:
| D208
36 | Returns"""
|
= help: Remove over-indentation

ℹ Safe fix
32 32 |
33 33 | class Platform:
34 34 | """All lines are over indented including the last
35 |- Args:
35 |+ Args:
36 36 | Returns"""
37 37 |
38 38 | # OK: This doesn't get flagged because it is accepted when the closing quotes are on a separate line (see next test). Raises D209

D208.py:36:1: D208 [*] Docstring is over-indented
|
34 | """All lines are over indented including the last
35 | Args:
36 | Returns"""
| D208
37 |
38 | # OK: This doesn't get flagged because it is accepted when the closing quotes are on a separate line (see next test). Raises D209
|
= help: Remove over-indentation

ℹ Safe fix
33 33 | class Platform:
34 34 | """All lines are over indented including the last
35 35 | Args:
36 |- Returns"""
36 |+ Returns"""
37 37 |
38 38 | # OK: This doesn't get flagged because it is accepted when the closing quotes are on a separate line (see next test). Raises D209
39 39 | class Platform:
Loading