[ty] Add a diagnostic for prohibited NamedTuple attribute overrides (#21717)

## Summary

Closes https://github.com/astral-sh/ty/issues/1684.
This commit is contained in:
Charlie Marsh
2025-12-01 21:46:58 -05:00
committed by GitHub
parent ec854c7199
commit 72304b01eb
6 changed files with 269 additions and 67 deletions

View File

@@ -545,7 +545,8 @@ declare_lint! {
///
/// ## Why is this bad?
/// An invalidly defined `NamedTuple` class may lead to the type checker
/// drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
/// drawing incorrect conclusions. It may also lead to `TypeError`s or
/// `AttributeError`s at runtime.
///
/// ## Examples
/// A class definition cannot combine `NamedTuple` with other base classes
@@ -558,6 +559,27 @@ declare_lint! {
/// >>> class Foo(NamedTuple, object): ...
/// TypeError: can only inherit from a NamedTuple type and Generic
/// ```
///
/// Further, `NamedTuple` field names cannot start with an underscore:
///
/// ```pycon
/// >>> from typing import NamedTuple
/// >>> class Foo(NamedTuple):
/// ... _bar: int
/// ValueError: Field names cannot start with an underscore: '_bar'
/// ```
///
/// `NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,
/// `_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes
/// without a type annotation will raise an `AttributeError` at runtime.
///
/// ```pycon
/// >>> from typing import NamedTuple
/// >>> class Foo(NamedTuple):
/// ... x: int
/// ... _asdict = 42
/// AttributeError: Cannot overwrite NamedTuple attribute _asdict
/// ```
pub(crate) static INVALID_NAMED_TUPLE = {
summary: "detects invalid `NamedTuple` class definitions",
status: LintStatus::stable("0.0.1-alpha.19"),

View File

@@ -11,20 +11,40 @@ use crate::{
Db,
lint::LintId,
place::Place,
semantic_index::{place_table, scope::ScopeId, symbol::ScopedSymbolId, use_def_map},
semantic_index::{
definition::DefinitionKind, place_table, scope::ScopeId, symbol::ScopedSymbolId,
use_def_map,
},
types::{
ClassBase, ClassLiteral, ClassType, KnownClass, Type,
class::CodeGeneratorKind,
context::InferContext,
diagnostic::{
INVALID_EXPLICIT_OVERRIDE, INVALID_METHOD_OVERRIDE, OVERRIDE_OF_FINAL_METHOD,
report_invalid_method_override, report_overridden_final_method,
INVALID_EXPLICIT_OVERRIDE, INVALID_METHOD_OVERRIDE, INVALID_NAMED_TUPLE,
OVERRIDE_OF_FINAL_METHOD, report_invalid_method_override,
report_overridden_final_method,
},
function::{FunctionDecorators, FunctionType, KnownFunction},
ide_support::{MemberWithDefinition, all_declarations_and_bindings},
},
};
/// Prohibited `NamedTuple` attributes that cannot be overwritten.
/// See <https://github.com/python/cpython/blob/main/Lib/typing.py> for the list.
const PROHIBITED_NAMEDTUPLE_ATTRS: &[&str] = &[
"__new__",
"__init__",
"__slots__",
"__getnewargs__",
"_fields",
"_field_defaults",
"_field_types",
"_make",
"_replace",
"_asdict",
"_source",
];
pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLiteral<'db>) {
let db = context.db();
let configuration = OverrideRulesConfig::from(context);
@@ -126,6 +146,27 @@ fn check_class_declaration<'db>(
let (literal, specialization) = class.class_literal(db);
let class_kind = CodeGeneratorKind::from_class(db, literal, specialization);
// Check for prohibited `NamedTuple` attribute overrides.
//
// `NamedTuple` classes have certain synthesized attributes (like `_asdict`, `_make`, etc.)
// that cannot be overwritten. Attempting to assign to these attributes (without type
// annotations) or define methods with these names will raise an `AttributeError` at runtime.
if class_kind == Some(CodeGeneratorKind::NamedTuple)
&& configuration.check_prohibited_named_tuple_attrs()
&& PROHIBITED_NAMEDTUPLE_ATTRS.contains(&member.name.as_str())
&& !matches!(definition.kind(db), DefinitionKind::AnnotatedAssignment(_))
&& let Some(builder) = context.report_lint(
&INVALID_NAMED_TUPLE,
definition.focus_range(db, context.module()),
)
{
let mut diagnostic = builder.into_diagnostic(format_args!(
"Cannot overwrite NamedTuple attribute `{}`",
&member.name
));
diagnostic.info("This will cause the class creation to fail at runtime");
}
let mut subclass_overrides_superclass_declaration = false;
let mut has_dynamic_superclass = false;
let mut has_typeddict_in_mro = false;
@@ -349,6 +390,7 @@ bitflags! {
const LISKOV_METHODS = 1 << 0;
const EXPLICIT_OVERRIDE = 1 << 1;
const FINAL_METHOD_OVERRIDDEN = 1 << 2;
const PROHIBITED_NAMED_TUPLE_ATTR = 1 << 3;
}
}
@@ -368,6 +410,9 @@ impl From<&InferContext<'_, '_>> for OverrideRulesConfig {
if rule_selection.is_enabled(LintId::of(&OVERRIDE_OF_FINAL_METHOD)) {
config |= OverrideRulesConfig::FINAL_METHOD_OVERRIDDEN;
}
if rule_selection.is_enabled(LintId::of(&INVALID_NAMED_TUPLE)) {
config |= OverrideRulesConfig::PROHIBITED_NAMED_TUPLE_ATTR;
}
config
}
@@ -385,4 +430,8 @@ impl OverrideRulesConfig {
const fn check_final_method_overridden(self) -> bool {
self.contains(OverrideRulesConfig::FINAL_METHOD_OVERRIDDEN)
}
const fn check_prohibited_named_tuple_attrs(self) -> bool {
self.contains(OverrideRulesConfig::PROHIBITED_NAMED_TUPLE_ATTR)
}
}