[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

View file

@ -11,6 +11,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;

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 itertools::Itertools;
use ruff_db::diagnostic::DiagnosticId;
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_python_ast as ast;
@ -25,7 +25,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};
@ -2308,9 +2308,9 @@ impl<'db> CallOutcome<'db> {
not_callable_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
DiagnosticId::lint("call-non-callable"),
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(db)
@ -2323,9 +2323,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
DiagnosticId::lint("call-non-callable"),
format_args!(
"Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(db),
@ -2339,9 +2339,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
DiagnosticId::lint("call-non-callable"),
format_args!(
"Object of type `{}` is not callable (due to union elements {})",
called_ty.display(db),
@ -2354,9 +2354,9 @@ impl<'db> CallOutcome<'db> {
callable_ty: called_ty,
return_ty,
}) => {
diagnostics.add(
diagnostics.add_lint(
&CALL_NON_CALLABLE,
node,
DiagnosticId::lint("call-non-callable"),
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(db)
@ -2383,6 +2383,7 @@ impl<'db> CallOutcome<'db> {
diagnostics.add(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
);
Ok(*return_ty)

View file

@ -1,5 +1,6 @@
use crate::lint::{Level, LintMetadata, LintStatus};
use crate::types::{ClassLiteralType, Type};
use crate::Db;
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};
@ -9,11 +10,418 @@ use std::fmt::Formatter;
use std::ops::Deref;
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)]
pub struct TypeCheckDiagnostic {
pub(super) id: DiagnosticId,
pub(super) message: String,
pub(super) range: TextRange,
pub(super) severity: Severity,
pub(super) file: File,
}
@ -49,7 +457,7 @@ impl Diagnostic for TypeCheckDiagnostic {
}
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
pub(super) fn add_not_iterable(&mut self, node: AnyNodeRef, not_iterable_ty: Type<'db>) {
self.add(
self.add_lint(
&NOT_ITERABLE,
node,
DiagnosticId::lint("not-iterable"),
format_args!(
"Object of type `{}` is not iterable",
not_iterable_ty.display(self.db)
@ -166,9 +574,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
node: AnyNodeRef,
element_ty: Type<'db>,
) {
self.add(
self.add_lint(
&NOT_ITERABLE,
node,
DiagnosticId::lint("not-iterable"),
format_args!(
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
element_ty.display(self.db)
@ -185,9 +593,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
length: usize,
index: i64,
) {
self.add(
self.add_lint(
&INDEX_OUT_OF_BOUNDS,
node,
DiagnosticId::lint("index-out-of-bounds"),
format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(self.db)
@ -202,9 +610,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
non_subscriptable_ty: Type<'db>,
method: &str,
) {
self.add(
self.add_lint(
&NON_SUBSCRIPTABLE,
node,
DiagnosticId::lint("non-subscriptable"),
format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(self.db)
@ -218,9 +626,9 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
level: u32,
module: Option<&str>,
) {
self.add(
self.add_lint(
&UNRESOLVED_IMPORT,
import_node.into(),
DiagnosticId::lint("unresolved-import"),
format_args!(
"Cannot resolve import `{}{}`",
".".repeat(level as usize),
@ -230,9 +638,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,
DiagnosticId::lint("zero-stepsize-in-slice"),
format_args!("Slice step size can not be zero"),
);
}
@ -245,19 +653,19 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
) {
match declared_ty {
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",
class.name(self.db)));
}
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",
function.name(self.db)));
}
_ => {
self.add(
self.add_lint(
&INVALID_ASSIGNMENT,
node,
DiagnosticId::lint("invalid-assignment"),
format_args!(
"Object of type `{}` is not assignable to `{}`",
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) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
self.add_lint(
&POSSIBLY_UNRESOLVED_REFERENCE,
expr_name_node.into(),
DiagnosticId::lint("possibly-unresolved-reference"),
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) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
self.add_lint(
&UNRESOLVED_REFERENCE,
expr_name_node.into(),
DiagnosticId::lint("unresolved-reference"),
format_args!("Name `{id}` used when not defined"),
);
}
pub(super) fn add_invalid_exception(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
self.add(
pub(super) fn add_invalid_exception_caught(&mut self, db: &dyn Db, node: &ast::Expr, ty: Type) {
self.add_lint(
&INVALID_EXCEPTION_CAUGHT,
node.into(),
DiagnosticId::lint("invalid-exception"),
format_args!(
"Cannot catch object of type `{}` in an exception handler \
(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.
///
/// 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) {
return;
}
@ -319,6 +746,7 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
id,
message: message.to_string(),
range: node.range(),
severity,
});
}

View file

@ -29,10 +29,9 @@
use std::num::NonZeroU32;
use itertools::Itertools;
use ruff_db::diagnostic::DiagnosticId;
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;
@ -49,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 +71,9 @@ use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
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.
/// 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 {
// (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(),
DiagnosticId::lint("cyclic-class-def"),
format_args!(
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
class.name(self.db),
@ -546,9 +555,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(),
DiagnosticId::lint("duplicate-base"),
format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
);
}
@ -556,9 +565,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(),
DiagnosticId::lint("invalid-base"),
format_args!(
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
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(),
DiagnosticId::lint("inconsistent-mro"),
format_args!(
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
class.name(self.db),
@ -596,9 +605,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,
DiagnosticId::lint("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}`) \
@ -608,12 +617,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,
DiagnosticId::lint("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}`) \
@ -622,7 +631,7 @@ impl<'db> TypeInferenceBuilder<'db> {
metaclass_of_class = metaclass1.name(self.db),
metaclass_of_base = metaclass2.name(self.db),
base = class2.name(self.db),
)
),
);
}
}
@ -760,9 +769,9 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => return,
};
self.diagnostics.add(
self.diagnostics.add_lint(
&DIVISION_BY_ZERO,
expr.into(),
DiagnosticId::lint("division-by-zero"),
format_args!(
"Cannot {op} object of type `{}` {by_zero}",
left.display(self.db)
@ -785,9 +794,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,
DiagnosticId::lint("conflicting-declarations"),
format_args!(
"Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db)
@ -815,9 +824,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,
DiagnosticId::lint("invalid-declaration"),
format_args!(
"Cannot declare type `{}` for inferred type `{}`",
ty.display(self.db),
@ -1112,12 +1121,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(),
DiagnosticId::lint("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
}
@ -1424,9 +1433,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(),
DiagnosticId::lint("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)
@ -1435,9 +1444,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
(Symbol::Unbound, _) => {
self.diagnostics.add(
self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(),
DiagnosticId::lint("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)
@ -1447,9 +1456,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(),
DiagnosticId::lint("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),
@ -1461,9 +1470,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(),
DiagnosticId::lint("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)
),
@ -1473,9 +1482,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match exit {
Symbol::Unbound => {
self.diagnostics.add(
self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(),
DiagnosticId::lint("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)
@ -1486,9 +1495,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(),
DiagnosticId::lint("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),
@ -1513,9 +1522,9 @@ impl<'db> TypeInferenceBuilder<'db> {
)
.is_err()
{
self.diagnostics.add(
self.diagnostics.add_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(),
DiagnosticId::lint("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),
@ -1555,7 +1564,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else {
if let Some(node) = node {
self.diagnostics
.add_invalid_exception(self.db, node, element);
.add_invalid_exception_caught(self.db, node, element);
}
Type::Unknown
});
@ -1572,7 +1581,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else {
if let Some(node) = node {
self.diagnostics
.add_invalid_exception(self.db, node, node_ty);
.add_invalid_exception_caught(self.db, node, node_ty);
}
Type::Unknown
}
@ -1609,9 +1618,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(),
DiagnosticId::lint("invalid-typevar-constraints"),
format_args!("TypeVar must have at least two constrained types"),
);
self.infer_expression(expr);
@ -1932,9 +1941,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) {
Ok(t) => t,
Err(e) => {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
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)
.unwrap_or_else(|| {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
assignment.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db),
@ -1982,9 +1991,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(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Operator `{op}=` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db),
@ -2014,11 +2023,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)
}
@ -2235,19 +2244,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),
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
}
Symbol::Unbound => {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNRESOLVED_IMPORT,
AnyNodeRef::Alias(alias),
DiagnosticId::lint("unresolved-import"),
format_args!("Module `{module_name}` has no member `{name}`",),
);
Type::Unknown
@ -2952,9 +2961,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(),
DiagnosticId::lint("undefined-reveal"),
format_args!(
"`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) {
Symbol::Type(member_ty, boundness) => {
if boundness == Boundness::PossiblyUnbound {
self.diagnostics.add(
self.diagnostics.add_lint(
&POSSIBLY_UNBOUND_ATTRIBUTE,
attribute.into(),
DiagnosticId::lint("possibly-unbound-attribute"),
format_args!(
"Attribute `{}` on type `{}` is possibly unbound",
attr.id,
@ -3063,9 +3072,9 @@ impl<'db> TypeInferenceBuilder<'db> {
member_ty
}
Symbol::Unbound => {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNRESOLVED_ATTRIBUTE,
attribute.into(),
DiagnosticId::lint("unresolved-attribute"),
format_args!(
"Type `{}` has no attribute `{}`",
value_ty.display(self.db),
@ -3144,9 +3153,9 @@ impl<'db> TypeInferenceBuilder<'db> {
) {
Ok(t) => t,
Err(e) => {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db),
@ -3156,9 +3165,9 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
} else {
self.diagnostics.add(
self.diagnostics.add_lint(
&UNSUPPORTED_OPERATOR,
unary.into(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db),
@ -3197,9 +3206,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(),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Operator `{op}` is unsupported between objects of type `{}` and `{}`",
left_ty.display(self.db),
@ -3507,9 +3516,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),
DiagnosticId::lint("unsupported-operator"),
format_args!(
"Operator `{}` is not supported for types `{}` and `{}`{}",
error.op,
@ -4164,9 +4173,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(),
DiagnosticId::lint("call-possibly-unbound-method"),
format_args!(
"Method `__getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db),
@ -4178,9 +4187,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(),
DiagnosticId::lint("call-non-callable"),
format_args!(
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
err.called_ty().display(self.db),
@ -4208,9 +4217,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(),
DiagnosticId::lint("call-possibly-unbound-method"),
format_args!(
"Method `__class_getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db),
@ -4222,9 +4231,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(),
DiagnosticId::lint("call-non-callable"),
format_args!(
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
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::BytesLiteral(bytes) => {
self.diagnostics.add(
self.diagnostics.add_lint(
&BYTE_STRING_TYPE_ANNOTATION,
bytes.into(),
DiagnosticId::lint("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(),
DiagnosticId::lint("annotation-f-string"),
format_args!("Type expressions cannot use f-strings"),
);
self.infer_fstring_expression(fstring);
@ -4717,9 +4726,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(),
DiagnosticId::lint("invalid-type-form"),
format_args!("type[...] must have exactly one type argument"),
);
Type::Unknown
@ -4793,9 +4802,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(),
DiagnosticId::lint("invalid-literal-parameter"),
format_args!(
"Type arguments for `Literal` must be `None`, \
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")
}
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
self.diagnostics.add(
self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(),
DiagnosticId::lint("invalid-type-parameter"),
format_args!(
"Type `{}` expected no type parameter",
known_instance.repr(self.db)
@ -4840,9 +4849,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
KnownInstanceType::LiteralString => {
self.diagnostics.add(
self.diagnostics.add_lint(
&INVALID_TYPE_PARAMETER,
subscript.into(),
DiagnosticId::lint("invalid-type-parameter"),
format_args!(
"Type `{}` expected no type parameter. Did you mean to use `Literal[...]` instead?",
known_instance.repr(self.db)

View file

@ -1,4 +1,3 @@
use ruff_db::diagnostic::DiagnosticId;
use ruff_db::files::File;
use ruff_db::source::source_text;
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_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 #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>;
@ -26,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(),
DiagnosticId::lint("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
@ -50,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(),
DiagnosticId::lint("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(),
DiagnosticId::lint("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(),
DiagnosticId::lint("annotation-implicit-concat"),
format_args!("Type expressions cannot span multiple string literals"),
);
}