[red-knot] Add infrastructure to declare lints (#14873)

## Summary

This is the second PR out of three that adds support for
enabling/disabling lint rules in Red Knot. You may want to take a look
at the [first PR](https://github.com/astral-sh/ruff/pull/14869) in this
stack to familiarize yourself with the used terminology.

This PR adds a new syntax to define a lint: 

```rust
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,
    }
}
```

A lint has a name and metadata about its status (preview, stable,
removed, deprecated), the default diagnostic level (unless the
configuration changes), and documentation. I use a macro here to derive
the kebab-case name and extract the documentation automatically.

This PR doesn't yet add any mechanism to discover all known lints. This
will be added in the next and last PR in this stack.


## Documentation
I documented some rules but then decided that it's probably not my best
use of time if I document all of them now (it also means that I play
catch-up with all of you forever). That's why I left some rules
undocumented (marked with TODO)

## Where is the best place to define all lints?

I'm not sure. I think what I have in this PR is fine but I also don't
love it because most lints are in a single place but not all of them. If
you have ideas, let me know.


## Why is the message not part of the lint, unlike Ruff's `Violation`

I understand that the main motivation for defining `message` on
`Violation` in Ruff is to remove the need to repeat the same message
over and over again. I'm not sure if this is an actual problem. Most
rules only emit a diagnostic in a single place and they commonly use
different messages if they emit diagnostics in different code paths,
requiring extra fields on the `Violation` struct.

That's why I'm not convinced that there's an actual need for it and
there are alternatives that can reduce the repetition when creating a
diagnostic:

* Create a helper function. We already do this in red knot with the
`add_xy` methods
* Create a custom `Diagnostic` implementation that tailors the entire
diagnostic and pre-codes e.g. the message

Avoiding an extra field on the `Violation` also removes the need to
allocate intermediate strings as it is commonly the place in Ruff.
Instead, Red Knot can use a borrowed string with `format_args`

## Test Plan

`cargo test`
This commit is contained in:
Micha Reiser 2024-12-10 17:14:44 +01:00 committed by GitHub
parent 5f548072d9
commit 5fc8e5d80e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 999 additions and 186 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

@ -71,21 +71,21 @@ class Foo: ...
```py ```py
def f1( def f1(
# error: [annotation-raw-string] "Type expressions cannot use raw string literal" # error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
a: r"int", a: r"int",
# error: [annotation-f-string] "Type expressions cannot use f-strings" # error: [fstring-type-annotation] "Type expressions cannot use f-strings"
b: f"int", b: f"int",
# error: [annotation-byte-string] "Type expressions cannot use bytes literal" # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
c: b"int", c: b"int",
d: "int", d: "int",
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals" # error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
e: "in" "t", e: "in" "t",
# error: [annotation-escape-character] "Type expressions cannot contain escape characters" # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
f: "\N{LATIN SMALL LETTER I}nt", f: "\N{LATIN SMALL LETTER I}nt",
# error: [annotation-escape-character] "Type expressions cannot contain escape characters" # error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
g: "\x69nt", g: "\x69nt",
h: """int""", h: """int""",
# error: [annotation-byte-string] "Type expressions cannot use bytes literal" # error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
i: "b'int'", i: "b'int'",
): ):
reveal_type(a) # revealed: Unknown reveal_type(a) # revealed: Unknown
@ -164,9 +164,9 @@ i: "{i for i in range(5)}"
j: "{i: i for i in range(5)}" j: "{i: i for i in range(5)}"
k: "(i for i in range(5))" k: "(i for i in range(5))"
l: "await 1" l: "await 1"
# error: [forward-annotation-syntax-error] # error: [invalid-syntax-in-forward-annotation]
m: "yield 1" m: "yield 1"
# error: [forward-annotation-syntax-error] # error: [invalid-syntax-in-forward-annotation]
n: "yield from 1" n: "yield from 1"
o: "1 < 2" o: "1 < 2"
p: "call()" p: "call()"

View file

@ -62,14 +62,14 @@ def foo(
```py ```py
try: try:
pass pass
# error: [invalid-exception] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" # error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except 3 as e: except 3 as e:
reveal_type(e) # revealed: Unknown reveal_type(e) # revealed: Unknown
try: try:
pass pass
# error: [invalid-exception] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" # error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
# error: [invalid-exception] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)" # error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except (ValueError, OSError, "foo", b"bar") as e: except (ValueError, OSError, "foo", b"bar") as e:
reveal_type(e) # revealed: ValueError | OSError | Unknown reveal_type(e) # revealed: ValueError | OSError | Unknown
@ -80,10 +80,10 @@ def foo(
): ):
try: try:
help() help()
# error: [invalid-exception] # error: [invalid-exception-caught]
except x as e: except x as e:
reveal_type(e) # revealed: Unknown reveal_type(e) # revealed: Unknown
# error: [invalid-exception] # error: [invalid-exception-caught]
except y as f: except y as f:
reveal_type(f) # revealed: OSError | RuntimeError | Unknown reveal_type(f) # revealed: OSError | RuntimeError | Unknown
except z as g: except z as g:

View file

@ -47,13 +47,13 @@ except* (KeyboardInterrupt, AttributeError) as e:
```py ```py
try: try:
help() help()
except* 3 as e: # error: [invalid-exception] except* 3 as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex # TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup reveal_type(e) # revealed: BaseExceptionGroup
try: try:
help() help()
except* (AttributeError, 42) as e: # error: [invalid-exception] except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex # TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup reveal_type(e) # revealed: BaseExceptionGroup
``` ```

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@ pub use semantic_model::{HasTy, SemanticModel};
pub mod ast_node_ref; pub mod ast_node_ref;
mod db; mod db;
pub mod lint;
mod module_name; mod module_name;
mod module_resolver; mod module_resolver;
mod node_key; mod node_key;

View file

@ -0,0 +1,226 @@
use itertools::Itertools;
use ruff_db::diagnostic::{LintName, Severity};
#[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,
/// The source file in which the lint is declared.
pub file: &'static str,
/// The 1-based line number in the source `file` where the lint is declared.
pub line: u32,
}
#[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 one leading space and all 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 file(&self) -> &str {
self.file
}
pub fn line(&self) -> u32 {
self.line
}
}
#[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"),
file: "",
line: 1,
}
}
#[derive(Copy, Clone, Debug)]
pub enum LintStatus {
/// The lint has been added to the linter, but is not yet stable.
Preview {
/// The version in which the lint was added.
since: &'static str,
},
/// The lint is stable.
Stable {
/// The version in which the lint was stabilized.
since: &'static str,
},
/// The lint is deprecated and no longer recommended for use.
Deprecated {
/// The version in which the lint was deprecated.
since: &'static str,
/// The reason why the lint has been deprecated.
///
/// This should explain why the lint has been deprecated and if there's a replacement lint that users
/// can use instead.
reason: &'static str,
},
/// The lint has been removed and can no longer be used.
Removed {
/// The version in which the lint was removed.
since: &'static str,
/// The reason why the lint has been removed.
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 { .. })
}
}
/// Declares a lint rule with the given metadata.
///
/// ```rust
/// use red_knot_python_semantic::declare_lint;
/// use red_knot_python_semantic::lint::{LintStatus, Level};
///
/// 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,
/// }
/// }
/// ```
#[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,
file: file!(),
line: line!(),
$( $key: $value, )*
..$crate::lint::lint_metadata_defaults()
};
};
}

View file

@ -2,7 +2,7 @@ use std::hash::Hash;
use indexmap::IndexSet; use indexmap::IndexSet;
use itertools::Itertools; use itertools::Itertools;
use ruff_db::diagnostic::DiagnosticId; use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast as ast; use ruff_python_ast as ast;
@ -25,7 +25,7 @@ use crate::stdlib::{
builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule, builtins_symbol, core_module_symbol, typing_extensions_symbol, CoreStdlibModule,
}; };
use crate::symbol::{Boundness, Symbol}; use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder; use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator}; use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint; use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
@ -2308,9 +2308,9 @@ impl<'db> CallOutcome<'db> {
not_callable_ty, not_callable_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Object of type `{}` is not callable", "Object of type `{}` is not callable",
not_callable_ty.display(db) not_callable_ty.display(db)
@ -2323,9 +2323,9 @@ impl<'db> CallOutcome<'db> {
called_ty, called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Object of type `{}` is not callable (due to union element `{}`)", "Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db), called_ty.display(db),
@ -2339,9 +2339,9 @@ impl<'db> CallOutcome<'db> {
called_ty, called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Object of type `{}` is not callable (due to union elements {})", "Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db), called_ty.display(db),
@ -2354,9 +2354,9 @@ impl<'db> CallOutcome<'db> {
callable_ty: called_ty, callable_ty: called_ty,
return_ty, return_ty,
}) => { }) => {
diagnostics.add( diagnostics.add_lint(
&CALL_NON_CALLABLE,
node, node,
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)", "Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db) called_ty.display(db)
@ -2383,6 +2383,7 @@ impl<'db> CallOutcome<'db> {
diagnostics.add( diagnostics.add(
node, node,
DiagnosticId::RevealedType, DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)), format_args!("Revealed type is `{}`", revealed_ty.display(db)),
); );
Ok(*return_ty) Ok(*return_ty)

View file

@ -1,5 +1,6 @@
use crate::lint::{Level, LintMetadata, LintStatus};
use crate::types::{ClassLiteralType, Type}; use crate::types::{ClassLiteralType, Type};
use crate::Db; use crate::{declare_lint, Db};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
@ -9,11 +10,418 @@ use std::fmt::Formatter;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
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 undefined names",
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 iteration over an object that is not iterable",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// TODO #14889
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 subscripting objects that do not support subscripting.
///
/// ## 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 subscripting 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 #14889
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 #14889
pub(crate) static INVALID_ASSIGNMENT = {
summary: "detects invalid assignments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INVALID_DECLARATION = {
summary: "detects invalid declarations",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
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 #14889
pub(crate) static INVALID_TYPE_PARAMETER = {
summary: "detects invalid type parameters",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
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 #14889
pub(crate) static CYCLIC_CLASS_DEFINITION = {
summary: "detects cyclic class definitions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
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 #14889
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 #14889
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`.
///
/// TODO #14889
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.
///
/// TODO #14889
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.
///
/// TODO #14889
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.
///
/// TODO #14889
pub(crate) static UNRESOLVED_ATTRIBUTE = {
summary: "detects references to unresolved attributes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
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.
///
/// TODO #14889
pub(crate) static UNSUPPORTED_OPERATOR = {
summary: "detects binary, unary, or comparison expressions where the operands don't support the operator",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
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.
///
/// ## Examples
/// TODO #14889
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.
///
/// ## Why is this bad?
/// TODO #14889
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 invalid type expressions.
///
/// ## Why is this bad?
/// TODO #14889
pub(crate) static INVALID_TYPE_FORM = {
summary: "detects invalid type forms",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
/// Catching classes that do not inherit from `BaseException` will raise a TypeError at runtime.
///
/// ## Example
/// ```python
/// try:
/// 1 / 0
/// except 1:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// 1 / 0
/// except ZeroDivisionError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: except clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)
///
/// ## Ruff rule
/// This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](https://docs.astral.sh/ruff/rules/except-with-non-exception-classes)
pub(crate) static INVALID_EXCEPTION_CAUGHT = {
summary: "detects exception handlers that catch classes that do not inherit from `BaseException`",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic { pub struct TypeCheckDiagnostic {
pub(super) id: DiagnosticId, pub(super) id: DiagnosticId,
pub(super) message: String, pub(super) message: String,
pub(super) range: TextRange, pub(super) range: TextRange,
pub(super) severity: Severity,
pub(super) file: File, pub(super) file: File,
} }
@ -49,7 +457,7 @@ impl Diagnostic for TypeCheckDiagnostic {
} }
fn severity(&self) -> Severity { fn severity(&self) -> Severity {
Severity::Error self.severity
} }
} }
@ -149,9 +557,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
/// Emit a diagnostic declaring that the object represented by `node` is not iterable /// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) { pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
self.add( self.add_lint(
&NOT_ITERABLE,
node, node,
DiagnosticId::lint("not-iterable"),
format_args!( format_args!(
"Object of type `{}` is not iterable", "Object of type `{}` is not iterable",
not_iterable_ty.display(self.db) not_iterable_ty.display(self.db)
@ -166,9 +574,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
node: AnyNodeRef, node: AnyNodeRef,
element_ty: Type<'db>, element_ty: Type<'db>,
) { ) {
self.add( self.add_lint(
&NOT_ITERABLE,
node, node,
DiagnosticId::lint("not-iterable"),
format_args!( format_args!(
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound", "Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
element_ty.display(self.db) element_ty.display(self.db)
@ -185,9 +593,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
length: usize, length: usize,
index: i64, index: i64,
) { ) {
self.add( self.add_lint(
&INDEX_OUT_OF_BOUNDS,
node, node,
DiagnosticId::lint("index-out-of-bounds"),
format_args!( format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}", "Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(self.db) tuple_ty.display(self.db)
@ -202,9 +610,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
non_subscriptable_ty: Type<'db>, non_subscriptable_ty: Type<'db>,
method: &str, method: &str,
) { ) {
self.add( self.add_lint(
&NON_SUBSCRIPTABLE,
node, node,
DiagnosticId::lint("non-subscriptable"),
format_args!( format_args!(
"Cannot subscript object of type `{}` with no `{method}` method", "Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(self.db) non_subscriptable_ty.display(self.db)
@ -218,9 +626,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
level: u32, level: u32,
module: Option<&str>, module: Option<&str>,
) { ) {
self.add( self.add_lint(
&UNRESOLVED_IMPORT,
import_node.into(), import_node.into(),
DiagnosticId::lint("unresolved-import"),
format_args!( format_args!(
"Cannot resolve import `{}{}`", "Cannot resolve import `{}{}`",
".".repeat(level as usize), ".".repeat(level as usize),
@ -230,9 +638,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
} }
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) { pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
self.add( self.add_lint(
&ZERO_STEPSIZE_IN_SLICE,
node, node,
DiagnosticId::lint("zero-stepsize-in-slice"),
format_args!("Slice step size can not be zero"), format_args!("Slice step size can not be zero"),
); );
} }
@ -245,19 +653,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
) { ) {
match declared_ty { match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => { Type::ClassLiteral(ClassLiteralType { class }) => {
self.add(node, DiagnosticId::lint("invalid-assignment"), format_args!( self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional", "Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(self.db))); class.name(self.db)));
} }
Type::FunctionLiteral(function) => { Type::FunctionLiteral(function) => {
self.add(node, DiagnosticId::lint("invalid-assignment"), format_args!( self.add_lint(&INVALID_ASSIGNMENT, node, format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional", "Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(self.db))); function.name(self.db)));
} }
_ => { _ => {
self.add( self.add_lint(
&INVALID_ASSIGNMENT,
node, node,
DiagnosticId::lint("invalid-assignment"),
format_args!( format_args!(
"Object of type `{}` is not assignable to `{}`", "Object of type `{}` is not assignable to `{}`",
assigned_ty.display(self.db), assigned_ty.display(self.db),
@ -271,9 +679,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) { pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node; let ast::ExprName { id, .. } = expr_name_node;
self.add( self.add_lint(
&POSSIBLY_UNRESOLVED_REFERENCE,
expr_name_node.into(), expr_name_node.into(),
DiagnosticId::lint("possibly-unresolved-reference"),
format_args!("Name `{id}` used when possibly not defined"), format_args!("Name `{id}` used when possibly not defined"),
); );
} }
@ -281,17 +689,17 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) { pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node; let ast::ExprName { id, .. } = expr_name_node;
self.add( self.add_lint(
&UNRESOLVED_REFERENCE,
expr_name_node.into(), expr_name_node.into(),
DiagnosticId::lint("unresolved-reference"),
format_args!("Name `{id}` used when not defined"), format_args!("Name `{id}` used when not defined"),
); );
} }
pub(super) fn add_invalid_exception(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) { pub(super) fn add_invalid_exception_caught(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
self.add( self.add_lint(
&INVALID_EXCEPTION_CAUGHT,
node.into(), node.into(),
DiagnosticId::lint("invalid-exception"),
format_args!( format_args!(
"Cannot catch object of type `{}` in an exception handler \ "Cannot catch object of type `{}` in an exception handler \
(must be a `BaseException` subclass or a tuple of `BaseException` subclasses)", (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)",
@ -300,10 +708,29 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
); );
} }
pub(super) fn add_lint(
&mut self,
lint: &LintMetadata,
node: AnyNodeRef,
message: std::fmt::Arguments,
) {
let Ok(severity) = Severity::try_from(lint.default_level()) else {
return;
};
self.add(node, DiagnosticId::Lint(lint.name()), severity, message);
}
/// Adds a new diagnostic. /// Adds a new diagnostic.
/// ///
/// The diagnostic does not get added if the rule isn't enabled for this file. /// The diagnostic does not get added if the rule isn't enabled for this file.
pub(super) fn add(&mut self, node: AnyNodeRef, id: DiagnosticId, message: std::fmt::Arguments) { pub(super) fn add(
&mut self,
node: AnyNodeRef,
id: DiagnosticId,
severity: Severity,
message: std::fmt::Arguments,
) {
if !self.db.is_file_open(self.file) { if !self.db.is_file_open(self.file) {
return; return;
} }
@ -319,6 +746,7 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
id, id,
message: message.to_string(), message: message.to_string(),
range: node.range(), range: node.range(),
severity,
}); });
} }

View file

@ -29,10 +29,9 @@
use std::num::NonZeroU32; use std::num::NonZeroU32;
use itertools::Itertools; use itertools::Itertools;
use ruff_db::diagnostic::DiagnosticId;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp}; use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, UnaryOp};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use salsa; use salsa;
use salsa::plumbing::AsId; use salsa::plumbing::AsId;
@ -49,7 +48,15 @@ use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId}; use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex; use crate::semantic_index::SemanticIndex;
use crate::stdlib::builtins_module_scope; use crate::stdlib::builtins_module_scope;
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder}; use crate::types::diagnostic::{
TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE,
CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE,
INVALID_CONTEXT_MANAGER, INVALID_DECLARATION, INVALID_LITERAL_PARAMETER,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_PARAMETER,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind; use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{ use crate::types::{
@ -64,7 +71,9 @@ use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice}; use crate::util::subscript::{PyIndex, PySlice};
use crate::Db; use crate::Db;
use super::string_annotation::parse_string_annotation; use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@ -526,9 +535,9 @@ impl<'db> TypeInferenceBuilder<'db> {
for (class, class_node) in class_definitions { for (class, class_node) in class_definitions {
// (1) Check that the class does not have a cyclic definition // (1) Check that the class does not have a cyclic definition
if class.is_cyclically_defined(self.db) { if class.is_cyclically_defined(self.db) {
self.diagnostics.add( self.diagnostics.add_lint(
&CYCLIC_CLASS_DEFINITION,
class_node.into(), class_node.into(),
DiagnosticId::lint("cyclic-class-def"),
format_args!( format_args!(
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)", "Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
class.name(self.db), class.name(self.db),
@ -546,9 +555,9 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::DuplicateBases(duplicates) => { MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases(); let base_nodes = class_node.bases();
for (index, duplicate) in duplicates { for (index, duplicate) in duplicates {
self.diagnostics.add( self.diagnostics.add_lint(
&DUPLICATE_BASE,
(&base_nodes[*index]).into(), (&base_nodes[*index]).into(),
DiagnosticId::lint("duplicate-base"),
format_args!("Duplicate base class `{}`", duplicate.name(self.db)), format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
); );
} }
@ -556,9 +565,9 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::InvalidBases(bases) => { MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases(); let base_nodes = class_node.bases();
for (index, base_ty) in bases { for (index, base_ty) in bases {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_BASE,
(&base_nodes[*index]).into(), (&base_nodes[*index]).into(),
DiagnosticId::lint("invalid-base"),
format_args!( format_args!(
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)", "Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
base_ty.display(self.db) base_ty.display(self.db)
@ -566,9 +575,9 @@ impl<'db> TypeInferenceBuilder<'db> {
); );
} }
} }
MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add( MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add_lint(
&INCONSISTENT_MRO,
class_node.into(), class_node.into(),
DiagnosticId::lint("inconsistent-mro"),
format_args!( format_args!(
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`", "Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
class.name(self.db), class.name(self.db),
@ -596,9 +605,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} => { } => {
let node = class_node.into(); let node = class_node.into();
if *candidate1_is_base_class { if *candidate1_is_base_class {
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_METACLASS,
node, node,
DiagnosticId::lint("conflicting-metaclass"),
format_args!( format_args!(
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \ but `{metaclass1}` (metaclass of base class `{base1}`) and `{metaclass2}` (metaclass of base class `{base2}`) \
@ -608,12 +617,12 @@ impl<'db> TypeInferenceBuilder<'db> {
base1 = class1.name(self.db), base1 = class1.name(self.db),
metaclass2 = metaclass2.name(self.db), metaclass2 = metaclass2.name(self.db),
base2 = class2.name(self.db), base2 = class2.name(self.db),
) ),
); );
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_METACLASS,
node, node,
DiagnosticId::lint("conflicting-metaclass"),
format_args!( format_args!(
"The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \ "The metaclass of a derived class (`{class}`) must be a subclass of the metaclasses of all its bases, \
but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \ but `{metaclass_of_class}` (metaclass of `{class}`) and `{metaclass_of_base}` (metaclass of base class `{base}`) \
@ -622,7 +631,7 @@ impl<'db> TypeInferenceBuilder<'db> {
metaclass_of_class = metaclass1.name(self.db), metaclass_of_class = metaclass1.name(self.db),
metaclass_of_base = metaclass2.name(self.db), metaclass_of_base = metaclass2.name(self.db),
base = class2.name(self.db), base = class2.name(self.db),
) ),
); );
} }
} }
@ -760,9 +769,9 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => return, _ => return,
}; };
self.diagnostics.add( self.diagnostics.add_lint(
&DIVISION_BY_ZERO,
expr.into(), expr.into(),
DiagnosticId::lint("division-by-zero"),
format_args!( format_args!(
"Cannot {op} object of type `{}` {by_zero}", "Cannot {op} object of type `{}` {by_zero}",
left.display(self.db) left.display(self.db)
@ -785,9 +794,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO point out the conflicting declarations in the diagnostic? // TODO point out the conflicting declarations in the diagnostic?
let symbol_table = self.index.symbol_table(binding.file_scope(self.db)); let symbol_table = self.index.symbol_table(binding.file_scope(self.db));
let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name(); let symbol_name = symbol_table.symbol(binding.symbol(self.db)).name();
self.diagnostics.add( self.diagnostics.add_lint(
&CONFLICTING_DECLARATIONS,
node, node,
DiagnosticId::lint("conflicting-declarations"),
format_args!( format_args!(
"Conflicting declared types for `{symbol_name}`: {}", "Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db) conflicting.display(self.db)
@ -815,9 +824,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let ty = if inferred_ty.is_assignable_to(self.db, ty) { let ty = if inferred_ty.is_assignable_to(self.db, ty) {
ty ty
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_DECLARATION,
node, node,
DiagnosticId::lint("invalid-declaration"),
format_args!( format_args!(
"Cannot declare type `{}` for inferred type `{}`", "Cannot declare type `{}` for inferred type `{}`",
ty.display(self.db), ty.display(self.db),
@ -1112,12 +1121,12 @@ impl<'db> TypeInferenceBuilder<'db> {
if default_ty.is_assignable_to(self.db, declared_ty) { if default_ty.is_assignable_to(self.db, declared_ty) {
UnionType::from_elements(self.db, [declared_ty, default_ty]) UnionType::from_elements(self.db, [declared_ty, default_ty])
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_PARAMETER_DEFAULT,
parameter_with_default.into(), parameter_with_default.into(),
DiagnosticId::lint("invalid-parameter-default"),
format_args!( format_args!(
"Default value of type `{}` is not assignable to annotated parameter type `{}`", "Default value of type `{}` is not assignable to annotated parameter type `{}`",
default_ty.display(self.db), declared_ty.display(self.db)) default_ty.display(self.db), declared_ty.display(self.db)),
); );
declared_ty declared_ty
} }
@ -1424,9 +1433,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
match (enter, exit) { match (enter, exit) {
(Symbol::Unbound, Symbol::Unbound) => { (Symbol::Unbound, Symbol::Unbound) => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@ -1435,9 +1444,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown Type::Unknown
} }
(Symbol::Unbound, _) => { (Symbol::Unbound, _) => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@ -1447,9 +1456,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
(Symbol::Type(enter_ty, enter_boundness), exit) => { (Symbol::Type(enter_ty, enter_boundness), exit) => {
if enter_boundness == Boundness::PossiblyUnbound { if enter_boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound", "Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@ -1461,9 +1470,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[context_expression_ty]) .call(self.db, &[context_expression_ty])
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics) .return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!(" format_args!("
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db) Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db)
), ),
@ -1473,9 +1482,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match exit { match exit {
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`", "Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
context_expression_ty.display(self.db) context_expression_ty.display(self.db)
@ -1486,9 +1495,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed. // TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
if exit_boundness == Boundness::PossiblyUnbound { if exit_boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound", "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@ -1513,9 +1522,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) )
.is_err() .is_err()
{ {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(), context_expression.into(),
DiagnosticId::lint("invalid-context-manager"),
format_args!( format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable", "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
context_expression = context_expression_ty.display(self.db), context_expression = context_expression_ty.display(self.db),
@ -1555,7 +1564,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else { } else {
if let Some(node) = node { if let Some(node) = node {
self.diagnostics self.diagnostics
.add_invalid_exception(self.db, node, element); .add_invalid_exception_caught(self.db, node, element);
} }
Type::Unknown Type::Unknown
}); });
@ -1572,7 +1581,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else { } else {
if let Some(node) = node { if let Some(node) = node {
self.diagnostics self.diagnostics
.add_invalid_exception(self.db, node, node_ty); .add_invalid_exception_caught(self.db, node, node_ty);
} }
Type::Unknown Type::Unknown
} }
@ -1609,9 +1618,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let bound_or_constraint = match bound.as_deref() { let bound_or_constraint = match bound.as_deref() {
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => { Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
if elts.len() < 2 { if elts.len() < 2 {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_VARIABLE_CONSTRAINTS,
expr.into(), expr.into(),
DiagnosticId::lint("invalid-typevar-constraints"),
format_args!("TypeVar must have at least two constrained types"), format_args!("TypeVar must have at least two constrained types"),
); );
self.infer_expression(expr); self.infer_expression(expr);
@ -1932,9 +1941,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) { ) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
target_type.display(self.db), target_type.display(self.db),
@ -1953,9 +1962,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let binary_return_ty = self.infer_binary_expression_type(left_ty, right_ty, op) let binary_return_ty = self.infer_binary_expression_type(left_ty, right_ty, op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@ -1982,9 +1991,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, op) self.infer_binary_expression_type(left_ty, right_ty, op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(), assignment.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`", "Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@ -2014,11 +2023,11 @@ impl<'db> TypeInferenceBuilder<'db> {
// Resolve the target type, assuming a load context. // Resolve the target type, assuming a load context.
let target_type = match &**target { let target_type = match &**target {
Expr::Name(name) => { ast::Expr::Name(name) => {
self.store_expression_type(target, Type::Never); self.store_expression_type(target, Type::Never);
self.infer_name_load(name) self.infer_name_load(name)
} }
Expr::Attribute(attr) => { ast::Expr::Attribute(attr) => {
self.store_expression_type(target, Type::Never); self.store_expression_type(target, Type::Never);
self.infer_attribute_load(attr) self.infer_attribute_load(attr)
} }
@ -2235,19 +2244,19 @@ impl<'db> TypeInferenceBuilder<'db> {
match module_ty.member(self.db, &ast::name::Name::new(&name.id)) { match module_ty.member(self.db, &ast::name::Name::new(&name.id)) {
Symbol::Type(ty, boundness) => { Symbol::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_IMPORT,
AnyNodeRef::Alias(alias), AnyNodeRef::Alias(alias),
DiagnosticId::lint("possibly-unbound-import"), format_args!("Member `{name}` of module `{module_name}` is possibly unbound", ),
format_args!("Member `{name}` of module `{module_name}` is possibly unbound",),
); );
} }
ty ty
} }
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias), AnyNodeRef::Alias(alias),
DiagnosticId::lint("unresolved-import"),
format_args!("Module `{module_name}` has no member `{name}`",), format_args!("Module `{module_name}` has no member `{name}`",),
); );
Type::Unknown Type::Unknown
@ -2952,9 +2961,9 @@ impl<'db> TypeInferenceBuilder<'db> {
{ {
let mut builtins_symbol = builtins_symbol(self.db, name); let mut builtins_symbol = builtins_symbol(self.db, name);
if builtins_symbol.is_unbound() && name == "reveal_type" { if builtins_symbol.is_unbound() && name == "reveal_type" {
self.diagnostics.add( self.diagnostics.add_lint(
&UNDEFINED_REVEAL,
name_node.into(), name_node.into(),
DiagnosticId::lint("undefined-reveal"),
format_args!( format_args!(
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"), "`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
); );
@ -3049,9 +3058,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty.member(self.db, &attr.id) { match value_ty.member(self.db, &attr.id) {
Symbol::Type(member_ty, boundness) => { Symbol::Type(member_ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_ATTRIBUTE,
attribute.into(), attribute.into(),
DiagnosticId::lint("possibly-unbound-attribute"),
format_args!( format_args!(
"Attribute `{}` on type `{}` is possibly unbound", "Attribute `{}` on type `{}` is possibly unbound",
attr.id, attr.id,
@ -3063,9 +3072,9 @@ impl<'db> TypeInferenceBuilder<'db> {
member_ty member_ty
} }
Symbol::Unbound => { Symbol::Unbound => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNRESOLVED_ATTRIBUTE,
attribute.into(), attribute.into(),
DiagnosticId::lint("unresolved-attribute"),
format_args!( format_args!(
"Type `{}` has no attribute `{}`", "Type `{}` has no attribute `{}`",
value_ty.display(self.db), value_ty.display(self.db),
@ -3144,9 +3153,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) { ) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(), unary.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Unary operator `{op}` is unsupported for type `{}`", "Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db), operand_type.display(self.db),
@ -3156,9 +3165,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
} }
} else { } else {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(), unary.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Unary operator `{op}` is unsupported for type `{}`", "Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db), operand_type.display(self.db),
@ -3197,9 +3206,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, *op) self.infer_binary_expression_type(left_ty, right_ty, *op)
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
binary.into(), binary.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Operator `{op}` is unsupported between objects of type `{}` and `{}`", "Operator `{op}` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db), left_ty.display(self.db),
@ -3507,9 +3516,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_type_comparison(left_ty, *op, right_ty) self.infer_binary_type_comparison(left_ty, *op, right_ty)
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome) // Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
self.diagnostics.add( self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
AnyNodeRef::ExprCompare(compare), AnyNodeRef::ExprCompare(compare),
DiagnosticId::lint("unsupported-operator"),
format_args!( format_args!(
"Operator `{}` is not supported for types `{}` and `{}`{}", "Operator `{}` is not supported for types `{}` and `{}`{}",
error.op, error.op,
@ -4164,9 +4173,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {} Symbol::Unbound => {}
Symbol::Type(dunder_getitem_method, boundness) => { Symbol::Type(dunder_getitem_method, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
value_node.into(), value_node.into(),
DiagnosticId::lint("call-possibly-unbound-method"),
format_args!( format_args!(
"Method `__getitem__` of type `{}` is possibly unbound", "Method `__getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db), value_ty.display(self.db),
@ -4178,9 +4187,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[slice_ty]) .call(self.db, &[slice_ty])
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics) .return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_NON_CALLABLE,
value_node.into(), value_node.into(),
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Method `__getitem__` of type `{}` is not callable on object of type `{}`", "Method `__getitem__` of type `{}` is not callable on object of type `{}`",
err.called_ty().display(self.db), err.called_ty().display(self.db),
@ -4208,9 +4217,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {} Symbol::Unbound => {}
Symbol::Type(ty, boundness) => { Symbol::Type(ty, boundness) => {
if boundness == Boundness::PossiblyUnbound { if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
value_node.into(), value_node.into(),
DiagnosticId::lint("call-possibly-unbound-method"),
format_args!( format_args!(
"Method `__class_getitem__` of type `{}` is possibly unbound", "Method `__class_getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db), value_ty.display(self.db),
@ -4222,9 +4231,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.call(self.db, &[slice_ty]) .call(self.db, &[slice_ty])
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics) .return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
self.diagnostics.add( self.diagnostics.add_lint(
&CALL_NON_CALLABLE,
value_node.into(), value_node.into(),
DiagnosticId::lint("call-non-callable"),
format_args!( format_args!(
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`", "Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
err.called_ty().display(self.db), err.called_ty().display(self.db),
@ -4364,18 +4373,18 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Expr::Starred(starred) => self.infer_starred_expression(starred), ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
ast::Expr::BytesLiteral(bytes) => { ast::Expr::BytesLiteral(bytes) => {
self.diagnostics.add( self.diagnostics.add_lint(
&BYTE_STRING_TYPE_ANNOTATION,
bytes.into(), bytes.into(),
DiagnosticId::lint("annotation-byte-string"),
format_args!("Type expressions cannot use bytes literal"), format_args!("Type expressions cannot use bytes literal"),
); );
Type::Unknown Type::Unknown
} }
ast::Expr::FString(fstring) => { ast::Expr::FString(fstring) => {
self.diagnostics.add( self.diagnostics.add_lint(
&FSTRING_TYPE_ANNOTATION,
fstring.into(), fstring.into(),
DiagnosticId::lint("annotation-f-string"),
format_args!("Type expressions cannot use f-strings"), format_args!("Type expressions cannot use f-strings"),
); );
self.infer_fstring_expression(fstring); self.infer_fstring_expression(fstring);
@ -4717,9 +4726,9 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
ast::Expr::Tuple(_) => { ast::Expr::Tuple(_) => {
self.infer_type_expression(slice); self.infer_type_expression(slice);
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_FORM,
slice.into(), slice.into(),
DiagnosticId::lint("invalid-type-form"),
format_args!("type[...] must have exactly one type argument"), format_args!("type[...] must have exactly one type argument"),
); );
Type::Unknown Type::Unknown
@ -4793,9 +4802,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Ok(ty) => ty, Ok(ty) => ty,
Err(nodes) => { Err(nodes) => {
for node in nodes { for node in nodes {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_LITERAL_PARAMETER,
node.into(), node.into(),
DiagnosticId::lint("invalid-literal-parameter"),
format_args!( format_args!(
"Type arguments for `Literal` must be `None`, \ "Type arguments for `Literal` must be `None`, \
a literal value (int, bool, str, or bytes), or an enum value" a literal value (int, bool, str, or bytes), or an enum value"
@ -4829,9 +4838,9 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("generic type alias") todo_type!("generic type alias")
} }
KnownInstanceType::NoReturn | KnownInstanceType::Never => { KnownInstanceType::NoReturn | KnownInstanceType::Never => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(), subscript.into(),
DiagnosticId::lint("invalid-type-parameter"),
format_args!( format_args!(
"Type `{}` expected no type parameter", "Type `{}` expected no type parameter",
known_instance.repr(self.db) known_instance.repr(self.db)
@ -4840,9 +4849,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown Type::Unknown
} }
KnownInstanceType::LiteralString => { KnownInstanceType::LiteralString => {
self.diagnostics.add( self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(), subscript.into(),
DiagnosticId::lint("invalid-type-parameter"),
format_args!( format_args!(
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?", "Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
known_instance.repr(self.db) known_instance.repr(self.db)

View file

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

View file

@ -27,28 +27,28 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
// We don't support `*` imports yet: // We don't support `*` imports yet:
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`", "error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet: // We don't support terminal statements in control flow yet:
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None", "error[lint:conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
]; ];
fn get_test_file(name: &str) -> TestFile { fn get_test_file(name: &str) -> TestFile {

View file

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

View file

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