Skip to content

Commit

Permalink
[red-knot] Support --exit-zero and --error-on-warning (#15746)
Browse files Browse the repository at this point in the history
Co-authored-by: Micha Reiser <[email protected]>
  • Loading branch information
InSyncWithFoo and MichaReiser authored Feb 3, 2025
1 parent a613345 commit 30d5e9a
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 15 deletions.
8 changes: 8 additions & 0 deletions crates/red_knot/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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,
Expand Down
35 changes: 29 additions & 6 deletions crates/red_knot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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;

Expand Down Expand Up @@ -96,13 +96,20 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {

let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let min_error_severity = if args.error_on_warning {
Severity::Warning
} else {
Severity::Error
};

let cli_options = args.into_options();
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());

let mut db = ProjectDatabase::new(workspace_metadata, system)?;

let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);

// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
Expand All @@ -124,7 +131,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {

std::mem::forget(db);

Ok(exit_status)
if exit_zero {
Ok(ExitStatus::Success)
} else {
Ok(exit_status)
}
}

#[derive(Copy, Clone)]
Expand Down Expand Up @@ -156,10 +167,18 @@ struct MainLoop {
watcher: Option<ProjectWatcher>,

cli_options: Options,

/// The minimum severity to consider an error when deciding the exit status.
///
/// TODO(micha): Get from the terminal settings.
min_error_severity: Severity,
}

impl MainLoop {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
fn new(
cli_options: Options,
min_error_severity: Severity,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);

(
Expand All @@ -168,6 +187,7 @@ impl MainLoop {
receiver,
watcher: None,
cli_options,
min_error_severity,
},
MainLoopCancellationToken { sender },
)
Expand Down Expand Up @@ -225,7 +245,10 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let has_diagnostics = !result.is_empty();
let failed = result
.iter()
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);

if check_revision == revision {
#[allow(clippy::print_stdout)]
for diagnostic in result {
Expand All @@ -238,7 +261,7 @@ impl MainLoop {
}

if self.watcher.is_none() {
return if has_diagnostics {
return if failed {
ExitStatus::Failure
} else {
ExitStatus::Success
Expand Down
231 changes: 222 additions & 9 deletions crates/red_knot/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,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
--> <temp_dir>/test.py:2:5
Expand Down Expand Up @@ -316,8 +316,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
--> <temp_dir>/test.py:2:8
Expand Down Expand Up @@ -402,8 +402,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
--> <temp_dir>/test.py:2:5
Expand Down Expand Up @@ -437,8 +437,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
--> <temp_dir>/pyproject.toml:3:1
Expand All @@ -461,10 +461,223 @@ 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
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - 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
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| -------------- info: 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
--> <temp_dir>/test.py:3:1
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| -------------- info: 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
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - 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
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ 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
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ 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
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
----- stderr -----
Expand Down

0 comments on commit 30d5e9a

Please sign in to comment.