mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Detect NamedTuple
classes where fields without default values follow fields with default values (#19945)
This commit is contained in:
parent
c20d906503
commit
4242905b36
6 changed files with 358 additions and 70 deletions
|
@ -11,8 +11,11 @@ use super::{
|
|||
use crate::FxOrderMap;
|
||||
use crate::module_resolver::KnownModule;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionState};
|
||||
use crate::semantic_index::place::ScopedPlaceId;
|
||||
use crate::semantic_index::scope::NodeWithScopeKind;
|
||||
use crate::semantic_index::{DeclarationWithConstraint, SemanticIndex, attribute_declarations};
|
||||
use crate::semantic_index::{
|
||||
BindingWithConstraints, DeclarationWithConstraint, SemanticIndex, attribute_declarations,
|
||||
};
|
||||
use crate::types::context::InferContext;
|
||||
use crate::types::diagnostic::{INVALID_LEGACY_TYPE_VARIABLE, INVALID_TYPE_ALIAS_TYPE};
|
||||
use crate::types::enums::enum_metadata;
|
||||
|
@ -2982,6 +2985,54 @@ impl<'db> ClassLiteral<'db> {
|
|||
.unwrap_or_else(|| class_name.end()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn declarations_of_name(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
) -> Option<impl Iterator<Item = DeclarationWithConstraint<'db>>> {
|
||||
let class_body_scope = self.body_scope(db).file_scope_id(db);
|
||||
let symbol_id = index.place_table(class_body_scope).symbol_id(name)?;
|
||||
let use_def = index.use_def_map(class_body_scope);
|
||||
Some(use_def.end_of_scope_declarations(ScopedPlaceId::Symbol(symbol_id)))
|
||||
}
|
||||
|
||||
pub(super) fn first_declaration_of_name(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
) -> Option<DeclarationWithConstraint<'db>> {
|
||||
self.declarations_of_name(db, name, index)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.next()
|
||||
}
|
||||
|
||||
pub(super) fn bindings_of_name(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
) -> Option<impl Iterator<Item = BindingWithConstraints<'db, 'db>>> {
|
||||
let class_body_scope = self.body_scope(db).file_scope_id(db);
|
||||
let symbol_id = index.place_table(class_body_scope).symbol_id(name)?;
|
||||
let use_def = index.use_def_map(class_body_scope);
|
||||
Some(use_def.end_of_scope_bindings(ScopedPlaceId::Symbol(symbol_id)))
|
||||
}
|
||||
|
||||
pub(super) fn first_binding_of_name(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
) -> Option<BindingWithConstraints<'db, 'db>> {
|
||||
self.bindings_of_name(db, name, index)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<ClassLiteral<'db>> for Type<'db> {
|
||||
|
|
|
@ -6,6 +6,7 @@ use super::{
|
|||
add_inferred_python_version_hint_to_diagnostic,
|
||||
};
|
||||
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::suppression::FileSuppressionId;
|
||||
use crate::types::LintDiagnosticGuard;
|
||||
use crate::types::class::{Field, SolidBase, SolidBaseKind};
|
||||
|
@ -2676,6 +2677,60 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
class: ClassLiteral<'db>,
|
||||
index: &'db SemanticIndex<'db>,
|
||||
field_name: &str,
|
||||
field_with_default: &str,
|
||||
) {
|
||||
let db = context.db();
|
||||
let module = context.module();
|
||||
|
||||
let diagnostic_range = class
|
||||
.first_declaration_of_name(db, field_name, index)
|
||||
.and_then(|definition| definition.declaration.definition())
|
||||
.map(|definition| definition.kind(db).full_range(module))
|
||||
.unwrap_or_else(|| class.header_range(db));
|
||||
|
||||
let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"NamedTuple field without default value cannot follow field(s) with default value(s)",
|
||||
));
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Field `{field_name}` defined here without a default value"
|
||||
));
|
||||
|
||||
let Some(field_with_default_range) = class
|
||||
.first_binding_of_name(db, field_with_default, index)
|
||||
.and_then(|definition| definition.binding.definition())
|
||||
.map(|definition| definition.kind(db).full_range(module))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the end-of-scope definition in the class scope of the field-with-a-default-value
|
||||
// occurs after the range of the field-without-a-default-value,
|
||||
// avoid adding a subdiagnostic that points to the definition of the
|
||||
// field-with-a-default-value. It's confusing to talk about a field "before" the
|
||||
// field without the default value but then point to a definition that actually
|
||||
// occurs after the field without-a-default-value.
|
||||
if field_with_default_range.end() < diagnostic_range.start() {
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(field_with_default_range)).message(format_args!(
|
||||
"Earlier field `{field_with_default}` defined here with a default value",
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
diagnostic.info(format_args!(
|
||||
"Earlier field `{field_with_default}` was defined with a default value"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// This function receives an unresolved `from foo import bar` import,
|
||||
/// where `foo` can be resolved to a module but that module does not
|
||||
/// have a `bar` member or submodule.
|
||||
|
|
|
@ -105,6 +105,7 @@ use crate::types::diagnostic::{
|
|||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
|
||||
report_invalid_key_on_typed_dict, report_invalid_return_type,
|
||||
report_namedtuple_field_without_default_after_field_with_default,
|
||||
report_possibly_unbound_attribute,
|
||||
};
|
||||
use crate::types::enums::is_enum_class;
|
||||
|
@ -1110,11 +1111,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let is_protocol = class.is_protocol(self.db());
|
||||
let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class);
|
||||
|
||||
// (2) If it's a `NamedTuple` class, check that no field without a default value
|
||||
// appears after a field with a default value.
|
||||
if is_named_tuple {
|
||||
let mut field_with_default_encountered = None;
|
||||
|
||||
for (field_name, field) in class.own_fields(self.db(), None) {
|
||||
if field.default_ty.is_some() {
|
||||
field_with_default_encountered = Some(field_name);
|
||||
} else if let Some(field_with_default) = field_with_default_encountered.as_ref()
|
||||
{
|
||||
report_namedtuple_field_without_default_after_field_with_default(
|
||||
&self.context,
|
||||
class,
|
||||
self.index,
|
||||
&field_name,
|
||||
field_with_default,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_protocol = class.is_protocol(self.db());
|
||||
|
||||
let mut solid_bases = IncompatibleBases::default();
|
||||
|
||||
// (2) Iterate through the class's explicit bases to check for various possible errors:
|
||||
// (3) Iterate through the class's explicit bases to check for various possible errors:
|
||||
// - Check for inheritance from plain `Generic`,
|
||||
// - Check for inheritance from a `@final` classes
|
||||
// - If the class is a protocol class: check for inheritance from a non-protocol class
|
||||
|
@ -1208,7 +1232,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
// (3) Check that the class's MRO is resolvable
|
||||
// (4) Check that the class's MRO is resolvable
|
||||
match class.try_mro(self.db(), None) {
|
||||
Err(mro_error) => match mro_error.reason() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
|
@ -1279,7 +1303,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
// (4) Check that the class's metaclass can be determined without error.
|
||||
// (5) Check that the class's metaclass can be determined without error.
|
||||
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
|
||||
match metaclass_error.reason() {
|
||||
MetaclassErrorKind::Cycle => {
|
||||
|
@ -1376,7 +1400,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
}
|
||||
}
|
||||
|
||||
// (5) Check that a dataclass does not have more than one `KW_ONLY`.
|
||||
// (6) Check that a dataclass does not have more than one `KW_ONLY`.
|
||||
if let Some(field_policy @ CodeGeneratorKind::DataclassLike) =
|
||||
CodeGeneratorKind::from_class(self.db(), class)
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue