Skip to content

Commit 05ea77b

Browse files
authored
Create Unknown rule diagnostics with a source range (#15648)
1 parent 1e790d3 commit 05ea77b

File tree

10 files changed

+341
-115
lines changed

10 files changed

+341
-115
lines changed

crates/red_knot/src/main.rs

+4-2
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ division-by-zer = "warn" # incorrect rule name
287287
success: false
288288
exit_code: 1
289289
----- stdout -----
290-
warning[unknown-rule] Unknown lint rule `division-by-zer`
290+
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
291291
292292
----- stderr -----
293293
");

crates/red_knot/tests/file_watching.rs

+10-6
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

+1-1
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

+60-24
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

+4-4
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)]

0 commit comments

Comments
 (0)