Compare commits

...

3 Commits

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

1
Cargo.lock generated
View File

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

View File

@@ -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 }

View File

@@ -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()"

View File

@@ -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
```

View File

@@ -179,9 +179,9 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```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
```

View File

@@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
These are invalid, but we need to be able to handle them gracefully without panicking.
```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]]

View File

@@ -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]

View File

@@ -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);
}

View File

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

View File

@@ -2,11 +2,12 @@ use std::hash::Hash;
use indexmap::IndexSet;
use 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)

View File

@@ -1,6 +1,12 @@
use crate::lint::{Level, LintId, LintMetadata, LintRegistryBuilder, LintStatus};
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{ClassLiteralType, Type};
use crate::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,
});
}

View File

@@ -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)

View File

@@ -5,8 +5,127 @@ use ruff_python_ast::{self as ast, ModExpression, StringFlags};
use ruff_python_parser::{parse_expression_range, Parsed};
use ruff_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"),
);
}

View File

@@ -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(),

View File

@@ -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]

View File

@@ -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> {

View File

@@ -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""#,
],
)],
);

View File

@@ -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]

View File

@@ -1,3 +1,15 @@
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder};
use red_knot_python_semantic::register_semantic_lints;
pub mod db;
pub mod 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()
}

View File

@@ -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> {

View File

@@ -1,14 +1,125 @@
use std::borrow::Cow;
use std::fmt::Formatter;
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
use crate::{
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> {

View File

@@ -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]

View File

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

View File

@@ -10,6 +10,7 @@ mod cache_key;
mod combine_options;
mod 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: