diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index e1c114a66e..f1d38336f2 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -415,8 +415,13 @@ pub struct CheckCommand { )] pub statistics: bool, /// Enable automatic additions of `noqa` directives to failing lines. + /// Optionally provide a reason to append after the codes. #[arg( long, + value_name = "REASON", + default_missing_value = "", + num_args = 0..=1, + require_equals = true, // conflicts_with = "add_noqa", conflicts_with = "show_files", conflicts_with = "show_settings", @@ -428,7 +433,7 @@ pub struct CheckCommand { conflicts_with = "fix", conflicts_with = "diff", )] - pub add_noqa: bool, + pub add_noqa: Option, /// See the files Ruff will be run against with the current settings. #[arg( long, @@ -1057,7 +1062,7 @@ Possible choices: /// etc.). #[expect(clippy::struct_excessive_bools)] pub struct CheckArguments { - pub add_noqa: bool, + pub add_noqa: Option, pub diff: bool, pub exit_non_zero_on_fix: bool, pub exit_zero: bool, diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index d5eaeb0170..ff6a07c758 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -21,6 +21,7 @@ pub(crate) fn add_noqa( files: &[PathBuf], pyproject_config: &PyprojectConfig, config_arguments: &ConfigArguments, + reason: Option<&str>, ) -> Result { // Collect all the files to check. let start = Instant::now(); @@ -76,7 +77,14 @@ pub(crate) fn add_noqa( return None; } }; - match add_noqa_to_path(path, package, &source_kind, source_type, &settings.linter) { + match add_noqa_to_path( + path, + package, + &source_kind, + source_type, + &settings.linter, + reason, + ) { Ok(count) => Some(count), Err(e) => { error!("Failed to add noqa to {}: {e}", path.display()); diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 3bd457de8c..3ea0d94fad 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -319,12 +319,20 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result cannot contain newline characters" + )); + } + + let reason_opt = (!reason.is_empty()).then_some(reason.as_str()); + let modifications = - commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?; + commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments, reason_opt)?; if modifications > 0 && config_arguments.log_level >= LogLevel::Default { let s = if modifications == 1 { "" } else { "s" }; #[expect(clippy::print_stderr)] diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index ebd202b052..25500ed346 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -1760,6 +1760,64 @@ from foo import ( # noqa: F401 Ok(()) } +#[test] +fn add_noqa_with_reason() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file( + "test.py", + r#"import os + +def foo(): + x = 1 +"#, + )?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--add-noqa=TODO: fix") + .arg("--select=F401,F841") + .arg("test.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Added 2 noqa directives. + "); + + let content = fs::read_to_string(fixture.root().join("test.py"))?; + insta::assert_snapshot!(content, @r" +import os # noqa: F401 TODO: fix + +def foo(): + x = 1 # noqa: F841 TODO: fix +"); + + Ok(()) +} + +#[test] +fn add_noqa_with_newline_in_reason() -> Result<()> { + let fixture = CliTest::new()?; + fixture.write_file("test.py", "import os\n")?; + + assert_cmd_snapshot!(fixture + .check_command() + .arg("--add-noqa=line1\nline2") + .arg("--select=F401") + .arg("test.py"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ruff failed + Cause: --add-noqa cannot contain newline characters + "###); + + Ok(()) +} + /// Infer `3.11` from `requires-python` in `pyproject.toml`. #[test] fn requires_python() -> Result<()> { diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 2e4f284bee..3ec070dd26 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -377,6 +377,7 @@ pub fn add_noqa_to_path( source_kind: &SourceKind, source_type: PySourceType, settings: &LinterSettings, + reason: Option<&str>, ) -> Result { // Parse once. let target_version = settings.resolve_target_version(path); @@ -425,6 +426,7 @@ pub fn add_noqa_to_path( &settings.external, &directives.noqa_line_for, stylist.line_ending(), + reason, ) } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 606ac5ad3b..da9535817e 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -39,7 +39,7 @@ pub fn generate_noqa_edits( let exemption = FileExemption::from(&file_directives); 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) + build_noqa_edits_by_diagnostic(comments, locator, line_ending, None) } /// A directive to ignore a set of rules either for a given line of Python source code or an entire file (e.g., @@ -715,6 +715,7 @@ impl Display for LexicalError { impl Error for LexicalError {} /// Adds noqa comments to suppress all messages of a file. +#[expect(clippy::too_many_arguments)] pub(crate) fn add_noqa( path: &Path, diagnostics: &[Diagnostic], @@ -723,6 +724,7 @@ pub(crate) fn add_noqa( external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, + reason: Option<&str>, ) -> Result { let (count, output) = add_noqa_inner( path, @@ -732,12 +734,14 @@ pub(crate) fn add_noqa( external, noqa_line_for, line_ending, + reason, ); fs::write(path, output)?; Ok(count) } +#[expect(clippy::too_many_arguments)] fn add_noqa_inner( path: &Path, diagnostics: &[Diagnostic], @@ -746,6 +750,7 @@ fn add_noqa_inner( external: &[String], noqa_line_for: &NoqaMapping, line_ending: LineEnding, + reason: Option<&str>, ) -> (usize, String) { let mut count = 0; @@ -757,7 +762,7 @@ fn add_noqa_inner( let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for); - let edits = build_noqa_edits_by_line(comments, locator, line_ending); + let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason); let contents = locator.contents(); @@ -783,6 +788,7 @@ fn build_noqa_edits_by_diagnostic( comments: Vec>, locator: &Locator, line_ending: LineEnding, + reason: Option<&str>, ) -> Vec> { let mut edits = Vec::default(); for comment in comments { @@ -794,6 +800,7 @@ fn build_noqa_edits_by_diagnostic( FxHashSet::from_iter([comment.code]), locator, line_ending, + reason, ) { edits.push(Some(noqa_edit.into_edit())); } @@ -808,6 +815,7 @@ fn build_noqa_edits_by_line<'a>( comments: Vec>>, locator: &Locator, line_ending: LineEnding, + reason: Option<&'a str>, ) -> BTreeMap> { let mut comments_by_line = BTreeMap::default(); for comment in comments.into_iter().flatten() { @@ -831,6 +839,7 @@ fn build_noqa_edits_by_line<'a>( .collect(), locator, line_ending, + reason, ) { edits.insert(offset, edit); } @@ -927,6 +936,7 @@ struct NoqaEdit<'a> { noqa_codes: FxHashSet<&'a SecondaryCode>, codes: Option<&'a Codes<'a>>, line_ending: LineEnding, + reason: Option<&'a str>, } impl NoqaEdit<'_> { @@ -954,6 +964,9 @@ impl NoqaEdit<'_> { push_codes(writer, self.noqa_codes.iter().sorted_unstable()); } } + if let Some(reason) = self.reason { + write!(writer, " {reason}").unwrap(); + } write!(writer, "{}", self.line_ending.as_str()).unwrap(); } } @@ -970,6 +983,7 @@ fn generate_noqa_edit<'a>( noqa_codes: FxHashSet<&'a SecondaryCode>, locator: &Locator, line_ending: LineEnding, + reason: Option<&'a str>, ) -> Option> { let line_range = locator.full_line_range(offset); @@ -999,6 +1013,7 @@ fn generate_noqa_edit<'a>( noqa_codes, codes, line_ending, + reason, }) } @@ -2832,6 +2847,7 @@ mod tests { &[], &noqa_line_for, LineEnding::Lf, + None, ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); @@ -2855,6 +2871,7 @@ mod tests { &[], &noqa_line_for, LineEnding::Lf, + None, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); @@ -2885,6 +2902,7 @@ mod tests { &[], &noqa_line_for, LineEnding::Lf, + None, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); @@ -2915,6 +2933,7 @@ mod tests { &[], &noqa_line_for, LineEnding::Lf, + None, ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); diff --git a/docs/configuration.md b/docs/configuration.md index 8d3297fbca..7a5f62fc60 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -618,8 +618,9 @@ Options: notebooks, use `--extension ipy:ipynb` --statistics Show counts for every rule with at least one violation - --add-noqa - Enable automatic additions of `noqa` directives to failing lines + --add-noqa[=] + Enable automatic additions of `noqa` directives to failing lines. + Optionally provide a reason to append after the codes --show-files See the files Ruff will be run against with the current settings --show-settings