Compare commits

...

3 Commits

Author SHA1 Message Date
Micha Reiser
f6b2544993 Add rule registry and rule selection 2024-12-09 14:10:40 +01:00
Micha Reiser
fe78d50560 Add declare_lint 2024-12-09 14:08:19 +01:00
Micha Reiser
b39def2915 Introduce DiagnosticId 2024-12-09 14:05:09 +01:00
24 changed files with 1519 additions and 270 deletions

1
Cargo.lock generated
View File

@@ -2300,6 +2300,7 @@ dependencies = [
"red_knot_vendored", "red_knot_vendored",
"ruff_db", "ruff_db",
"ruff_index", "ruff_index",
"ruff_macros",
"ruff_python_ast", "ruff_python_ast",
"ruff_python_literal", "ruff_python_literal",
"ruff_python_parser", "ruff_python_parser",

View File

@@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies] [dependencies]
ruff_db = { workspace = true } ruff_db = { workspace = true }
ruff_index = { workspace = true } ruff_index = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true } ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true } ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true } ruff_python_stdlib = { workspace = true }

View File

@@ -95,37 +95,37 @@ reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
## Various string kinds ## Various string kinds
```py ```py
# error: [annotation-raw-string] "Type expressions cannot use raw string literal" # error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
def f1() -> r"int": def f1() -> r"int":
return 1 return 1
# error: [annotation-f-string] "Type expressions cannot use f-strings" # error: [fstring-type-annotation] "Type expressions cannot use f-strings"
def f2() -> f"int": def f2() -> f"int":
return 1 return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal" # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f3() -> b"int": def f3() -> b"int":
return 1 return 1
def f4() -> "int": def f4() -> "int":
return 1 return 1
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals" # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
def f5() -> "in" "t": def f5() -> "in" "t":
return 1 return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters" # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f6() -> "\N{LATIN SMALL LETTER I}nt": def f6() -> "\N{LATIN SMALL LETTER I}nt":
return 1 return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters" # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
def f7() -> "\x69nt": def f7() -> "\x69nt":
return 1 return 1
def f8() -> """int""": def f8() -> """int""":
return 1 return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal" # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
def f9() -> "b'int'": def f9() -> "b'int'":
return 1 return 1
@@ -208,9 +208,9 @@ i: "{i for i in range(5)}"
j: "{i: i for i in range(5)}" j: "{i: i for i in range(5)}"
k: "(i for i in range(5))" k: "(i for i in range(5))"
l: "await 1" l: "await 1"
# error: [forward-annotation-syntax-error] # error: [invalid-syntax-in-forward-annotation]
m: "yield 1" m: "yield 1"
# error: [forward-annotation-syntax-error] # error: [invalid-syntax-in-forward-annotation]
n: "yield from 1" n: "yield from 1"
o: "1 < 2" o: "1 < 2"
p: "call()" p: "call()"

View File

@@ -73,7 +73,7 @@ def f[T]():
A typevar with less than two constraints emits a diagnostic: A typevar with less than two constraints emits a diagnostic:
```py ```py
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types" # error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
def f[T: (int,)](): def f[T: (int,)]():
pass pass
``` ```

View File

@@ -179,9 +179,9 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop. Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```py path=a.pyi ```py path=a.pyi
class A(B): ... # error: [cyclic-class-def] class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-def] class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-def] class C(A): ... # error: [cyclic-class-definition]
reveal_type(A.__class__) # revealed: Unknown reveal_type(A.__class__) # revealed: Unknown
``` ```

View File

@@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
These are invalid, but we need to be able to handle them gracefully without panicking. These are invalid, but we need to be able to handle them gracefully without panicking.
```py path=a.pyi ```py path=a.pyi
class Foo(Foo): ... # error: [cyclic-class-def] class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo] reveal_type(Foo) # revealed: Literal[Foo]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar: ... class Bar: ...
class Baz: ... class Baz: ...
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def] class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
reveal_type(Boz) # revealed: Literal[Boz] reveal_type(Boz) # revealed: Literal[Boz]
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]] reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
@@ -366,9 +366,9 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
These are similarly unlikely, but we still shouldn't crash: These are similarly unlikely, but we still shouldn't crash:
```py path=a.pyi ```py path=a.pyi
class Foo(Bar): ... # error: [cyclic-class-def] class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-def] class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-def] class Baz(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]] reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
```py path=a.pyi ```py path=a.pyi
class Spam: ... class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-def] class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-def] class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo, Spam): ... # error: [cyclic-class-def] class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]] reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -391,16 +391,16 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MRO, and a sub-graph ## Classes with cycles in their MRO, and a sub-graph
```py path=a.pyi ```py path=a.pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-def] class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ... class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-def] class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ... class Bar(Foo): ...
# TODO: can we avoid emitting the errors for these? # TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses, # The classes have cyclic superclasses,
# but are not themselves cyclic... # but are not themselves cyclic...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def] class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
class Spam(Baz): ... # error: [cyclic-class-def] class Spam(Baz): ... # error: [cyclic-class-definition]
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]] reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]] reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]

View File

@@ -1,3 +1,4 @@
use crate::lint::RuleSelection;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
@@ -5,6 +6,8 @@ use ruff_db::{Db as SourceDb, Upcast};
#[salsa::db] #[salsa::db]
pub trait Db: SourceDb + Upcast<dyn SourceDb> { pub trait Db: SourceDb + Upcast<dyn SourceDb> {
fn is_file_open(&self, file: File) -> bool; fn is_file_open(&self, file: File) -> bool;
fn rule_selection(&self) -> &RuleSelection;
} }
#[cfg(test)] #[cfg(test)]
@@ -13,23 +16,24 @@ pub(crate) mod tests {
use crate::program::{Program, SearchPathSettings}; use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion; use crate::python_version::PythonVersion;
use crate::ProgramSettings; use crate::{default_lint_registry, ProgramSettings};
use super::Db;
use crate::lint::RuleSelection;
use anyhow::Context; use anyhow::Context;
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem}; use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
use super::Db;
#[salsa::db] #[salsa::db]
pub(crate) struct TestDb { pub(crate) struct TestDb {
storage: salsa::Storage<Self>, storage: salsa::Storage<Self>,
files: Files, files: Files,
system: TestSystem, system: TestSystem,
vendored: VendoredFileSystem, vendored: VendoredFileSystem,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>, events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
rule_selection: Arc<RuleSelection>,
} }
impl TestDb { impl TestDb {
@@ -38,8 +42,9 @@ pub(crate) mod tests {
storage: salsa::Storage::default(), storage: salsa::Storage::default(),
system: TestSystem::default(), system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(), vendored: red_knot_vendored::file_system().clone(),
events: std::sync::Arc::default(), events: Arc::default(),
files: Files::default(), files: Files::default(),
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
} }
} }
@@ -102,6 +107,10 @@ pub(crate) mod tests {
fn is_file_open(&self, file: File) -> bool { fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
} }
#[salsa::db] #[salsa::db]

View File

@@ -2,6 +2,7 @@ use std::hash::BuildHasherDefault;
use rustc_hash::FxHasher; use rustc_hash::FxHasher;
use crate::lint::{LintRegistry, LintRegistryBuilder};
pub use db::Db; pub use db::Db;
pub use module_name::ModuleName; pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, Module}; pub use module_resolver::{resolve_module, system_module_search_paths, Module};
@@ -11,6 +12,7 @@ pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref; pub mod ast_node_ref;
mod db; mod db;
pub mod lint;
mod module_name; mod module_name;
mod module_resolver; mod module_resolver;
mod node_key; mod node_key;
@@ -26,3 +28,13 @@ mod unpack;
mod util; mod util;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>; type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
pub fn default_lint_registry() -> LintRegistry {
let mut registry = LintRegistryBuilder::default();
register_semantic_lints(&mut registry);
registry.build()
}
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
types::register_type_lints(registry);
}

View File

@@ -0,0 +1,419 @@
use itertools::Itertools;
use ruff_db::diagnostic::{LintName, Severity};
use rustc_hash::FxHashMap;
use std::hash::Hasher;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct LintMetadata {
/// The unique identifier for the lint.
pub name: LintName,
/// A one-sentence summary of what the lint catches.
pub summary: &'static str,
/// An in depth explanation of the lint in markdown. Covers what the lint does, why it's bad and possible fixes.
///
/// The documentation may require post-processing to be rendered correctly. For example, lines
/// might have leading or trailing whitespace that should be removed.
pub raw_documentation: &'static str,
/// The default level of the lint if the user doesn't specify one.
pub default_level: Level,
pub status: LintStatus,
/// Location where this lint is declared: `file_name:line`
pub source: &'static str,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Level {
/// The lint is disabled and should not run.
Ignore,
/// The lint is enabled and diagnostic should have a warning severity.
Warn,
/// The lint is enabled and diagnostics have an error severity.
Error,
}
impl Level {
pub const fn is_error(self) -> bool {
matches!(self, Level::Error)
}
pub const fn is_warn(self) -> bool {
matches!(self, Level::Warn)
}
pub const fn is_ignore(self) -> bool {
matches!(self, Level::Ignore)
}
}
impl TryFrom<Level> for Severity {
type Error = ();
fn try_from(level: Level) -> Result<Self, ()> {
match level {
Level::Ignore => Err(()),
Level::Warn => Ok(Severity::Warning),
Level::Error => Ok(Severity::Error),
}
}
}
impl LintMetadata {
pub fn name(&self) -> LintName {
self.name
}
pub fn summary(&self) -> &str {
self.summary
}
/// Returns the documentation line by line with leading and trailing whitespace removed.
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
self.raw_documentation
.lines()
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
}
/// Returns the documentation as a single string.
pub fn documentation(&self) -> String {
self.documentation_lines().join("\n")
}
pub fn default_level(&self) -> Level {
self.default_level
}
pub fn status(&self) -> &LintStatus {
&self.status
}
pub fn source(&self) -> &str {
self.source
}
}
#[doc(hidden)]
pub const fn lint_metadata_defaults() -> LintMetadata {
LintMetadata {
name: LintName::of(""),
summary: "",
raw_documentation: "",
default_level: Level::Error,
status: LintStatus::preview("0.0.0"),
source: "",
}
}
#[derive(Copy, Clone, Debug)]
pub enum LintStatus {
/// The rule has been added to the linter, but is not yet stable.
Preview {
/// When the rule was added to preview
since: &'static str,
},
/// Stable rule that was added in the version defined by `since`.
Stable { since: &'static str },
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
Deprecated {
since: &'static str,
reason: &'static str,
},
/// The rule has been removed [`since`] (version) and using it will result in an error.
Removed {
since: &'static str,
reason: &'static str,
},
}
impl LintStatus {
pub const fn preview(since: &'static str) -> Self {
LintStatus::Preview { since }
}
pub const fn stable(since: &'static str) -> Self {
LintStatus::Stable { since }
}
pub const fn deprecated(since: &'static str, reason: &'static str) -> Self {
LintStatus::Deprecated { since, reason }
}
pub const fn removed(since: &'static str, reason: &'static str) -> Self {
LintStatus::Removed { since, reason }
}
pub const fn is_removed(&self) -> bool {
matches!(self, LintStatus::Removed { .. })
}
}
#[macro_export]
macro_rules! declare_lint {
(
$(#[doc = $doc:literal])+
$vis: vis static $name: ident = {
summary: $summary: literal,
status: $status: expr,
// Optional properties
$( $key:ident: $value:expr, )*
}
) => {
$( #[doc = $doc] )+
#[allow(clippy::needless_update)]
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
summary: $summary,
raw_documentation: concat!($($doc,)+ "\n"),
status: $status,
source: concat!(file!(), ":", line!()),
$( $key: $value, )*
..$crate::lint::lint_metadata_defaults()
};
};
}
/// A unique identifier for a lint rule.
///
/// Implements `PartialEq`, `Eq`, and `Hash` based on the `LintMetadata` pointer
/// for fast comparison and lookup.
#[derive(Debug, Clone, Copy)]
pub struct LintId {
definition: &'static LintMetadata,
}
impl LintId {
pub const fn of(definition: &'static LintMetadata) -> Self {
LintId { definition }
}
}
impl PartialEq for LintId {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.definition, other.definition)
}
}
impl Eq for LintId {}
impl std::hash::Hash for LintId {
fn hash<H: Hasher>(&self, state: &mut H) {
std::ptr::hash(self.definition, state);
}
}
impl std::ops::Deref for LintId {
type Target = LintMetadata;
fn deref(&self) -> &Self::Target {
self.definition
}
}
#[derive(Default, Debug)]
pub struct LintRegistryBuilder {
/// Registered lints that haven't been removed.
lints: Vec<LintId>,
/// Lints indexed by name, including aliases and removed rules.
by_name: FxHashMap<&'static str, LintEntry>,
}
impl LintRegistryBuilder {
#[track_caller]
pub fn register_lint(&mut self, lint: &'static LintMetadata) {
assert_eq!(
self.by_name.insert(&*lint.name, lint.into()),
None,
"duplicate lint registration for '{name}'",
name = lint.name
);
if !lint.status.is_removed() {
self.lints.push(LintId::of(lint));
}
}
#[track_caller]
pub fn register_alias(&mut self, from: LintName, to: &'static LintMetadata) {
let target = match self.by_name.get(to.name.as_str()) {
Some(LintEntry::Lint(target) | LintEntry::Removed(target)) => target,
Some(LintEntry::Alias(target)) => {
panic!(
"lint alias {from} -> {to:?} points to another alias {target:?}",
target = target.name()
)
}
None => panic!(
"lint alias {from} -> {to} points to non-registered lint",
to = to.name
),
};
assert_eq!(
self.by_name
.insert(from.as_str(), LintEntry::Alias(*target)),
None,
"duplicate lint registration for '{from}'",
);
}
pub fn build(self) -> LintRegistry {
LintRegistry {
lints: self.lints,
by_name: self.by_name,
}
}
}
#[derive(Default, Debug)]
pub struct LintRegistry {
lints: Vec<LintId>,
by_name: FxHashMap<&'static str, LintEntry>,
}
impl LintRegistry {
/// Looks up a lint by its name.
pub fn get(&self, code: &str) -> Result<LintId, GetLintError> {
match self.by_name.get(code) {
Some(LintEntry::Lint(metadata)) => Ok(*metadata),
Some(LintEntry::Alias(lint)) => {
if lint.status.is_removed() {
Err(GetLintError::Removed(lint.name()))
} else {
Ok(*lint)
}
}
Some(LintEntry::Removed(lint)) => Err(GetLintError::Removed(lint.name())),
None => Err(GetLintError::Unknown(code.to_string())),
}
}
/// Returns all registered, non-removed lints.
pub fn lints(&self) -> &[LintId] {
&self.lints
}
/// Returns an iterator over all known aliases and to their target lints.
///
/// This iterator includes aliases that point to removed lints.
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
self.by_name.iter().filter_map(|(key, value)| {
if let LintEntry::Alias(alias) = value {
Some((LintName::of(key), *alias))
} else {
None
}
})
}
/// Iterates over all removed lints.
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
self.by_name.iter().filter_map(|(_, value)| {
if let LintEntry::Removed(metadata) = value {
Some(*metadata)
} else {
None
}
})
}
}
#[derive(Error, Debug, Clone)]
pub enum GetLintError {
/// The name maps to this removed lint.
#[error("lint {0} has been removed")]
Removed(LintName),
/// No lint with the given name is known.
#[error("unknown lint {0}")]
Unknown(String),
}
#[derive(Debug, PartialEq, Eq)]
pub enum LintEntry {
/// An existing lint rule. Can be in preview, stable or deprecated.
Lint(LintId),
/// A lint rule that has been removed.
Removed(LintId),
Alias(LintId),
}
impl From<&'static LintMetadata> for LintEntry {
fn from(metadata: &'static LintMetadata) -> Self {
if metadata.status.is_removed() {
LintEntry::Removed(LintId::of(metadata))
} else {
LintEntry::Lint(LintId::of(metadata))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RuleSelection {
/// Map with the severity for each enabled lint rule.
///
/// If a rule isn't present in this map, then it should be considered disabled.
lints: FxHashMap<LintId, Severity>,
}
impl RuleSelection {
/// Creates a new rule selection from all known lints in the registry that are enabled
/// according to their default severity.
pub fn from_registry(registry: &LintRegistry) -> Self {
let lints = registry
.lints()
.iter()
.filter_map(|lint| {
Severity::try_from(lint.default_level())
.ok()
.map(|severity| (*lint, severity))
})
.collect();
RuleSelection { lints }
}
/// Returns an iterator over all enabled lints.
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
self.lints.keys().copied()
}
/// Returns an iterator over all enabled lints and their severity.
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
}
/// Returns the configured severity for the lint with the given id or `None` if the lint is disabled.
pub fn severity(&self, lint: LintId) -> Option<Severity> {
self.lints.get(&lint).copied()
}
/// Enables `lint` and configures with the given `severity`.
///
/// Overrides any previous configuration for the lint.
pub fn enable(&mut self, lint: LintId, severity: Severity) {
self.lints.insert(lint, severity);
}
/// Disables `lint` if it was previously enabled.
pub fn disable(&mut self, lint: LintId) {
self.lints.remove(&lint);
}
/// Merges the enabled lints from `other` into this selection.
///
/// Lints from `other` will override any existing configuration.
pub fn merge(&mut self, other: &RuleSelection) {
self.lints.extend(other.iter());
}
}

View File

@@ -2,11 +2,12 @@ use std::hash::Hash;
use indexmap::IndexSet; use indexmap::IndexSet;
use itertools::Itertools; use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast as ast; use ruff_python_ast as ast;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub(crate) use self::diagnostic::register_type_lints;
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics}; pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay; pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{ pub(crate) use self::infer::{
@@ -25,7 +26,7 @@ use crate::stdlib::{
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule, builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
}; };
use crate::symbol::{Boundness, Symbol}; use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder; use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator}; use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint; use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
@@ -2300,9 +2301,9 @@ impl<'db> CallOutcome<'db> {
not_callable_ty, not_callable_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
"call-non-callable",
format_args!( format_args!(
"Object of type `{}` is not callable", "Object of type `{}` is not callable",
not_callable_ty.display(db) not_callable_ty.display(db)
@@ -2315,9 +2316,9 @@ impl<'db> CallOutcome<'db> {
called_ty, called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
"call-non-callable",
format_args!( format_args!(
"Object of type `{}` is not callable (due to union element `{}`)", "Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db), called_ty.display(db),
@@ -2331,9 +2332,9 @@ impl<'db> CallOutcome<'db> {
called_ty, called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
"call-non-callable",
format_args!( format_args!(
"Object of type `{}` is not callable (due to union elements {})", "Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db), called_ty.display(db),
@@ -2346,9 +2347,9 @@ impl<'db> CallOutcome<'db> {
callable_ty: called_ty, callable_ty: called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
"call-non-callable",
format_args!( format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)", "Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db) called_ty.display(db)
@@ -2374,7 +2375,8 @@ impl<'db> CallOutcome<'db> {
} => { } => {
diagnostics.add( diagnostics.add(
node, node,
"revealed-type", DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)), format_args!("Revealed type is `{}`", revealed_ty.display(db)),
); );
Ok(*return_ty) Ok(*return_ty)

View File

@@ -1,6 +1,12 @@
use crate::lint::{Level, LintId, LintMetadata, LintRegistryBuilder, LintStatus};
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{ClassLiteralType, Type}; use crate::types::{ClassLiteralType, Type};
use crate::Db; use crate::{declare_lint, Db};
use ruff_db::diagnostic::{Diagnostic, Severity}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@@ -9,18 +15,411 @@ use std::fmt::Formatter;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
/// Registers all known type check lints.
pub(crate) fn register_type_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&NOT_ITERABLE);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&NON_SUBSCRIPTABLE);
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&CONFLICTING_DECLARATIONS);
registry.register_lint(&DIVISION_BY_ZERO);
registry.register_lint(&CALL_NON_CALLABLE);
registry.register_lint(&INVALID_TYPE_PARAMETER);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
registry.register_lint(&DUPLICATE_BASE);
registry.register_lint(&INVALID_BASE);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INVALID_LITERAL_PARAMETER);
registry.register_lint(&CALL_POSSIBLY_UNBOUND_METHOD);
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
registry.register_lint(&CONFLICTING_METACLASS);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_TYPE_FORM);
// String annotations
registry.register_lint(&FSTRING_TYPE_ANNOTATION);
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
registry.register_lint(&RAW_STRING_TYPE_ANNOTATION);
registry.register_lint(&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION);
registry.register_lint(&INVALID_SYNTAX_IN_FORWARD_ANNOTATION);
registry.register_lint(&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION);
}
declare_lint! {
/// ## What it does
/// Checks for references to names that are not defined.
///
/// ## Why is this bad?
/// Using an undefined variable will raise a `NameError` at runtime.
///
/// ## Example
///
/// ```python
/// print(x) # NameError: name 'x' is not defined
/// ```
pub(crate) static UNRESOLVED_REFERENCE = {
summary: "detects references to names that are not defined",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for references to names that are possibly not defined..
///
/// ## Why is this bad?
/// Using an undefined variable will raise a `NameError` at runtime.
///
/// ## Example
///
/// ```python
/// for i in range(0):
/// x = i
///
/// print(x) # NameError: name 'x' is not defined
/// ```
pub(crate) static POSSIBLY_UNRESOLVED_REFERENCE = {
summary: "detects references to possibly unresolved references",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for objects that are not iterable but are used in a context that requires them to be.
///
/// ## Why is this bad?
/// Iterating over an object that is not iterable will raise a `TypeError` at runtime.
///
/// ## Examples
///
/// ```python
/// for i in 34: # TypeError: 'int' object is not iterable
/// pass
/// ```
pub(crate) static NOT_ITERABLE = {
summary: "detects objects that are not iterable",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// TODO
pub(crate) static INDEX_OUT_OF_BOUNDS = {
summary: "detects index out of bounds errors",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for objects that do not support subscripting but are used in a context that requires them to be.
///
/// ## Why is this bad?
/// Subscripting an object that does not support it will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// 4[1] # TypeError: 'int' object is not subscriptable
/// ```
pub(crate) static NON_SUBSCRIPTABLE = {
summary: "detects objects that do not support subscripting",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for import statements for which the module cannot be resolved.
///
/// ## Why is this bad?
/// Importing a module that cannot be resolved will raise an `ImportError` at runtime.
pub(crate) static UNRESOLVED_IMPORT = {
summary: "detects unresolved imports",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static POSSIBLY_UNBOUND_IMPORT = {
summary: "detects possibly unbound imports",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for step size 0 in slices.
///
/// ## Why is this bad?
/// A slice with a step size of zero will raise a `ValueError` at runtime.
///
/// ## Examples
/// ```python
/// l = list(range(10))
/// l[1:10:0] # ValueError: slice step cannot be zero
pub(crate) static ZERO_STEPSIZE_IN_SLICE = {
summary: "detects a slice step size of zero",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_ASSIGNMENT = {
summary: "detects invalid assignments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_DECLARATION = {
summary: "detects invalid declarations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static CONFLICTING_DECLARATIONS = {
summary: "detects conflicting declarations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// It detects division by zero.
///
/// ## Why is this bad?
/// Dividing by zero raises a `ZeroDivisionError` at runtime.
///
/// ## Examples
/// ```python
/// 5 / 0
/// ```
pub(crate) static DIVISION_BY_ZERO = {
summary: "detects division by zero",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to non-callable objects.
///
/// ## Why is this bad?
/// Calling a non-callable object will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// 4() # TypeError: 'int' object is not callable
/// ```
pub(crate) static CALL_NON_CALLABLE = {
summary: "detects calls to non-callable objects",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// TODO
pub(crate) static INVALID_TYPE_PARAMETER = {
summary: "detects invalid type parameters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_TYPE_VARIABLE_CONSTRAINTS = {
summary: "detects invalid type variable constraints",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for class definitions with a cyclic inheritance chain.
///
/// ## Why is it bad?
/// TODO
pub(crate) static CYCLIC_CLASS_DEFINITION = {
summary: "detects cyclic class definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static DUPLICATE_BASE = {
summary: "detects class definitions with duplicate bases",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_BASE = {
summary: "detects class definitions with an invalid base",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INCONSISTENT_MRO = {
summary: "detects class definitions with an inconsistent MRO",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for invalid parameters to `typing.Literal`.
pub(crate) static INVALID_LITERAL_PARAMETER = {
summary: "detects invalid literal parameters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to possibly unbound methods.
pub(crate) static CALL_POSSIBLY_UNBOUND_METHOD = {
summary: "detects calls to possibly unbound methods",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for possibly unbound attributes.
pub(crate) static POSSIBLY_UNBOUND_ATTRIBUTE = {
summary: "detects references to possibly unbound attributes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for unresolved attributes.
pub(crate) static UNRESOLVED_ATTRIBUTE = {
summary: "detects references to unresolved attributes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static CONFLICTING_METACLASS = {
summary: "detects conflicting metaclasses",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.
pub(crate) static UNSUPPORTED_OPERATOR = {
summary: "detects binary expressions where the operands don't support the operator",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_CONTEXT_MANAGER = {
summary: "detects expressions used in with statements that don't implement the context manager protocol",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.
///
/// ## Why is this bad?
/// Using `reveal_type` without importing it will raise a `NameError` at runtime.
pub(crate) static UNDEFINED_REVEAL = {
summary: "detects usages of `reveal_type` without importing it",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.
pub(crate) static INVALID_PARAMETER_DEFAULT = {
summary: "detects default values that can't be assigned to the parameter's annotated type",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for `type[]` usages that have too many or too few type arguments.
pub(crate) static INVALID_TYPE_FORM = {
summary: "detects invalid type forms",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic { pub struct TypeCheckDiagnostic {
// TODO: Don't use string keys for rules pub(super) id: DiagnosticId,
pub(super) rule: String,
pub(super) message: String, pub(super) message: String,
pub(super) range: TextRange, pub(super) range: TextRange,
pub(super) severity: Severity,
pub(super) file: File, pub(super) file: File,
} }
impl TypeCheckDiagnostic { impl TypeCheckDiagnostic {
pub fn rule(&self) -> &str { pub fn id(&self) -> DiagnosticId {
&self.rule self.id
} }
pub fn message(&self) -> &str { pub fn message(&self) -> &str {
@@ -33,8 +432,8 @@ impl TypeCheckDiagnostic {
} }
impl Diagnostic for TypeCheckDiagnostic { impl Diagnostic for TypeCheckDiagnostic {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
TypeCheckDiagnostic::rule(self) self.id
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {
@@ -50,7 +449,7 @@ impl Diagnostic for TypeCheckDiagnostic {
} }
fn severity(&self) -> Severity { fn severity(&self) -> Severity {
Severity::Error self.severity
} }
} }
@@ -150,9 +549,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
/// Emit a diagnostic declaring that the object represented by `node` is not iterable /// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) { pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
self.add( self.add_lint(
&NOT_ITERABLE,
node, node,
"not-iterable",
format_args!( format_args!(
"Object of type `{}` is not iterable", "Object of type `{}` is not iterable",
not_iterable_ty.display(self.db) not_iterable_ty.display(self.db)
@@ -167,9 +566,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
node: AnyNodeRef, node: AnyNodeRef,
element_ty: Type<'db>, element_ty: Type<'db>,
) { ) {
self.add( self.add_lint(
&NOT_ITERABLE,
node, node,
"not-iterable",
format_args!( format_args!(
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound", "Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
element_ty.display(self.db) element_ty.display(self.db)
@@ -186,9 +585,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
length: usize, length: usize,
index: i64, index: i64,
) { ) {
self.add( self.add_lint(
&INDEX_OUT_OF_BOUNDS,
node, node,
"index-out-of-bounds",
format_args!( format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}", "Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(self.db) tuple_ty.display(self.db)
@@ -203,9 +602,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
non_subscriptable_ty: Type<'db>, non_subscriptable_ty: Type<'db>,
method: &str, method: &str,
) { ) {
self.add( self.add_lint(
&NON_SUBSCRIPTABLE,
node, node,
"non-subscriptable",
format_args!( format_args!(
"Cannot subscript object of type `{}` with no `{method}` method", "Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(self.db) non_subscriptable_ty.display(self.db)
@@ -219,9 +618,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
level: u32, level: u32,
module: Option<&str>, module: Option<&str>,
) { ) {
self.add( self.add_lint(
&UNRESOLVED_IMPORT,
import_node.into(), import_node.into(),
"unresolved-import",
format_args!( format_args!(
"Cannot resolve import `{}{}`", "Cannot resolve import `{}{}`",
".".repeat(level as usize), ".".repeat(level as usize),
@@ -231,9 +630,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
} }
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) { pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
self.add( self.add_lint(
&ZERO_STEPSIZE_IN_SLICE,
node, node,
"zero-stepsize-in-slice",
format_args!("Slice step size can not be zero"), format_args!("Slice step size can not be zero"),
); );
} }
@@ -246,19 +645,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
) { ) {
match declared_ty { match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => { Type::ClassLiteral(ClassLiteralType { class }) => {
self.add(node, "invalid-assignment", format_args!( self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional", "Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(self.db))); class.name(self.db)));
} }
Type::FunctionLiteral(function) => { Type::FunctionLiteral(function) => {
self.add(node, "invalid-assignment", format_args!( self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional", "Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(self.db))); function.name(self.db)));
} }
_ => { _ => {
self.add( self.add_lint(
&INVALID_ASSIGNMENT,
node, node,
"invalid-assignment",
format_args!( format_args!(
"Object of type `{}` is not assignable to `{}`", "Object of type `{}` is not assignable to `{}`",
assigned_ty.display(self.db), assigned_ty.display(self.db),
@@ -272,9 +671,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) { pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node; let ast::ExprName { id, .. } = expr_name_node;
self.add( self.add_lint(
&POSSIBLY_UNRESOLVED_REFERENCE,
expr_name_node.into(), expr_name_node.into(),
"possibly-unresolved-reference",
format_args!("Name `{id}` used when possibly not defined"), format_args!("Name `{id}` used when possibly not defined"),
); );
} }
@@ -282,17 +681,37 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) { pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node; let ast::ExprName { id, .. } = expr_name_node;
self.add( self.add_lint(
&UNRESOLVED_REFERENCE,
expr_name_node.into(), expr_name_node.into(),
"unresolved-reference",
format_args!("Name `{id}` used when not defined"), format_args!("Name `{id}` used when not defined"),
); );
} }
pub(super) fn add_lint(
&mut self,
lint: &'static LintMetadata,
node: AnyNodeRef,
message: std::fmt::Arguments,
) {
// Skip over diagnostics if the rule is disabled.
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
return;
};
self.add(node, DiagnosticId::Lint(lint.name()), severity, message);
}
/// Adds a new diagnostic. /// Adds a new diagnostic.
/// ///
/// The diagnostic does not get added if the rule isn't enabled for this file. /// The diagnostic does not get added if the rule isn't enabled for this file.
pub(super) fn add(&mut self, node: AnyNodeRef, rule: &str, message: std::fmt::Arguments) { pub(super) fn add(
&mut self,
node: AnyNodeRef,
id: DiagnosticId,
severity: Severity,
message: std::fmt::Arguments,
) {
if !self.db.is_file_open(self.file) { if !self.db.is_file_open(self.file) {
return; return;
} }
@@ -305,9 +724,10 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
self.diagnostics.push(TypeCheckDiagnostic { self.diagnostics.push(TypeCheckDiagnostic {
file: self.file, file: self.file,
rule: rule.to_string(), id,
message: message.to_string(), message: message.to_string(),
range: node.range(), range: node.range(),
severity,
}); });
} }

View File

@@ -31,7 +31,7 @@ use std::num::NonZeroU32;
use itertools::Itertools; use itertools::Itertools;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp}; use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, UnaryOp};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use salsa; use salsa;
use salsa::plumbing::AsId; use salsa::plumbing::AsId;
@@ -48,7 +48,15 @@ use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId}; use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex; use crate::semantic_index::SemanticIndex;
use crate::stdlib::builtins_module_scope; use crate::stdlib::builtins_module_scope;
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder}; use crate::types::diagnostic::{
TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE,
CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE,
INVALID_CONTEXT_MANAGER, INVALID_DECLARATION, INVALID_LITERAL_PARAMETER,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_PARAMETER,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind; use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{ use crate::types::{
@@ -64,7 +72,9 @@ use crate::util::subscript::{PyIndex, PySlice};
use crate::Db; use crate::Db;
use super::expression_ty; use super::expression_ty;
use super::string_annotation::parse_string_annotation; use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@@ -502,9 +512,9 @@ impl<'db> TypeInferenceBuilder<'db> {
for (class, class_node) in class_definitions { for (class, class_node) in class_definitions {
// (1) Check that the class does not have a cyclic definition // (1) Check that the class does not have a cyclic definition
if class.is_cyclically_defined(self.db) { if class.is_cyclically_defined(self.db) {
self.diagnostics.add( self.diagnostics.add_lint(
&CYCLIC_CLASS_DEFINITION,
class_node.into(), class_node.into(),
"cyclic-class-def",
format_args!( format_args!(
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)", "Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
class.name(self.db), class.name(self.db),
@@ -522,9 +532,9 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::DuplicateBases(duplicates) => { MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases(); let base_nodes = class_node.bases();
for (index, duplicate) in duplicates { for (index, duplicate) in duplicates {
self.diagnostics.add( self.diagnostics.add_lint(
&DUPLICATE_BASE,
(&base_nodes[*index]).into(), (&base_nodes[*index]).into(),
"duplicate-base",
format_args!("Duplicate base class `{}`", duplicate.name(self.db)), format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
); );
} }
@@ -532,9 +542,9 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::InvalidBases(bases) => { MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases(); let base_nodes = class_node.bases();
for (index, base_ty) in bases { for (index, base_ty) in bases {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_BASE,
(&base_nodes[*index]).into(), (&base_nodes[*index]).into(),
"invalid-base",
format_args!( format_args!(
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)", "Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
base_ty.display(self.db) base_ty.display(self.db)
@@ -542,9 +552,9 @@ impl<'db> TypeInferenceBuilder<'db> {
); );
} }
} }
MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add( MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add_lint(
&INCONSISTENT_MRO,
class_node.into(), class_node.into(),
"inconsistent-mro",
format_args!( format_args!(
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`", "Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
class.name(self.db), class.name(self.db),
@@ -572,9 +582,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} => { } => {
let node = class_node.into(); let node = class_node.into();
if *candidate1_is_base_class { if *candidate1_is_base_class {
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_METACLASS,
node, node,
"conflicting-metaclass",
format_args!( format_args!(
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \
@@ -584,12 +594,12 @@ impl<'db> TypeInferenceBuilder<'db> {
base1 = class1.name(self.db), base1 = class1.name(self.db),
metaclass2 = metaclass2.name(self.db), metaclass2 = metaclass2.name(self.db),
base2 = class2.name(self.db), base2 = class2.name(self.db),
) ),
); );
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_METACLASS,
node, node,
"conflicting-metaclass",
format_args!( format_args!(
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \
@@ -598,7 +608,7 @@ impl<'db> TypeInferenceBuilder<'db> {
metaclass_of_class = metaclass1.name(self.db), metaclass_of_class = metaclass1.name(self.db),
metaclass_of_base = metaclass2.name(self.db), metaclass_of_base = metaclass2.name(self.db),
base = class2.name(self.db), base = class2.name(self.db),
) ),
); );
} }
} }
@@ -736,9 +746,9 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => return, _ => return,
}; };
self.diagnostics.add( self.diagnostics.add_lint(
&DIVISION_BY_ZERO,
expr.into(), expr.into(),
"division-by-zero",
format_args!( format_args!(
"Cannot {op} object of type `{}` {by_zero}", "Cannot {op} object of type `{}` {by_zero}",
left.display(self.db) left.display(self.db)
@@ -761,9 +771,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO point out the conflicting declarations in the diagnostic? // TODO point out the conflicting declarations in the diagnostic?
let symbol_table = self.index.symbol_table(binding.file_scope(self.db)); let symbol_table = self.index.symbol_table(binding.file_scope(self.db));
let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name(); let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name();
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_DECLARATIONS,
node, node,
"conflicting-declarations",
format_args!( format_args!(
"Conflicting declared types for `{symbol_name}`: {}", "Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db) conflicting.display(self.db)
@@ -791,9 +801,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let ty = if inferred_ty.is_assignable_to(self.db, ty) { let ty = if inferred_ty.is_assignable_to(self.db, ty) {
ty ty
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_DECLARATION,
node, node,
"invalid-declaration",
format_args!( format_args!(
"Cannot declare type `{}` for inferred type `{}`", "Cannot declare type `{}` for inferred type `{}`",
ty.display(self.db), ty.display(self.db),
@@ -1088,12 +1098,12 @@ impl<'db> TypeInferenceBuilder<'db> {
if default_ty.is_assignable_to(self.db, declared_ty) { if default_ty.is_assignable_to(self.db, declared_ty) {
UnionType::from_elements(self.db, [declared_ty, default_ty]) UnionType::from_elements(self.db, [declared_ty, default_ty])
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_PARAMETER_DEFAULT,
parameter_with_default.into(), parameter_with_default.into(),
"invalid-parameter-default",
format_args!( format_args!(
"Default value of type `{}` is not assignable to annotated parameter type `{}`", "Default value of type `{}` is not assignable to annotated parameter type `{}`",
default_ty.display(self.db), declared_ty.display(self.db)) default_ty.display(self.db), declared_ty.display(self.db)),
); );
declared_ty declared_ty
} }
@@ -1400,9 +1410,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
match (enter, exit) { match (enter, exit) {
(Symbol::Unbound, Symbol::Unbound) => { (Symbol::Unbound, Symbol::Unbound) => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@@ -1411,9 +1421,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown Type::Unknown
} }
(Symbol::Unbound, _) => { (Symbol::Unbound, _) => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@@ -1423,9 +1433,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
(Symbol::Type(enter_ty, enter_boundness), exit) => { (Symbol::Type(enter_ty, enter_boundness), exit) => {
if enter_boundness == Boundness::PossiblyUnbound { if enter_boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound", "Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@@ -1437,9 +1447,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[context_expression_ty]) .call(self.db, &[context_expression_ty])
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics) .return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!(" format_args!("
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db) Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db)
), ),
@@ -1449,9 +1459,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match exit { match exit {
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@@ -1462,9 +1472,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed. // TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
if exit_boundness == Boundness::PossiblyUnbound { if exit_boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound", "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@@ -1489,9 +1499,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) )
.is_err() .is_err()
{ {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
"invalid-context-manager",
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable", "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@@ -1569,9 +1579,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let bound_or_constraint = match bound.as_deref() { let bound_or_constraint = match bound.as_deref() {
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
if elts.len() < 2 { if elts.len() < 2 {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_VARIABLE_CONSTRAINTS,
expr.into(), expr.into(),
"invalid-typevar-constraints",
format_args!("TypeVar must have at least two constrained types"), format_args!("TypeVar must have at least two constrained types"),
); );
self.infer_expression(expr); self.infer_expression(expr);
@@ -1892,9 +1902,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) { ) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
"unsupported-operator",
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
target_type.display(self.db), target_type.display(self.db),
@@ -1913,9 +1923,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let binary_return_ty = self.infer_binary_expression_type(left_ty, right_ty, op) let binary_return_ty = self.infer_binary_expression_type(left_ty, right_ty, op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
"unsupported-operator",
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@@ -1942,9 +1952,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, op) self.infer_binary_expression_type(left_ty, right_ty, op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
"unsupported-operator",
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@@ -1974,11 +1984,11 @@ impl<'db> TypeInferenceBuilder<'db> {
// Resolve the target type, assuming a load context. // Resolve the target type, assuming a load context.
let target_type = match &**target { let target_type = match &**target {
Expr::Name(name) => { ast::Expr::Name(name) => {
self.store_expression_type(target, Type::Never); self.store_expression_type(target, Type::Never);
self.infer_name_load(name) self.infer_name_load(name)
} }
Expr::Attribute(attr) => { ast::Expr::Attribute(attr) => {
self.store_expression_type(target, Type::Never); self.store_expression_type(target, Type::Never);
self.infer_attribute_load(attr) self.infer_attribute_load(attr)
} }
@@ -2195,19 +2205,19 @@ impl<'db> TypeInferenceBuilder<'db> {
match module_ty.member(self.db, &ast::name::Name::new(&name.id)) { match module_ty.member(self.db, &ast::name::Name::new(&name.id)) {
Symbol::Type(ty, boundness) => { Symbol::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_IMPORT,
AnyNodeRef::Alias(alias), AnyNodeRef::Alias(alias),
"possibly-unbound-import", format_args!("Member `{name}` of module `{module_name}` is possibly unbound", ),
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
); );
} }
ty ty
} }
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias), AnyNodeRef::Alias(alias),
"unresolved-import",
format_args!("Module `{module_name}` has no member `{name}`",), format_args!("Module `{module_name}` has no member `{name}`",),
); );
Type::Unknown Type::Unknown
@@ -2912,9 +2922,9 @@ impl<'db> TypeInferenceBuilder<'db> {
{ {
let mut builtins_symbol = builtins_symbol(self.db, name); let mut builtins_symbol = builtins_symbol(self.db, name);
if builtins_symbol.is_unbound() && name == "reveal_type" { if builtins_symbol.is_unbound() && name == "reveal_type" {
self.diagnostics.add( self.diagnostics.add_lint(
&UNDEFINED_REVEAL,
name_node.into(), name_node.into(),
"undefined-reveal",
format_args!( format_args!(
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"), "`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
); );
@@ -3009,9 +3019,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty.member(self.db, &attr.id) { match value_ty.member(self.db, &attr.id) {
Symbol::Type(member_ty, boundness) => { Symbol::Type(member_ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_ATTRIBUTE,
attribute.into(), attribute.into(),
"possibly-unbound-attribute",
format_args!( format_args!(
"Attribute `{}` on type `{}` is possibly unbound", "Attribute `{}` on type `{}` is possibly unbound",
attr.id, attr.id,
@@ -3023,9 +3033,9 @@ impl<'db> TypeInferenceBuilder<'db> {
member_ty member_ty
} }
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNRESOLVED_ATTRIBUTE,
attribute.into(), attribute.into(),
"unresolved-attribute",
format_args!( format_args!(
"Type `{}` has no attribute `{}`", "Type `{}` has no attribute `{}`",
value_ty.display(self.db), value_ty.display(self.db),
@@ -3104,9 +3114,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) { ) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(), unary.into(),
"unsupported-operator",
format_args!( format_args!(
"Unary operator `{op}` is unsupported for type `{}`", "Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db), operand_type.display(self.db),
@@ -3116,9 +3126,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
} }
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(), unary.into(),
"unsupported-operator",
format_args!( format_args!(
"Unary operator `{op}` is unsupported for type `{}`", "Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db), operand_type.display(self.db),
@@ -3157,9 +3167,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, *op) self.infer_binary_expression_type(left_ty, right_ty, *op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
binary.into(), binary.into(),
"unsupported-operator",
format_args!( format_args!(
"Operator `{op}` is unsupported between objects of type `{}` and `{}`", "Operator `{op}` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@@ -3467,9 +3477,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_type_comparison(left_ty, *op, right_ty) self.infer_binary_type_comparison(left_ty, *op, right_ty)
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
AnyNodeRef::ExprCompare(compare), AnyNodeRef::ExprCompare(compare),
"unsupported-operator",
format_args!( format_args!(
"Operator `{}` is not supported for types `{}` and `{}`{}", "Operator `{}` is not supported for types `{}` and `{}`{}",
error.op, error.op,
@@ -4124,9 +4134,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {} Symbol::Unbound => {}
Symbol::Type(dunder_getitem_method, boundness) => { Symbol::Type(dunder_getitem_method, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
value_node.into(), value_node.into(),
"call-possibly-unbound-method",
format_args!( format_args!(
"Method `__getitem__` of type `{}` is possibly unbound", "Method `__getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db), value_ty.display(self.db),
@@ -4138,9 +4148,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[slice_ty]) .call(self.db, &[slice_ty])
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics) .return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_NON_CALLABLE,
value_node.into(), value_node.into(),
"call-non-callable",
format_args!( format_args!(
"Method `__getitem__` of type `{}` is not callable on object of type `{}`", "Method `__getitem__` of type `{}` is not callable on object of type `{}`",
err.called_ty().display(self.db), err.called_ty().display(self.db),
@@ -4168,9 +4178,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {} Symbol::Unbound => {}
Symbol::Type(ty, boundness) => { Symbol::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
value_node.into(), value_node.into(),
"call-possibly-unbound-method",
format_args!( format_args!(
"Method `__class_getitem__` of type `{}` is possibly unbound", "Method `__class_getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db), value_ty.display(self.db),
@@ -4182,9 +4192,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[slice_ty]) .call(self.db, &[slice_ty])
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics) .return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_NON_CALLABLE,
value_node.into(), value_node.into(),
"call-non-callable",
format_args!( format_args!(
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`", "Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
err.called_ty().display(self.db), err.called_ty().display(self.db),
@@ -4324,18 +4334,18 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Expr::Starred(starred) => self.infer_starred_expression(starred), ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::BytesLiteral(bytes) => { ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add( self.diagnostics.add_lint(
&BYTE_STRING_TYPE_ANNOTATION,
bytes.into(), bytes.into(),
"annotation-byte-string",
format_args!("Type expressions cannot use bytes literal"), format_args!("Type expressions cannot use bytes literal"),
); );
Type::Unknown Type::Unknown
} }
ast::Expr::FString(fstring) => { ast::Expr::FString(fstring) => {
self.diagnostics.add( self.diagnostics.add_lint(
&FSTRING_TYPE_ANNOTATION,
fstring.into(), fstring.into(),
"annotation-f-string",
format_args!("Type expressions cannot use f-strings"), format_args!("Type expressions cannot use f-strings"),
); );
self.infer_fstring_expression(fstring); self.infer_fstring_expression(fstring);
@@ -4677,9 +4687,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
ast::Expr::Tuple(_) => { ast::Expr::Tuple(_) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_FORM,
slice.into(), slice.into(),
"invalid-type-form",
format_args!("type[...] must have exactly one type argument"), format_args!("type[...] must have exactly one type argument"),
); );
Type::Unknown Type::Unknown
@@ -4726,9 +4736,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Ok(ty) => ty, Ok(ty) => ty,
Err(nodes) => { Err(nodes) => {
for node in nodes { for node in nodes {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_LITERAL_PARAMETER,
node.into(), node.into(),
"invalid-literal-parameter",
format_args!( format_args!(
"Type arguments for `Literal` must be `None`, \ "Type arguments for `Literal` must be `None`, \
a literal value (int, bool, str, or bytes), or an enum value" a literal value (int, bool, str, or bytes), or an enum value"
@@ -4762,9 +4772,9 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("generic type alias") todo_type!("generic type alias")
} }
KnownInstanceType::NoReturn | KnownInstanceType::Never => { KnownInstanceType::NoReturn | KnownInstanceType::Never => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(), subscript.into(),
"invalid-type-parameter",
format_args!( format_args!(
"Type `{}` expected no type parameter", "Type `{}` expected no type parameter",
known_instance.repr(self.db) known_instance.repr(self.db)
@@ -4773,9 +4783,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown Type::Unknown
} }
KnownInstanceType::LiteralString => { KnownInstanceType::LiteralString => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(), subscript.into(),
"invalid-type-parameter",
format_args!( format_args!(
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?", "Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
known_instance.repr(self.db) known_instance.repr(self.db)

View File

@@ -5,8 +5,127 @@ use ruff_python_ast::{self as ast, ModExpression, StringFlags};
use ruff_python_parser::{parse_expression_range, Parsed}; use ruff_python_parser::{parse_expression_range, Parsed};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::lint::{Level, LintStatus};
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder}; use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::Db; use crate::{declare_lint, Db};
declare_lint! {
/// ## What it does
/// Checks for f-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use f-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> f"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static FSTRING_TYPE_ANNOTATION = {
summary: "detects F-strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for byte-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use byte-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> b"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static BYTE_STRING_TYPE_ANNOTATION = {
summary: "detects byte strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for raw-strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use raw-string notation.
///
/// ## Examples
/// ```python
/// def test(): -> r"int":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "int":
/// ...
/// ```
pub(crate) static RAW_STRING_TYPE_ANNOTATION = {
summary: "detects raw strings in type annotation positions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for implicit concatenated strings in type annotation positions.
///
/// ## Why is this bad?
/// Static analysis tools like Red Knot can't analyse type annotations that use implicit concatenated strings.
///
/// ## Examples
/// ```python
/// def test(): -> "Literal[" "5" "]":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test(): -> "Literal[5]":
/// ...
/// ```
pub(crate) static IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION = {
summary: "detects implicit concatenated strings in type annotations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static INVALID_SYNTAX_IN_FORWARD_ANNOTATION = {
summary: "detects invalid syntax in forward annotations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO
pub(crate) static ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION = {
summary: "detects forward type annotations with escape characters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>; type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
@@ -25,9 +144,9 @@ pub(crate) fn parse_string_annotation(
if let [string_literal] = string_expr.value.as_slice() { if let [string_literal] = string_expr.value.as_slice() {
let prefix = string_literal.flags.prefix(); let prefix = string_literal.flags.prefix();
if prefix.is_raw() { if prefix.is_raw() {
diagnostics.add( diagnostics.add_lint(
&RAW_STRING_TYPE_ANNOTATION,
string_literal.into(), string_literal.into(),
"annotation-raw-string",
format_args!("Type expressions cannot use raw string literal"), format_args!("Type expressions cannot use raw string literal"),
); );
// Compare the raw contents (without quotes) of the expression with the parsed contents // Compare the raw contents (without quotes) of the expression with the parsed contents
@@ -49,26 +168,26 @@ pub(crate) fn parse_string_annotation(
// ``` // ```
match parse_expression_range(source.as_str(), range_excluding_quotes) { match parse_expression_range(source.as_str(), range_excluding_quotes) {
Ok(parsed) => return Ok(parsed), Ok(parsed) => return Ok(parsed),
Err(parse_error) => diagnostics.add( Err(parse_error) => diagnostics.add_lint(
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
string_literal.into(), string_literal.into(),
"forward-annotation-syntax-error",
format_args!("Syntax error in forward annotation: {}", parse_error.error), format_args!("Syntax error in forward annotation: {}", parse_error.error),
), ),
} }
} else { } else {
// The raw contents of the string doesn't match the parsed content. This could be the // The raw contents of the string doesn't match the parsed content. This could be the
// case for annotations that contain escape sequences. // case for annotations that contain escape sequences.
diagnostics.add( diagnostics.add_lint(
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
string_expr.into(), string_expr.into(),
"annotation-escape-character",
format_args!("Type expressions cannot contain escape characters"), format_args!("Type expressions cannot contain escape characters"),
); );
} }
} else { } else {
// String is implicitly concatenated. // String is implicitly concatenated.
diagnostics.add( diagnostics.add_lint(
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
string_expr.into(), string_expr.into(),
"annotation-implicit-concat",
format_args!("Type expressions cannot span multiple string literals"), format_args!("Type expressions cannot span multiple string literals"),
); );
} }

View File

@@ -86,14 +86,15 @@ fn to_lsp_diagnostic(
let severity = match diagnostic.severity() { let severity = match diagnostic.severity() {
Severity::Info => DiagnosticSeverity::INFORMATION, Severity::Info => DiagnosticSeverity::INFORMATION,
Severity::Error => DiagnosticSeverity::ERROR, Severity::Warning => DiagnosticSeverity::WARNING,
Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR,
}; };
Diagnostic { Diagnostic {
range, range,
severity: Some(severity), severity: Some(severity),
tags: None, tags: None,
code: Some(NumberOrString::String(diagnostic.rule().to_string())), code: Some(NumberOrString::String(diagnostic.id().to_string())),
code_description: None, code_description: None,
source: Some("red-knot".into()), source: Some("red-knot".into()),
message: diagnostic.message().into_owned(), message: diagnostic.message().into_owned(),

View File

@@ -1,5 +1,7 @@
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{ use red_knot_python_semantic::{
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings, default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonVersion,
SearchPathSettings,
}; };
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
@@ -13,16 +15,20 @@ pub(crate) struct Db {
files: Files, files: Files,
system: TestSystem, system: TestSystem,
vendored: VendoredFileSystem, vendored: VendoredFileSystem,
rule_selection: RuleSelection,
} }
impl Db { impl Db {
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self { pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
let db = Self { let db = Self {
workspace_root, workspace_root,
storage: salsa::Storage::default(), storage: salsa::Storage::default(),
system: TestSystem::default(), system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(), vendored: red_knot_vendored::file_system().clone(),
files: Files::default(), files: Files::default(),
rule_selection,
}; };
db.memory_file_system() db.memory_file_system()
@@ -85,6 +91,10 @@ impl SemanticDb for Db {
fn is_file_open(&self, file: File) -> bool { fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
} }
#[salsa::db] #[salsa::db]

View File

@@ -144,7 +144,7 @@ struct DiagnosticWithLine<T> {
mod tests { mod tests {
use crate::db::Db; use crate::db::Db;
use crate::diagnostic::Diagnostic; use crate::diagnostic::Diagnostic;
use ruff_db::diagnostic::Severity; use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::source::line_index; use ruff_db::source::line_index;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -190,8 +190,8 @@ mod tests {
} }
impl Diagnostic for DummyDiagnostic { impl Diagnostic for DummyDiagnostic {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
"dummy" DiagnosticId::Lint(LintName::of("dummy"))
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {

View File

@@ -4,7 +4,7 @@ use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
use crate::db::Db; use crate::db::Db;
use crate::diagnostic::SortedDiagnostics; use crate::diagnostic::SortedDiagnostics;
use colored::Colorize; use colored::Colorize;
use ruff_db::diagnostic::Diagnostic; use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::source::{line_index, source_text, SourceText}; use ruff_db::source::{line_index, source_text, SourceText};
use ruff_source_file::{LineIndex, OneIndexed}; use ruff_source_file::{LineIndex, OneIndexed};
@@ -146,7 +146,7 @@ fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
diagnostic: &T, diagnostic: &T,
original: std::fmt::Arguments, original: std::fmt::Arguments,
) -> String { ) -> String {
if diagnostic.rule() == "undefined-reveal" { if diagnostic.id().is_lint_named("undefined-reveal") {
format!( format!(
"{} add a `# revealed` assertion on this line (original diagnostic: {original})", "{} add a `# revealed` assertion on this line (original diagnostic: {original})",
"used built-in `reveal_type`:".yellow() "used built-in `reveal_type`:".yellow()
@@ -163,7 +163,7 @@ where
fn unmatched(&self) -> String { fn unmatched(&self) -> String {
maybe_add_undefined_reveal_clarification( maybe_add_undefined_reveal_clarification(
self, self,
format_args!(r#"[{}] "{}""#, self.rule(), self.message()), format_args!(r#"[{}] "{}""#, self.id(), self.message()),
) )
} }
} }
@@ -175,7 +175,7 @@ where
fn unmatched_with_column(&self, column: OneIndexed) -> String { fn unmatched_with_column(&self, column: OneIndexed) -> String {
maybe_add_undefined_reveal_clarification( maybe_add_undefined_reveal_clarification(
self, self,
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()), format_args!(r#"{column} [{}] "{}""#, self.id(), self.message()),
) )
} }
} }
@@ -270,10 +270,11 @@ impl Matcher {
match assertion { match assertion {
Assertion::Error(error) => { Assertion::Error(error) => {
let position = unmatched.iter().position(|diagnostic| { let position = unmatched.iter().position(|diagnostic| {
!error.rule.is_some_and(|rule| rule != diagnostic.rule()) !error.rule.is_some_and(|rule| {
&& !error !(diagnostic.id().is_lint_named(rule) || diagnostic.id().matches(rule))
.column }) && !error
.is_some_and(|col| col != self.column(*diagnostic)) .column
.is_some_and(|col| col != self.column(*diagnostic))
&& !error && !error
.message_contains .message_contains
.is_some_and(|needle| !diagnostic.message().contains(needle)) .is_some_and(|needle| !diagnostic.message().contains(needle))
@@ -294,12 +295,12 @@ impl Matcher {
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`"); let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
for (index, diagnostic) in unmatched.iter().enumerate() { for (index, diagnostic) in unmatched.iter().enumerate() {
if matched_revealed_type.is_none() if matched_revealed_type.is_none()
&& diagnostic.rule() == "revealed-type" && diagnostic.id() == DiagnosticId::RevealedType
&& diagnostic.message() == expected_reveal_type_message && diagnostic.message() == expected_reveal_type_message
{ {
matched_revealed_type = Some(index); matched_revealed_type = Some(index);
} else if matched_undefined_reveal.is_none() } else if matched_undefined_reveal.is_none()
&& diagnostic.rule() == "undefined-reveal" && diagnostic.id().is_lint_named("undefined-reveal")
{ {
matched_undefined_reveal = Some(index); matched_undefined_reveal = Some(index);
} }
@@ -323,7 +324,7 @@ impl Matcher {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::FailuresByLine; use super::FailuresByLine;
use ruff_db::diagnostic::{Diagnostic, Severity}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
@@ -332,16 +333,16 @@ mod tests {
use std::borrow::Cow; use std::borrow::Cow;
struct ExpectedDiagnostic { struct ExpectedDiagnostic {
rule: &'static str, id: DiagnosticId,
message: &'static str, message: &'static str,
range: TextRange, range: TextRange,
} }
impl ExpectedDiagnostic { impl ExpectedDiagnostic {
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self { fn new(id: DiagnosticId, message: &'static str, offset: usize) -> Self {
let offset: u32 = offset.try_into().unwrap(); let offset: u32 = offset.try_into().unwrap();
Self { Self {
rule, id,
message, message,
range: TextRange::new(offset.into(), (offset + 1).into()), range: TextRange::new(offset.into(), (offset + 1).into()),
} }
@@ -349,7 +350,7 @@ mod tests {
fn into_diagnostic(self, file: File) -> TestDiagnostic { fn into_diagnostic(self, file: File) -> TestDiagnostic {
TestDiagnostic { TestDiagnostic {
rule: self.rule, id: self.id,
message: self.message, message: self.message,
range: self.range, range: self.range,
file, file,
@@ -359,15 +360,15 @@ mod tests {
#[derive(Debug)] #[derive(Debug)]
struct TestDiagnostic { struct TestDiagnostic {
rule: &'static str, id: DiagnosticId,
message: &'static str, message: &'static str,
range: TextRange, range: TextRange,
file: File, file: File,
} }
impl Diagnostic for TestDiagnostic { impl Diagnostic for TestDiagnostic {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
self.rule self.id
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {
@@ -437,7 +438,7 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"revealed-type", DiagnosticId::RevealedType,
"Revealed type is `Foo`", "Revealed type is `Foo`",
0, 0,
)], )],
@@ -451,7 +452,7 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"not-revealed-type", DiagnosticId::lint("not-revealed-type"),
"Revealed type is `Foo`", "Revealed type is `Foo`",
0, 0,
)], )],
@@ -463,7 +464,7 @@ mod tests {
0, 0,
&[ &[
"unmatched assertion: revealed: Foo", "unmatched assertion: revealed: Foo",
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#, r#"unexpected error: 1 [lint/not-revealed-type] "Revealed type is `Foo`""#,
], ],
)], )],
); );
@@ -474,7 +475,7 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"revealed-type", DiagnosticId::RevealedType,
"Something else", "Something else",
0, 0,
)], )],
@@ -504,8 +505,12 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ vec![
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0), ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Foo`", 0),
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0), ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
], ],
); );
@@ -517,7 +522,7 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"undefined-reveal", DiagnosticId::lint("undefined-reveal"),
"Doesn't matter", "Doesn't matter",
0, 0,
)], )],
@@ -531,8 +536,12 @@ mod tests {
let result = get_result( let result = get_result(
"x # revealed: Foo", "x # revealed: Foo",
vec![ vec![
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0), ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Bar`", 0),
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0), ExpectedDiagnostic::new(
DiagnosticId::lint("undefined-reveal"),
"Doesn't matter",
0,
),
], ],
); );
@@ -553,8 +562,16 @@ mod tests {
let result = get_result( let result = get_result(
"reveal_type(1)", "reveal_type(1)",
vec![ vec![
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0), ExpectedDiagnostic::new(
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12), DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[1]`",
12,
),
], ],
); );
@@ -564,7 +581,7 @@ mod tests {
0, 0,
&[ &[
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\ "used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: [undefined-reveal] \"undefined reveal message\")", original diagnostic: [lint/undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#, r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
], ],
)], )],
@@ -576,8 +593,16 @@ mod tests {
let result = get_result( let result = get_result(
"reveal_type(1) # error: [something-else]", "reveal_type(1) # error: [something-else]",
vec![ vec![
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0), ExpectedDiagnostic::new(
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12), DiagnosticId::lint("undefined-reveal"),
"undefined reveal message",
0,
),
ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[1]`",
12,
),
], ],
); );
@@ -588,7 +613,7 @@ mod tests {
&[ &[
"unmatched assertion: error: [something-else]", "unmatched assertion: error: [something-else]",
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\ "used built-in `reveal_type`: add a `# revealed` assertion on this line (\
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")", original diagnostic: 1 [lint/undefined-reveal] \"undefined reveal message\")",
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#, r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
], ],
)], )],
@@ -606,7 +631,11 @@ mod tests {
fn error_match_rule() { fn error_match_rule() {
let result = get_result( let result = get_result(
"x # error: [some-rule]", "x # error: [some-rule]",
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
); );
assert_ok(&result); assert_ok(&result);
@@ -616,7 +645,11 @@ mod tests {
fn error_wrong_rule() { fn error_wrong_rule() {
let result = get_result( let result = get_result(
"x # error: [some-rule]", "x # error: [some-rule]",
vec![ExpectedDiagnostic::new("anything", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
); );
assert_fail( assert_fail(
@@ -625,7 +658,7 @@ mod tests {
0, 0,
&[ &[
"unmatched assertion: error: [some-rule]", "unmatched assertion: error: [some-rule]",
r#"unexpected error: 1 [anything] "Any message""#, r#"unexpected error: 1 [lint/anything] "Any message""#,
], ],
)], )],
); );
@@ -636,7 +669,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: "contains this""#, r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"anything", DiagnosticId::lint("anything"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -649,7 +682,11 @@ mod tests {
fn error_wrong_message() { fn error_wrong_message() {
let result = get_result( let result = get_result(
r#"x # error: "contains this""#, r#"x # error: "contains this""#,
vec![ExpectedDiagnostic::new("anything", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("anything"),
"Any message",
0,
)],
); );
assert_fail( assert_fail(
@@ -658,7 +695,7 @@ mod tests {
0, 0,
&[ &[
r#"unmatched assertion: error: "contains this""#, r#"unmatched assertion: error: "contains this""#,
r#"unexpected error: 1 [anything] "Any message""#, r#"unexpected error: 1 [lint/anything] "Any message""#,
], ],
)], )],
); );
@@ -668,7 +705,11 @@ mod tests {
fn error_match_column_and_rule() { fn error_match_column_and_rule() {
let result = get_result( let result = get_result(
"x # error: 1 [some-rule]", "x # error: 1 [some-rule]",
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
); );
assert_ok(&result); assert_ok(&result);
@@ -678,7 +719,11 @@ mod tests {
fn error_wrong_column() { fn error_wrong_column() {
let result = get_result( let result = get_result(
"x # error: 2 [rule]", "x # error: 2 [rule]",
vec![ExpectedDiagnostic::new("rule", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("rule"),
"Any message",
0,
)],
); );
assert_fail( assert_fail(
@@ -687,7 +732,7 @@ mod tests {
0, 0,
&[ &[
"unmatched assertion: error: 2 [rule]", "unmatched assertion: error: 2 [rule]",
r#"unexpected error: 1 [rule] "Any message""#, r#"unexpected error: 1 [lint/rule] "Any message""#,
], ],
)], )],
); );
@@ -698,7 +743,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: 1 "contains this""#, r#"x # error: 1 "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"anything", DiagnosticId::lint("anything"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -712,7 +757,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: [a-rule] "contains this""#, r#"x # error: [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"a-rule", DiagnosticId::lint("a-rule"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -726,7 +771,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: 1 [a-rule] "contains this""#, r#"x # error: 1 [a-rule] "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"a-rule", DiagnosticId::lint("a-rule"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -740,7 +785,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: 2 [some-rule] "contains this""#, r#"x # error: 2 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"some-rule", DiagnosticId::lint("some-rule"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -752,7 +797,7 @@ mod tests {
0, 0,
&[ &[
r#"unmatched assertion: error: 2 [some-rule] "contains this""#, r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "message contains this""#, r#"unexpected error: 1 [lint/some-rule] "message contains this""#,
], ],
)], )],
); );
@@ -763,7 +808,7 @@ mod tests {
let result = get_result( let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#, r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new( vec![ExpectedDiagnostic::new(
"other-rule", DiagnosticId::lint("other-rule"),
"message contains this", "message contains this",
0, 0,
)], )],
@@ -775,7 +820,7 @@ mod tests {
0, 0,
&[ &[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#, r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [other-rule] "message contains this""#, r#"unexpected error: 1 [lint/other-rule] "message contains this""#,
], ],
)], )],
); );
@@ -785,7 +830,11 @@ mod tests {
fn error_match_all_wrong_message() { fn error_match_all_wrong_message() {
let result = get_result( let result = get_result(
r#"x # error: 1 [some-rule] "contains this""#, r#"x # error: 1 [some-rule] "contains this""#,
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"Any message",
0,
)],
); );
assert_fail( assert_fail(
@@ -794,7 +843,7 @@ mod tests {
0, 0,
&[ &[
r#"unmatched assertion: error: 1 [some-rule] "contains this""#, r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
r#"unexpected error: 1 [some-rule] "Any message""#, r#"unexpected error: 1 [lint/some-rule] "Any message""#,
], ],
)], )],
); );
@@ -818,9 +867,9 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("line-two", "msg", two), ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
ExpectedDiagnostic::new("line-three", "msg", three), ExpectedDiagnostic::new(DiagnosticId::lint("line-three"), "msg", three),
ExpectedDiagnostic::new("line-five", "msg", five), ExpectedDiagnostic::new(DiagnosticId::lint("line-five"), "msg", five),
], ],
); );
@@ -828,9 +877,9 @@ mod tests {
result, result,
&[ &[
(1, &["unmatched assertion: error: [line-one]"]), (1, &["unmatched assertion: error: [line-one]"]),
(2, &[r#"unexpected error: [line-two] "msg""#]), (2, &[r#"unexpected error: [lint/line-two] "msg""#]),
(4, &["unmatched assertion: error: [line-four]"]), (4, &["unmatched assertion: error: [line-four]"]),
(5, &[r#"unexpected error: [line-five] "msg""#]), (5, &[r#"unexpected error: [lint/line-five] "msg""#]),
(6, &["unmatched assertion: error: [line-six]"]), (6, &["unmatched assertion: error: [line-six]"]),
], ],
); );
@@ -849,12 +898,15 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("line-one", "msg", one), ExpectedDiagnostic::new(DiagnosticId::lint("line-one"), "msg", one),
ExpectedDiagnostic::new("line-two", "msg", two), ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
], ],
); );
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]); assert_fail(
result,
&[(2, &[r#"unexpected error: [lint/line-two] "msg""#])],
);
} }
#[test] #[test]
@@ -870,8 +922,8 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("one-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new("other-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
], ],
); );
@@ -891,8 +943,8 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("one-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new("one-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
], ],
); );
@@ -912,15 +964,15 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("one-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
ExpectedDiagnostic::new("other-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
ExpectedDiagnostic::new("third-rule", "msg", x), ExpectedDiagnostic::new(DiagnosticId::lint("third-rule"), "msg", x),
], ],
); );
assert_fail( assert_fail(
result, result,
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])], &[(3, &[r#"unexpected error: 1 [lint/third-rule] "msg""#])],
); );
} }
@@ -938,8 +990,12 @@ mod tests {
let result = get_result( let result = get_result(
&source, &source,
vec![ vec![
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal), ExpectedDiagnostic::new(DiagnosticId::lint("undefined-reveal"), "msg", reveal),
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal), ExpectedDiagnostic::new(
DiagnosticId::RevealedType,
"Revealed type is `Literal[5]`",
reveal,
),
], ],
); );
@@ -952,7 +1008,11 @@ mod tests {
let x = source.find('x').unwrap(); let x = source.find('x').unwrap();
let result = get_result( let result = get_result(
source, source,
vec![ExpectedDiagnostic::new("some-rule", "some message", x)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
); );
assert_fail( assert_fail(
@@ -961,7 +1021,7 @@ mod tests {
0, 0,
&[ &[
"invalid assertion: no rule or message text", "invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#, r#"unexpected error: 1 [lint/some-rule] "some message""#,
], ],
)], )],
); );
@@ -973,7 +1033,11 @@ mod tests {
let x = source.find('x').unwrap(); let x = source.find('x').unwrap();
let result = get_result( let result = get_result(
source, source,
vec![ExpectedDiagnostic::new("some-rule", "some message", x)], vec![ExpectedDiagnostic::new(
DiagnosticId::lint("some-rule"),
"some message",
x,
)],
); );
assert_fail( assert_fail(
@@ -982,7 +1046,7 @@ mod tests {
0, 0,
&[ &[
"invalid assertion: no rule or message text", "invalid assertion: no rule or message text",
r#"unexpected error: 1 [some-rule] "some message""#, r#"unexpected error: 1 [lint/some-rule] "some message""#,
], ],
)], )],
); );

View File

@@ -1,16 +1,17 @@
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;
use std::sync::Arc; use std::sync::Arc;
use salsa::plumbing::ZalsaDatabase;
use salsa::{Cancelled, Event};
use crate::workspace::{check_file, Workspace, WorkspaceMetadata}; use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{Db as SemanticDb, Program}; use red_knot_python_semantic::{Db as SemanticDb, Program};
use ruff_db::diagnostic::Diagnostic; use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::System; use ruff_db::system::System;
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
use salsa::plumbing::ZalsaDatabase;
use salsa::{Cancelled, Event};
mod changes; mod changes;
@@ -25,6 +26,7 @@ pub struct RootDatabase {
storage: salsa::Storage<RootDatabase>, storage: salsa::Storage<RootDatabase>,
files: Files, files: Files,
system: Arc<dyn System + Send + Sync + RefUnwindSafe>, system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
rule_selection: Arc<RuleSelection>,
} }
impl RootDatabase { impl RootDatabase {
@@ -32,11 +34,14 @@ impl RootDatabase {
where where
S: System + 'static + Send + Sync + RefUnwindSafe, S: System + 'static + Send + Sync + RefUnwindSafe,
{ {
let rule_selection = RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY);
let mut db = Self { let mut db = Self {
workspace: None, workspace: None,
storage: salsa::Storage::default(), storage: salsa::Storage::default(),
files: Files::default(), files: Files::default(),
system: Arc::new(system), system: Arc::new(system),
rule_selection: Arc::new(rule_selection),
}; };
// Initialize the `Program` singleton // Initialize the `Program` singleton
@@ -83,6 +88,7 @@ impl RootDatabase {
storage: self.storage.clone(), storage: self.storage.clone(),
files: self.files.snapshot(), files: self.files.snapshot(),
system: Arc::clone(&self.system), system: Arc::clone(&self.system),
rule_selection: Arc::clone(&self.rule_selection),
} }
} }
} }
@@ -116,6 +122,10 @@ impl SemanticDb for RootDatabase {
workspace.is_file_open(self, file) workspace.is_file_open(self, file)
} }
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
} }
#[salsa::db] #[salsa::db]
@@ -162,6 +172,7 @@ pub(crate) mod tests {
use salsa::Event; use salsa::Event;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::Db as SemanticDb; use red_knot_python_semantic::Db as SemanticDb;
use ruff_db::files::Files; use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem}; use ruff_db::system::{DbWithTestSystem, System, TestSystem};
@@ -170,14 +181,16 @@ pub(crate) mod tests {
use crate::db::Db; use crate::db::Db;
use crate::workspace::{Workspace, WorkspaceMetadata}; use crate::workspace::{Workspace, WorkspaceMetadata};
use crate::DEFAULT_LINT_REGISTRY;
#[salsa::db] #[salsa::db]
pub(crate) struct TestDb { pub(crate) struct TestDb {
storage: salsa::Storage<Self>, storage: salsa::Storage<Self>,
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>, events: Arc<std::sync::Mutex<Vec<Event>>>,
files: Files, files: Files,
system: TestSystem, system: TestSystem,
vendored: VendoredFileSystem, vendored: VendoredFileSystem,
rule_selection: RuleSelection,
workspace: Option<Workspace>, workspace: Option<Workspace>,
} }
@@ -189,6 +202,7 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(), vendored: red_knot_vendored::file_system().clone(),
files: Files::default(), files: Files::default(),
events: Arc::default(), events: Arc::default(),
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
workspace: None, workspace: None,
}; };
@@ -259,6 +273,10 @@ pub(crate) mod tests {
fn is_file_open(&self, file: ruff_db::files::File) -> bool { fn is_file_open(&self, file: ruff_db::files::File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
} }
#[salsa::db] #[salsa::db]

View File

@@ -1,3 +1,15 @@
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
use red_knot_python_semantic::register_semantic_lints;
pub mod db; pub mod db;
pub mod watch; pub mod watch;
pub mod workspace; pub mod workspace;
pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock<LintRegistry> =
std::sync::LazyLock::new(default_lints_registry);
pub fn default_lints_registry() -> LintRegistry {
let mut builder = LintRegistryBuilder::default();
register_semantic_lints(&mut builder);
builder.build()
}

View File

@@ -6,7 +6,7 @@ use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
pub use metadata::{PackageMetadata, WorkspaceDiscoveryError, WorkspaceMetadata}; pub use metadata::{PackageMetadata, WorkspaceDiscoveryError, WorkspaceMetadata};
use red_knot_python_semantic::types::check_types; use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::SearchPathSettings; use red_knot_python_semantic::SearchPathSettings;
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic, Severity}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::source::{source_text, SourceTextError}; use ruff_db::source::{source_text, SourceTextError};
use ruff_db::system::FileType; use ruff_db::system::FileType;
@@ -533,8 +533,8 @@ pub struct IOErrorDiagnostic {
} }
impl Diagnostic for IOErrorDiagnostic { impl Diagnostic for IOErrorDiagnostic {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
"io" DiagnosticId::Io
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {

View File

@@ -1,14 +1,125 @@
use std::borrow::Cow;
use std::fmt::Formatter;
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
use crate::{ use crate::{
files::File, files::File,
source::{line_index, source_text}, source::{line_index, source_text},
Db, Db,
}; };
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange; /// A string identifier for a lint rule.
use std::borrow::Cow; ///
/// This string is used in command line and configuration interfaces. The name should always
/// be in kebab case, e.g. `no-foo` (all lower case).
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct LintName(&'static str);
impl LintName {
pub const fn of(name: &'static str) -> Self {
Self(name)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl std::ops::Deref for LintName {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl std::fmt::Display for LintName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl PartialEq<str> for LintName {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for LintName {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum DiagnosticId {
/// Some I/O operation failed
Io,
/// Some code contains a syntax error
InvalidSyntax,
/// A lint violation.
///
/// Lint's can be suppressed and some lints can be enabled or disabled in the configuration.
Lint(LintName),
/// Some code is incorrectly formatted.
Format,
/// A revealed type: Created by `reveal_type(expression)`.
RevealedType,
}
impl DiagnosticId {
/// Creates a new `DiagnosticId` for a lint with the given name.
pub const fn lint(name: &'static str) -> Self {
Self::Lint(LintName::of(name))
}
/// Returns `true` if this `DiagnosticId` represents a lint.
pub fn is_lint(&self) -> bool {
matches!(self, DiagnosticId::Lint(_))
}
/// Returns `true` if this `DiagnosticId` represents a lint with the given name.
pub fn is_lint_named(&self, name: &str) -> bool {
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
}
pub fn matches(&self, name: &str) -> bool {
match self {
DiagnosticId::Lint(self_name) => name
.strip_prefix("lint/")
.is_some_and(|rest| rest == &**self_name),
DiagnosticId::Io => name == "io",
DiagnosticId::InvalidSyntax => name == "invalid-syntax",
DiagnosticId::Format => name == "format",
DiagnosticId::RevealedType => name == "revealed-type",
}
}
}
impl std::fmt::Display for DiagnosticId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticId::InvalidSyntax => f.write_str("invalid-syntax"),
DiagnosticId::Io => f.write_str("io"),
DiagnosticId::Lint(name) => write!(f, "lint/{name}"),
DiagnosticId::Format => f.write_str("format"),
DiagnosticId::RevealedType => f.write_str("revealed-type"),
}
}
}
pub trait Diagnostic: Send + Sync + std::fmt::Debug { pub trait Diagnostic: Send + Sync + std::fmt::Debug {
fn rule(&self) -> &str; fn id(&self) -> DiagnosticId;
fn message(&self) -> std::borrow::Cow<str>; fn message(&self) -> std::borrow::Cow<str>;
@@ -29,10 +140,12 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
pub enum Severity { pub enum Severity {
Info, Info,
Warning,
Error, Error,
Fatal,
} }
pub struct DisplayDiagnostic<'db> { pub struct DisplayDiagnostic<'db> {
@@ -50,13 +163,15 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.diagnostic.severity() { match self.diagnostic.severity() {
Severity::Info => f.write_str("info")?, Severity::Info => f.write_str("info")?,
Severity::Warning => f.write_str("warning")?,
Severity::Error => f.write_str("error")?, Severity::Error => f.write_str("error")?,
Severity::Fatal => f.write_str("fatal")?,
} }
write!( write!(
f, f,
"[{rule}] {path}", "[{rule}] {path}",
rule = self.diagnostic.rule(), rule = self.diagnostic.id(),
path = self.diagnostic.file().path(self.db) path = self.diagnostic.file().path(self.db)
)?; )?;
@@ -77,8 +192,8 @@ impl<T> Diagnostic for Box<T>
where where
T: Diagnostic, T: Diagnostic,
{ {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
(**self).rule() (**self).id()
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {
@@ -102,8 +217,8 @@ impl<T> Diagnostic for std::sync::Arc<T>
where where
T: Diagnostic, T: Diagnostic,
{ {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
(**self).rule() (**self).id()
} }
fn message(&self) -> std::borrow::Cow<str> { fn message(&self) -> std::borrow::Cow<str> {
@@ -124,8 +239,8 @@ where
} }
impl Diagnostic for Box<dyn Diagnostic> { impl Diagnostic for Box<dyn Diagnostic> {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
(**self).rule() (**self).id()
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {
@@ -158,8 +273,8 @@ impl ParseDiagnostic {
} }
impl Diagnostic for ParseDiagnostic { impl Diagnostic for ParseDiagnostic {
fn rule(&self) -> &str { fn id(&self) -> DiagnosticId {
"invalid-syntax" DiagnosticId::InvalidSyntax
} }
fn message(&self) -> Cow<str> { fn message(&self) -> Cow<str> {

View File

@@ -1,6 +1,8 @@
use anyhow::Result; use anyhow::Result;
use std::sync::Arc;
use zip::CompressionMethod; use zip::CompressionMethod;
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings}; use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings};
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{OsSystem, System, SystemPathBuf}; use ruff_db::system::{OsSystem, System, SystemPathBuf};
@@ -19,6 +21,7 @@ pub struct ModuleDb {
storage: salsa::Storage<Self>, storage: salsa::Storage<Self>,
files: Files, files: Files,
system: OsSystem, system: OsSystem,
rule_selection: Arc<RuleSelection>,
} }
impl ModuleDb { impl ModuleDb {
@@ -60,6 +63,7 @@ impl ModuleDb {
storage: self.storage.clone(), storage: self.storage.clone(),
system: self.system.clone(), system: self.system.clone(),
files: self.files.snapshot(), files: self.files.snapshot(),
rule_selection: Arc::clone(&self.rule_selection),
} }
} }
} }
@@ -93,6 +97,10 @@ impl Db for ModuleDb {
fn is_file_open(&self, file: File) -> bool { fn is_file_open(&self, file: File) -> bool {
!file.path(self).is_vendored_path() !file.path(self).is_vendored_path()
} }
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
} }
#[salsa::db] #[salsa::db]

View File

@@ -0,0 +1,19 @@
use proc_macro2::TokenStream;
pub(crate) fn kebab_case(input: &syn::Ident) -> TokenStream {
let screaming_snake_case = input.to_string();
let mut kebab_case = String::with_capacity(screaming_snake_case.len());
for (i, word) in screaming_snake_case.split('_').enumerate() {
if i > 0 {
kebab_case.push('-');
}
kebab_case.push_str(&word.to_lowercase());
}
let kebab_case_lit = syn::LitStr::new(&kebab_case, input.span());
quote::quote!(#kebab_case_lit)
}

View File

@@ -10,6 +10,7 @@ mod cache_key;
mod combine_options; mod combine_options;
mod config; mod config;
mod derive_message_formats; mod derive_message_formats;
mod kebab_case;
mod map_codes; mod map_codes;
mod newtype_index; mod newtype_index;
mod rule_code_prefix; mod rule_code_prefix;
@@ -34,6 +35,14 @@ pub fn derive_combine_options(input: TokenStream) -> TokenStream {
.into() .into()
} }
/// Converts a screaming snake case identifier to a kebab case string.
#[proc_macro]
pub fn kebab_case(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::Ident);
kebab_case::kebab_case(&input).into()
}
/// Generates a [`CacheKey`] implementation for the attributed type. /// Generates a [`CacheKey`] implementation for the attributed type.
/// ///
/// Struct fields can be attributed with the `cache_key` field-attribute that supports: /// Struct fields can be attributed with the `cache_key` field-attribute that supports: