mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:12:22 +00:00
Ignore end-of-line file exemption comments (#6160)
## Summary This PR protects against code like: ```python from typing import Optional import bar # ruff: noqa import baz class Foo: x: Optional[str] = None ``` In which the user wrote `# ruff: noqa` to ignore a specific error, not realizing that it was a file-level exemption that thus turned off all lint rules. Specifically, if a `# ruff: noqa` directive is not at the start of a line, we now ignore it and warn, since this is almost certainly a mistake.
This commit is contained in:
parent
e0d5c7564f
commit
646ff6497c
11 changed files with 86 additions and 26 deletions
5
crates/ruff/resources/test/fixtures/ruff/ruff_noqa_invalid.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/ruff/ruff_noqa_invalid.py
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import os # ruff: noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x = 1
|
|
@ -12,6 +12,7 @@ use ruff_python_ast::Ranged;
|
||||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
use ruff_diagnostics::Diagnostic;
|
use ruff_diagnostics::Diagnostic;
|
||||||
|
use ruff_python_trivia::indentation_at_offset;
|
||||||
use ruff_source_file::{LineEnding, Locator};
|
use ruff_source_file::{LineEnding, Locator};
|
||||||
|
|
||||||
use crate::codes::NoqaCode;
|
use crate::codes::NoqaCode;
|
||||||
|
@ -248,22 +249,34 @@ impl FileExemption {
|
||||||
let path_display = relativize_path(path);
|
let path_display = relativize_path(path);
|
||||||
warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}");
|
warn!("Invalid `# ruff: noqa` directive at {path_display}:{line}: {err}");
|
||||||
}
|
}
|
||||||
Ok(Some(ParsedFileExemption::All)) => {
|
Ok(Some(exemption)) => {
|
||||||
return Some(Self::All);
|
if indentation_at_offset(range.start(), locator).is_none() {
|
||||||
}
|
#[allow(deprecated)]
|
||||||
Ok(Some(ParsedFileExemption::Codes(codes))) => {
|
let line = locator.compute_line_index(range.start());
|
||||||
exempt_codes.extend(codes.into_iter().filter_map(|code| {
|
let path_display = relativize_path(path);
|
||||||
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code))
|
warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line.");
|
||||||
{
|
continue;
|
||||||
Some(rule.noqa_code())
|
}
|
||||||
} else {
|
|
||||||
#[allow(deprecated)]
|
match exemption {
|
||||||
let line = locator.compute_line_index(range.start());
|
ParsedFileExemption::All => {
|
||||||
let path_display = relativize_path(path);
|
return Some(Self::All);
|
||||||
warn!("Invalid code provided to `# ruff: noqa` at {path_display}:{line}: {code}");
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}));
|
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) => {}
|
Ok(None) => {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,9 +167,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ruff_noqa() -> Result<()> {
|
fn ruff_noqa_all() -> Result<()> {
|
||||||
let diagnostics = test_path(
|
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]),
|
&settings::Settings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]),
|
||||||
)?;
|
)?;
|
||||||
assert_messages!(diagnostics);
|
assert_messages!(diagnostics);
|
||||||
|
@ -177,9 +177,19 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ruff_targeted_noqa() -> Result<()> {
|
fn ruff_noqa_codes() -> Result<()> {
|
||||||
let diagnostics = test_path(
|
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]),
|
&settings::Settings::for_rules(vec![Rule::UnusedImport, Rule::UnusedVariable]),
|
||||||
)?;
|
)?;
|
||||||
assert_messages!(diagnostics);
|
assert_messages!(diagnostics);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff/src/rules/ruff/mod.rs
|
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():
|
7 | def f():
|
||||||
8 | x = 1
|
8 | x = 1
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ pub fn indentation<'a, T>(locator: &'a Locator, located: &T) -> Option<&'a str>
|
||||||
where
|
where
|
||||||
T: Ranged,
|
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.
|
/// Return the end offset at which the empty lines following a statement.
|
||||||
|
|
|
@ -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
|
// 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.
|
// 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()
|
.unwrap_or_default()
|
||||||
.len();
|
.len();
|
||||||
|
|
||||||
|
@ -402,7 +402,7 @@ fn handle_match_comment<'a>(
|
||||||
|
|
||||||
let next_case = match_stmt.cases.get(current_case_index + 1);
|
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()
|
.unwrap_or_default()
|
||||||
.len();
|
.len();
|
||||||
let match_case_indentation = indentation(locator, match_case).unwrap().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
|
// 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).
|
// 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()
|
.unwrap_or_default()
|
||||||
.len();
|
.len();
|
||||||
|
|
||||||
|
@ -493,7 +493,7 @@ fn handle_own_line_comment_after_branch<'a>(
|
||||||
// # Trailing if comment
|
// # Trailing if comment
|
||||||
// ```
|
// ```
|
||||||
// Here we keep the comment a trailing comment of the `if`
|
// 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()
|
.unwrap_or_default()
|
||||||
.len();
|
.len();
|
||||||
if comment_indentation == preceding_indentation {
|
if comment_indentation == preceding_indentation {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use ruff_source_file::Locator;
|
||||||
use ruff_text_size::{TextRange, TextSize};
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
|
|
||||||
/// Extract the leading indentation from a line.
|
/// 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 line_start = locator.line_start(offset);
|
||||||
let indentation = &locator.contents()[TextRange::new(line_start, offset)];
|
let indentation = &locator.contents()[TextRange::new(line_start, offset)];
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue