Skip to content

Commit 5fa96e4

Browse files
committed
Use RangedValue in Options
1 parent 10b8841 commit 5fa96e4

File tree

10 files changed

+123
-53
lines changed

10 files changed

+123
-53
lines changed

crates/red_knot/src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use colored::Colorize;
77
use crossbeam::channel as crossbeam_channel;
88
use python_version::PythonVersion;
99
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
10-
use red_knot_project::metadata::value::RelativePathBuf;
10+
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
1111
use red_knot_project::watch;
1212
use red_knot_project::watch::ProjectWatcher;
1313
use red_knot_project::{ProjectDatabase, ProjectMetadata};
@@ -73,7 +73,9 @@ impl Args {
7373
fn to_options(&self) -> Options {
7474
Options {
7575
environment: Some(EnvironmentOptions {
76-
python_version: self.python_version.map(Into::into),
76+
python_version: self
77+
.python_version
78+
.map(|version| RangedValue::cli(version.into())),
7779
venv_path: self.venv_path.as_ref().map(RelativePathBuf::cli),
7880
typeshed: self.typeshed.as_ref().map(RelativePathBuf::cli),
7981
extra_paths: self.extra_search_path.as_ref().map(|extra_search_paths| {

crates/red_knot/tests/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ division-by-zer = "warn" # incorrect rule name
255255
success: false
256256
exit_code: 1
257257
----- stdout -----
258-
warning[unknown-rule] Unknown lint rule `division-by-zer`
258+
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
259259
260260
----- stderr -----
261261
");

crates/red_knot/tests/file_watching.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
66
use anyhow::{anyhow, Context};
77
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
88
use red_knot_project::metadata::pyproject::{PyProject, Tool};
9-
use red_knot_project::metadata::value::RelativePathBuf;
9+
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
1010
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
1111
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
1212
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
@@ -897,8 +897,10 @@ print(sys.last_exc, os.getegid())
897897
|_root_path, _project_path| {
898898
Some(Options {
899899
environment: Some(EnvironmentOptions {
900-
python_version: Some(PythonVersion::PY311),
901-
python_platform: Some(PythonPlatform::Identifier("win32".to_string())),
900+
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
901+
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
902+
"win32".to_string(),
903+
))),
902904
..EnvironmentOptions::default()
903905
}),
904906
..Options::default()
@@ -921,8 +923,10 @@ print(sys.last_exc, os.getegid())
921923
// Change the python version
922924
case.update_options(Options {
923925
environment: Some(EnvironmentOptions {
924-
python_version: Some(PythonVersion::PY312),
925-
python_platform: Some(PythonPlatform::Identifier("linux".to_string())),
926+
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
927+
python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
928+
"linux".to_string(),
929+
))),
926930
..EnvironmentOptions::default()
927931
}),
928932
..Options::default()
@@ -1382,7 +1386,7 @@ mod unix {
13821386
extra_paths: Some(vec![RelativePathBuf::cli(
13831387
".venv/lib/python3.12/site-packages",
13841388
)]),
1385-
python_version: Some(PythonVersion::PY312),
1389+
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
13861390
..EnvironmentOptions::default()
13871391
}),
13881392
..Options::default()

crates/red_knot_project/src/metadata.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl ProjectMetadata {
5555
) -> Self {
5656
let name = project
5757
.and_then(|project| project.name.as_ref())
58-
.map(|name| Name::new(&**name))
58+
.map(|name| Name::new(&***name))
5959
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
6060

6161
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python

crates/red_knot_project/src/metadata/options.rs

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use crate::metadata::value::{RelativePathBuf, ValueSource, ValueSourceGuard};
1+
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
22
use crate::Db;
3-
use red_knot_python_semantic::lint::{GetLintError, Level, RuleSelection};
3+
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
44
use red_knot_python_semantic::{
55
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
66
};
77
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
8-
use ruff_db::files::File;
8+
use ruff_db::files::{system_path_to_file, File};
99
use ruff_db::system::{System, SystemPath};
1010
use ruff_macros::Combine;
1111
use ruff_text_size::TextRange;
@@ -44,7 +44,12 @@ impl Options {
4444
let (python_version, python_platform) = self
4545
.environment
4646
.as_ref()
47-
.map(|env| (env.python_version, env.python_platform.as_ref()))
47+
.map(|env| {
48+
(
49+
env.python_version.as_deref().copied(),
50+
env.python_platform.as_deref(),
51+
)
52+
})
4853
.unwrap_or_default();
4954

5055
ProgramSettings {
@@ -116,27 +121,42 @@ impl Options {
116121
.flat_map(|rules| rules.inner.iter());
117122

118123
for (rule_name, level) in rules {
124+
let source = rule_name.source();
119125
match registry.get(rule_name) {
120126
Ok(lint) => {
121-
if let Ok(severity) = Severity::try_from(*level) {
122-
selection.enable(lint, severity);
127+
let lint_source = match source {
128+
ValueSource::File(_) => LintSource::File,
129+
ValueSource::Cli => LintSource::Cli,
130+
};
131+
if let Ok(severity) = Severity::try_from(**level) {
132+
selection.enable(lint, severity, lint_source);
123133
} else {
124134
selection.disable(lint);
125135
}
126136
}
127-
Err(GetLintError::Unknown(_)) => {
128-
diagnostics.push(OptionDiagnostic::new(
129-
DiagnosticId::UnknownRule,
130-
format!("Unknown lint rule `{rule_name}`"),
131-
Severity::Warning,
132-
));
133-
}
134-
Err(GetLintError::Removed(_)) => {
135-
diagnostics.push(OptionDiagnostic::new(
136-
DiagnosticId::UnknownRule,
137-
format!("The lint rule `{rule_name}` has been removed and is no longer supported"),
138-
Severity::Warning,
139-
));
137+
Err(error) => {
138+
// `system_path_to_file` can return `Err` if the file was deleted since the configuration
139+
// was read. This should be rare and it should be okay to default to not showing a configuration
140+
// file in that case.
141+
let file = source
142+
.file()
143+
.and_then(|path| system_path_to_file(db.upcast(), path).ok());
144+
145+
// TODO: Add a note if the value was configured on the CLI
146+
let diagnostic = match error {
147+
GetLintError::Unknown(_) => OptionDiagnostic::new(
148+
DiagnosticId::UnknownRule,
149+
format!("Unknown lint rule `{rule_name}`"),
150+
Severity::Warning,
151+
),
152+
GetLintError::Removed(_) => OptionDiagnostic::new(
153+
DiagnosticId::UnknownRule,
154+
format!("Unknown lint rule `{rule_name}`"),
155+
Severity::Warning,
156+
),
157+
};
158+
159+
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
140160
}
141161
}
142162
}
@@ -149,10 +169,10 @@ impl Options {
149169
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
150170
pub struct EnvironmentOptions {
151171
#[serde(skip_serializing_if = "Option::is_none")]
152-
pub python_version: Option<PythonVersion>,
172+
pub python_version: Option<RangedValue<PythonVersion>>,
153173

154174
#[serde(skip_serializing_if = "Option::is_none")]
155-
pub python_platform: Option<PythonPlatform>,
175+
pub python_platform: Option<RangedValue<PythonPlatform>>,
156176

157177
/// List of user-provided paths that should take first priority in the module resolution.
158178
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
@@ -183,7 +203,7 @@ pub struct SrcOptions {
183203
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
184204
#[serde(rename_all = "kebab-case", transparent)]
185205
pub struct Rules {
186-
inner: FxHashMap<String, Level>,
206+
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
187207
}
188208

189209
#[derive(Error, Debug)]
@@ -197,6 +217,8 @@ pub struct OptionDiagnostic {
197217
id: DiagnosticId,
198218
message: String,
199219
severity: Severity,
220+
file: Option<File>,
221+
range: Option<TextRange>,
200222
}
201223

202224
impl OptionDiagnostic {
@@ -205,8 +227,22 @@ impl OptionDiagnostic {
205227
id,
206228
message,
207229
severity,
230+
file: None,
231+
range: None,
208232
}
209233
}
234+
235+
#[must_use]
236+
fn with_file(mut self, file: Option<File>) -> Self {
237+
self.file = file;
238+
self
239+
}
240+
241+
#[must_use]
242+
fn with_range(mut self, range: Option<TextRange>) -> Self {
243+
self.range = range;
244+
self
245+
}
210246
}
211247

212248
impl Diagnostic for OptionDiagnostic {
@@ -219,11 +255,11 @@ impl Diagnostic for OptionDiagnostic {
219255
}
220256

221257
fn file(&self) -> Option<File> {
222-
None
258+
self.file
223259
}
224260

225261
fn range(&self) -> Option<TextRange> {
226-
None
262+
self.range
227263
}
228264

229265
fn severity(&self) -> Severity {

crates/red_knot_project/src/metadata/pyproject.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::ops::Deref;
44
use thiserror::Error;
55

66
use crate::metadata::options::Options;
7-
use crate::metadata::value::{ValueSource, ValueSourceGuard};
7+
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
88

99
/// A `pyproject.toml` as specified in PEP 517.
1010
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
@@ -48,11 +48,11 @@ pub struct Project {
4848
///
4949
/// Note: Intentionally option to be more permissive during deserialization.
5050
/// `PackageMetadata::from_pyproject` reports missing names.
51-
pub name: Option<PackageName>,
51+
pub name: Option<RangedValue<PackageName>>,
5252
/// The version of the project
53-
pub version: Option<Version>,
53+
pub version: Option<RangedValue<Version>>,
5454
/// The Python versions this project is compatible with.
55-
pub requires_python: Option<VersionSpecifiers>,
55+
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
5656
}
5757

5858
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]

crates/red_knot_project/src/metadata/value.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ pub enum ValueSource {
2323
Cli,
2424
}
2525

26+
impl ValueSource {
27+
pub fn file(&self) -> Option<&SystemPath> {
28+
match self {
29+
ValueSource::File(path) => Some(&**path),
30+
ValueSource::Cli => None,
31+
}
32+
}
33+
}
34+
2635
impl fmt::Display for ValueSource {
2736
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2837
match self {
@@ -62,7 +71,7 @@ impl Drop for ValueSourceGuard {
6271
}
6372
}
6473

65-
/// A configuration value that tracks where it originates from and the range in the source document.
74+
/// A value that "remembers" where it comes from (source) and its range in source.
6675
///
6776
/// ## Equality, Hash, and Ordering
6877
/// The equality, hash, and ordering are solely based on the value. They disregard the value's range
@@ -74,6 +83,11 @@ impl Drop for ValueSourceGuard {
7483
pub struct RangedValue<T> {
7584
value: T,
7685
source: ValueSource,
86+
87+
/// The byte range of `value` in `source`.
88+
///
89+
/// Can be `None` because not all sources support a range.
90+
/// For example, arguments provided on the CLI won't have a range attached.
7791
range: Option<TextRange>,
7892
}
7993

@@ -98,6 +112,10 @@ impl<T> RangedValue<T> {
98112
self.range
99113
}
100114

115+
pub fn source(&self) -> &ValueSource {
116+
&self.source
117+
}
118+
101119
#[must_use]
102120
pub fn with_source(mut self, source: ValueSource) -> Self {
103121
self.source = source;

crates/red_knot_python_semantic/src/lint.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ pub struct RuleSelection {
414414
/// Map with the severity for each enabled lint rule.
415415
///
416416
/// If a rule isn't present in this map, then it should be considered disabled.
417-
lints: FxHashMap<LintId, Severity>,
417+
lints: FxHashMap<LintId, (Severity, LintSource)>,
418418
}
419419

420420
impl RuleSelection {
@@ -427,7 +427,7 @@ impl RuleSelection {
427427
.filter_map(|lint| {
428428
Severity::try_from(lint.default_level())
429429
.ok()
430-
.map(|severity| (*lint, severity))
430+
.map(|severity| (*lint, (severity, LintSource::Default)))
431431
})
432432
.collect();
433433

@@ -441,12 +441,14 @@ impl RuleSelection {
441441

442442
/// Returns an iterator over all enabled lints and their severity.
443443
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
444-
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
444+
self.lints
445+
.iter()
446+
.map(|(&lint, &(severity, _))| (lint, severity))
445447
}
446448

447449
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
448450
pub fn severity(&self, lint: LintId) -> Option<Severity> {
449-
self.lints.get(&lint).copied()
451+
self.lints.get(&lint).map(|(severity, _)| *severity)
450452
}
451453

452454
/// Returns `true` if the `lint` is enabled.
@@ -457,19 +459,25 @@ impl RuleSelection {
457459
/// Enables `lint` and configures with the given `severity`.
458460
///
459461
/// Overrides any previous configuration for the lint.
460-
pub fn enable(&mut self, lint: LintId, severity: Severity) {
461-
self.lints.insert(lint, severity);
462+
pub fn enable(&mut self, lint: LintId, severity: Severity, source: LintSource) {
463+
self.lints.insert(lint, (severity, source));
462464
}
463465

464466
/// Disables `lint` if it was previously enabled.
465467
pub fn disable(&mut self, lint: LintId) {
466468
self.lints.remove(&lint);
467469
}
470+
}
468471

469-
/// Merges the enabled lints from `other` into this selection.
470-
///
471-
/// Lints from `other` will override any existing configuration.
472-
pub fn merge(&mut self, other: &RuleSelection) {
473-
self.lints.extend(other.iter());
474-
}
472+
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
473+
pub enum LintSource {
474+
/// The user didn't enable the rule explicitly, instead it's enabled by default.
475+
#[default]
476+
Default,
477+
478+
/// The rule was enabled by using a CLI argument
479+
Cli,
480+
481+
/// The rule was enabled in a configuration file.
482+
File,
475483
}

crates/red_knot_wasm/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use js_sys::Error;
44
use wasm_bindgen::prelude::*;
55

66
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
7+
use red_knot_project::metadata::value::RangedValue;
78
use red_knot_project::ProjectMetadata;
89
use red_knot_project::{Db, ProjectDatabase};
910
use ruff_db::diagnostic::Diagnostic;
@@ -48,7 +49,7 @@ impl Workspace {
4849

4950
workspace.apply_cli_options(Options {
5051
environment: Some(EnvironmentOptions {
51-
python_version: Some(settings.python_version.into()),
52+
python_version: Some(RangedValue::cli(settings.python_version.into())),
5253
..EnvironmentOptions::default()
5354
}),
5455
..Options::default()

0 commit comments

Comments
 (0)