use super::names::written_name;
use crate::code_lint::ast::tree::ParsedFile;
use ruff_python_ast::{Decorator, Expr};
#[derive(Clone)]
pub(super) struct DecoratorInfo {
path: String,
terminal_name: String,
resolved_path: Option<String>,
keyword_args: Vec<String>,
}
impl DecoratorInfo {
#[must_use]
pub(super) fn has_arg(&self, key: &str) -> bool {
self.keyword_args.iter().any(|arg| arg == key)
}
#[must_use]
pub(super) fn matches_any(&self, candidates: &[&str]) -> bool {
self.resolved_path
.as_deref()
.is_some_and(|path| candidates.contains(&path))
}
#[must_use]
pub(super) fn is_attribute_named(&self, attrs: &[&str]) -> bool {
self.path.contains('.') && attrs.contains(&self.terminal_name.as_str())
}
}
pub(super) fn extract_decorators_from_slice(
decorators: &[Decorator],
file: &ParsedFile,
) -> Vec<DecoratorInfo> {
let mut out = Vec::with_capacity(decorators.len());
for decorator in decorators {
let (target_expr, keyword_args) = if let Expr::Call(call) = &decorator.expression {
let kwargs = call
.arguments
.keywords
.iter()
.filter_map(|kw| Some(kw.arg.as_ref()?.id.to_string()))
.collect();
(call.func.as_ref(), kwargs)
} else {
(&decorator.expression, Vec::new())
};
let written = written_name(target_expr, &file.source);
let resolved_path = written.clone().resolve(file).map(|name| name.path);
out.push(DecoratorInfo {
path: written.path,
terminal_name: written.terminal,
resolved_path,
keyword_args,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::Language;
use ruff_python_ast::{Stmt, StmtFunctionDef};
fn first_function_def(file: &ParsedFile) -> &StmtFunctionDef {
let parsed = file.py_module().expect("source should parse");
let Some(Stmt::FunctionDef(func_def)) = parsed.syntax().body.first() else {
unreachable!("source should start with a function definition");
};
func_def
}
fn first_function_decorators(file: &ParsedFile) -> Vec<DecoratorInfo> {
extract_decorators_from_slice(&first_function_def(file).decorator_list, file)
}
#[test]
fn test_extract_decorators_count() {
let source = indoc::indoc! {r#"
@dataclass(frozen=True, slots=False)
@pytest.mark.parametrize("x", [1, 2])
@custom
def foo():
pass
"#};
let file = ParsedFile::new(source, Language::Python);
let decorators = first_function_decorators(&file);
assert_eq!(decorators.len(), 3);
}
#[test]
fn test_extract_decorator_keyword_args() {
let source = indoc::indoc! {r"
@dataclass(frozen=True, slots=False)
def foo():
pass
"};
let file = ParsedFile::new(source, Language::Python);
let decorators = first_function_decorators(&file);
let dec0 = &decorators[0];
assert_eq!(dec0.terminal_name, "dataclass");
assert!(dec0.has_arg("frozen"));
assert!(dec0.has_arg("slots"));
assert!(!dec0.has_arg("unknown"));
}
#[test]
fn test_extract_decorators_metadata_and_path() {
let source = indoc::indoc! {r#"
@pytest.mark.parametrize("x", [1, 2])
@custom
def foo():
pass
"#};
let file = ParsedFile::new(source, Language::Python);
let decorators = first_function_decorators(&file);
let dec0 = &decorators[0];
let dec1 = &decorators[1];
assert_eq!(dec0.terminal_name, "parametrize");
assert_eq!(dec0.path, "pytest.mark.parametrize");
assert_eq!(dec1.terminal_name, "custom");
assert_eq!(dec1.path, "custom");
}
}