Skip to content

Commit 9020e41

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

11 files changed

Lines changed: 184 additions & 56 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().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: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {
114114
project.is_file_open(self, file)
115115
}
116116

117-
fn rule_selection(&self) -> &RuleSelection {
118-
self.project().rule_selection(self)
117+
fn rule_selection(&self) -> Arc<RuleSelection> {
118+
self.project().rules(self)
119119
}
120120

121121
fn lint_registry(&self) -> &LintRegistry {
@@ -186,7 +186,6 @@ pub(crate) mod tests {
186186
files: Files,
187187
system: TestSystem,
188188
vendored: VendoredFileSystem,
189-
rule_selection: RuleSelection,
190189
project: Option<Project>,
191190
}
192191

@@ -198,7 +197,6 @@ pub(crate) mod tests {
198197
vendored: red_knot_vendored::file_system().clone(),
199198
files: Files::default(),
200199
events: Arc::default(),
201-
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
202200
project: None,
203201
};
204202

@@ -270,8 +268,8 @@ pub(crate) mod tests {
270268
!file.path(self).is_vendored_path()
271269
}
272270

273-
fn rule_selection(&self) -> &RuleSelection {
274-
&self.rule_selection
271+
fn rule_selection(&self) -> Arc<RuleSelection> {
272+
self.project().rules(self)
275273
}
276274

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

crates/red_knot_project/src/lib.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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};
78
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
89
use red_knot_python_semantic::register_lints;
@@ -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)
@@ -86,30 +97,37 @@ impl Project {
8697
self.metadata(db).name()
8798
}
8899

100+
/// Returns the resolved linter rules for the project.
101+
///
102+
/// This is a salsa query to prevent re-computing queries if other, unrelated
103+
/// settings change. For example, we don't want that changing the terminal settings
104+
/// invalidates any type checking queries.
105+
#[salsa::tracked]
106+
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
107+
self.settings(db).to_rules()
108+
}
109+
89110
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
90111
tracing::debug!("Reloading project");
91112
assert_eq!(self.root(db), metadata.root());
92113

93114
if &metadata != self.metadata(db) {
115+
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
116+
117+
if self.settings(db) != &settings {
118+
self.set_settings(db).to(settings);
119+
}
120+
121+
if self.settings_diagnostics(db) != &settings_diagnostics {
122+
self.set_settings_diagnostics(db).to(settings_diagnostics);
123+
}
124+
94125
self.set_metadata(db).to(metadata);
95126
}
96127

97128
self.reload_files(db);
98129
}
99130

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-
113131
/// Checks all open files in the project and its dependencies.
114132
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
115133
let project_span = tracing::debug_span!("Project::check");
@@ -118,8 +136,7 @@ impl Project {
118136
tracing::debug!("Checking project '{name}'", name = self.name(db));
119137

120138
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| {
139+
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
123140
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
124141
diagnostic
125142
}));
@@ -151,9 +168,8 @@ impl Project {
151168
}
152169

153170
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
171+
let mut file_diagnostics: Vec<_> = self
172+
.settings_diagnostics(db)
157173
.iter()
158174
.map(|diagnostic| {
159175
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: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use std::borrow::Cow;
1515
use std::fmt::Debug;
1616
use thiserror::Error;
1717

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

2830
#[serde(skip_serializing_if = "Option::is_none")]
2931
pub rules: Option<Rules>,
32+
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub terminal: Option<TerminalOptions>,
3035
}
3136

3237
impl Options {
@@ -107,7 +112,22 @@ impl Options {
107112
}
108113

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

@@ -226,6 +246,15 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
226246
}
227247
}
228248

249+
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
250+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
251+
pub struct TerminalOptions {
252+
/// Use exit code 1 if there are any warning-level diagnostics.
253+
///
254+
/// Defaults to `false`.
255+
pub error_on_warning: Option<bool>,
256+
}
257+
229258
#[derive(Error, Debug)]
230259
pub enum KnotTomlError {
231260
#[error(transparent)]

0 commit comments

Comments
 (0)