From 2bcd2b41470706fac9e9e995be3721caf2271b02 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 14 Mar 2025 10:55:00 -0400 Subject: [PATCH] red_knot: plumb through `DiagnosticFormat` to the CLI The CLI calls this `OutputFormat`, and so does the type where the CLI is defined. But it's called `DiagnosticFormat` in `ruff_db` to be consistent with `DisplayDiagnosticConfig`. Ref https://github.com/astral-sh/ruff/issues/15697#issuecomment-2706477278 --- crates/red_knot/src/args.rs | 36 +++++++++++++++++++ crates/red_knot/src/main.rs | 1 + crates/red_knot/tests/cli.rs | 24 +++++++++++++ .../red_knot_project/src/metadata/options.rs | 12 ++++++- .../red_knot_project/src/metadata/settings.rs | 2 ++ knot.schema.json | 30 ++++++++++++++++ 6 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/red_knot/src/args.rs b/crates/red_knot/src/args.rs index 7b4a0319f4..ffcf5ca911 100644 --- a/crates/red_knot/src/args.rs +++ b/crates/red_knot/src/args.rs @@ -75,6 +75,10 @@ pub(crate) struct CheckCommand { #[clap(flatten)] pub(crate) rules: RulesArg, + /// The format to use for printing diagnostic messages. + #[arg(long)] + pub(crate) output_format: Option, + /// Use exit code 1 if there are any warning-level diagnostics. #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] pub(crate) error_on_warning: Option, @@ -117,6 +121,9 @@ impl CheckCommand { ..EnvironmentOptions::default() }), terminal: Some(TerminalOptions { + output_format: self + .output_format + .map(|output_format| RangedValue::cli(output_format.into())), error_on_warning: self.error_on_warning, }), rules, @@ -211,3 +218,32 @@ impl clap::Args for RulesArg { Self::augment_args(cmd) } } + +/// The diagnostic output format. +#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)] +pub enum OutputFormat { + /// Print diagnostics verbosely, with context and helpful hints. + /// + /// Diagnostic messages may include additional context and + /// annotations on the input to help understand the message. + #[default] + #[value(name = "full")] + Full, + /// Print diagnostics concisely, one per line. + /// + /// This will guarantee that each diagnostic is printed on + /// a single line. Only the most important or primary aspects + /// of the diagnostic are included. Contextual information is + /// dropped. + #[value(name = "concise")] + Concise, +} + +impl From for ruff_db::diagnostic::DiagnosticFormat { + fn from(format: OutputFormat) -> ruff_db::diagnostic::DiagnosticFormat { + match format { + OutputFormat::Full => Self::Full, + OutputFormat::Concise => Self::Concise, + } + } +} diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index a9c132894e..0bb8b26efa 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -256,6 +256,7 @@ impl MainLoop { revision: check_revision, } => { let display_config = DisplayDiagnosticConfig::default() + .format(db.project().settings(db).terminal().output_format) .color(colored::control::SHOULD_COLORIZE.should_colorize()); let min_error_severity = diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 7369654de9..d4efa7b967 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -967,6 +967,30 @@ fn check_non_existing_path() -> anyhow::Result<()> { Ok(()) } +#[test] +fn concise_diagnostics() -> 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("--output-format=concise"), @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 + Found 2 diagnostics + + ----- stderr ----- + "); + + Ok(()) +} + struct TestCase { _temp_dir: TempDir, _settings_scope: SettingsBindDropGuard, diff --git a/crates/red_knot_project/src/metadata/options.rs b/crates/red_knot_project/src/metadata/options.rs index 7201d81d8e..f5a106df43 100644 --- a/crates/red_knot_project/src/metadata/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -2,7 +2,7 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSou use crate::Db; use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection}; use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings}; -use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span}; +use ruff_db::diagnostic::{DiagnosticFormat, DiagnosticId, OldDiagnosticTrait, Severity, Span}; use ruff_db::files::system_path_to_file; use ruff_db::system::{System, SystemPath}; use ruff_macros::Combine; @@ -120,6 +120,11 @@ impl Options { if let Some(terminal) = self.terminal.as_ref() { settings.set_terminal(TerminalSettings { + output_format: terminal + .output_format + .as_deref() + .copied() + .unwrap_or_default(), error_on_warning: terminal.error_on_warning.unwrap_or_default(), }); } @@ -277,6 +282,11 @@ impl FromIterator<(RangedValue, RangedValue)> for Rules { #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct TerminalOptions { + /// The format to use for printing diagnostic messages. + /// + /// Defaults to `full`. + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option>, /// Use exit code 1 if there are any warning-level diagnostics. /// /// Defaults to `false`. diff --git a/crates/red_knot_project/src/metadata/settings.rs b/crates/red_knot_project/src/metadata/settings.rs index c29d23ed61..e16a72d4a0 100644 --- a/crates/red_knot_project/src/metadata/settings.rs +++ b/crates/red_knot_project/src/metadata/settings.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use red_knot_python_semantic::lint::RuleSelection; +use ruff_db::diagnostic::DiagnosticFormat; /// The resolved [`super::Options`] for the project. /// @@ -49,5 +50,6 @@ impl Settings { #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct TerminalSettings { + pub output_format: DiagnosticFormat, pub error_on_warning: bool, } diff --git a/knot.schema.json b/knot.schema.json index 43591dce0b..b5dbbb921d 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -49,6 +49,25 @@ }, "additionalProperties": false, "definitions": { + "DiagnosticFormat": { + "description": "The diagnostic output format.", + "oneOf": [ + { + "description": "The default full mode will print \"pretty\" diagnostics.\n\nThat is, color will be used when printing to a `tty`. Moreover, diagnostic messages may include additional context and annotations on the input to help understand the message.", + "type": "string", + "enum": [ + "full" + ] + }, + { + "description": "Print diagnostics in a concise mode.\n\nThis will guarantee that each diagnostic is printed on a single line. Only the most important or primary aspects of the diagnostic are included. Contextual information is dropped.\n\nThis may use color when printing to a `tty`.", + "type": "string", + "enum": [ + "concise" + ] + } + ] + }, "EnvironmentOptions": { "type": "object", "properties": { @@ -748,6 +767,17 @@ "boolean", "null" ] + }, + "output-format": { + "description": "The format to use for printing diagnostic messages.\n\nDefaults to `full`.", + "anyOf": [ + { + "$ref": "#/definitions/DiagnosticFormat" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false