mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
[ty] Improve UX for [duplicate-base]
diagnostics (#17914)
This commit is contained in:
parent
ad658f4d68
commit
2ec0d7e072
6 changed files with 594 additions and 44 deletions
|
@ -1799,26 +1799,32 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the [`Span`] of the class's "header": the class name
|
||||
/// Returns a [`Span`] with the range of the class's header.
|
||||
///
|
||||
/// See [`Self::header_range`] for more details.
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
Span::from(self.file(db)).with_range(self.header_range(db))
|
||||
}
|
||||
|
||||
/// Returns the range of the class's "header": the class name
|
||||
/// and any arguments passed to the `class` statement. E.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// class Foo(Bar, metaclass=Baz): ...
|
||||
/// ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
|
||||
let class_scope = self.body_scope(db);
|
||||
let class_node = class_scope.node(db).expect_class();
|
||||
let class_name = &class_node.name;
|
||||
let header_range = TextRange::new(
|
||||
TextRange::new(
|
||||
class_name.start(),
|
||||
class_node
|
||||
.arguments
|
||||
.as_deref()
|
||||
.map(Ranged::end)
|
||||
.unwrap_or_else(|| class_name.end()),
|
||||
);
|
||||
Span::from(class_scope.file(db)).with_range(header_range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::context::InferContext;
|
||||
use super::mro::DuplicateBaseError;
|
||||
use super::{ClassLiteral, KnownClass};
|
||||
use crate::db::Db;
|
||||
use crate::declare_lint;
|
||||
|
@ -1595,3 +1596,51 @@ pub(crate) fn report_attempted_protocol_instantiation(
|
|||
);
|
||||
diagnostic.sub(class_def_diagnostic);
|
||||
}
|
||||
|
||||
pub(crate) fn report_duplicate_bases(
|
||||
context: &InferContext,
|
||||
class: ClassLiteral,
|
||||
duplicate_base_error: &DuplicateBaseError,
|
||||
bases_list: &[ast::Expr],
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&DUPLICATE_BASE, class.header_range(db)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let DuplicateBaseError {
|
||||
duplicate_base,
|
||||
first_index,
|
||||
later_indices,
|
||||
} = duplicate_base_error;
|
||||
|
||||
let duplicate_name = duplicate_base.name(db);
|
||||
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic(format_args!("Duplicate base class `{duplicate_name}`",));
|
||||
|
||||
let mut sub_diagnostic = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format_args!(
|
||||
"The definition of class `{}` will raise `TypeError` at runtime",
|
||||
class.name(db)
|
||||
),
|
||||
);
|
||||
sub_diagnostic.annotate(
|
||||
Annotation::secondary(
|
||||
Span::from(context.file()).with_range(bases_list[*first_index].range()),
|
||||
)
|
||||
.message(format_args!(
|
||||
"Class `{duplicate_name}` first included in bases list here"
|
||||
)),
|
||||
);
|
||||
for index in later_indices {
|
||||
sub_diagnostic.annotate(
|
||||
Annotation::primary(Span::from(context.file()).with_range(bases_list[*index].range()))
|
||||
.message(format_args!("Class `{duplicate_name}` later repeated here")),
|
||||
);
|
||||
}
|
||||
|
||||
diagnostic.sub(sub_diagnostic);
|
||||
}
|
||||
|
|
|
@ -72,9 +72,9 @@ use crate::types::diagnostic::{
|
|||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_return_type, report_possibly_unbound_attribute, TypeCheckDiagnostics,
|
||||
CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS,
|
||||
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
|
||||
INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
|
||||
INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, INCONSISTENT_MRO,
|
||||
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
|
||||
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
|
||||
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
|
||||
POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
|
||||
UNSUPPORTED_OPERATOR,
|
||||
|
@ -99,9 +99,10 @@ use crate::{Db, FxOrderSet};
|
|||
use super::context::{InNoTypeCheck, InferContext};
|
||||
use super::diagnostic::{
|
||||
report_attempted_protocol_instantiation, report_bad_argument_to_get_protocol_members,
|
||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_invalid_type_checking_constant,
|
||||
report_non_subscriptable, report_possibly_unresolved_reference,
|
||||
report_duplicate_bases, report_index_out_of_bounds, report_invalid_exception_caught,
|
||||
report_invalid_exception_cause, report_invalid_exception_raised,
|
||||
report_invalid_type_checking_constant, report_non_subscriptable,
|
||||
report_possibly_unresolved_reference,
|
||||
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
|
||||
report_unresolved_reference, INVALID_METACLASS, INVALID_OVERLOAD, INVALID_PROTOCOL,
|
||||
REDUNDANT_CAST, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
|
||||
|
@ -855,17 +856,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
match mro_error.reason() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&DUPLICATE_BASE, &base_nodes[*index])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Duplicate base class `{}`",
|
||||
duplicate.name(self.db())
|
||||
));
|
||||
for duplicate in duplicates {
|
||||
report_duplicate_bases(&self.context, class, duplicate, base_nodes);
|
||||
}
|
||||
}
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::ops::Deref;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::generics::Specialization;
|
||||
|
@ -154,25 +154,40 @@ impl<'db> Mro<'db> {
|
|||
);
|
||||
|
||||
c3_merge(seqs).ok_or_else(|| {
|
||||
let mut seen_bases = FxHashSet::default();
|
||||
let mut duplicate_bases = vec![];
|
||||
for (index, base) in valid_bases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, base)| Some((index, base.into_class()?)))
|
||||
{
|
||||
if !seen_bases.insert(base) {
|
||||
let (base_class_literal, _) = base.class_literal(db);
|
||||
duplicate_bases.push((index, base_class_literal));
|
||||
let duplicate_bases: Box<[DuplicateBaseError<'db>]> = {
|
||||
let mut base_to_indices: FxHashMap<ClassType<'db>, Vec<usize>> =
|
||||
FxHashMap::default();
|
||||
|
||||
for (index, base) in valid_bases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, base)| Some((index, base.into_class()?)))
|
||||
{
|
||||
base_to_indices.entry(base).or_default().push(index);
|
||||
}
|
||||
}
|
||||
|
||||
base_to_indices
|
||||
.iter()
|
||||
.filter_map(|(base, indices)| {
|
||||
let (first_index, later_indices) = indices.split_first()?;
|
||||
if later_indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(DuplicateBaseError {
|
||||
duplicate_base: base.class_literal(db).0,
|
||||
first_index: *first_index,
|
||||
later_indices: later_indices.iter().copied().collect(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
if duplicate_bases.is_empty() {
|
||||
MroErrorKind::UnresolvableMro {
|
||||
bases_list: valid_bases.into_boxed_slice(),
|
||||
}
|
||||
} else {
|
||||
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
MroErrorKind::DuplicateBases(duplicate_bases)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -328,12 +343,8 @@ pub(super) enum MroErrorKind<'db> {
|
|||
InvalidBases(Box<[(usize, Type<'db>)]>),
|
||||
|
||||
/// The class has one or more duplicate bases.
|
||||
///
|
||||
/// This variant records the indices and [`ClassLiteral`]s
|
||||
/// of the duplicate bases. The indices are the indices of nodes
|
||||
/// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node.
|
||||
/// Each index is the index of a node representing a duplicate base.
|
||||
DuplicateBases(Box<[(usize, ClassLiteral<'db>)]>),
|
||||
/// See [`DuplicateBaseError`] for more details.
|
||||
DuplicateBases(Box<[DuplicateBaseError<'db>]>),
|
||||
|
||||
/// The MRO is otherwise unresolvable through the C3-merge algorithm.
|
||||
///
|
||||
|
@ -350,6 +361,17 @@ impl<'db> MroErrorKind<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Error recording the fact that a class definition was found to have duplicate bases.
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct DuplicateBaseError<'db> {
|
||||
/// The base that is duplicated in the class's bases list.
|
||||
pub(super) duplicate_base: ClassLiteral<'db>,
|
||||
/// The index of the first occurrence of the base in the class's bases list.
|
||||
pub(super) first_index: usize,
|
||||
/// The indices of the base's later occurrences in the class's bases list.
|
||||
pub(super) later_indices: Box<[usize]>,
|
||||
}
|
||||
|
||||
/// Implementation of the [C3-merge algorithm] for calculating a Python class's
|
||||
/// [method resolution order].
|
||||
///
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue