diff --git a/crates/ruff/resources/test/fixtures/ruff/ruff_noqa.py b/crates/ruff/resources/test/fixtures/ruff/ruff_noqa_all.py similarity index 100% rename from crates/ruff/resources/test/fixtures/ruff/ruff_noqa.py rename to crates/ruff/resources/test/fixtures/ruff/ruff_noqa_all.py diff --git a/crates/ruff/resources/test/fixtures/ruff/ruff_targeted_noqa.py b/crates/ruff/resources/test/fixtures/ruff/ruff_noqa_codes.py similarity index 100% rename from crates/ruff/resources/test/fixtures/ruff/ruff_targeted_noqa.py rename to crates/ruff/resources/test/fixtures/ruff/ruff_noqa_codes.py diff --git a/crates/ruff/resources/test/fixtures/ruff/ruff_noqa_invalid.py b/crates/ruff/resources/test/fixtures/ruff/ruff_noqa_invalid.py new file mode 100644 index 0000000000..cff06b194f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/ruff_noqa_invalid.py @@ -0,0 +1,5 @@ +import os # ruff: noqa: F401 + + +def f(): + x = 1 diff --git a/crates/ruff/src/noqa.rs b/crates/ruff/src/noqa.rs index e609ceb4a8..c2517699b0 100644 --- a/crates/ruff/src/noqa.rs +++ b/crates/ruff/src/noqa.rs @@ -12,6 +12,7 @@ use ruff_python_ast::Ranged; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_diagnostics::Diagnostic; +use ruff_python_trivia::indentation_at_offset; use ruff_source_file::{LineEnding, Locator}; use crate::codes::NoqaCode; @@ -248,22 +249,34 @@ impl FileExemption { let path_display = relativize_path(path); warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}"); } - Ok(Some(ParsedFileExemption::All)) => { - return Some(Self::All); - } - Ok(Some(ParsedFileExemption::Codes(codes))) => { - exempt_codes.extend(codes.into_iter().filter_map(|code| { - if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) - { - Some(rule.noqa_code()) - } else { - #[allow(deprecated)] - let line = locator.compute_line_index(range.start()); - let path_display = relativize_path(path); - warn!("Invalid code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); - None + Ok(Some(exemption)) => { + if indentation_at_offset(range.start(), locator).is_none() { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + let path_display = relativize_path(path); + warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line."); + continue; + } + + match exemption { + ParsedFileExemption::All => { + return Some(Self::All); } - })); + ParsedFileExemption::Codes(codes) => { + exempt_codes.extend(codes.into_iter().filter_map(|code| { + if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) + { + Some(rule.noqa_code()) + } else { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + let path_display = relativize_path(path); + warn!("Invalid rule code provided to `# ruff: noqa` at {path_display}:{line}: {code}"); + None + } + })); + } + } } Ok(None) => {} } diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index f85b8045ce..42350547ab 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -167,9 +167,9 @@ mod tests { } #[test] - fn ruff_noqa() -> Result<()> { + fn ruff_noqa_all() -> Result<()> { let diagnostics = test_path( - Path::new("ruff/ruff_noqa.py"), + Path::new("ruff/ruff_noqa_all.py"), &settings::Settings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; assert_messages!(diagnostics); @@ -177,9 +177,19 @@ mod tests { } #[test] - fn ruff_targeted_noqa() -> Result<()> { + fn ruff_noqa_codes() -> Result<()> { let diagnostics = test_path( - Path::new("ruff/ruff_targeted_noqa.py"), + Path::new("ruff/ruff_noqa_codes.py"), + &settings::Settings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn ruff_noqa_invalid() -> Result<()> { + let diagnostics = test_path( + Path::new("ruff/ruff_noqa_invalid.py"), &settings::Settings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]), )?; assert_messages!(diagnostics); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_all.snap similarity index 100% rename from crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa.snap rename to crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_all.snap diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_targeted_noqa.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_codes.snap similarity index 73% rename from crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_targeted_noqa.snap rename to crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_codes.snap index 52cb5f53af..5821686708 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_targeted_noqa.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_codes.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/ruff/mod.rs --- -ruff_targeted_noqa.py:8:5: F841 [*] Local variable `x` is assigned to but never used +ruff_noqa_codes.py:8:5: F841 [*] Local variable `x` is assigned to but never used | 7 | def f(): 8 | x = 1 diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_invalid.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_invalid.snap new file mode 100644 index 0000000000..73a4e55545 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__ruff_noqa_invalid.snap @@ -0,0 +1,32 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +ruff_noqa_invalid.py:1:8: F401 [*] `os` imported but unused + | +1 | import os # ruff: noqa: F401 + | ^^ F401 + | + = help: Remove unused import: `os` + +ℹ Fix +1 |-import os # ruff: noqa: F401 +2 1 | +3 2 | +4 3 | def f(): + +ruff_noqa_invalid.py:5:5: F841 [*] Local variable `x` is assigned to but never used + | +4 | def f(): +5 | x = 1 + | ^ F841 + | + = help: Remove assignment to unused variable `x` + +ℹ Suggested fix +2 2 | +3 3 | +4 4 | def f(): +5 |- x = 1 + 5 |+ pass + + diff --git a/crates/ruff_python_ast/src/whitespace.rs b/crates/ruff_python_ast/src/whitespace.rs index aceec2ec99..282076ee29 100644 --- a/crates/ruff_python_ast/src/whitespace.rs +++ b/crates/ruff_python_ast/src/whitespace.rs @@ -12,7 +12,7 @@ pub fn indentation<'a, T>(locator: &'a Locator, located: &T) -> Option<&'a str> where T: Ranged, { - indentation_at_offset(locator, located.start()) + indentation_at_offset(located.start(), locator) } /// Return the end offset at which the empty lines following a statement. diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 4d8488360c..a09a5b1e6d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -297,7 +297,7 @@ fn handle_own_line_comment_between_branches<'a>( // It depends on the indentation level of the comment if it is a leading comment for the // following branch or if it a trailing comment of the previous body's last statement. - let comment_indentation = indentation_at_offset(locator, comment.slice().range().start()) + let comment_indentation = indentation_at_offset(comment.slice().range().start(), locator) .unwrap_or_default() .len(); @@ -402,7 +402,7 @@ fn handle_match_comment<'a>( let next_case = match_stmt.cases.get(current_case_index + 1); - let comment_indentation = indentation_at_offset(locator, comment.slice().range().start()) + let comment_indentation = indentation_at_offset(comment.slice().range().start(), locator) .unwrap_or_default() .len(); let match_case_indentation = indentation(locator, match_case).unwrap().len(); @@ -480,7 +480,7 @@ fn handle_own_line_comment_after_branch<'a>( // We only care about the length because indentations with mixed spaces and tabs are only valid if // the indent-level doesn't depend on the tab width (the indent level must be the same if the tab width is 1 or 8). - let comment_indentation = indentation_at_offset(locator, comment.slice().range().start()) + let comment_indentation = indentation_at_offset(comment.slice().range().start(), locator) .unwrap_or_default() .len(); @@ -493,7 +493,7 @@ fn handle_own_line_comment_after_branch<'a>( // # Trailing if comment // ``` // Here we keep the comment a trailing comment of the `if` - let preceding_indentation = indentation_at_offset(locator, preceding_node.start()) + let preceding_indentation = indentation_at_offset(preceding_node.start(), locator) .unwrap_or_default() .len(); if comment_indentation == preceding_indentation { diff --git a/crates/ruff_python_trivia/src/whitespace.rs b/crates/ruff_python_trivia/src/whitespace.rs index 05c40e90b3..cc8f5563b7 100644 --- a/crates/ruff_python_trivia/src/whitespace.rs +++ b/crates/ruff_python_trivia/src/whitespace.rs @@ -2,7 +2,7 @@ use ruff_source_file::Locator; use ruff_text_size::{TextRange, TextSize}; /// Extract the leading indentation from a line. -pub fn indentation_at_offset<'a>(locator: &'a Locator, offset: TextSize) -> Option<&'a str> { +pub fn indentation_at_offset<'a>(offset: TextSize, locator: &'a Locator) -> Option<&'a str> { let line_start = locator.line_start(offset); let indentation = &locator.contents()[TextRange::new(line_start, offset)];