use crate::code_lint::ast::ParsedFile;
use crate::code_lint::contract::{CodeRule, RuleTarget};
use crate::diagnostic::{Diagnostic, Language, RuleName, ViolationTemplate, violation_template};
use crate::rule_declaration::{
Classification, Consensus, Declaration, Example, FilterListDefaults, ImpactedQuality, ListKind,
ListOption, Precision, Reference, RuleDoc, RuleOptions, Topic,
};
use std::collections::HashSet;
use std::path::Path;
const BANNED: ListOption = ListOption {
kind: ListKind::Deny,
doc: "Reflection functions flagged when called.",
default: FilterListDefaults {
base: &[
"getattr",
"hasattr",
"setattr",
"delattr",
"vars",
"builtins.getattr",
"builtins.hasattr",
"builtins.setattr",
"builtins.delattr",
"builtins.vars",
"object.__setattr__",
"object.__getattribute__",
"object.__delattr__",
"type.__setattr__",
],
extend: &[],
remove: &[],
},
};
const TEMPLATE: ViolationTemplate = violation_template! {
summary: "An attribute is accessed dynamically with `{callee}()`.",
rationale: "Runtime reflection erases the attribute's static type to `Any`, hides the reference from refactoring tools, and `hasattr` can swallow exceptions raised by properties.",
suggestion: "Access the attribute directly on a typed object, use a `Mapping` lookup (`dict.get`), or define a structural `Protocol`.",
};
pub const RULE: CodeRule<ListOption> = CodeRule {
declaration: Declaration {
name: RuleName("dynamic-attribute-access"),
template: &TEMPLATE,
languages: &[Language::Python],
options: RuleOptions::code_rule(BANNED),
classification: Classification {
topics: &[Topic::TYPE_CHECKER_BYPASS],
precision: Precision::Exact,
consensus: Consensus::Opinionated,
impacted_quality: ImpactedQuality::Reliability,
},
doc: RuleDoc {
summary: "Flags `getattr`, `hasattr`, `setattr` and `delattr` calls in Python.",
what_it_does: indoc::indoc! {r#"
Flags calls to the built-in reflection functions and dunder equivalents, such as
`getattr(service, "timeout")`, `hasattr(record, field_name)`, `vars(service)`, or
`object.__setattr__(self, "replies", [])`."#},
why_is_this_bad: indoc::indoc! {r#"
These functions take the attribute name as a runtime string. The type checker cannot
verify that the attribute exists and usually types the result as `Any`; renaming
tools and "find references" miss the access; a typo fails only at runtime. `hasattr`
also returns `False` when a property raises `AttributeError` internally, which hides
the real bug.
Access attributes directly on a typed object. For data keyed by runtime strings, use
a `dict` or `Mapping`; to accept several types that share attributes, declare a
`Protocol`."#},
known_problems: None,
references: &[
Reference {
title: "Python docs: built-in getattr",
url: "https://docs.python.org/3/library/functions.html#getattr",
},
Reference::NAME_RESOLUTION,
],
examples: &[Example {
language: Language::Python,
flagged: indoc::indoc! {r"
def is_feature_enabled(flags: FeatureFlags, feature: str) -> bool:
return getattr(flags, feature, False)
"},
flagged_span: "getattr(flags, feature, False)",
fixed: indoc::indoc! {r"
def is_feature_enabled(flags: Mapping[str, bool], feature: str) -> bool:
return flags.get(feature, False)
"},
}],
},
},
target: RuleTarget::All,
check: check_file,
};
fn check_file(
rule: &CodeRule<ListOption>,
path: &Path,
file: &ParsedFile,
banned_functions: &HashSet<String>,
) -> Vec<Diagnostic> {
rule.check_banned_calls(path, file, banned_functions)
}
#[cfg(test)]
crate::test_utils::rule_test!(
RULE,
{
Python => {
pass: [
direct_attribute_access => r#"
value = service.timeout
"#,
dict_lookup => r#"
email = data.get("email")
"#,
custom_receiver_getattr => r#"
registry.getattr("key")
"#,
custom_receiver_setattr => r#"
registry.setattr("key", 1)
"#,
custom_receiver_hasattr => r#"
registry.hasattr("key")
"#,
custom_receiver_delattr => r#"
registry.delattr("key")
"#,
],
fail: [
unqualified_getattr => r#"
val = getattr(service, "timeout", 10)
"# => r#"getattr(service, "timeout", 10)"#,
unqualified_hasattr => r#"
if hasattr(record, field_name):
pass
"# => r#"hasattr(record, field_name)"#,
unqualified_setattr => r#"
setattr(record, field_name, 42)
"# => r#"setattr(record, field_name, 42)"#,
unqualified_delattr => r#"
delattr(record, "deprecated_key")
"# => r#"delattr(record, "deprecated_key")"#,
builtins_qualified_getattr => r#"
builtins.getattr(service, "name")
"# => r#"builtins.getattr(service, "name")"#,
builtins_qualified_hasattr => r#"
builtins.hasattr(service, "name")
"# => r#"builtins.hasattr(service, "name")"#,
builtins_qualified_setattr => r#"
builtins.setattr(service, "name", "new_name")
"# => r#"builtins.setattr(service, "name", "new_name")"#,
builtins_qualified_delattr => r#"
builtins.delattr(service, "name")
"# => r#"builtins.delattr(service, "name")"#,
object_dunder_setattr => r#"
object.__setattr__(self, "replies", [])
"# => r#"object.__setattr__(self, "replies", [])"#,
vars_call => r#"
fields = vars(service)
"# => r#"vars(service)"#,
],
},
}
);