Use accumulator for diagnostics

This commit is contained in:
Micha Reiser
2024-11-05 20:30:08 +01:00
parent 4ece8e5c1e
commit 1cefe505ea
23 changed files with 1069 additions and 553 deletions

View File

@@ -145,13 +145,8 @@ reveal_type(f) # revealed: Unknown
### Non-iterable unpacking
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like assignment target,
multiple definitions are created and the inference engine runs on each of them which results in
duplicate diagnostics.
```py
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[1]` is not iterable"
a, b = 1
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown

View File

@@ -1,8 +1,11 @@
use diagnostic::{is_any_diagnostic_enabled, report_not_iterable, report_type_diagnostic};
use mro::{ClassBase, Mro, MroError, MroIterator};
use ruff_db::diagnostic::{CompileDiagnostic, Diagnostic, Severity};
use ruff_db::files::File;
use ruff_python_ast as ast;
use itertools::Itertools;
use ruff_text_size::{Ranged as _, TextRange};
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::definition::Definition;
@@ -15,12 +18,11 @@ use crate::stdlib::{
builtins_symbol, types_symbol, typeshed_symbol, typing_extensions_symbol, typing_symbol,
};
use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, HasTy, Module, SemanticModel};
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub use self::diagnostic::TypeCheckDiagnostic;
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
@@ -34,18 +36,15 @@ mod mro;
mod narrow;
mod unpacker;
pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
#[salsa::tracked]
pub fn check_types(db: &dyn Db, file: File) {
let _span = tracing::trace_span!("check_types", file=?file.path(db)).entered();
let index = semantic_index(db, file);
let mut diagnostics = TypeCheckDiagnostics::new();
for scope_id in index.scope_ids() {
let result = infer_scope_types(db, scope_id);
diagnostics.extend(result.diagnostics());
let _ = infer_scope_types(db, scope_id);
}
diagnostics
}
/// Infer the public type of a symbol (its type as seen from outside its scope).
@@ -1598,19 +1597,21 @@ impl<'db> CallOutcome<'db> {
}
/// Get the return type of the call, emitting default diagnostics if needed.
fn unwrap_with_diagnostic<'a>(
fn unwrap_with_diagnostic(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
file: File,
) -> Type<'db> {
match self.return_ty_result(db, node, diagnostics) {
match self.return_ty_result(db, node, file) {
Ok(return_ty) => return_ty,
Err(NotCallableError::Type {
not_callable_ty,
return_ty,
}) => {
diagnostics.add(
report_type_diagnostic(
db,
file,
node,
"call-non-callable",
format_args!(
@@ -1625,7 +1626,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
report_type_diagnostic(
db,
file,
node,
"call-non-callable",
format_args!(
@@ -1641,7 +1644,9 @@ impl<'db> CallOutcome<'db> {
called_ty,
return_ty,
}) => {
diagnostics.add(
report_type_diagnostic(
db,
file,
node,
"call-non-callable",
format_args!(
@@ -1656,11 +1661,11 @@ impl<'db> CallOutcome<'db> {
}
/// Get the return type of the call as a result.
fn return_ty_result<'a>(
fn return_ty_result(
&self,
db: &'db dyn Db,
node: ast::AnyNodeRef,
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
file: File,
) -> Result<Type<'db>, NotCallableError<'db>> {
match self {
Self::Callable { return_ty } => Ok(*return_ty),
@@ -1668,11 +1673,17 @@ impl<'db> CallOutcome<'db> {
return_ty,
revealed_ty,
} => {
diagnostics.add(
node,
"revealed-type",
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
);
if is_any_diagnostic_enabled(db, file) {
CompileDiagnostic::report(
db.upcast(),
RevealTypeDiagnostic {
file,
range: node.range(),
ty: revealed_ty.display(db).to_string(),
},
);
}
Ok(*return_ty)
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
@@ -1700,10 +1711,10 @@ impl<'db> CallOutcome<'db> {
*return_ty
} else {
revealed = true;
outcome.unwrap_with_diagnostic(db, node, diagnostics)
outcome.unwrap_with_diagnostic(db, node, file)
}
}
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
_ => outcome.unwrap_with_diagnostic(db, node, file),
};
union_builder = union_builder.add(return_ty);
}
@@ -1785,13 +1796,14 @@ enum IterationOutcome<'db> {
impl<'db> IterationOutcome<'db> {
fn unwrap_with_diagnostic(
self,
db: &dyn Db,
iterable_node: ast::AnyNodeRef,
diagnostics: &mut TypeCheckDiagnosticsBuilder<'db>,
file: File,
) -> Type<'db> {
match self {
Self::Iterable { element_ty } => element_ty,
Self::NotIterable { not_iterable_ty } => {
diagnostics.add_not_iterable(iterable_node, not_iterable_ty);
report_not_iterable(db, file, iterable_node, not_iterable_ty);
Type::Unknown
}
}
@@ -2228,6 +2240,35 @@ impl<'db> TupleType<'db> {
}
}
#[derive(Debug)]
pub struct RevealTypeDiagnostic {
range: TextRange,
ty: String,
file: File,
}
impl Diagnostic for RevealTypeDiagnostic {
fn rule(&self) -> &str {
"revealed-type"
}
fn message(&self) -> std::borrow::Cow<str> {
format!("Revealed type is `{}`", self.ty).into()
}
fn file(&self) -> File {
self.file
}
fn range(&self) -> Option<TextRange> {
Some(self.range)
}
fn severity(&self) -> Severity {
Severity::Info
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,13 +1,198 @@
use ruff_db::diagnostic::{CompileDiagnostic, Diagnostic, Severity};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use std::borrow::Cow;
use crate::types::{ClassLiteralType, Type};
use crate::Db;
/// Returns `true` if any diagnostic is enabled for this file.
pub(crate) fn is_any_diagnostic_enabled(db: &dyn Db, file: File) -> bool {
db.is_file_open(file)
}
pub(crate) fn report_type_diagnostic(
db: &dyn Db,
file: File,
node: AnyNodeRef,
rule: &str,
message: std::fmt::Arguments,
) {
if !is_any_diagnostic_enabled(db, file) {
return;
}
// TODO: Don't emit the diagnostic if:
// * The enclosing node contains any syntax errors
// * The rule is disabled for this file. We probably want to introduce a new query that
// returns a rule selector for a given file that respects the package's settings,
// any global pragma comments in the file, and any per-file-ignores.
CompileDiagnostic::report(
db.upcast(),
TypeCheckDiagnostic {
file,
rule: rule.to_string(),
message: message.to_string(),
range: node.range(),
},
);
}
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn report_not_iterable(
db: &dyn Db,
file: File,
node: AnyNodeRef,
not_iterable_ty: Type,
) {
report_type_diagnostic(
db,
file,
node,
"not-iterable",
format_args!(
"Object of type `{}` is not iterable",
not_iterable_ty.display(db)
),
);
}
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
pub(super) fn report_index_out_of_bounds(
db: &dyn Db,
file: File,
kind: &'static str,
node: AnyNodeRef,
tuple_ty: Type,
length: usize,
index: i64,
) {
report_type_diagnostic(
db,
file,
node,
"index-out-of-bounds",
format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(db)
),
);
}
/// Emit a diagnostic declaring that a type does not support subscripting.
pub(super) fn report_non_subscriptable(
db: &dyn Db,
file: File,
node: AnyNodeRef,
non_subscriptable_ty: Type,
method: &str,
) {
report_type_diagnostic(
db,
file,
node,
"non-subscriptable",
format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(db)
),
);
}
pub(super) fn report_unresolved_module<'a>(
db: &dyn Db,
file: File,
import_node: impl Into<AnyNodeRef<'a>>,
level: u32,
module: Option<&str>,
) {
report_type_diagnostic(
db,
file,
import_node.into(),
"unresolved-import",
format_args!(
"Cannot resolve import `{}{}`",
".".repeat(level as usize),
module.unwrap_or_default()
),
);
}
pub(super) fn report_slice_step_size_zero(db: &dyn Db, file: File, node: AnyNodeRef) {
report_type_diagnostic(
db,
file,
node,
"zero-stepsize-in-slice",
format_args!("Slice step size can not be zero"),
);
}
pub(super) fn report_invalid_assignment(
db: &dyn Db,
file: File,
node: AnyNodeRef,
declared_ty: Type,
assigned_ty: Type,
) {
match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => {
report_type_diagnostic(db, file, node, "invalid-assignment", format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(db)));
}
Type::FunctionLiteral(function) => {
report_type_diagnostic(db, file, node, "invalid-assignment", format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(db)));
}
_ => {
report_type_diagnostic(
db,
file,
node,
"invalid-assignment",
format_args!(
"Object of type `{}` is not assignable to `{}`",
assigned_ty.display(db),
declared_ty.display(db),
),
);
}
}
}
pub(super) fn report_possibly_unresolved_reference(
db: &dyn Db,
file: File,
expr_name_node: &ast::ExprName,
) {
let ast::ExprName { id, .. } = expr_name_node;
report_type_diagnostic(
db,
file,
expr_name_node.into(),
"possibly-unresolved-reference",
format_args!("Name `{id}` used when possibly not defined"),
);
}
pub(super) fn report_unresolved_reference(db: &dyn Db, file: File, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
report_type_diagnostic(
db,
file,
expr_name_node.into(),
"unresolved-reference",
format_args!("Name `{id}` used when not defined"),
);
}
#[derive(Debug, Eq, PartialEq)]
pub struct TypeCheckDiagnostic {
// TODO: Don't use string keys for rules
@@ -22,265 +207,29 @@ impl TypeCheckDiagnostic {
&self.rule
}
pub fn message(&self) -> &str {
&self.message
}
pub fn file(&self) -> File {
self.file
}
}
impl Ranged for TypeCheckDiagnostic {
fn range(&self) -> TextRange {
pub fn range(&self) -> TextRange {
self.range
}
}
/// A collection of type check diagnostics.
///
/// The diagnostics are wrapped in an `Arc` because they need to be cloned multiple times
/// when going from `infer_expression` to `check_file`. We could consider
/// making [`TypeCheckDiagnostic`] a Salsa struct to have them Arena-allocated (once the Tables refactor is done).
/// Using Salsa struct does have the downside that it leaks the Salsa dependency into diagnostics and
/// each Salsa-struct comes with an overhead.
#[derive(Default, Eq, PartialEq)]
pub struct TypeCheckDiagnostics {
inner: Vec<std::sync::Arc<TypeCheckDiagnostic>>,
}
impl TypeCheckDiagnostics {
pub fn new() -> Self {
Self { inner: Vec::new() }
impl Diagnostic for TypeCheckDiagnostic {
fn message(&self) -> std::borrow::Cow<str> {
Cow::Borrowed(&self.message)
}
pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
self.inner.push(Arc::new(diagnostic));
fn file(&self) -> File {
self.file
}
pub(crate) fn shrink_to_fit(&mut self) {
self.inner.shrink_to_fit();
}
}
impl Extend<TypeCheckDiagnostic> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = TypeCheckDiagnostic>>(&mut self, iter: T) {
self.inner.extend(iter.into_iter().map(std::sync::Arc::new));
}
}
impl Extend<std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
self.inner.extend(iter);
}
}
impl<'a> Extend<&'a std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = &'a Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
self.inner
.extend(iter.into_iter().map(std::sync::Arc::clone));
}
}
impl std::fmt::Debug for TypeCheckDiagnostics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl Deref for TypeCheckDiagnostics {
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl IntoIterator for TypeCheckDiagnostics {
type Item = Arc<TypeCheckDiagnostic>;
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
type Item = &'a Arc<TypeCheckDiagnostic>;
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.inner.iter()
}
}
pub(super) struct TypeCheckDiagnosticsBuilder<'db> {
db: &'db dyn Db,
file: File,
diagnostics: TypeCheckDiagnostics,
}
impl<'db> TypeCheckDiagnosticsBuilder<'db> {
pub(super) fn new(db: &'db dyn Db, file: File) -> Self {
Self {
db,
file,
diagnostics: TypeCheckDiagnostics::new(),
}
}
/// 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(
node,
"not-iterable",
format_args!(
"Object of type `{}` is not iterable",
not_iterable_ty.display(self.db)
),
);
}
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
pub(super) fn add_index_out_of_bounds(
&mut self,
kind: &'static str,
node: AnyNodeRef,
tuple_ty: Type<'db>,
length: usize,
index: i64,
) {
self.add(
node,
"index-out-of-bounds",
format_args!(
"Index {index} is out of bounds for {kind} `{}` with length {length}",
tuple_ty.display(self.db)
),
);
}
/// Emit a diagnostic declaring that a type does not support subscripting.
pub(super) fn add_non_subscriptable(
&mut self,
node: AnyNodeRef,
non_subscriptable_ty: Type<'db>,
method: &str,
) {
self.add(
node,
"non-subscriptable",
format_args!(
"Cannot subscript object of type `{}` with no `{method}` method",
non_subscriptable_ty.display(self.db)
),
);
}
pub(super) fn add_unresolved_module(
&mut self,
import_node: impl Into<AnyNodeRef<'db>>,
level: u32,
module: Option<&str>,
) {
self.add(
import_node.into(),
"unresolved-import",
format_args!(
"Cannot resolve import `{}{}`",
".".repeat(level as usize),
module.unwrap_or_default()
),
);
}
pub(super) fn add_slice_step_size_zero(&mut self, node: AnyNodeRef) {
self.add(
node,
"zero-stepsize-in-slice",
format_args!("Slice step size can not be zero"),
);
}
pub(super) fn add_invalid_assignment(
&mut self,
node: AnyNodeRef,
declared_ty: Type<'db>,
assigned_ty: Type<'db>,
) {
match declared_ty {
Type::ClassLiteral(ClassLiteralType { class }) => {
self.add(node, "invalid-assignment", format_args!(
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
class.name(self.db)));
}
Type::FunctionLiteral(function) => {
self.add(node, "invalid-assignment", format_args!(
"Implicit shadowing of function `{}`; annotate to make it explicit if this is intentional",
function.name(self.db)));
}
_ => {
self.add(
node,
"invalid-assignment",
format_args!(
"Object of type `{}` is not assignable to `{}`",
assigned_ty.display(self.db),
declared_ty.display(self.db),
),
);
}
}
}
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
expr_name_node.into(),
"possibly-unresolved-reference",
format_args!("Name `{id}` used when possibly not defined"),
);
}
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
let ast::ExprName { id, .. } = expr_name_node;
self.add(
expr_name_node.into(),
"unresolved-reference",
format_args!("Name `{id}` used when not defined"),
);
}
/// Adds a new diagnostic.
///
/// The diagnostic does not get added if the rule isn't enabled for this file.
pub(super) fn add(&mut self, node: AnyNodeRef, rule: &str, message: std::fmt::Arguments) {
if !self.db.is_file_open(self.file) {
return;
}
// TODO: Don't emit the diagnostic if:
// * The enclosing node contains any syntax errors
// * The rule is disabled for this file. We probably want to introduce a new query that
// returns a rule selector for a given file that respects the package's settings,
// any global pragma comments in the file, and any per-file-ignores.
self.diagnostics.push(TypeCheckDiagnostic {
file: self.file,
rule: rule.to_string(),
message: message.to_string(),
range: node.range(),
});
}
pub(super) fn extend(&mut self, diagnostics: &TypeCheckDiagnostics) {
self.diagnostics.extend(diagnostics);
}
pub(super) fn finish(mut self) -> TypeCheckDiagnostics {
self.diagnostics.shrink_to_fit();
self.diagnostics
fn range(&self) -> Option<TextRange> {
Some(self.range)
}
fn severity(&self) -> Severity {
Severity::Error
}
fn rule(&self) -> &str {
&self.rule
}
}

View File

@@ -48,9 +48,6 @@ 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::{
TypeCheckDiagnostic, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder,
};
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
@@ -64,6 +61,12 @@ use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use super::diagnostic::{
is_any_diagnostic_enabled, report_index_out_of_bounds, report_invalid_assignment,
report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero,
report_type_diagnostic, report_unresolved_module, report_unresolved_reference,
};
/// 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
/// scope.
@@ -212,9 +215,6 @@ pub(crate) struct TypeInference<'db> {
/// The types of every declaration in this region.
declarations: FxHashMap<Definition<'db>, Type<'db>>,
/// The diagnostics for this region.
diagnostics: TypeCheckDiagnostics,
/// Are there deferred type expressions in this region?
has_deferred: bool,
@@ -228,7 +228,6 @@ impl<'db> TypeInference<'db> {
expressions: FxHashMap::default(),
bindings: FxHashMap::default(),
declarations: FxHashMap::default(),
diagnostics: TypeCheckDiagnostics::default(),
has_deferred: false,
scope,
}
@@ -253,15 +252,10 @@ impl<'db> TypeInference<'db> {
self.declarations[&definition]
}
pub(crate) fn diagnostics(&self) -> &[std::sync::Arc<TypeCheckDiagnostic>] {
&self.diagnostics
}
fn shrink_to_fit(&mut self) {
self.expressions.shrink_to_fit();
self.bindings.shrink_to_fit();
self.declarations.shrink_to_fit();
self.diagnostics.shrink_to_fit();
}
}
@@ -321,8 +315,6 @@ pub(super) struct TypeInferenceBuilder<'db> {
/// The type inference results
types: TypeInference<'db>,
diagnostics: TypeCheckDiagnosticsBuilder<'db>,
}
impl<'db> TypeInferenceBuilder<'db> {
@@ -352,7 +344,6 @@ impl<'db> TypeInferenceBuilder<'db> {
region,
file,
types: TypeInference::empty(scope),
diagnostics: TypeCheckDiagnosticsBuilder::new(db, file),
}
}
@@ -364,7 +355,6 @@ impl<'db> TypeInferenceBuilder<'db> {
.declarations
.extend(inference.declarations.iter());
self.types.expressions.extend(inference.expressions.iter());
self.diagnostics.extend(&inference.diagnostics);
self.types.has_deferred |= inference.has_deferred;
}
@@ -439,13 +429,14 @@ impl<'db> TypeInferenceBuilder<'db> {
if infer_definition_types(self.db, *definition).has_deferred {
let deferred = infer_deferred_types(self.db, *definition);
self.types.expressions.extend(&deferred.expressions);
self.diagnostics.extend(&deferred.diagnostics);
}
}
}
// TODO: Only call this function when diagnostics are enabled.
self.check_class_definitions();
// TODO: Test specifically for the check class definition rules
if is_any_diagnostic_enabled(self.db, self.file) {
self.check_class_definitions();
}
}
/// Iterate over all class definitions to check that Python will be able to create a
@@ -473,14 +464,18 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class.node(self.db).bases();
for (index, duplicate) in duplicates {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
(&base_nodes[*index]).into(),
"duplicate-base",
format_args!("Duplicate base class `{}`", duplicate.name(self.db))
);
}
}
MroErrorKind::CyclicClassDefinition => self.diagnostics.add(
MroErrorKind::CyclicClassDefinition => report_type_diagnostic(
self.db,
self.file,
class.node(self.db).into(),
"cyclic-class-def",
format_args!(
@@ -492,7 +487,9 @@ impl<'db> TypeInferenceBuilder<'db> {
MroErrorKind::InvalidBases(bases) => {
let base_nodes = class.node(self.db).bases();
for (index, base_ty) in bases {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
(&base_nodes[*index]).into(),
"invalid-base",
format_args!(
@@ -502,7 +499,9 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
},
MroErrorKind::UnresolvableMro{bases_list} => self.diagnostics.add(
MroErrorKind::UnresolvableMro{bases_list} => report_type_diagnostic(
self.db,
self.file,
class.node(self.db).into(),
"inconsistent-mro",
format_args!(
@@ -627,7 +626,9 @@ impl<'db> TypeInferenceBuilder<'db> {
_ => return,
};
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
expr.into(),
"division-by-zero",
format_args!(
@@ -652,7 +653,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(
report_type_diagnostic(
self.db,
self.file,
node,
"conflicting-declarations",
format_args!(
@@ -664,8 +667,7 @@ impl<'db> TypeInferenceBuilder<'db> {
},
);
if !bound_ty.is_assignable_to(self.db, declared_ty) {
self.diagnostics
.add_invalid_assignment(node, declared_ty, bound_ty);
report_invalid_assignment(self.db, self.file, node, declared_ty, bound_ty);
// allow declarations to override inference in case of invalid assignment
bound_ty = declared_ty;
};
@@ -682,7 +684,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let ty = if inferred_ty.is_assignable_to(self.db, ty) {
ty
} else {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
node,
"invalid-declaration",
format_args!(
@@ -708,8 +712,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let inferred_ty = if inferred_ty.is_assignable_to(self.db, declared_ty) {
inferred_ty
} else {
self.diagnostics
.add_invalid_assignment(node, declared_ty, inferred_ty);
report_invalid_assignment(self.db, self.file, node, declared_ty, inferred_ty);
// if the assignment is invalid, fall back to assuming the annotation is correct
declared_ty
};
@@ -1150,7 +1153,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(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1161,7 +1166,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
}
(Symbol::Unbound, _) => {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1173,7 +1180,9 @@ impl<'db> TypeInferenceBuilder<'db> {
}
(Symbol::Type(enter_ty, enter_boundness), exit) => {
if enter_boundness == Boundness::MayBeUnbound {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1185,9 +1194,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let target_ty = enter_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(), self.file)
.unwrap_or_else(|err| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!("
@@ -1201,7 +1212,9 @@ impl<'db> TypeInferenceBuilder<'db> {
match exit {
Symbol::Unbound => {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1214,7 +1227,9 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
if exit_boundness == Boundness::MayBeUnbound {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1234,14 +1249,12 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::none(self.db),
],
)
.return_ty_result(
self.db,
context_expression.into(),
&mut self.diagnostics,
)
.return_ty_result(self.db, context_expression.into(), self.file)
.is_err()
{
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
context_expression.into(),
"invalid-context-manager",
format_args!(
@@ -1440,7 +1453,6 @@ impl<'db> TypeInferenceBuilder<'db> {
let target_ty = match target {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db, unpack);
self.diagnostics.extend(unpacked.diagnostics());
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
}
TargetKind::Name => value_ty,
@@ -1545,11 +1557,13 @@ impl<'db> TypeInferenceBuilder<'db> {
let augmented_return_ty = match call.return_ty_result(
self.db,
AnyNodeRef::StmtAugAssign(assignment),
&mut self.diagnostics,
self.file,
) {
Ok(t) => t,
Err(e) => {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
assignment.into(),
"unsupported-operator",
format_args!(
@@ -1570,7 +1584,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(
report_type_diagnostic(
self.db,
self.file,
assignment.into(),
"unsupported-operator",
format_args!(
@@ -1599,7 +1615,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, op)
.unwrap_or_else(|| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
assignment.into(),
"unsupported-operator",
format_args!(
@@ -1697,7 +1715,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else {
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(iterable.into(), &mut self.diagnostics)
.unwrap_with_diagnostic(self.db, iterable.into(), self.file)
};
self.store_expression_type(target, loop_var_value_ty);
@@ -1736,7 +1754,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(module) = self.module_ty_from_name(&module_name) {
module
} else {
self.diagnostics.add_unresolved_module(alias, 0, Some(name));
report_unresolved_module(self.db, self.file, alias, 0, Some(name));
Type::Unknown
}
} else {
@@ -1867,7 +1885,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.member(self.db, &ast::name::Name::new(&name.id))
.as_type()
.unwrap_or_else(|| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
AnyNodeRef::Alias(alias),
"unresolved-import",
format_args!("Module `{module_name}` has no member `{name}`",),
@@ -1876,8 +1896,7 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Unknown
})
} else {
self.diagnostics
.add_unresolved_module(import_from, *level, module);
report_unresolved_module(self.db, self.file, import_from, *level, module);
Type::Unknown
}
}
@@ -1891,8 +1910,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"Relative module resolution `{}` failed: too many leading dots",
format_import_from_module(*level, module),
);
self.diagnostics
.add_unresolved_module(import_from, *level, module);
report_unresolved_module(self.db, self.file, import_from, *level, module);
Type::Unknown
}
Err(ModuleNameResolutionError::UnknownCurrentModule) => {
@@ -1901,8 +1919,7 @@ impl<'db> TypeInferenceBuilder<'db> {
format_import_from_module(*level, module),
self.file.path(self.db)
);
self.diagnostics
.add_unresolved_module(import_from, *level, module);
report_unresolved_module(self.db, self.file, import_from, *level, module);
Type::Unknown
}
};
@@ -2364,7 +2381,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else {
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(iterable.into(), &mut self.diagnostics)
.unwrap_with_diagnostic(self.db, iterable.into(), self.file)
};
self.types
@@ -2456,7 +2473,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let function_type = self.infer_expression(func);
function_type
.call(self.db, arg_types.as_slice())
.unwrap_with_diagnostic(self.db, func.as_ref().into(), &mut self.diagnostics)
.unwrap_with_diagnostic(self.db, func.as_ref().into(), self.file)
}
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
@@ -2467,9 +2484,11 @@ impl<'db> TypeInferenceBuilder<'db> {
} = starred;
let iterable_ty = self.infer_expression(value);
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
iterable_ty.iterate(self.db).unwrap_with_diagnostic(
self.db,
value.as_ref().into(),
self.file,
);
// TODO
Type::Todo
@@ -2488,9 +2507,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let ast::ExprYieldFrom { range: _, value } = yield_from;
let iterable_ty = self.infer_expression(value);
iterable_ty
.iterate(self.db)
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
iterable_ty.iterate(self.db).unwrap_with_diagnostic(
self.db,
value.as_ref().into(),
self.file,
);
// TODO get type from `ReturnType` of generator
Type::Todo
@@ -2558,7 +2579,9 @@ impl<'db> TypeInferenceBuilder<'db> {
{
let mut symbol = builtins_symbol(self.db, name);
if symbol.is_unbound() && name == "reveal_type" {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
name_node.into(),
"undefined-reveal",
format_args!(
@@ -2611,7 +2634,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match self.lookup_name(name) {
Symbol::Type(looked_up_ty, looked_up_boundness) => {
if looked_up_boundness == Boundness::MayBeUnbound {
self.diagnostics.add_possibly_unresolved_reference(name);
report_possibly_unresolved_reference(self.db, self.file, name);
}
bindings_ty
@@ -2620,9 +2643,9 @@ impl<'db> TypeInferenceBuilder<'db> {
}
Symbol::Unbound => {
if bindings_ty.is_some() {
self.diagnostics.add_possibly_unresolved_reference(name);
report_possibly_unresolved_reference(self.db, self.file, name);
} else {
self.diagnostics.add_unresolved_reference(name);
report_unresolved_reference(self.db, self.file, name);
}
bindings_ty.unwrap_or(Type::Unknown)
}
@@ -2715,14 +2738,13 @@ impl<'db> TypeInferenceBuilder<'db> {
{
let call = class_member.call(self.db, &[operand_type]);
match call.return_ty_result(
self.db,
AnyNodeRef::ExprUnaryOp(unary),
&mut self.diagnostics,
) {
match call.return_ty_result(self.db, AnyNodeRef::ExprUnaryOp(unary), self.file)
{
Ok(t) => t,
Err(e) => {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
unary.into(),
"unsupported-operator",
format_args!(
@@ -2734,7 +2756,9 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
} else {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
unary.into(),
"unsupported-operator",
format_args!(
@@ -2775,7 +2799,9 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_binary_expression_type(left_ty, right_ty, *op)
.unwrap_or_else(|| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
binary.into(),
"unsupported-operator",
format_args!(
@@ -3105,7 +3131,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(
report_type_diagnostic(
self.db,
self.file,
AnyNodeRef::ExprCompare(compare),
"unsupported-operator",
format_args!(
@@ -3495,7 +3523,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.py_index(i32::try_from(int).expect("checked in branch arm"))
.copied()
.unwrap_or_else(|_| {
self.diagnostics.add_index_out_of_bounds(
report_index_out_of_bounds(
self.db,
self.file,
"tuple",
value_node.into(),
value_ty,
@@ -3514,7 +3544,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let new_elements: Vec<_> = new_elements.copied().collect();
Type::Tuple(TupleType::new(self.db, new_elements.into_boxed_slice()))
} else {
self.diagnostics.add_slice_step_size_zero(value_node.into());
report_slice_step_size_zero(self.db, self.file, value_node.into());
Type::Unknown
}
}
@@ -3533,7 +3563,9 @@ impl<'db> TypeInferenceBuilder<'db> {
))
})
.unwrap_or_else(|_| {
self.diagnostics.add_index_out_of_bounds(
report_index_out_of_bounds(
self.db,
self.file,
"string",
value_node.into(),
value_ty,
@@ -3553,7 +3585,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let literal: String = new_chars.collect();
Type::StringLiteral(StringLiteralType::new(self.db, literal.into_boxed_str()))
} else {
self.diagnostics.add_slice_step_size_zero(value_node.into());
report_slice_step_size_zero(self.db, self.file, value_node.into());
Type::Unknown
};
result
@@ -3570,7 +3602,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::BytesLiteral(BytesLiteralType::new(self.db, [*byte].as_slice()))
})
.unwrap_or_else(|_| {
self.diagnostics.add_index_out_of_bounds(
report_index_out_of_bounds(
self.db,
self.file,
"bytes literal",
value_node.into(),
value_ty,
@@ -3589,7 +3623,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let new_bytes: Vec<u8> = new_bytes.copied().collect();
Type::BytesLiteral(BytesLiteralType::new(self.db, new_bytes.into_boxed_slice()))
} else {
self.diagnostics.add_slice_step_size_zero(value_node.into());
report_slice_step_size_zero(self.db, self.file, value_node.into());
Type::Unknown
}
}
@@ -3613,7 +3647,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {}
Symbol::Type(dunder_getitem_method, boundness) => {
if boundness == Boundness::MayBeUnbound {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
value_node.into(),
"call-possibly-unbound-method",
format_args!(
@@ -3625,9 +3661,11 @@ impl<'db> TypeInferenceBuilder<'db> {
return dunder_getitem_method
.call(self.db, &[slice_ty])
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
.return_ty_result(self.db, value_node.into(), self.file)
.unwrap_or_else(|err| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
value_node.into(),
"call-non-callable",
format_args!(
@@ -3657,7 +3695,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Symbol::Unbound => {}
Symbol::Type(ty, boundness) => {
if boundness == Boundness::MayBeUnbound {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
value_node.into(),
"call-possibly-unbound-method",
format_args!(
@@ -3669,9 +3709,11 @@ impl<'db> TypeInferenceBuilder<'db> {
return 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(), self.file)
.unwrap_or_else(|err| {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
value_node.into(),
"call-non-callable",
format_args!(
@@ -3690,13 +3732,17 @@ impl<'db> TypeInferenceBuilder<'db> {
return KnownClass::GenericAlias.to_instance(self.db);
}
self.diagnostics.add_non_subscriptable(
report_non_subscriptable(
self.db,
self.file,
value_node.into(),
value_ty,
"__class_getitem__",
);
} else {
self.diagnostics.add_non_subscriptable(
report_non_subscriptable(
self.db,
self.file,
value_node.into(),
value_ty,
"__getitem__",
@@ -3791,7 +3837,6 @@ impl<'db> TypeInferenceBuilder<'db> {
pub(super) fn finish(mut self) -> TypeInference<'db> {
self.infer_region();
self.types.diagnostics = self.diagnostics.finish();
self.types.shrink_to_fit();
self.types
}
@@ -4079,7 +4124,9 @@ impl<'db> TypeInferenceBuilder<'db> {
Ok(ty) => ty,
Err(nodes) => {
for node in nodes {
self.diagnostics.add(
report_type_diagnostic(
self.db,
self.file,
node.into(),
"invalid-literal-parameter",
format_args!(
@@ -4401,8 +4448,9 @@ fn perform_membership_test_comparison<'db>(
#[cfg(test)]
mod tests {
use anyhow::Context;
use ruff_db::diagnostic::CompileDiagnostic;
use ruff_db::diagnostic::Diagnostic;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
@@ -4503,22 +4551,18 @@ mod tests {
}
#[track_caller]
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
let messages: Vec<&str> = diagnostics
fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types::accumulated::<CompileDiagnostic>(db, file);
let messages: Vec<_> = diagnostics
.iter()
.map(|diagnostic| diagnostic.message())
.collect();
assert_eq!(&messages, expected);
}
#[track_caller]
fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types(db, file);
assert_diagnostic_messages(&diagnostics, expected);
}
#[test]
fn from_import_with_no_module_name() -> anyhow::Result<()> {
// This test checks that invalid syntax in a `StmtImportFrom` node
@@ -5077,7 +5121,14 @@ mod tests {
",
)?;
assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `Unknown`"]);
assert_file_diagnostics(
&db,
"src/a.py",
&[
"Expected one or more exception types",
"Revealed type is `Unknown`",
],
);
Ok(())
}
@@ -5337,8 +5388,15 @@ mod tests {
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unknown");
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
assert_file_diagnostics(
&db,
"src/a.py",
&[
"Expected an identifier, but found a keyword 'in' that cannot be used here",
"Expected 'in', found name",
"Name `z` used when not defined",
],
);
Ok(())
}
@@ -5424,7 +5482,7 @@ mod tests {
return 42
class Iterable:
def __iter__(self) -> Iterator:
def __iter__(self) -> Iterator: ...
x = [*NotIterable()]
y = [*Iterable()]

View File

@@ -6,14 +6,14 @@ use rustc_hash::FxHashMap;
use crate::semantic_index::ast_ids::{HasScopedAstId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{TupleType, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::types::{TupleType, Type};
use crate::Db;
/// Unpacks the value expression type to their respective targets.
pub(crate) struct Unpacker<'db> {
db: &'db dyn Db,
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
diagnostics: TypeCheckDiagnosticsBuilder<'db>,
file: File,
}
impl<'db> Unpacker<'db> {
@@ -21,7 +21,7 @@ impl<'db> Unpacker<'db> {
Self {
db,
targets: FxHashMap::default(),
diagnostics: TypeCheckDiagnosticsBuilder::new(db, file),
file,
}
}
@@ -104,9 +104,11 @@ impl<'db> Unpacker<'db> {
let value_ty = if value_ty.is_literal_string() {
Type::LiteralString
} else {
value_ty
.iterate(self.db)
.unwrap_with_diagnostic(AnyNodeRef::from(target), &mut self.diagnostics)
value_ty.iterate(self.db).unwrap_with_diagnostic(
self.db,
AnyNodeRef::from(target),
self.file,
)
};
for element in elts {
self.unpack(element, value_ty, scope);
@@ -120,7 +122,6 @@ impl<'db> Unpacker<'db> {
pub(crate) fn finish(mut self) -> UnpackResult<'db> {
self.targets.shrink_to_fit();
UnpackResult {
diagnostics: self.diagnostics.finish(),
targets: self.targets,
}
}
@@ -129,15 +130,10 @@ impl<'db> Unpacker<'db> {
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct UnpackResult<'db> {
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
diagnostics: TypeCheckDiagnostics,
}
impl<'db> UnpackResult<'db> {
pub(crate) fn get(&self, expr_id: ScopedExpressionId) -> Option<Type<'db>> {
self.targets.get(&expr_id).copied()
}
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
&self.diagnostics
}
}