[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:
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user