diff --git a/crates/ruff_linter/src/checkers/noqa.rs b/crates/ruff_linter/src/checkers/noqa.rs index e893551dfcd4b..f392f568ccfdf 100644 --- a/crates/ruff_linter/src/checkers/noqa.rs +++ b/crates/ruff_linter/src/checkers/noqa.rs @@ -43,7 +43,8 @@ pub(crate) fn check_noqa( let exemption = FileExemption::from(&file_noqa_directives); // Extract all `noqa` directives. - let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator); + let mut noqa_directives = + NoqaDirectives::from_commented_ranges(comment_ranges, &settings.external, path, locator); // Indices of diagnostics that were ignored by a `noqa` directive. let mut ignored_diagnostics = vec![]; diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 1653d3f3f5538..ebfc40823f9fa 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -36,7 +36,7 @@ pub fn generate_noqa_edits( let file_directives = FileNoqaDirectives::extract(locator.contents(), comment_ranges, external, path, locator); let exemption = FileExemption::from(&file_directives); - let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator); + let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); build_noqa_edits_by_diagnostic(comments, locator, line_ending) } @@ -183,7 +183,7 @@ impl<'a> Directive<'a> { // Extract, e.g., the `401` in `F401`. let suffix = line[prefix..] .chars() - .take_while(char::is_ascii_digit) + .take_while(char::is_ascii_alphanumeric) .count(); if prefix > 0 && suffix > 0 { Some(&line[..prefix + suffix]) @@ -550,7 +550,7 @@ impl<'a> ParsedFileExemption<'a> { // Extract, e.g., the `401` in `F401`. let suffix = line[prefix..] .chars() - .take_while(char::is_ascii_digit) + .take_while(char::is_ascii_alphanumeric) .count(); if prefix > 0 && suffix > 0 { Some(&line[..prefix + suffix]) @@ -623,7 +623,7 @@ fn add_noqa_inner( FileNoqaDirectives::extract(locator.contents(), comment_ranges, external, path, locator); let exemption = FileExemption::from(&directives); - let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator); + let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator); let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); @@ -897,7 +897,7 @@ pub(crate) struct NoqaDirectiveLine<'a> { pub(crate) directive: Directive<'a>, /// The codes that are ignored by the directive. pub(crate) matches: Vec, - // Whether the directive applies to range.end + /// Whether the directive applies to `range.end`. pub(crate) includes_end: bool, } @@ -916,6 +916,7 @@ pub(crate) struct NoqaDirectives<'a> { impl<'a> NoqaDirectives<'a> { pub(crate) fn from_commented_ranges( comment_ranges: &CommentRanges, + external: &[String], path: &Path, locator: &'a Locator<'a>, ) -> Self { @@ -930,7 +931,29 @@ impl<'a> NoqaDirectives<'a> { warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}"); } Ok(Some(directive)) => { - // noqa comments are guaranteed to be single line. + if let Directive::Codes(codes) = &directive { + // Warn on invalid rule codes. + for code in &codes.codes { + // Ignore externally-defined rules. + if !external + .iter() + .any(|external| code.as_str().starts_with(external)) + { + if Rule::from_code( + get_redirect_target(code.as_str()).unwrap_or(code.as_str()), + ) + .is_err() + { + #[allow(deprecated)] + let line = locator.compute_line_index(range.start()); + let path_display = relativize_path(path); + warn!("Invalid rule code provided to `# noqa` at {path_display}:{line}: {code}"); + } + } + } + } + + // `# noqa` comments are guaranteed to be single line. let range = locator.line_range(range.start()); directives.push(NoqaDirectiveLine { range, @@ -1193,6 +1216,18 @@ mod tests { assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); } + #[test] + fn noqa_squashed_codes() { + let source = "# noqa: F401F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + + #[test] + fn noqa_empty_comma() { + let source = "# noqa: F401,,F841"; + assert_debug_snapshot!(Directive::try_extract(source, TextSize::default())); + } + #[test] fn noqa_invalid_suffix() { let source = "# noqa[F401]"; diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_empty_comma.snap b/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_empty_comma.snap new file mode 100644 index 0000000000000..7a83ed9db2271 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_empty_comma.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..18, + codes: [ + Code { + code: "F401", + range: 8..12, + }, + Code { + code: "F841", + range: 14..18, + }, + ], + }, + ), + ), +) diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_squashed_codes.snap b/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_squashed_codes.snap new file mode 100644 index 0000000000000..8a6adc1cc7a11 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__noqa__tests__noqa_squashed_codes.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff_linter/src/noqa.rs +expression: "Directive::try_extract(source, TextSize::default())" +--- +Ok( + Some( + Codes( + Codes { + range: 0..16, + codes: [ + Code { + code: "F401F841", + range: 8..16, + }, + ], + }, + ), + ), +)