diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs index 9e8d0d66a5435..ad60966e466cf 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/red_knot/src/args.rs @@ -63,6 +63,14 @@ pub(crate) struct CheckCommand { #[clap(flatten)] pub(crate) rules: RulesArg, + /// Use exit code 1 if there are any warning-level diagnostics. + #[arg(long, conflicts_with = "exit_zero")] + pub(crate) error_on_warning: bool, + + /// Always use exit code 0, even when there are error-level diagnostics. + #[arg(long)] + pub(crate) exit_zero: bool, + /// Run in watch mode by re-running whenever files change. #[arg(long, short = 'W')] pub(crate) watch: bool, @@ -97,6 +105,7 @@ impl CheckCommand { ..EnvironmentOptions::default() }), rules, + error_on_warning: Some(self.error_on_warning), ..Default::default() } } diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 3f717434f42fe..dfe2d0f71abd6 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -12,7 +12,7 @@ use red_knot_project::watch; use red_knot_project::watch::ProjectWatcher; use red_knot_project::{ProjectDatabase, ProjectMetadata}; use red_knot_server::run_server; -use ruff_db::diagnostic::Diagnostic; +use ruff_db::diagnostic::{Diagnostic, Severity}; use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf}; use salsa::plumbing::ZalsaDatabase; @@ -84,6 +84,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result { let system = OsSystem::new(cwd); let watch = args.watch; + let exit_zero = args.exit_zero; let cli_options = args.into_options(); let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?; workspace_metadata.apply_cli_options(cli_options.clone()); @@ -112,7 +113,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result { std::mem::forget(db); - Ok(exit_status) + if exit_zero { + Ok(ExitStatus::Success) + } else { + Ok(exit_status) + } } #[derive(Copy, Clone)] @@ -213,7 +218,18 @@ impl MainLoop { result, revision: check_revision, } => { - let has_diagnostics = !result.is_empty(); + let (has_warnings, has_errors) = result.iter().fold( + (false, false), + |(has_warnings, has_errors), diagnostic| { + let severity = diagnostic.severity(); + + ( + has_warnings || severity == Severity::Warning, + has_errors || severity >= Severity::Error, + ) + }, + ); + if check_revision == revision { #[allow(clippy::print_stdout)] for diagnostic in result { @@ -226,10 +242,21 @@ impl MainLoop { } if self.watcher.is_none() { - return if has_diagnostics { - ExitStatus::Failure - } else { - ExitStatus::Success + let error_on_warning = + self.cli_options.error_on_warning.unwrap_or_default(); + + return match (has_warnings, has_errors) { + (false, false) => ExitStatus::Success, + + (true, false) => { + if error_on_warning { + ExitStatus::Failure + } else { + ExitStatus::Success + } + } + + (_, true) => ExitStatus::Failure, }; } diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index c114f24e3b722..1ab37ee5dc0f1 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -200,8 +200,8 @@ fn configuration_rule_severity() -> anyhow::Result<()> { )?; assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- warning[lint:division-by-zero] /test.py:2:5 Cannot divide object of type `Literal[4]` by zero @@ -251,8 +251,8 @@ fn cli_rule_severity() -> anyhow::Result<()> { .arg("--warn") .arg("unresolved-import"), @r" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- warning[lint:unresolved-import] /test.py:2:8 Cannot resolve import `does_not_exit` warning[lint:division-by-zero] /test.py:4:5 Cannot divide object of type `Literal[4]` by zero @@ -303,8 +303,8 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { .arg("--ignore") .arg("possibly-unresolved-reference"), @r" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- warning[lint:division-by-zero] /test.py:2:5 Cannot divide object of type `Literal[4]` by zero @@ -330,8 +330,8 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { ])?; assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- warning[unknown-rule] /pyproject.toml:3:1 Unknown lint rule `division-by-zer` @@ -347,10 +347,155 @@ fn cli_unknown_rules() -> anyhow::Result<()> { let case = TestCase::with_file("test.py", "print(10)")?; assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unknown-rule] Unknown lint rule `division-by-zer` + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_only_warnings() -> anyhow::Result<()> { + let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[lint:unresolved-reference] /test.py:1:7 Name `x` used when not defined + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_only_info() -> anyhow::Result<()> { + let case = TestCase::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type] /test.py:3:1 Revealed type is `Literal[1]` + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = TestCase::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type] /test.py:3:1 Revealed type is `Literal[1]` + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { + let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" success: false exit_code: 1 ----- stdout ----- - warning[unknown-rule] Unknown lint rule `division-by-zer` + warning[lint:unresolved-reference] /test.py:1:7 Name `x` used when not defined + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { + let case = TestCase::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[lint:unresolved-reference] /test.py:2:7 Name `x` used when not defined + error[lint:non-subscriptable] /test.py:3:7 Cannot subscript object of type `Literal[4]` with no `__getitem__` method + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = TestCase::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[lint:unresolved-reference] /test.py:2:7 Name `x` used when not defined + error[lint:non-subscriptable] /test.py:3:7 Cannot subscript object of type `Literal[4]` with no `__getitem__` method + + ----- stderr ----- + "); + + Ok(()) +} + +#[test] +fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { + let case = TestCase::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[lint:unresolved-reference] /test.py:2:7 Name `x` used when not defined + error[lint:non-subscriptable] /test.py:3:7 Cannot subscript object of type `Literal[4]` with no `__getitem__` method ----- stderr ----- "); diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index 13c27837c75f6..a2da43ce80e11 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -27,6 +27,9 @@ pub struct Options { #[serde(skip_serializing_if = "Option::is_none")] pub rules: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub error_on_warning: Option, } impl Options {