Skip to content

Commit f84f5cf

Browse files
committed
[red-knot] Resolve Options to Settings
1 parent 5588c75 commit f84f5cf

8 files changed

Lines changed: 133 additions & 43 deletions

File tree

crates/red_knot/src/args.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::logging::Verbosity;
22
use crate::python_version::PythonVersion;
33
use clap::{ArgAction, ArgMatches, Error, Parser};
4-
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
4+
use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
55
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
66
use red_knot_python_semantic::lint;
77
use ruff_db::system::SystemPathBuf;
@@ -67,8 +67,8 @@ pub(crate) struct CheckCommand {
6767
pub(crate) rules: RulesArg,
6868

6969
/// Use exit code 1 if there are any warning-level diagnostics.
70-
#[arg(long, conflicts_with = "exit_zero")]
71-
pub(crate) error_on_warning: bool,
70+
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
71+
pub(crate) error_on_warning: Option<bool>,
7272

7373
/// Always use exit code 0, even when there are error-level diagnostics.
7474
#[arg(long)]
@@ -107,6 +107,9 @@ impl CheckCommand {
107107
}),
108108
..EnvironmentOptions::default()
109109
}),
110+
terminal: Some(TerminalOptions {
111+
error_on_warning: self.error_on_warning,
112+
}),
110113
rules,
111114
..Default::default()
112115
}

crates/red_knot/src/main.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use clap::Parser;
1111
use colored::Colorize;
1212
use crossbeam::channel as crossbeam_channel;
1313
use red_knot_project::metadata::options::Options;
14-
use red_knot_project::watch;
1514
use red_knot_project::watch::ProjectWatcher;
15+
use red_knot_project::{watch, Db};
1616
use red_knot_project::{ProjectDatabase, ProjectMetadata};
1717
use red_knot_server::run_server;
1818
use ruff_db::diagnostic::{Diagnostic, Severity};
@@ -97,19 +97,14 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
9797
let system = OsSystem::new(cwd);
9898
let watch = args.watch;
9999
let exit_zero = args.exit_zero;
100-
let min_error_severity = if args.error_on_warning {
101-
Severity::Warning
102-
} else {
103-
Severity::Error
104-
};
105100

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

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

112-
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
107+
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
113108

114109
// Listen to Ctrl+C and abort the watch mode.
115110
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -167,18 +162,10 @@ struct MainLoop {
167162
watcher: Option<ProjectWatcher>,
168163

169164
cli_options: Options,
170-
171-
/// The minimum severity to consider an error when deciding the exit status.
172-
///
173-
/// TODO(micha): Get from the terminal settings.
174-
min_error_severity: Severity,
175165
}
176166

177167
impl MainLoop {
178-
fn new(
179-
cli_options: Options,
180-
min_error_severity: Severity,
181-
) -> (Self, MainLoopCancellationToken) {
168+
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
182169
let (sender, receiver) = crossbeam_channel::bounded(10);
183170

184171
(
@@ -187,7 +174,6 @@ impl MainLoop {
187174
receiver,
188175
watcher: None,
189176
cli_options,
190-
min_error_severity,
191177
},
192178
MainLoopCancellationToken { sender },
193179
)
@@ -245,9 +231,16 @@ impl MainLoop {
245231
result,
246232
revision: check_revision,
247233
} => {
234+
let min_error_severity =
235+
if db.project().settings(db).terminal(db).error_on_warning {
236+
Severity::Warning
237+
} else {
238+
Severity::Error
239+
};
240+
248241
let failed = result
249242
.iter()
250-
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
243+
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
251244

252245
if check_revision == revision {
253246
#[allow(clippy::print_stdout)]

crates/red_knot/tests/cli.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,37 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
575575
Ok(())
576576
}
577577

578+
#[test]
579+
fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
580+
let case = TestCase::with_files([
581+
("test.py", r"print(x) # [unresolved-reference]"),
582+
(
583+
"knot.toml",
584+
r#"
585+
[terminal]
586+
error-on-warning = true
587+
"#,
588+
),
589+
])?;
590+
591+
assert_cmd_snapshot!(case.command(), @r###"
592+
success: false
593+
exit_code: 1
594+
----- stdout -----
595+
warning: lint:unresolved-reference
596+
--> <temp_dir>/test.py:1:7
597+
|
598+
1 | print(x) # [unresolved-reference]
599+
| - Name `x` used when not defined
600+
|
601+
602+
603+
----- stderr -----
604+
"###);
605+
606+
Ok(())
607+
}
608+
578609
#[test]
579610
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
580611
let case = TestCase::with_file(

crates/red_knot_project/src/db.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ impl SemanticDb for ProjectDatabase {
115115
}
116116

117117
fn rule_selection(&self) -> &RuleSelection {
118-
self.project().rule_selection(self)
118+
self.project().settings(self).rules(self)
119119
}
120120

121121
fn lint_registry(&self) -> &LintRegistry {

crates/red_knot_project/src/lib.rs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
use crate::metadata::options::OptionDiagnostic;
44
pub use db::{Db, ProjectDatabase};
55
use files::{Index, Indexed, IndexedFiles};
6+
use metadata::settings::Settings;
67
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
7-
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
8+
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
89
use red_knot_python_semantic::register_lints;
910
use red_knot_python_semantic::types::check_types;
1011
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
@@ -66,12 +67,22 @@ pub struct Project {
6667
/// The metadata describing the project, including the unresolved options.
6768
#[return_ref]
6869
pub metadata: ProjectMetadata,
70+
71+
/// The resolved project settings.
72+
#[return_ref]
73+
pub settings: Settings,
74+
75+
/// Diagnostics that were generated when resolving the project settings.
76+
#[return_ref]
77+
settings_diagnostics: Vec<OptionDiagnostic>,
6978
}
7079

7180
#[salsa::tracked]
7281
impl Project {
7382
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
74-
Project::builder(metadata)
83+
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
84+
85+
Project::builder(metadata, settings, settings_diagnostics)
7586
.durability(Durability::MEDIUM)
7687
.open_fileset_durability(Durability::LOW)
7788
.file_set_durability(Durability::LOW)
@@ -91,25 +102,22 @@ impl Project {
91102
assert_eq!(self.root(db), metadata.root());
92103

93104
if &metadata != self.metadata(db) {
105+
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
106+
107+
if self.settings(db) != &settings {
108+
self.set_settings(db).to(settings);
109+
}
110+
111+
if self.settings_diagnostics(db) != &settings_diagnostics {
112+
self.set_settings_diagnostics(db).to(settings_diagnostics);
113+
}
114+
94115
self.set_metadata(db).to(metadata);
95116
}
96117

97118
self.reload_files(db);
98119
}
99120

100-
pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {
101-
let (selection, _) = self.rule_selection_with_diagnostics(db);
102-
selection
103-
}
104-
105-
#[salsa::tracked(return_ref)]
106-
fn rule_selection_with_diagnostics(
107-
self,
108-
db: &dyn Db,
109-
) -> (RuleSelection, Vec<OptionDiagnostic>) {
110-
self.metadata(db).options().to_rule_selection(db)
111-
}
112-
113121
/// Checks all open files in the project and its dependencies.
114122
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
115123
let project_span = tracing::debug_span!("Project::check");
@@ -118,8 +126,7 @@ impl Project {
118126
tracing::debug!("Checking project '{name}'", name = self.name(db));
119127

120128
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
121-
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
122-
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
129+
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
123130
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
124131
diagnostic
125132
}));
@@ -151,9 +158,8 @@ impl Project {
151158
}
152159

153160
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
154-
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
155-
156-
let mut file_diagnostics: Vec<_> = options_diagnostics
161+
let mut file_diagnostics: Vec<_> = self
162+
.settings_diagnostics(db)
157163
.iter()
158164
.map(|diagnostic| {
159165
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());

crates/red_knot_project/src/metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use options::Options;
1212

1313
pub mod options;
1414
pub mod pyproject;
15+
pub mod settings;
1516
pub mod value;
1617

1718
#[derive(Debug, PartialEq, Eq)]

crates/red_knot_project/src/metadata/options.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ use ruff_db::system::{System, SystemPath};
1010
use ruff_macros::Combine;
1111
use ruff_text_size::TextRange;
1212
use rustc_hash::FxHashMap;
13+
use salsa::Durability;
1314
use serde::{Deserialize, Serialize};
1415
use std::borrow::Cow;
1516
use std::fmt::Debug;
1617
use thiserror::Error;
1718

19+
use super::settings::{Settings, TerminalSettings};
20+
1821
/// The options for the project.
1922
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
2023
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@@ -27,6 +30,9 @@ pub struct Options {
2730

2831
#[serde(skip_serializing_if = "Option::is_none")]
2932
pub rules: Option<Rules>,
33+
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
pub terminal: Option<TerminalOptions>,
3036
}
3137

3238
impl Options {
@@ -107,7 +113,22 @@ impl Options {
107113
}
108114

109115
#[must_use]
110-
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
116+
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
117+
let (rules, diagnostics) = self.to_rule_selection(db);
118+
119+
let mut settings = Settings::builder(rules).durability(Durability::MEDIUM);
120+
121+
if let Some(terminal) = self.terminal.as_ref() {
122+
settings = settings.terminal(TerminalSettings {
123+
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
124+
});
125+
}
126+
127+
(settings.new(db), diagnostics)
128+
}
129+
130+
#[must_use]
131+
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
111132
let registry = db.lint_registry();
112133
let mut diagnostics = Vec::new();
113134

@@ -226,6 +247,15 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
226247
}
227248
}
228249

250+
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
251+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
252+
pub struct TerminalOptions {
253+
/// Use exit code 1 if there are any warning-level diagnostics.
254+
///
255+
/// Defaults to `false`.
256+
pub error_on_warning: Option<bool>,
257+
}
258+
229259
#[derive(Error, Debug)]
230260
pub enum KnotTomlError {
231261
#[error(transparent)]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use red_knot_python_semantic::lint::RuleSelection;
2+
3+
/// The resolved [`Options`] for the project.
4+
///
5+
/// Unlike [`Options`], the struct has default values filled in and
6+
/// uses representations that are optimized for reads (instead of preserving the source representation).
7+
///
8+
/// The struct is a salsa-input for better caching. Changing the
9+
/// terminal settings shouldn't invalidate queries that only read
10+
/// the rule selection.
11+
///
12+
/// Settings that are part of [`ProgramSettings`] are not included here.
13+
#[salsa::input]
14+
pub struct Settings {
15+
#[return_ref]
16+
pub rules: RuleSelection,
17+
18+
#[default]
19+
#[return_ref]
20+
pub terminal: TerminalSettings,
21+
}
22+
23+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
24+
pub struct TerminalSettings {
25+
pub error_on_warning: bool,
26+
}

0 commit comments

Comments
 (0)