Skip to content

Commit 7b237d3

Browse files
MichaReiserclaude
andauthored
Add option to provide a reason to --add-noqa (#21294)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 36cce34 commit 7b237d3

7 files changed

Lines changed: 110 additions & 9 deletions

File tree

crates/ruff/src/args.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,13 @@ pub struct CheckCommand {
415415
)]
416416
pub statistics: bool,
417417
/// Enable automatic additions of `noqa` directives to failing lines.
418+
/// Optionally provide a reason to append after the codes.
418419
#[arg(
419420
long,
421+
value_name = "REASON",
422+
default_missing_value = "",
423+
num_args = 0..=1,
424+
require_equals = true,
420425
// conflicts_with = "add_noqa",
421426
conflicts_with = "show_files",
422427
conflicts_with = "show_settings",
@@ -428,7 +433,7 @@ pub struct CheckCommand {
428433
conflicts_with = "fix",
429434
conflicts_with = "diff",
430435
)]
431-
pub add_noqa: bool,
436+
pub add_noqa: Option<String>,
432437
/// See the files Ruff will be run against with the current settings.
433438
#[arg(
434439
long,
@@ -1057,7 +1062,7 @@ Possible choices:
10571062
/// etc.).
10581063
#[expect(clippy::struct_excessive_bools)]
10591064
pub struct CheckArguments {
1060-
pub add_noqa: bool,
1065+
pub add_noqa: Option<String>,
10611066
pub diff: bool,
10621067
pub exit_non_zero_on_fix: bool,
10631068
pub exit_zero: bool,

crates/ruff/src/commands/add_noqa.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub(crate) fn add_noqa(
2121
files: &[PathBuf],
2222
pyproject_config: &PyprojectConfig,
2323
config_arguments: &ConfigArguments,
24+
reason: Option<&str>,
2425
) -> Result<usize> {
2526
// Collect all the files to check.
2627
let start = Instant::now();
@@ -76,7 +77,14 @@ pub(crate) fn add_noqa(
7677
return None;
7778
}
7879
};
79-
match add_noqa_to_path(path, package, &source_kind, source_type, &settings.linter) {
80+
match add_noqa_to_path(
81+
path,
82+
package,
83+
&source_kind,
84+
source_type,
85+
&settings.linter,
86+
reason,
87+
) {
8088
Ok(count) => Some(count),
8189
Err(e) => {
8290
error!("Failed to add noqa to {}: {e}", path.display());

crates/ruff/src/lib.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,20 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
319319
warn_user!("Detected debug build without --no-cache.");
320320
}
321321

322-
if cli.add_noqa {
322+
if let Some(reason) = &cli.add_noqa {
323323
if !fix_mode.is_generate() {
324324
warn_user!("--fix is incompatible with --add-noqa.");
325325
}
326+
if reason.contains(['\n', '\r']) {
327+
return Err(anyhow::anyhow!(
328+
"--add-noqa <reason> cannot contain newline characters"
329+
));
330+
}
331+
332+
let reason_opt = (!reason.is_empty()).then_some(reason.as_str());
333+
326334
let modifications =
327-
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?;
335+
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments, reason_opt)?;
328336
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
329337
let s = if modifications == 1 { "" } else { "s" };
330338
#[expect(clippy::print_stderr)]

crates/ruff/tests/cli/lint.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,64 @@ from foo import ( # noqa: F401
17601760
Ok(())
17611761
}
17621762

1763+
#[test]
1764+
fn add_noqa_with_reason() -> Result<()> {
1765+
let fixture = CliTest::new()?;
1766+
fixture.write_file(
1767+
"test.py",
1768+
r#"import os
1769+
1770+
def foo():
1771+
x = 1
1772+
"#,
1773+
)?;
1774+
1775+
assert_cmd_snapshot!(fixture
1776+
.check_command()
1777+
.arg("--add-noqa=TODO: fix")
1778+
.arg("--select=F401,F841")
1779+
.arg("test.py"), @r"
1780+
success: true
1781+
exit_code: 0
1782+
----- stdout -----
1783+
1784+
----- stderr -----
1785+
Added 2 noqa directives.
1786+
");
1787+
1788+
let content = fs::read_to_string(fixture.root().join("test.py"))?;
1789+
insta::assert_snapshot!(content, @r"
1790+
import os # noqa: F401 TODO: fix
1791+
1792+
def foo():
1793+
x = 1 # noqa: F841 TODO: fix
1794+
");
1795+
1796+
Ok(())
1797+
}
1798+
1799+
#[test]
1800+
fn add_noqa_with_newline_in_reason() -> Result<()> {
1801+
let fixture = CliTest::new()?;
1802+
fixture.write_file("test.py", "import os\n")?;
1803+
1804+
assert_cmd_snapshot!(fixture
1805+
.check_command()
1806+
.arg("--add-noqa=line1\nline2")
1807+
.arg("--select=F401")
1808+
.arg("test.py"), @r###"
1809+
success: false
1810+
exit_code: 2
1811+
----- stdout -----
1812+
1813+
----- stderr -----
1814+
ruff failed
1815+
Cause: --add-noqa <reason> cannot contain newline characters
1816+
"###);
1817+
1818+
Ok(())
1819+
}
1820+
17631821
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
17641822
#[test]
17651823
fn requires_python() -> Result<()> {

crates/ruff_linter/src/linter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ pub fn add_noqa_to_path(
377377
source_kind: &SourceKind,
378378
source_type: PySourceType,
379379
settings: &LinterSettings,
380+
reason: Option<&str>,
380381
) -> Result<usize> {
381382
// Parse once.
382383
let target_version = settings.resolve_target_version(path);
@@ -425,6 +426,7 @@ pub fn add_noqa_to_path(
425426
&settings.external,
426427
&directives.noqa_line_for,
427428
stylist.line_ending(),
429+
reason,
428430
)
429431
}
430432

crates/ruff_linter/src/noqa.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub fn generate_noqa_edits(
3939
let exemption = FileExemption::from(&file_directives);
4040
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
4141
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
42-
build_noqa_edits_by_diagnostic(comments, locator, line_ending)
42+
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
4343
}
4444

4545
/// 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 {
715715
impl Error for LexicalError {}
716716

717717
/// Adds noqa comments to suppress all messages of a file.
718+
#[expect(clippy::too_many_arguments)]
718719
pub(crate) fn add_noqa(
719720
path: &Path,
720721
diagnostics: &[Diagnostic],
@@ -723,6 +724,7 @@ pub(crate) fn add_noqa(
723724
external: &[String],
724725
noqa_line_for: &NoqaMapping,
725726
line_ending: LineEnding,
727+
reason: Option<&str>,
726728
) -> Result<usize> {
727729
let (count, output) = add_noqa_inner(
728730
path,
@@ -732,12 +734,14 @@ pub(crate) fn add_noqa(
732734
external,
733735
noqa_line_for,
734736
line_ending,
737+
reason,
735738
);
736739

737740
fs::write(path, output)?;
738741
Ok(count)
739742
}
740743

744+
#[expect(clippy::too_many_arguments)]
741745
fn add_noqa_inner(
742746
path: &Path,
743747
diagnostics: &[Diagnostic],
@@ -746,6 +750,7 @@ fn add_noqa_inner(
746750
external: &[String],
747751
noqa_line_for: &NoqaMapping,
748752
line_ending: LineEnding,
753+
reason: Option<&str>,
749754
) -> (usize, String) {
750755
let mut count = 0;
751756

@@ -757,7 +762,7 @@ fn add_noqa_inner(
757762

758763
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
759764

760-
let edits = build_noqa_edits_by_line(comments, locator, line_ending);
765+
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
761766

762767
let contents = locator.contents();
763768

@@ -783,6 +788,7 @@ fn build_noqa_edits_by_diagnostic(
783788
comments: Vec<Option<NoqaComment>>,
784789
locator: &Locator,
785790
line_ending: LineEnding,
791+
reason: Option<&str>,
786792
) -> Vec<Option<Edit>> {
787793
let mut edits = Vec::default();
788794
for comment in comments {
@@ -794,6 +800,7 @@ fn build_noqa_edits_by_diagnostic(
794800
FxHashSet::from_iter([comment.code]),
795801
locator,
796802
line_ending,
803+
reason,
797804
) {
798805
edits.push(Some(noqa_edit.into_edit()));
799806
}
@@ -808,6 +815,7 @@ fn build_noqa_edits_by_line<'a>(
808815
comments: Vec<Option<NoqaComment<'a>>>,
809816
locator: &Locator,
810817
line_ending: LineEnding,
818+
reason: Option<&'a str>,
811819
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
812820
let mut comments_by_line = BTreeMap::default();
813821
for comment in comments.into_iter().flatten() {
@@ -831,6 +839,7 @@ fn build_noqa_edits_by_line<'a>(
831839
.collect(),
832840
locator,
833841
line_ending,
842+
reason,
834843
) {
835844
edits.insert(offset, edit);
836845
}
@@ -927,6 +936,7 @@ struct NoqaEdit<'a> {
927936
noqa_codes: FxHashSet<&'a SecondaryCode>,
928937
codes: Option<&'a Codes<'a>>,
929938
line_ending: LineEnding,
939+
reason: Option<&'a str>,
930940
}
931941

932942
impl NoqaEdit<'_> {
@@ -954,6 +964,9 @@ impl NoqaEdit<'_> {
954964
push_codes(writer, self.noqa_codes.iter().sorted_unstable());
955965
}
956966
}
967+
if let Some(reason) = self.reason {
968+
write!(writer, " {reason}").unwrap();
969+
}
957970
write!(writer, "{}", self.line_ending.as_str()).unwrap();
958971
}
959972
}
@@ -970,6 +983,7 @@ fn generate_noqa_edit<'a>(
970983
noqa_codes: FxHashSet<&'a SecondaryCode>,
971984
locator: &Locator,
972985
line_ending: LineEnding,
986+
reason: Option<&'a str>,
973987
) -> Option<NoqaEdit<'a>> {
974988
let line_range = locator.full_line_range(offset);
975989

@@ -999,6 +1013,7 @@ fn generate_noqa_edit<'a>(
9991013
noqa_codes,
10001014
codes,
10011015
line_ending,
1016+
reason,
10021017
})
10031018
}
10041019

@@ -2832,6 +2847,7 @@ mod tests {
28322847
&[],
28332848
&noqa_line_for,
28342849
LineEnding::Lf,
2850+
None,
28352851
);
28362852
assert_eq!(count, 0);
28372853
assert_eq!(output, format!("{contents}"));
@@ -2855,6 +2871,7 @@ mod tests {
28552871
&[],
28562872
&noqa_line_for,
28572873
LineEnding::Lf,
2874+
None,
28582875
);
28592876
assert_eq!(count, 1);
28602877
assert_eq!(output, "x = 1 # noqa: F841\n");
@@ -2885,6 +2902,7 @@ mod tests {
28852902
&[],
28862903
&noqa_line_for,
28872904
LineEnding::Lf,
2905+
None,
28882906
);
28892907
assert_eq!(count, 1);
28902908
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
@@ -2915,6 +2933,7 @@ mod tests {
29152933
&[],
29162934
&noqa_line_for,
29172935
LineEnding::Lf,
2936+
None,
29182937
);
29192938
assert_eq!(count, 0);
29202939
assert_eq!(output, "x = 1 # noqa");

docs/configuration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,9 @@ Options:
618618
notebooks, use `--extension ipy:ipynb`
619619
--statistics
620620
Show counts for every rule with at least one violation
621-
--add-noqa
622-
Enable automatic additions of `noqa` directives to failing lines
621+
--add-noqa[=<REASON>]
622+
Enable automatic additions of `noqa` directives to failing lines.
623+
Optionally provide a reason to append after the codes
623624
--show-files
624625
See the files Ruff will be run against with the current settings
625626
--show-settings

0 commit comments

Comments
 (0)