mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 12:16:43 +00:00
Add option to provide a reason to --add-noqa (#21294)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
36cce347fd
commit
7b237d316f
7 changed files with 110 additions and 9 deletions
|
|
@ -415,8 +415,13 @@ pub struct CheckCommand {
|
||||||
)]
|
)]
|
||||||
pub statistics: bool,
|
pub statistics: bool,
|
||||||
/// Enable automatic additions of `noqa` directives to failing lines.
|
/// Enable automatic additions of `noqa` directives to failing lines.
|
||||||
|
/// Optionally provide a reason to append after the codes.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
value_name = "REASON",
|
||||||
|
default_missing_value = "",
|
||||||
|
num_args = 0..=1,
|
||||||
|
require_equals = true,
|
||||||
// conflicts_with = "add_noqa",
|
// conflicts_with = "add_noqa",
|
||||||
conflicts_with = "show_files",
|
conflicts_with = "show_files",
|
||||||
conflicts_with = "show_settings",
|
conflicts_with = "show_settings",
|
||||||
|
|
@ -428,7 +433,7 @@ pub struct CheckCommand {
|
||||||
conflicts_with = "fix",
|
conflicts_with = "fix",
|
||||||
conflicts_with = "diff",
|
conflicts_with = "diff",
|
||||||
)]
|
)]
|
||||||
pub add_noqa: bool,
|
pub add_noqa: Option<String>,
|
||||||
/// See the files Ruff will be run against with the current settings.
|
/// See the files Ruff will be run against with the current settings.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
|
@ -1057,7 +1062,7 @@ Possible choices:
|
||||||
/// etc.).
|
/// etc.).
|
||||||
#[expect(clippy::struct_excessive_bools)]
|
#[expect(clippy::struct_excessive_bools)]
|
||||||
pub struct CheckArguments {
|
pub struct CheckArguments {
|
||||||
pub add_noqa: bool,
|
pub add_noqa: Option<String>,
|
||||||
pub diff: bool,
|
pub diff: bool,
|
||||||
pub exit_non_zero_on_fix: bool,
|
pub exit_non_zero_on_fix: bool,
|
||||||
pub exit_zero: bool,
|
pub exit_zero: bool,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ pub(crate) fn add_noqa(
|
||||||
files: &[PathBuf],
|
files: &[PathBuf],
|
||||||
pyproject_config: &PyprojectConfig,
|
pyproject_config: &PyprojectConfig,
|
||||||
config_arguments: &ConfigArguments,
|
config_arguments: &ConfigArguments,
|
||||||
|
reason: Option<&str>,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
// Collect all the files to check.
|
// Collect all the files to check.
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
@ -76,7 +77,14 @@ pub(crate) fn add_noqa(
|
||||||
return None;
|
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),
|
Ok(count) => Some(count),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to add noqa to {}: {e}", path.display());
|
error!("Failed to add noqa to {}: {e}", path.display());
|
||||||
|
|
|
||||||
|
|
@ -319,12 +319,20 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
||||||
warn_user!("Detected debug build without --no-cache.");
|
warn_user!("Detected debug build without --no-cache.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.add_noqa {
|
if let Some(reason) = &cli.add_noqa {
|
||||||
if !fix_mode.is_generate() {
|
if !fix_mode.is_generate() {
|
||||||
warn_user!("--fix is incompatible with --add-noqa.");
|
warn_user!("--fix is incompatible with --add-noqa.");
|
||||||
}
|
}
|
||||||
|
if reason.contains(['\n', '\r']) {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"--add-noqa <reason> cannot contain newline characters"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason_opt = (!reason.is_empty()).then_some(reason.as_str());
|
||||||
|
|
||||||
let modifications =
|
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 {
|
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
|
||||||
let s = if modifications == 1 { "" } else { "s" };
|
let s = if modifications == 1 { "" } else { "s" };
|
||||||
#[expect(clippy::print_stderr)]
|
#[expect(clippy::print_stderr)]
|
||||||
|
|
|
||||||
|
|
@ -1760,6 +1760,64 @@ from foo import ( # noqa: F401
|
||||||
Ok(())
|
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 <reason> cannot contain newline characters
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
|
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
|
||||||
#[test]
|
#[test]
|
||||||
fn requires_python() -> Result<()> {
|
fn requires_python() -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,7 @@ pub fn add_noqa_to_path(
|
||||||
source_kind: &SourceKind,
|
source_kind: &SourceKind,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
settings: &LinterSettings,
|
settings: &LinterSettings,
|
||||||
|
reason: Option<&str>,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
// Parse once.
|
// Parse once.
|
||||||
let target_version = settings.resolve_target_version(path);
|
let target_version = settings.resolve_target_version(path);
|
||||||
|
|
@ -425,6 +426,7 @@ pub fn add_noqa_to_path(
|
||||||
&settings.external,
|
&settings.external,
|
||||||
&directives.noqa_line_for,
|
&directives.noqa_line_for,
|
||||||
stylist.line_ending(),
|
stylist.line_ending(),
|
||||||
|
reason,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ pub fn generate_noqa_edits(
|
||||||
let exemption = FileExemption::from(&file_directives);
|
let exemption = FileExemption::from(&file_directives);
|
||||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, 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);
|
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.,
|
/// 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 {}
|
impl Error for LexicalError {}
|
||||||
|
|
||||||
/// Adds noqa comments to suppress all messages of a file.
|
/// Adds noqa comments to suppress all messages of a file.
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
pub(crate) fn add_noqa(
|
pub(crate) fn add_noqa(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
diagnostics: &[Diagnostic],
|
diagnostics: &[Diagnostic],
|
||||||
|
|
@ -723,6 +724,7 @@ pub(crate) fn add_noqa(
|
||||||
external: &[String],
|
external: &[String],
|
||||||
noqa_line_for: &NoqaMapping,
|
noqa_line_for: &NoqaMapping,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&str>,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
let (count, output) = add_noqa_inner(
|
let (count, output) = add_noqa_inner(
|
||||||
path,
|
path,
|
||||||
|
|
@ -732,12 +734,14 @@ pub(crate) fn add_noqa(
|
||||||
external,
|
external,
|
||||||
noqa_line_for,
|
noqa_line_for,
|
||||||
line_ending,
|
line_ending,
|
||||||
|
reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::write(path, output)?;
|
fs::write(path, output)?;
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn add_noqa_inner(
|
fn add_noqa_inner(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
diagnostics: &[Diagnostic],
|
diagnostics: &[Diagnostic],
|
||||||
|
|
@ -746,6 +750,7 @@ fn add_noqa_inner(
|
||||||
external: &[String],
|
external: &[String],
|
||||||
noqa_line_for: &NoqaMapping,
|
noqa_line_for: &NoqaMapping,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&str>,
|
||||||
) -> (usize, String) {
|
) -> (usize, String) {
|
||||||
let mut count = 0;
|
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 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();
|
let contents = locator.contents();
|
||||||
|
|
||||||
|
|
@ -783,6 +788,7 @@ fn build_noqa_edits_by_diagnostic(
|
||||||
comments: Vec<Option<NoqaComment>>,
|
comments: Vec<Option<NoqaComment>>,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&str>,
|
||||||
) -> Vec<Option<Edit>> {
|
) -> Vec<Option<Edit>> {
|
||||||
let mut edits = Vec::default();
|
let mut edits = Vec::default();
|
||||||
for comment in comments {
|
for comment in comments {
|
||||||
|
|
@ -794,6 +800,7 @@ fn build_noqa_edits_by_diagnostic(
|
||||||
FxHashSet::from_iter([comment.code]),
|
FxHashSet::from_iter([comment.code]),
|
||||||
locator,
|
locator,
|
||||||
line_ending,
|
line_ending,
|
||||||
|
reason,
|
||||||
) {
|
) {
|
||||||
edits.push(Some(noqa_edit.into_edit()));
|
edits.push(Some(noqa_edit.into_edit()));
|
||||||
}
|
}
|
||||||
|
|
@ -808,6 +815,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||||
comments: Vec<Option<NoqaComment<'a>>>,
|
comments: Vec<Option<NoqaComment<'a>>>,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&'a str>,
|
||||||
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
|
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
|
||||||
let mut comments_by_line = BTreeMap::default();
|
let mut comments_by_line = BTreeMap::default();
|
||||||
for comment in comments.into_iter().flatten() {
|
for comment in comments.into_iter().flatten() {
|
||||||
|
|
@ -831,6 +839,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||||
.collect(),
|
.collect(),
|
||||||
locator,
|
locator,
|
||||||
line_ending,
|
line_ending,
|
||||||
|
reason,
|
||||||
) {
|
) {
|
||||||
edits.insert(offset, edit);
|
edits.insert(offset, edit);
|
||||||
}
|
}
|
||||||
|
|
@ -927,6 +936,7 @@ struct NoqaEdit<'a> {
|
||||||
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
||||||
codes: Option<&'a Codes<'a>>,
|
codes: Option<&'a Codes<'a>>,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NoqaEdit<'_> {
|
impl NoqaEdit<'_> {
|
||||||
|
|
@ -954,6 +964,9 @@ impl NoqaEdit<'_> {
|
||||||
push_codes(writer, self.noqa_codes.iter().sorted_unstable());
|
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();
|
write!(writer, "{}", self.line_ending.as_str()).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -970,6 +983,7 @@ fn generate_noqa_edit<'a>(
|
||||||
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
||||||
locator: &Locator,
|
locator: &Locator,
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
|
reason: Option<&'a str>,
|
||||||
) -> Option<NoqaEdit<'a>> {
|
) -> Option<NoqaEdit<'a>> {
|
||||||
let line_range = locator.full_line_range(offset);
|
let line_range = locator.full_line_range(offset);
|
||||||
|
|
||||||
|
|
@ -999,6 +1013,7 @@ fn generate_noqa_edit<'a>(
|
||||||
noqa_codes,
|
noqa_codes,
|
||||||
codes,
|
codes,
|
||||||
line_ending,
|
line_ending,
|
||||||
|
reason,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2832,6 +2847,7 @@ mod tests {
|
||||||
&[],
|
&[],
|
||||||
&noqa_line_for,
|
&noqa_line_for,
|
||||||
LineEnding::Lf,
|
LineEnding::Lf,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert_eq!(count, 0);
|
assert_eq!(count, 0);
|
||||||
assert_eq!(output, format!("{contents}"));
|
assert_eq!(output, format!("{contents}"));
|
||||||
|
|
@ -2855,6 +2871,7 @@ mod tests {
|
||||||
&[],
|
&[],
|
||||||
&noqa_line_for,
|
&noqa_line_for,
|
||||||
LineEnding::Lf,
|
LineEnding::Lf,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
assert_eq!(output, "x = 1 # noqa: F841\n");
|
assert_eq!(output, "x = 1 # noqa: F841\n");
|
||||||
|
|
@ -2885,6 +2902,7 @@ mod tests {
|
||||||
&[],
|
&[],
|
||||||
&noqa_line_for,
|
&noqa_line_for,
|
||||||
LineEnding::Lf,
|
LineEnding::Lf,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
|
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
|
||||||
|
|
@ -2915,6 +2933,7 @@ mod tests {
|
||||||
&[],
|
&[],
|
||||||
&noqa_line_for,
|
&noqa_line_for,
|
||||||
LineEnding::Lf,
|
LineEnding::Lf,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
assert_eq!(count, 0);
|
assert_eq!(count, 0);
|
||||||
assert_eq!(output, "x = 1 # noqa");
|
assert_eq!(output, "x = 1 # noqa");
|
||||||
|
|
|
||||||
|
|
@ -618,8 +618,9 @@ Options:
|
||||||
notebooks, use `--extension ipy:ipynb`
|
notebooks, use `--extension ipy:ipynb`
|
||||||
--statistics
|
--statistics
|
||||||
Show counts for every rule with at least one violation
|
Show counts for every rule with at least one violation
|
||||||
--add-noqa
|
--add-noqa[=<REASON>]
|
||||||
Enable automatic additions of `noqa` directives to failing lines
|
Enable automatic additions of `noqa` directives to failing lines.
|
||||||
|
Optionally provide a reason to append after the codes
|
||||||
--show-files
|
--show-files
|
||||||
See the files Ruff will be run against with the current settings
|
See the files Ruff will be run against with the current settings
|
||||||
--show-settings
|
--show-settings
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue