Skip to content

Commit 9d2b160

Browse files
E501/W505/formatter: Exclude nested pragma comments from line width calculation (#24071)
Closes #18470 ## Summary Exclude nested pragma comments like the `# noqa` in `test # comment 1 # noqa` from `E501`, `W505` and the formatter's line width calculation. ## Test Plan - Added linter test fixture `E501_5.py` with preview test cases for nested pragmas (`# comment #noqa`, `## noqa`, `# comment # type: ignore`) - Added formatter test fixture `trailing_pragma_nested.py` demonstrating stable vs preview reserved width behavior - Doctests for `find_trailing_pragma_offset` in `pragmas.rs` --------- Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent d8a0f0a commit 9d2b160

12 files changed

Lines changed: 215 additions & 11 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# OK - trailing noqa after another comment (88 characters before noqa pragma)
2+
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa" # comment # noqa: F401
3+
4+
# Error - trailing noqa after another comment (89 characters before noqa pragma)
5+
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # comment # noqa: F401
6+
7+
# OK - double hash before noqa (80 characters before noqa pragma)
8+
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa" ## noqa: F401
9+
10+
# OK - trailing type: ignore after another comment (88 characters before pragma)
11+
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaa" # comment # type: ignore
12+
13+
# Error - trailing type: ignore after another comment (89 characters before pragma)
14+
"shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # comment # type: ignore

crates/ruff_linter/src/preview.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,9 @@ pub(crate) const fn is_up006_future_annotations_fix_enabled(settings: &LinterSet
324324
pub const fn is_warning_severity_enabled(preview: PreviewMode) -> bool {
325325
preview.is_enabled()
326326
}
327+
328+
/// <https://github.com/astral-sh/ruff/pull/24071>
329+
/// Make sure to stabilize the corresponding formatter preview behavior when stabilizing this preview style.
330+
pub(crate) const fn is_trailing_pragma_in_line_length_enabled(preview: PreviewMode) -> bool {
331+
preview.is_enabled()
332+
}

crates/ruff_linter/src/rules/pycodestyle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ mod tests {
7575
Ok(())
7676
}
7777

78+
#[test_case(Rule::LineTooLong, Path::new("E501_5.py"))]
7879
#[test_case(Rule::RedundantBackslash, Path::new("E502.py"))]
7980
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))]
8081
#[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))]

crates/ruff_linter/src/rules/pycodestyle/overlong.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::ops::Deref;
22

3-
use ruff_python_trivia::{CommentRanges, is_pragma_comment};
3+
use ruff_python_trivia::{CommentRanges, find_trailing_pragma_offset, is_pragma_comment};
44
use ruff_source_file::Line;
55
use ruff_text_size::{TextLen, TextRange};
66

77
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
8+
use crate::preview::is_trailing_pragma_in_line_length_enabled;
9+
use crate::settings::types::PreviewMode;
810

911
#[derive(Debug)]
1012
pub(super) struct Overlong {
@@ -21,6 +23,7 @@ impl Overlong {
2123
limit: LineLength,
2224
task_tags: &[String],
2325
tab_size: IndentWidth,
26+
preview: PreviewMode,
2427
) -> Option<Self> {
2528
// The maximum width of the line is the number of bytes multiplied by the tab size (the
2629
// worst-case scenario is that the line is all tabs). If the maximum width is less than the
@@ -37,7 +40,7 @@ impl Overlong {
3740
}
3841

3942
// Strip trailing comments and re-measure the line, if needed.
40-
let line = StrippedLine::from_line(line, comment_ranges, task_tags);
43+
let line = StrippedLine::from_line(line, comment_ranges, task_tags, preview);
4144
let width = match &line {
4245
StrippedLine::WithoutPragma(line) => {
4346
let width = measure(line.as_str(), tab_size);
@@ -116,7 +119,12 @@ enum StrippedLine<'a> {
116119
impl<'a> StrippedLine<'a> {
117120
/// Strip trailing comments from a [`Line`], if the line ends with a pragma comment (like
118121
/// `# type: ignore`) or, if necessary, a task comment (like `# TODO`).
119-
fn from_line(line: &'a Line<'a>, comment_ranges: &CommentRanges, task_tags: &[String]) -> Self {
122+
fn from_line(
123+
line: &'a Line<'a>,
124+
comment_ranges: &CommentRanges,
125+
task_tags: &[String],
126+
preview: PreviewMode,
127+
) -> Self {
120128
let [comment_range] = comment_ranges.comments_in_range(line.range()) else {
121129
return Self::Unchanged(line);
122130
};
@@ -125,9 +133,18 @@ impl<'a> StrippedLine<'a> {
125133
let comment_range = comment_range - line.start();
126134
let comment = &line.as_str()[comment_range];
127135

128-
// Ex) `# type: ignore`
129-
if is_pragma_comment(comment) {
130-
// Remove the pragma from the line.
136+
// Ex) `# type: ignore` or (in preview) `# some comment # noqa: F401`
137+
if is_trailing_pragma_in_line_length_enabled(preview) {
138+
if let Some(offset) = find_trailing_pragma_offset(comment) {
139+
// Strip only the pragma suffix from the comment, preserving any
140+
// preceding non-pragma comment text.
141+
let pragma_start = usize::from(comment_range.start()) + offset;
142+
let prefix = line[..pragma_start].trim_end();
143+
return Self::WithoutPragma(Line::new(prefix, line.start()));
144+
}
145+
}
146+
// Stable behavior: only strip when the entire comment is a pragma.
147+
else if is_pragma_comment(comment) {
131148
let prefix = &line.as_str()[..usize::from(comment_range.start())].trim_end();
132149
return Self::WithoutPragma(Line::new(prefix, line.start()));
133150
}

crates/ruff_linter/src/rules/pycodestyle/rules/doc_line_too_long.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ pub(crate) fn doc_line_too_long(
104104
&[]
105105
},
106106
settings.tab_size,
107+
settings.preview,
107108
) {
108109
context.report_diagnostic(
109110
DocLineTooLong(overlong.width(), limit.value() as usize),

crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub(crate) fn line_too_long(
100100
&[]
101101
},
102102
settings.tab_size,
103+
settings.preview,
103104
) {
104105
context.report_diagnostic(
105106
LineTooLong(overlong.width(), limit.value() as usize),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3+
---
4+
E501 Line too long (89 > 88)
5+
--> E501_5.py:5:89
6+
|
7+
4 | # Error - trailing noqa after another comment (89 characters before noqa pragma)
8+
5 | "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # comment # noqa: F401
9+
| ^
10+
6 |
11+
7 | # OK - double hash before noqa (80 characters before noqa pragma)
12+
|
13+
14+
E501 Line too long (89 > 88)
15+
--> E501_5.py:14:89
16+
|
17+
13 | # Error - trailing type: ignore after another comment (89 characters before pragma)
18+
14 | "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:" + "shape:aaaa" # comment # type: ignore
19+
| ^
20+
|
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Trailing pragma after another comment - the pragma portion should not count
2+
# toward reserved width in preview mode.
3+
4+
# The expression is long enough that the formatter would break it if the full
5+
# comment width were reserved, but short enough to fit if only the non-pragma
6+
# prefix is reserved.
7+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # noqa: F401
8+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # type: ignore
9+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # pyright: ignore
10+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) ## noqa: F401
11+
12+
# Plain pragma (no nested comment) - should behave the same in stable and preview
13+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: F401
14+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: ignore
15+
16+
# Not a pragma - should reserve full width in both modes
17+
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # not a pragma

crates/ruff_python_formatter/src/comments/format.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ use std::borrow::Cow;
33
use ruff_formatter::{FormatError, FormatOptions, SourceCode, format_args, write};
44
use ruff_python_ast::{AnyNodeRef, NodeKind, PySourceType};
55
use ruff_python_trivia::{
6-
CommentLinePosition, is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before,
6+
CommentLinePosition, find_trailing_pragma_offset, is_pragma_comment, lines_after,
7+
lines_after_ignoring_trivia, lines_before,
78
};
89
use ruff_text_size::{Ranged, TextLen, TextRange};
910

1011
use crate::comments::SourceComment;
1112
use crate::context::NodeLevel;
1213
use crate::prelude::*;
14+
use crate::preview::is_trailing_pragma_in_comment_width_enabled;
1315
use crate::statement::suite::should_insert_blank_line_after_class_in_stub_file;
1416

1517
/// Formats the leading comments of a node.
@@ -376,14 +378,26 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
376378

377379
let normalized_comment = normalize_comment(self.comment, source)?;
378380

379-
// Don't reserve width for excluded pragma comments.
380-
let reserved_width = if is_pragma_comment(&normalized_comment) {
381+
// Don't reserve width for pragma comments. In preview, comments
382+
// containing a trailing pragma (e.g., `# comment # noqa: F401`) only
383+
// reserve width for the non-pragma prefix.
384+
let non_pragma_comment_part = if is_trailing_pragma_in_comment_width_enabled(f.context()) {
385+
match find_trailing_pragma_offset(&normalized_comment) {
386+
Some(offset) => normalized_comment[..offset].trim_end(),
387+
None => &normalized_comment,
388+
}
389+
} else if is_pragma_comment(&normalized_comment) {
390+
""
391+
} else {
392+
&normalized_comment
393+
};
394+
395+
let reserved_width = if non_pragma_comment_part.is_empty() {
381396
0
382397
} else {
383398
// Start with 2 because of the two leading spaces.
384-
385399
2u32.saturating_add(
386-
TextWidth::from_text(&normalized_comment, f.options().indent_width())
400+
TextWidth::from_text(non_pragma_comment_part, f.options().indent_width())
387401
.width()
388402
.expect("Expected comment not to contain any newlines")
389403
.value(),

crates/ruff_python_formatter/src/preview.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
2020
pub(crate) const fn is_fluent_layout_split_first_call_enabled(context: &PyFormatContext) -> bool {
2121
context.is_preview()
2222
}
23+
24+
/// Returns `true` if the
25+
/// [trailing pragma width handling in comments](https://github.com/astral-sh/ruff/pull/24071)
26+
/// is enabled.
27+
/// When enabled, comments like `# text # noqa: F401` only reserve width for
28+
/// the non-pragma prefix (`# text`), not the trailing pragma.
29+
/// Make sure to stabilize the corresponding linter preview behavior when stabilizing this preview style.
30+
pub(crate) const fn is_trailing_pragma_in_comment_width_enabled(context: &PyFormatContext) -> bool {
31+
context.is_preview()
32+
}

0 commit comments

Comments
 (0)