use ruff_python_ast::helpers::{Truthiness, map_callable, map_subscript};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_python_semantic::{BindingKind, Modules, SemanticModel, analyze};
pub(super) fn is_special_attribute(value: &Expr) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = value {
matches!(
id.as_str(),
"__slots__" | "__dict__" | "__weakref__" | "__annotations__"
)
} else {
false
}
}
fn is_stdlib_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["dataclasses", "field"]))
}
fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["attrs", "field" | "Factory"]
| ["attr", "ib" | "attr" | "attrib" | "field" | "Factory"]
)
})
}
pub(super) fn is_dataclass_field(
func: &Expr,
semantic: &SemanticModel,
dataclass_kind: DataclassKind,
) -> bool {
match dataclass_kind {
DataclassKind::Attrs(..) => is_attrs_field(func, semantic),
DataclassKind::Stdlib => is_stdlib_dataclass_field(func, semantic),
}
}
pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_typing() {
return false;
}
semantic.match_typing_expr(map_subscript(annotation), "ClassVar")
}
pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
if !semantic.seen_typing() {
return false;
}
semantic.match_typing_expr(map_subscript(annotation), "Final")
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum AttrsAutoAttribs {
True,
False,
None,
Unknown,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum DataclassKind {
Stdlib,
Attrs(AttrsAutoAttribs),
}
pub(super) fn dataclass_kind<'a>(
class_def: &'a ast::StmtClassDef,
semantic: &SemanticModel,
) -> Option<(DataclassKind, &'a ast::Decorator)> {
if !(semantic.seen_module(Modules::DATACLASSES) || semantic.seen_module(Modules::ATTRS)) {
return None;
}
for decorator in &class_def.decorator_list {
let Some(qualified_name) =
semantic.resolve_qualified_name(map_callable(&decorator.expression))
else {
continue;
};
match qualified_name.segments() {
["attrs" | "attr", func @ ("define" | "frozen" | "mutable")]
| ["attr", func @ ("s" | "attributes" | "attrs")] => {
let Expr::Call(ExprCall { arguments, .. }) = &decorator.expression else {
let auto_attribs = if *func == "s" {
AttrsAutoAttribs::False
} else {
AttrsAutoAttribs::None
};
return Some((DataclassKind::Attrs(auto_attribs), decorator));
};
let Some(auto_attribs) = arguments.find_keyword("auto_attribs") else {
return Some((DataclassKind::Attrs(AttrsAutoAttribs::None), decorator));
};
let auto_attribs = match Truthiness::from_expr(&auto_attribs.value, |id| {
semantic.has_builtin_binding(id)
}) {
Truthiness::True => AttrsAutoAttribs::True,
Truthiness::None => AttrsAutoAttribs::None,
Truthiness::Truthy | Truthiness::False | Truthiness::Falsey => {
AttrsAutoAttribs::False
}
Truthiness::Unknown => AttrsAutoAttribs::Unknown,
};
return Some((DataclassKind::Attrs(auto_attribs), decorator));
}
["dataclasses", "dataclass"] => return Some((DataclassKind::Stdlib, decorator)),
_ => continue,
}
}
None
}
pub(super) fn is_frozen_dataclass(
dataclass_decorator: &ast::Decorator,
semantic: &SemanticModel,
) -> bool {
let Some(qualified_name) =
semantic.resolve_qualified_name(map_callable(&dataclass_decorator.expression))
else {
return false;
};
match qualified_name.segments() {
["dataclasses", "dataclass"] => {
let Expr::Call(ExprCall { arguments, .. }) = &dataclass_decorator.expression else {
return false;
};
let Some(keyword) = arguments.find_keyword("frozen") else {
return false;
};
Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id))
.into_bool()
.unwrap_or_default()
}
["attrs" | "attr", "frozen"] => true,
_ => false,
}
}
pub(super) fn has_default_copy_semantics(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
) -> bool {
analyze::class::any_qualified_base_class(class_def, semantic, |qualified_name| {
matches!(
qualified_name.segments(),
[
"pydantic",
"BaseModel" | "RootModel" | "BaseSettings" | "BaseConfig"
] | ["pydantic", "generics", "GenericModel"]
| [
"pydantic",
"v1",
"BaseModel" | "BaseSettings" | "BaseConfig"
]
| ["pydantic", "v1", "generics", "GenericModel"]
| ["pydantic_settings", "BaseSettings"]
| ["msgspec", "Struct"]
| ["sqlmodel", "SQLModel"]
)
})
}
pub(super) fn is_descriptor_class(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.lookup_attribute(func).is_some_and(|id| {
let BindingKind::ClassDefinition(scope_id) = semantic.binding(id).kind else {
return false;
};
["__get__", "__set__", "__delete__"].iter().any(|method| {
semantic.scopes[scope_id]
.get(method)
.is_some_and(|id| semantic.binding(id).kind.is_function_definition())
})
})
}
pub(super) fn is_ctypes_structure_fields(
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
targets: &[Expr],
) -> bool {
let is_ctypes_structure =
analyze::class::any_qualified_base_class(class_def, semantic, |qualified_name| {
matches!(
qualified_name.segments(),
[
"ctypes",
"Structure"
| "BigEndianStructure"
| "LittleEndianStructure"
| "Union"
| "BigEndianUnion"
| "LittleEndianUnion"
]
)
});
let is_fields = matches!(
targets.first(),
Some(Expr::Name(ast::ExprName { id, .. })) if id == "_fields_"
);
is_ctypes_structure && is_fields
}