Skip to content

Commit

Permalink
[pygrep-hooks] Improve blanket-noqa error message (PGH004) (ast…
Browse files Browse the repository at this point in the history
…ral-sh#10851)

## Summary

Improve `blanket-noqa` error message in cases where codes are provided
but not detected due to formatting issues. Namely `# noqa X100` (missing
colon) or `noqa : X100` (space before colon). The behavior is similar to
`NQA002` and `NQA003` from `flake8-noqa` mentioned in astral-sh#850. The idea to
merge the rules into `PGH004` was suggested by @MichaReiser
astral-sh#10325 (comment).

## Test Plan

Test cases added to fixture.
  • Loading branch information
augustelalande authored and Glyphack committed Apr 12, 2024
1 parent 391808c commit c01737d
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 7 deletions.
Expand Up @@ -9,3 +9,22 @@
x = 1 # noqa: F401, W203
# noqa: F401
# noqa: F401, W203

# OK
x = 2 # noqa: X100
x = 2 # noqa:X100

# PGH004
x = 2 # noqa X100

# PGH004
x = 2 # noqa X100, X200

# PGH004
x = 2 # noqa : X300

# PGH004
x = 2 # noqa : X400

# PGH004
x = 2 # noqa :X500
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/noqa.rs
Expand Up @@ -147,7 +147,7 @@ impl<'a> Directive<'a> {

/// Lex an individual rule code (e.g., `F401`).
#[inline]
fn lex_code(line: &str) -> Option<&str> {
pub(crate) fn lex_code(line: &str) -> Option<&str> {
// Extract, e.g., the `F` in `F401`.
let prefix = line.chars().take_while(char::is_ascii_uppercase).count();
// Extract, e.g., the `401` in `F401`.
Expand Down
100 changes: 94 additions & 6 deletions crates/ruff_linter/src/rules/pygrep_hooks/rules/blanket_noqa.rs
@@ -1,8 +1,9 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_trivia::Cursor;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange, TextSize};

use crate::noqa::Directive;

Expand All @@ -27,15 +28,56 @@ use crate::noqa::Directive;
/// from .base import * # noqa: F403
/// ```
///
/// ## Fix safety
/// This rule will attempt to fix blanket `noqa` annotations that appear to
/// be unintentional. For example, given `# noqa F401`, the rule will suggest
/// inserting a colon, as in `# noqa: F401`.
///
/// While modifying `noqa` comments is generally safe, doing so may introduce
/// additional diagnostics.
///
/// ## References
/// - [Ruff documentation](https://docs.astral.sh/ruff/configuration/#error-suppression)
#[violation]
pub struct BlanketNOQA;
pub struct BlanketNOQA {
missing_colon: bool,
space_before_colon: bool,
}

impl Violation for BlanketNOQA {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;

#[derive_message_formats]
fn message(&self) -> String {
format!("Use specific rule codes when using `noqa`")
let BlanketNOQA {
missing_colon,
space_before_colon,
} = self;

// This awkward branching is necessary to ensure that the generic message is picked up by
// `derive_message_formats`.
if !missing_colon && !space_before_colon {
format!("Use specific rule codes when using `noqa`")
} else if *missing_colon {
format!("Use a colon when specifying `noqa` rule codes")
} else {
format!("Do not add spaces between `noqa` and its colon")
}
}

fn fix_title(&self) -> Option<String> {
let BlanketNOQA {
missing_colon,
space_before_colon,
} = self;

if *missing_colon {
Some("Add missing colon".to_string())
} else if *space_before_colon {
Some("Remove space(s) before colon".to_string())
} else {
None
}
}
}

Expand All @@ -47,8 +89,54 @@ pub(crate) fn blanket_noqa(
) {
for range in indexer.comment_ranges() {
let line = locator.slice(*range);
if let Ok(Some(Directive::All(all))) = Directive::try_extract(line, range.start()) {
diagnostics.push(Diagnostic::new(BlanketNOQA, all.range()));
let offset = range.start();
if let Ok(Some(Directive::All(all))) = Directive::try_extract(line, TextSize::new(0)) {
// The `all` range is that of the `noqa` directive in, e.g., `# noqa` or `# noqa F401`.
let noqa_start = offset + all.start();
let noqa_end = offset + all.end();

// Skip the `# noqa`, plus any trailing whitespace.
let mut cursor = Cursor::new(&line[all.end().to_usize()..]);
cursor.eat_while(char::is_whitespace);

// Check for extraneous spaces before the colon.
// Ex) `# noqa : F401`
if cursor.first() == ':' {
let start = offset + all.end();
let end = start + cursor.token_len();
let mut diagnostic = Diagnostic::new(
BlanketNOQA {
missing_colon: false,
space_before_colon: true,
},
TextRange::new(noqa_start, end),
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::deletion(start, end)));
diagnostics.push(diagnostic);
} else if Directive::lex_code(cursor.chars().as_str()).is_some() {
// Check for a missing colon.
// Ex) `# noqa F401`
let start = offset + all.end();
let end = start + TextSize::new(1);
let mut diagnostic = Diagnostic::new(
BlanketNOQA {
missing_colon: true,
space_before_colon: false,
},
TextRange::new(noqa_start, end),
);
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(':'.to_string(), start)));
diagnostics.push(diagnostic);
} else {
// Otherwise, it looks like an intentional blanket `noqa` annotation.
diagnostics.push(Diagnostic::new(
BlanketNOQA {
missing_colon: false,
space_before_colon: false,
},
TextRange::new(noqa_start, noqa_end),
));
}
}
}
}
Expand Up @@ -29,4 +29,97 @@ PGH004_0.py:4:1: PGH004 Use specific rule codes when using `noqa`
6 | # noqa:F401,W203
|

PGH004_0.py:18:8: PGH004 [*] Use a colon when specifying `noqa` rule codes
|
17 | # PGH004
18 | x = 2 # noqa X100
| ^^^^^^^ PGH004
19 |
20 | # PGH004
|
= help: Add missing colon

Unsafe fix
15 15 | x = 2 # noqa:X100
16 16 |
17 17 | # PGH004
18 |-x = 2 # noqa X100
18 |+x = 2 # noqa: X100
19 19 |
20 20 | # PGH004
21 21 | x = 2 # noqa X100, X200

PGH004_0.py:21:8: PGH004 [*] Use a colon when specifying `noqa` rule codes
|
20 | # PGH004
21 | x = 2 # noqa X100, X200
| ^^^^^^^ PGH004
22 |
23 | # PGH004
|
= help: Add missing colon

Unsafe fix
18 18 | x = 2 # noqa X100
19 19 |
20 20 | # PGH004
21 |-x = 2 # noqa X100, X200
21 |+x = 2 # noqa: X100, X200
22 22 |
23 23 | # PGH004
24 24 | x = 2 # noqa : X300

PGH004_0.py:24:8: PGH004 [*] Do not add spaces between `noqa` and its colon
|
23 | # PGH004
24 | x = 2 # noqa : X300
| ^^^^^^^ PGH004
25 |
26 | # PGH004
|
= help: Remove space(s) before colon

Unsafe fix
21 21 | x = 2 # noqa X100, X200
22 22 |
23 23 | # PGH004
24 |-x = 2 # noqa : X300
24 |+x = 2 # noqa: X300
25 25 |
26 26 | # PGH004
27 27 | x = 2 # noqa : X400

PGH004_0.py:27:8: PGH004 [*] Do not add spaces between `noqa` and its colon
|
26 | # PGH004
27 | x = 2 # noqa : X400
| ^^^^^^^^ PGH004
28 |
29 | # PGH004
|
= help: Remove space(s) before colon

Unsafe fix
24 24 | x = 2 # noqa : X300
25 25 |
26 26 | # PGH004
27 |-x = 2 # noqa : X400
27 |+x = 2 # noqa: X400
28 28 |
29 29 | # PGH004
30 30 | x = 2 # noqa :X500

PGH004_0.py:30:8: PGH004 [*] Do not add spaces between `noqa` and its colon
|
29 | # PGH004
30 | x = 2 # noqa :X500
| ^^^^^^^ PGH004
|
= help: Remove space(s) before colon

Unsafe fix
27 27 | x = 2 # noqa : X400
28 28 |
29 29 | # PGH004
30 |-x = 2 # noqa :X500
30 |+x = 2 # noqa:X500

0 comments on commit c01737d

Please sign in to comment.