Compare commits
3 Commits
codex/stab
...
micha/erro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b2544993 | ||
|
|
fe78d50560 | ||
|
|
b39def2915 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2300,6 +2300,7 @@ dependencies = [
|
||||
"red_knot_vendored",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_literal",
|
||||
"ruff_python_parser",
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
|
||||
@@ -95,37 +95,37 @@ reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
## Various string kinds
|
||||
|
||||
```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":
|
||||
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":
|
||||
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":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
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'":
|
||||
return 1
|
||||
|
||||
@@ -208,9 +208,9 @@ i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
|
||||
@@ -73,7 +73,7 @@ def f[T]():
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```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,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
```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.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
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.__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:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], 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
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], 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
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Spam(Baz): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::lint::RuleSelection;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
@@ -5,6 +6,8 @@ use ruff_db::{Db as SourceDb, Upcast};
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -13,23 +16,24 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
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 ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use super::Db;
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
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 {
|
||||
@@ -38,8 +42,9 @@ pub(crate) mod tests {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: std::sync::Arc::default(),
|
||||
events: Arc::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 {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::hash::BuildHasherDefault;
|
||||
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use crate::lint::{LintRegistry, LintRegistryBuilder};
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
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;
|
||||
mod db;
|
||||
pub mod lint;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
@@ -26,3 +28,13 @@ mod unpack;
|
||||
mod util;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
419
crates/red_knot_python_semantic/src/lint.rs
Normal file
419
crates/red_knot_python_semantic/src/lint.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ use std::hash::Hash;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_db::diagnostic::{DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::register_type_lints;
|
||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
pub(crate) use self::display::TypeArrayDisplay;
|
||||
pub(crate) use self::infer::{
|
||||
@@ -25,7 +26,7 @@ use crate::stdlib::{
|
||||
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
|
||||
};
|
||||
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::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
|
||||
@@ -2300,9 +2301,9 @@ impl<'db> CallOutcome<'db> {
|
||||
not_callable_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(db)
|
||||
@@ -2315,9 +2316,9 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union element `{}`)",
|
||||
called_ty.display(db),
|
||||
@@ -2331,9 +2332,9 @@ impl<'db> CallOutcome<'db> {
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union elements {})",
|
||||
called_ty.display(db),
|
||||
@@ -2346,9 +2347,9 @@ impl<'db> CallOutcome<'db> {
|
||||
callable_ty: called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_ty.display(db)
|
||||
@@ -2374,7 +2375,8 @@ impl<'db> CallOutcome<'db> {
|
||||
} => {
|
||||
diagnostics.add(
|
||||
node,
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
|
||||
);
|
||||
Ok(*return_ty)
|
||||
|
||||
@@ -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::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use crate::{declare_lint, Db};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -9,18 +15,411 @@ use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
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)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
// TODO: Don't use string keys for rules
|
||||
pub(super) rule: String,
|
||||
pub(super) id: DiagnosticId,
|
||||
pub(super) message: String,
|
||||
pub(super) range: TextRange,
|
||||
pub(super) severity: Severity,
|
||||
pub(super) file: File,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostic {
|
||||
pub fn rule(&self) -> &str {
|
||||
&self.rule
|
||||
pub fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
@@ -33,8 +432,8 @@ impl TypeCheckDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for TypeCheckDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
TypeCheckDiagnostic::rule(self)
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -50,7 +449,7 @@ impl Diagnostic for TypeCheckDiagnostic {
|
||||
}
|
||||
|
||||
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
|
||||
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable",
|
||||
not_iterable_ty.display(self.db)
|
||||
@@ -167,9 +566,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type<'db>,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NOT_ITERABLE,
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(self.db)
|
||||
@@ -186,9 +585,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
length: usize,
|
||||
index: i64,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&INDEX_OUT_OF_BOUNDS,
|
||||
node,
|
||||
"index-out-of-bounds",
|
||||
format_args!(
|
||||
"Index {index} is out of bounds for {kind} `{}` with length {length}",
|
||||
tuple_ty.display(self.db)
|
||||
@@ -203,9 +602,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
non_subscriptable_ty: Type<'db>,
|
||||
method: &str,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&NON_SUBSCRIPTABLE,
|
||||
node,
|
||||
"non-subscriptable",
|
||||
format_args!(
|
||||
"Cannot subscript object of type `{}` with no `{method}` method",
|
||||
non_subscriptable_ty.display(self.db)
|
||||
@@ -219,9 +618,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
import_node.into(),
|
||||
"unresolved-import",
|
||||
format_args!(
|
||||
"Cannot resolve import `{}{}`",
|
||||
".".repeat(level as usize),
|
||||
@@ -231,9 +630,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&ZERO_STEPSIZE_IN_SLICE,
|
||||
node,
|
||||
"zero-stepsize-in-slice",
|
||||
format_args!("Slice step size can not be zero"),
|
||||
);
|
||||
}
|
||||
@@ -246,19 +645,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
) {
|
||||
match declared_ty {
|
||||
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",
|
||||
class.name(self.db)));
|
||||
}
|
||||
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",
|
||||
function.name(self.db)));
|
||||
}
|
||||
_ => {
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&INVALID_ASSIGNMENT,
|
||||
node,
|
||||
"invalid-assignment",
|
||||
format_args!(
|
||||
"Object of type `{}` is not assignable to `{}`",
|
||||
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) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&POSSIBLY_UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
"possibly-unresolved-reference",
|
||||
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) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
self.add_lint(
|
||||
&UNRESOLVED_REFERENCE,
|
||||
expr_name_node.into(),
|
||||
"unresolved-reference",
|
||||
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.
|
||||
///
|
||||
/// 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) {
|
||||
return;
|
||||
}
|
||||
@@ -305,9 +724,10 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
|
||||
self.diagnostics.push(TypeCheckDiagnostic {
|
||||
file: self.file,
|
||||
rule: rule.to_string(),
|
||||
id,
|
||||
message: message.to_string(),
|
||||
range: node.range(),
|
||||
severity,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::num::NonZeroU32;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::files::File;
|
||||
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 salsa;
|
||||
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::SemanticIndex;
|
||||
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::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
@@ -64,7 +72,9 @@ use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
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.
|
||||
/// 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 {
|
||||
// (1) Check that the class does not have a cyclic definition
|
||||
if class.is_cyclically_defined(self.db) {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CYCLIC_CLASS_DEFINITION,
|
||||
class_node.into(),
|
||||
"cyclic-class-def",
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db),
|
||||
@@ -522,9 +532,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&DUPLICATE_BASE,
|
||||
(&base_nodes[*index]).into(),
|
||||
"duplicate-base",
|
||||
format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
|
||||
);
|
||||
}
|
||||
@@ -532,9 +542,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, base_ty) in bases {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_BASE,
|
||||
(&base_nodes[*index]).into(),
|
||||
"invalid-base",
|
||||
format_args!(
|
||||
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
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(),
|
||||
"inconsistent-mro",
|
||||
format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db),
|
||||
@@ -572,9 +582,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} => {
|
||||
let node = class_node.into();
|
||||
if *candidate1_is_base_class {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_METACLASS,
|
||||
node,
|
||||
"conflicting-metaclass",
|
||||
format_args!(
|
||||
"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}`) \
|
||||
@@ -584,12 +594,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
base1 = class1.name(self.db),
|
||||
metaclass2 = metaclass2.name(self.db),
|
||||
base2 = class2.name(self.db),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_METACLASS,
|
||||
node,
|
||||
"conflicting-metaclass",
|
||||
format_args!(
|
||||
"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}`) \
|
||||
@@ -598,7 +608,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
metaclass_of_class = metaclass1.name(self.db),
|
||||
metaclass_of_base = metaclass2.name(self.db),
|
||||
base = class2.name(self.db),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -736,9 +746,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&DIVISION_BY_ZERO,
|
||||
expr.into(),
|
||||
"division-by-zero",
|
||||
format_args!(
|
||||
"Cannot {op} object of type `{}` {by_zero}",
|
||||
left.display(self.db)
|
||||
@@ -761,9 +771,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO point out the conflicting declarations in the diagnostic?
|
||||
let symbol_table = self.index.symbol_table(binding.file_scope(self.db));
|
||||
let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name();
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CONFLICTING_DECLARATIONS,
|
||||
node,
|
||||
"conflicting-declarations",
|
||||
format_args!(
|
||||
"Conflicting declared types for `{symbol_name}`: {}",
|
||||
conflicting.display(self.db)
|
||||
@@ -791,9 +801,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let ty = if inferred_ty.is_assignable_to(self.db, ty) {
|
||||
ty
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_DECLARATION,
|
||||
node,
|
||||
"invalid-declaration",
|
||||
format_args!(
|
||||
"Cannot declare type `{}` for inferred type `{}`",
|
||||
ty.display(self.db),
|
||||
@@ -1088,12 +1098,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if default_ty.is_assignable_to(self.db, declared_ty) {
|
||||
UnionType::from_elements(self.db, [declared_ty, default_ty])
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_PARAMETER_DEFAULT,
|
||||
parameter_with_default.into(),
|
||||
"invalid-parameter-default",
|
||||
format_args!(
|
||||
"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
|
||||
}
|
||||
@@ -1400,9 +1410,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
|
||||
match (enter, exit) {
|
||||
(Symbol::Unbound, Symbol::Unbound) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
@@ -1411,9 +1421,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
(Symbol::Unbound, _) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
|
||||
context_expression_ty.display(self.db)
|
||||
@@ -1423,9 +1433,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
(Symbol::Type(enter_ty, enter_boundness), exit) => {
|
||||
if enter_boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"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),
|
||||
@@ -1437,9 +1447,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[context_expression_ty])
|
||||
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
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)
|
||||
),
|
||||
@@ -1449,9 +1459,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match exit {
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
|
||||
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.
|
||||
|
||||
if exit_boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"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),
|
||||
@@ -1489,9 +1499,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"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),
|
||||
@@ -1569,9 +1579,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let bound_or_constraint = match bound.as_deref() {
|
||||
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
|
||||
if elts.len() < 2 {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
expr.into(),
|
||||
"invalid-typevar-constraints",
|
||||
format_args!("TypeVar must have at least two constrained types"),
|
||||
);
|
||||
self.infer_expression(expr);
|
||||
@@ -1892,9 +1902,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
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)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -1942,9 +1952,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_binary_expression_type(left_ty, right_ty, op)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
assignment.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -1974,11 +1984,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// Resolve the target type, assuming a load context.
|
||||
let target_type = match &**target {
|
||||
Expr::Name(name) => {
|
||||
ast::Expr::Name(name) => {
|
||||
self.store_expression_type(target, Type::Never);
|
||||
self.infer_name_load(name)
|
||||
}
|
||||
Expr::Attribute(attr) => {
|
||||
ast::Expr::Attribute(attr) => {
|
||||
self.store_expression_type(target, Type::Never);
|
||||
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)) {
|
||||
Symbol::Type(ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&POSSIBLY_UNBOUND_IMPORT,
|
||||
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
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNRESOLVED_IMPORT,
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
Type::Unknown
|
||||
@@ -2912,9 +2922,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
{
|
||||
let mut builtins_symbol = builtins_symbol(self.db, name);
|
||||
if builtins_symbol.is_unbound() && name == "reveal_type" {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNDEFINED_REVEAL,
|
||||
name_node.into(),
|
||||
"undefined-reveal",
|
||||
format_args!(
|
||||
"`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) {
|
||||
Symbol::Type(member_ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&POSSIBLY_UNBOUND_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
"possibly-unbound-attribute",
|
||||
format_args!(
|
||||
"Attribute `{}` on type `{}` is possibly unbound",
|
||||
attr.id,
|
||||
@@ -3023,9 +3033,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
member_ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
"unresolved-attribute",
|
||||
format_args!(
|
||||
"Type `{}` has no attribute `{}`",
|
||||
value_ty.display(self.db),
|
||||
@@ -3104,9 +3114,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
@@ -3116,9 +3126,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
@@ -3157,9 +3167,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
self.infer_binary_expression_type(left_ty, right_ty, *op)
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
binary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{op}` is unsupported between objects of type `{}` and `{}`",
|
||||
left_ty.display(self.db),
|
||||
@@ -3467,9 +3477,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_binary_type_comparison(left_ty, *op, right_ty)
|
||||
.unwrap_or_else(|error| {
|
||||
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
AnyNodeRef::ExprCompare(compare),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Operator `{}` is not supported for types `{}` and `{}`{}",
|
||||
error.op,
|
||||
@@ -4124,9 +4134,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(dunder_getitem_method, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
value_node.into(),
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
@@ -4138,9 +4148,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
@@ -4168,9 +4178,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(ty, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
value_node.into(),
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
@@ -4182,9 +4192,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
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::BytesLiteral(bytes) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&BYTE_STRING_TYPE_ANNOTATION,
|
||||
bytes.into(),
|
||||
"annotation-byte-string",
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&FSTRING_TYPE_ANNOTATION,
|
||||
fstring.into(),
|
||||
"annotation-f-string",
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
self.infer_fstring_expression(fstring);
|
||||
@@ -4677,9 +4687,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
ast::Expr::Tuple(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
slice.into(),
|
||||
"invalid-type-form",
|
||||
format_args!("type[...] must have exactly one type argument"),
|
||||
);
|
||||
Type::Unknown
|
||||
@@ -4726,9 +4736,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Ok(ty) => ty,
|
||||
Err(nodes) => {
|
||||
for node in nodes {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_LITERAL_PARAMETER,
|
||||
node.into(),
|
||||
"invalid-literal-parameter",
|
||||
format_args!(
|
||||
"Type arguments for `Literal` must be `None`, \
|
||||
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")
|
||||
}
|
||||
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_PARAMETER,
|
||||
subscript.into(),
|
||||
"invalid-type-parameter",
|
||||
format_args!(
|
||||
"Type `{}` expected no type parameter",
|
||||
known_instance.repr(self.db)
|
||||
@@ -4773,9 +4783,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
KnownInstanceType::LiteralString => {
|
||||
self.diagnostics.add(
|
||||
self.diagnostics.add_lint(
|
||||
&INVALID_TYPE_PARAMETER,
|
||||
subscript.into(),
|
||||
"invalid-type-parameter",
|
||||
format_args!(
|
||||
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
|
||||
known_instance.repr(self.db)
|
||||
|
||||
@@ -5,8 +5,127 @@ use ruff_python_ast::{self as ast, ModExpression, StringFlags};
|
||||
use ruff_python_parser::{parse_expression_range, Parsed};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::lint::{Level, LintStatus};
|
||||
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>;
|
||||
|
||||
@@ -25,9 +144,9 @@ pub(crate) fn parse_string_annotation(
|
||||
if let [string_literal] = string_expr.value.as_slice() {
|
||||
let prefix = string_literal.flags.prefix();
|
||||
if prefix.is_raw() {
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&RAW_STRING_TYPE_ANNOTATION,
|
||||
string_literal.into(),
|
||||
"annotation-raw-string",
|
||||
format_args!("Type expressions cannot use raw string literal"),
|
||||
);
|
||||
// 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) {
|
||||
Ok(parsed) => return Ok(parsed),
|
||||
Err(parse_error) => diagnostics.add(
|
||||
Err(parse_error) => diagnostics.add_lint(
|
||||
&INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
string_literal.into(),
|
||||
"forward-annotation-syntax-error",
|
||||
format_args!("Syntax error in forward annotation: {}", parse_error.error),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// The raw contents of the string doesn't match the parsed content. This could be the
|
||||
// case for annotations that contain escape sequences.
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION,
|
||||
string_expr.into(),
|
||||
"annotation-escape-character",
|
||||
format_args!("Type expressions cannot contain escape characters"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// String is implicitly concatenated.
|
||||
diagnostics.add(
|
||||
diagnostics.add_lint(
|
||||
&IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION,
|
||||
string_expr.into(),
|
||||
"annotation-implicit-concat",
|
||||
format_args!("Type expressions cannot span multiple string literals"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,14 +86,15 @@ fn to_lsp_diagnostic(
|
||||
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => DiagnosticSeverity::INFORMATION,
|
||||
Severity::Error => DiagnosticSeverity::ERROR,
|
||||
Severity::Warning => DiagnosticSeverity::WARNING,
|
||||
Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR,
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
range,
|
||||
severity: Some(severity),
|
||||
tags: None,
|
||||
code: Some(NumberOrString::String(diagnostic.rule().to_string())),
|
||||
code: Some(NumberOrString::String(diagnostic.id().to_string())),
|
||||
code_description: None,
|
||||
source: Some("red-knot".into()),
|
||||
message: diagnostic.message().into_owned(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
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::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -13,16 +15,20 @@ pub(crate) struct Db {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
|
||||
|
||||
let db = Self {
|
||||
workspace_root,
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
rule_selection,
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
@@ -85,6 +91,10 @@ impl SemanticDb for Db {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -144,7 +144,7 @@ struct DiagnosticWithLine<T> {
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
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::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -190,8 +190,8 @@ mod tests {
|
||||
}
|
||||
|
||||
impl Diagnostic for DummyDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"dummy"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Lint(LintName::of("dummy"))
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
@@ -146,7 +146,7 @@ fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
|
||||
diagnostic: &T,
|
||||
original: std::fmt::Arguments,
|
||||
) -> String {
|
||||
if diagnostic.rule() == "undefined-reveal" {
|
||||
if diagnostic.id().is_lint_named("undefined-reveal") {
|
||||
format!(
|
||||
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
|
||||
"used built-in `reveal_type`:".yellow()
|
||||
@@ -163,7 +163,7 @@ where
|
||||
fn unmatched(&self) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
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 {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
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 {
|
||||
Assertion::Error(error) => {
|
||||
let position = unmatched.iter().position(|diagnostic| {
|
||||
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
|
||||
&& !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
!error.rule.is_some_and(|rule| {
|
||||
!(diagnostic.id().is_lint_named(rule) || diagnostic.id().matches(rule))
|
||||
}) && !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
&& !error
|
||||
.message_contains
|
||||
.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}`");
|
||||
for (index, diagnostic) in unmatched.iter().enumerate() {
|
||||
if matched_revealed_type.is_none()
|
||||
&& diagnostic.rule() == "revealed-type"
|
||||
&& diagnostic.id() == DiagnosticId::RevealedType
|
||||
&& diagnostic.message() == expected_reveal_type_message
|
||||
{
|
||||
matched_revealed_type = Some(index);
|
||||
} else if matched_undefined_reveal.is_none()
|
||||
&& diagnostic.rule() == "undefined-reveal"
|
||||
&& diagnostic.id().is_lint_named("undefined-reveal")
|
||||
{
|
||||
matched_undefined_reveal = Some(index);
|
||||
}
|
||||
@@ -323,7 +324,7 @@ impl Matcher {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
@@ -332,16 +333,16 @@ mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
struct ExpectedDiagnostic {
|
||||
rule: &'static str,
|
||||
id: DiagnosticId,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
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();
|
||||
Self {
|
||||
rule,
|
||||
id,
|
||||
message,
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
@@ -349,7 +350,7 @@ mod tests {
|
||||
|
||||
fn into_diagnostic(self, file: File) -> TestDiagnostic {
|
||||
TestDiagnostic {
|
||||
rule: self.rule,
|
||||
id: self.id,
|
||||
message: self.message,
|
||||
range: self.range,
|
||||
file,
|
||||
@@ -359,15 +360,15 @@ mod tests {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
id: DiagnosticId,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -437,7 +438,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
@@ -451,7 +452,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
DiagnosticId::lint("not-revealed-type"),
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
@@ -463,7 +464,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"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(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
DiagnosticId::RevealedType,
|
||||
"Something else",
|
||||
0,
|
||||
)],
|
||||
@@ -504,8 +505,12 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -517,7 +522,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"undefined-reveal",
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
)],
|
||||
@@ -531,8 +536,12 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new(DiagnosticId::RevealedType, "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("undefined-reveal"),
|
||||
"Doesn't matter",
|
||||
0,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -553,8 +562,16 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new(
|
||||
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,
|
||||
&[
|
||||
"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]`""#,
|
||||
],
|
||||
)],
|
||||
@@ -576,8 +593,16 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new(
|
||||
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]",
|
||||
"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]`""#,
|
||||
],
|
||||
)],
|
||||
@@ -606,7 +631,11 @@ mod tests {
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"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);
|
||||
@@ -616,7 +645,11 @@ mod tests {
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("anything"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -625,7 +658,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"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(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
DiagnosticId::lint("anything"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -649,7 +682,11 @@ mod tests {
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("anything"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -658,7 +695,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
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() {
|
||||
let result = get_result(
|
||||
"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);
|
||||
@@ -678,7 +719,11 @@ mod tests {
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![ExpectedDiagnostic::new("rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("rule"),
|
||||
"Any message",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -687,7 +732,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"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(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
DiagnosticId::lint("anything"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -712,7 +757,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
DiagnosticId::lint("a-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -726,7 +771,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
DiagnosticId::lint("a-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -740,7 +785,7 @@ mod tests {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"some-rule",
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -752,7 +797,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
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(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"other-rule",
|
||||
DiagnosticId::lint("other-rule"),
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
@@ -775,7 +820,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
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() {
|
||||
let result = get_result(
|
||||
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(
|
||||
@@ -794,7 +843,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
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(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-three", "msg", three),
|
||||
ExpectedDiagnostic::new("line-five", "msg", five),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-two"), "msg", two),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-three"), "msg", three),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-five"), "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -828,9 +877,9 @@ mod tests {
|
||||
result,
|
||||
&[
|
||||
(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]"]),
|
||||
(5, &[r#"unexpected error: [line-five] "msg""#]),
|
||||
(5, &[r#"unexpected error: [lint/line-five] "msg""#]),
|
||||
(6, &["unmatched assertion: error: [line-six]"]),
|
||||
],
|
||||
);
|
||||
@@ -849,12 +898,15 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("line-one", "msg", one),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("line-one"), "msg", one),
|
||||
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]
|
||||
@@ -870,8 +922,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -891,8 +943,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -912,15 +964,15 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("third-rule", "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("one-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("other-rule"), "msg", x),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("third-rule"), "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
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(
|
||||
&source,
|
||||
vec![
|
||||
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
ExpectedDiagnostic::new(DiagnosticId::lint("undefined-reveal"), "msg", reveal),
|
||||
ExpectedDiagnostic::new(
|
||||
DiagnosticId::RevealedType,
|
||||
"Revealed type is `Literal[5]`",
|
||||
reveal,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -952,7 +1008,11 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"some message",
|
||||
x,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -961,7 +1021,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"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 result = get_result(
|
||||
source,
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
DiagnosticId::lint("some-rule"),
|
||||
"some message",
|
||||
x,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -982,7 +1046,7 @@ mod tests {
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
r#"unexpected error: 1 [lint/some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
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 ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
mod changes;
|
||||
|
||||
@@ -25,6 +26,7 @@ pub struct RootDatabase {
|
||||
storage: salsa::Storage<RootDatabase>,
|
||||
files: Files,
|
||||
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl RootDatabase {
|
||||
@@ -32,11 +34,14 @@ impl RootDatabase {
|
||||
where
|
||||
S: System + 'static + Send + Sync + RefUnwindSafe,
|
||||
{
|
||||
let rule_selection = RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY);
|
||||
|
||||
let mut db = Self {
|
||||
workspace: None,
|
||||
storage: salsa::Storage::default(),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
rule_selection: Arc::new(rule_selection),
|
||||
};
|
||||
|
||||
// Initialize the `Program` singleton
|
||||
@@ -83,6 +88,7 @@ impl RootDatabase {
|
||||
storage: self.storage.clone(),
|
||||
files: self.files.snapshot(),
|
||||
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)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -162,6 +172,7 @@ pub(crate) mod tests {
|
||||
|
||||
use salsa::Event;
|
||||
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
@@ -170,14 +181,16 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::workspace::{Workspace, WorkspaceMetadata};
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
events: Arc<std::sync::Mutex<Vec<Event>>>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
workspace: Option<Workspace>,
|
||||
}
|
||||
|
||||
@@ -189,6 +202,7 @@ pub(crate) mod tests {
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
events: Arc::default(),
|
||||
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
|
||||
workspace: None,
|
||||
};
|
||||
|
||||
@@ -259,6 +273,10 @@ pub(crate) mod tests {
|
||||
fn is_file_open(&self, file: ruff_db::files::File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
@@ -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 watch;
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
pub use metadata::{PackageMetadata, WorkspaceDiscoveryError, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
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::source::{source_text, SourceTextError};
|
||||
use ruff_db::system::FileType;
|
||||
@@ -533,8 +533,8 @@ pub struct IOErrorDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"io"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -1,14 +1,125 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::{
|
||||
files::File,
|
||||
source::{line_index, source_text},
|
||||
Db,
|
||||
};
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A string identifier for a lint rule.
|
||||
///
|
||||
/// 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 {
|
||||
fn rule(&self) -> &str;
|
||||
fn id(&self) -> DiagnosticId;
|
||||
|
||||
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 {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
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 {
|
||||
match self.diagnostic.severity() {
|
||||
Severity::Info => f.write_str("info")?,
|
||||
Severity::Warning => f.write_str("warning")?,
|
||||
Severity::Error => f.write_str("error")?,
|
||||
Severity::Fatal => f.write_str("fatal")?,
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{rule}] {path}",
|
||||
rule = self.diagnostic.rule(),
|
||||
rule = self.diagnostic.id(),
|
||||
path = self.diagnostic.file().path(self.db)
|
||||
)?;
|
||||
|
||||
@@ -77,8 +192,8 @@ impl<T> Diagnostic for Box<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -102,8 +217,8 @@ impl<T> Diagnostic for std::sync::Arc<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str> {
|
||||
@@ -124,8 +239,8 @@ where
|
||||
}
|
||||
|
||||
impl Diagnostic for Box<dyn Diagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
@@ -158,8 +273,8 @@ impl ParseDiagnostic {
|
||||
}
|
||||
|
||||
impl Diagnostic for ParseDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"invalid-syntax"
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::InvalidSyntax
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use zip::CompressionMethod;
|
||||
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use red_knot_python_semantic::{Db, Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{OsSystem, System, SystemPathBuf};
|
||||
@@ -19,6 +21,7 @@ pub struct ModuleDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: OsSystem,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl ModuleDb {
|
||||
@@ -60,6 +63,7 @@ impl ModuleDb {
|
||||
storage: self.storage.clone(),
|
||||
system: self.system.clone(),
|
||||
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 {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
|
||||
19
crates/ruff_macros/src/kebab_case.rs
Normal file
19
crates/ruff_macros/src/kebab_case.rs
Normal 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)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod cache_key;
|
||||
mod combine_options;
|
||||
mod config;
|
||||
mod derive_message_formats;
|
||||
mod kebab_case;
|
||||
mod map_codes;
|
||||
mod newtype_index;
|
||||
mod rule_code_prefix;
|
||||
@@ -34,6 +35,14 @@ pub fn derive_combine_options(input: TokenStream) -> TokenStream {
|
||||
.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.
|
||||
///
|
||||
/// Struct fields can be attributed with the `cache_key` field-attribute that supports:
|
||||
|
||||
Reference in New Issue
Block a user