Skip to content
Merged
25 changes: 25 additions & 0 deletions crates/ruff/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ pub struct CheckCommand {
num_args = 0..=1,
require_equals = true,
// conflicts_with = "add_noqa",
conflicts_with = "add_ignore",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
Expand All @@ -466,6 +467,28 @@ pub struct CheckCommand {
conflicts_with = "diff",
)]
pub add_noqa: Option<String>,
/// Enable automatic additions of `ruff:ignore` comments to failing lines.
/// Optionally provide a reason to append after the rule names.
/// Requires preview mode.
#[arg(
long,
value_name = "REASON",
default_missing_value = "",
num_args = 0..=1,
require_equals = true,
// conflicts_with = "add_ignore",
conflicts_with = "add_noqa",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "ignore_noqa",
conflicts_with = "statistics",
conflicts_with = "stdin_filename",
conflicts_with = "watch",
conflicts_with = "fix",
conflicts_with = "diff",
)]
pub add_ignore: Option<String>,
/// See the files Ruff will be run against with the current settings.
#[arg(
long,
Expand Down Expand Up @@ -774,6 +797,7 @@ impl CheckCommand {
) -> anyhow::Result<(CheckArguments, ConfigArguments)> {
let check_arguments = CheckArguments {
add_noqa: self.add_noqa,
add_ignore: self.add_ignore,
diff: self.diff,
exit_non_zero_on_fix: self.exit_non_zero_on_fix,
exit_zero: self.exit_zero,
Expand Down Expand Up @@ -1112,6 +1136,7 @@ Possible choices:
#[expect(clippy::struct_excessive_bools)]
pub struct CheckArguments {
pub add_noqa: Option<String>,
pub add_ignore: Option<String>,
pub diff: bool,
pub exit_non_zero_on_fix: bool,
pub exit_zero: bool,
Expand Down
38 changes: 25 additions & 13 deletions crates/ruff/src/commands/add_noqa.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::path::PathBuf;
use std::time::Instant;

use anyhow::Result;
use anyhow::{Result, bail};
use log::{debug, error};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;

use ruff_linter::linter::add_noqa_to_path;
use ruff_linter::SuppressionKind;
use ruff_linter::linter::add_suppressions_to_path;
use ruff_linter::preview::is_human_readable_names_enabled;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
Expand All @@ -16,12 +18,13 @@ use ruff_workspace::resolver::{

use crate::args::ConfigArguments;

/// Add `noqa` directives to a collection of files.
/// Add suppression directives to a collection of files.
pub(crate) fn add_noqa(
files: &[PathBuf],
pyproject_config: &PyprojectConfig,
config_arguments: &ConfigArguments,
reason: Option<&str>,
suppression_kind: SuppressionKind,
) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
Expand Down Expand Up @@ -59,7 +62,7 @@ pub(crate) fn add_noqa(
let modifications: usize = paths
.par_iter()
.flatten()
.filter_map(|resolved_file| {
.map(|resolved_file| -> Result<usize> {
let source_type = SourceType::from(resolved_file.path());
let path = resolved_file.path();
let package = resolved_file
Expand All @@ -75,35 +78,44 @@ pub(crate) fn add_noqa(
&settings.linter.exclude,
)
{
return None;
return Ok(0);
}
if matches!(suppression_kind, SuppressionKind::Ignore)
&& !is_human_readable_names_enabled(settings.linter.preview)
{
bail!(
"`--add-ignore` requires preview mode, but preview is disabled for `{}`",
path.display()
);
}
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,
Ok(None) => return Ok(0),
Err(e) => {
error!("Failed to extract source from {}: {e}", path.display());
return None;
return Ok(0);
}
};
match add_noqa_to_path(
match add_suppressions_to_path(
path,
package,
&source_kind,
source_type.expect_python(),
&settings.linter,
reason,
suppression_kind,
) {
Ok(count) => Some(count),
Ok(count) => Ok(count),
Err(e) => {
error!("Failed to add noqa to {}: {e}", path.display());
None
error!("Failed to add suppression to {}: {e}", path.display());
Ok(0)
}
}
})
.sum();
.sum::<Result<usize>>()?;

let duration = start.elapsed();
debug!("Added noqa to files in: {duration:?}");
debug!("Added suppressions to files in: {duration:?}");

Ok(modifications)
}
33 changes: 26 additions & 7 deletions crates/ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use args::{GlobalConfigArgs, ServerCommand};
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_linter::logging::{LogLevel, set_up_logging};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::{fs, warn_user, warn_user_once};
use ruff_linter::{SuppressionKind, fs, warn_user, warn_user_once};
use ruff_workspace::Settings;

use crate::args::{
Expand Down Expand Up @@ -326,25 +326,44 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
warn_user!("Detected debug build without --no-cache.");
}

if let Some(reason) = &cli.add_noqa {
let suppression = cli
.add_noqa
.as_ref()
.map(|reason| (reason, SuppressionKind::Noqa, "--add-noqa"))
.or_else(|| {
cli.add_ignore
.as_ref()
.map(|reason| (reason, SuppressionKind::Ignore, "--add-ignore"))
});

if let Some((reason, suppression_kind, flag)) = suppression {
if !fix_mode.is_generate() {
warn_user!("--fix is incompatible with --add-noqa.");
warn_user!("--fix is incompatible with {flag}.");
}
if reason.contains(['\n', '\r']) {
return Err(anyhow::anyhow!(
"--add-noqa <reason> cannot contain newline characters"
"{flag} <reason> 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, reason_opt)?;
let modifications = commands::add_noqa::add_noqa(
&files,
&pyproject_config,
&config_arguments,
reason_opt,
suppression_kind,
)?;
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
let s = if modifications == 1 { "" } else { "s" };
let suppression = match suppression_kind {
SuppressionKind::Noqa => "noqa directive",
SuppressionKind::Ignore => "ignore comment",
};
#[expect(clippy::print_stderr)]
{
eprintln!("Added {modifications} noqa directive{s}.");
eprintln!("Added {modifications} {suppression}{s}.");
}
}
return Ok(ExitStatus::Success);
Expand Down
109 changes: 109 additions & 0 deletions crates/ruff/tests/cli/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,115 @@ fn add_noqa_with_newline_in_reason() -> Result<()> {
Ok(())
}

#[test]
fn add_ignore() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"noqa.py",
r#"
def first_square():
return [x * x for x in range(20)][0]
"#,
)?;

assert_cmd_snapshot!(
fixture
.check_command()
.arg("--select=RUF015")
.arg("--preview")
.arg("--add-ignore"),
@"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Added 1 ignore comment.
",
);

let test_code = fixture.read_file("noqa.py")?;

insta::assert_snapshot!(
test_code,
@"

def first_square():
return [x * x for x in range(20)][0] # ruff:ignore[unnecessary-iterable-allocation-for-first-element]
",
);

Ok(())
}

#[test]
fn add_ignore_requires_preview() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file("noqa.py", "import os\n")?;

assert_cmd_snapshot!(
fixture
.check_command()
.arg("--select=F401")
.arg("--add-ignore"),
@"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
ruff failed
Cause: `--add-ignore` requires preview mode, but preview is disabled for `[TMP]/noqa.py`
",
);

let test_code = fixture.read_file("noqa.py")?;
insta::assert_snapshot!(test_code, @"import os");

Ok(())
}

#[test]
fn add_noqa_existing_ignore() -> Result<()> {
let fixture = CliTest::new()?;
fixture.write_file(
"noqa.py",
r#"
def unused(x): # ruff:ignore[ANN001, ARG001, D103]
pass
"#,
)?;

assert_cmd_snapshot!(
fixture
.check_command()
.arg("--select=ANN001,ANN201,ARG001,D103")
.arg("--add-noqa"),
@"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
warning: #ruff:ignore comment found but not active, enable preview mode
Added 1 noqa directive.
",
);

let test_code = fixture.read_file("noqa.py")?;

insta::assert_snapshot!(
test_code,
@"

def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN001, ANN201, D103
pass
",
);

Ok(())
}

/// Infer `3.11` from `requires-python` in `pyproject.toml`.
#[test]
fn requires_python() -> Result<()> {
Expand Down
7 changes: 7 additions & 0 deletions crates/ruff/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ impl CliTest {
Ok(())
}

/// Reads a file from the test directory.
pub(crate) fn read_file(&self, path: impl AsRef<Path>) -> Result<String> {
let file_path = self.project_dir.join(path);
fs::read_to_string(&file_path)
.with_context(|| format!("Failed to read file `{}`", file_path.display()))
}

pub(crate) fn write_files<'a>(
&self,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! [Ruff]: https://github.com/astral-sh/ruff

pub use locator::Locator;
pub use noqa::generate_noqa_edits;
pub use noqa::{SuppressionKind, generate_suppression_edits};
#[cfg(feature = "clap")]
pub use registry::clap_completion::RuleParser;
#[cfg(feature = "clap")]
Expand Down
Loading
Loading