use rustpython_parser::ast::Expr;
pub fn is_fixture_decorator(expr: &Expr) -> bool {
match expr {
Expr::Name(name) => name.id.as_str() == "fixture",
Expr::Attribute(attr) => {
if let Expr::Name(value) = &*attr.value {
(value.id.as_str() == "pytest" || value.id.as_str() == "pytest_asyncio")
&& attr.attr.as_str() == "fixture"
} else {
false
}
}
Expr::Call(call) => is_fixture_decorator(&call.func),
_ => false,
}
}
pub fn extract_fixture_name_from_decorator(expr: &Expr) -> Option<String> {
let Expr::Call(call) = expr else { return None };
if !is_fixture_decorator(&call.func) {
return None;
}
call.keywords
.iter()
.filter(|kw| kw.arg.as_ref().is_some_and(|a| a.as_str() == "name"))
.find_map(|kw| match &kw.value {
Expr::Constant(c) => match &c.value {
rustpython_parser::ast::Constant::Str(s) => Some(s.to_string()),
_ => None,
},
_ => None,
})
}
fn is_pytest_mark_decorator(expr: &Expr, marker_name: &str) -> bool {
match expr {
Expr::Call(call) => is_pytest_mark_decorator(&call.func, marker_name),
Expr::Attribute(attr) => {
if attr.attr.as_str() != marker_name {
return false;
}
match &*attr.value {
Expr::Attribute(inner_attr) => {
if inner_attr.attr.as_str() != "mark" {
return false;
}
matches!(&*inner_attr.value, Expr::Name(name) if name.id.as_str() == "pytest")
}
Expr::Name(name) => name.id.as_str() == "mark",
_ => false,
}
}
_ => false,
}
}
pub fn is_usefixtures_decorator(expr: &Expr) -> bool {
is_pytest_mark_decorator(expr, "usefixtures")
}
pub fn extract_usefixtures_names(
expr: &Expr,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
let Expr::Call(call) = expr else {
return vec![];
};
if !is_usefixtures_decorator(&call.func) {
return vec![];
}
call.args
.iter()
.filter_map(|arg| {
if let Expr::Constant(c) = arg {
if let rustpython_parser::ast::Constant::Str(s) = &c.value {
return Some((s.to_string(), c.range));
}
}
None
})
.collect()
}
pub fn extract_usefixtures_from_expr(
expr: &Expr,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
match expr {
Expr::Call(_) => extract_usefixtures_names(expr),
Expr::List(list) => list
.elts
.iter()
.flat_map(extract_usefixtures_from_expr)
.collect(),
Expr::Tuple(tuple) => tuple
.elts
.iter()
.flat_map(extract_usefixtures_from_expr)
.collect(),
_ => vec![],
}
}
pub fn is_parametrize_decorator(expr: &Expr) -> bool {
is_pytest_mark_decorator(expr, "parametrize")
}
fn is_plain_identifier(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c == '_' || c.is_ascii_alphabetic())
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
fn split_argnames_from_source(
literal: &str,
range_start: rustpython_parser::text_size::TextSize,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
use rustpython_parser::text_size::{TextRange, TextSize};
let bytes = literal.as_bytes();
let mut prefix = 0;
while prefix < bytes.len()
&& matches!(
bytes[prefix],
b'r' | b'R' | b'b' | b'B' | b'u' | b'U' | b'f' | b'F'
)
{
prefix += 1;
}
let Some("e) = bytes.get(prefix) else {
return vec![];
};
if quote != b'"' && quote != b'\'' {
return vec![];
}
let quote_len =
if bytes.len() >= prefix + 3 && bytes[prefix + 1] == quote && bytes[prefix + 2] == quote {
3
} else {
1
};
let content_offset = prefix + quote_len;
let content_end = literal.len().saturating_sub(quote_len);
if content_offset > content_end {
return vec![];
}
let inner = &literal[content_offset..content_end];
let mut result = Vec::new();
let mut offset = content_offset;
for segment in inner.split(',') {
let leading_ws = segment.len() - segment.trim_start().len();
let trimmed = segment.trim();
if is_plain_identifier(trimmed) {
let start = range_start + TextSize::from((offset + leading_ws) as u32);
let end = start + TextSize::from(trimmed.len() as u32);
result.push((trimmed.to_string(), TextRange::new(start, end)));
}
offset += segment.len() + 1; }
result
}
pub fn extract_parametrize_argnames(
expr: &Expr,
content: &str,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
let Expr::Call(call) = expr else {
return vec![];
};
if !is_parametrize_decorator(&call.func) {
return vec![];
}
let argnames = call.args.first().or_else(|| {
call.keywords
.iter()
.find(|kw| kw.arg.as_ref().is_some_and(|a| a.as_str() == "argnames"))
.map(|kw| &kw.value)
});
let Some(argnames) = argnames else {
return vec![];
};
match argnames {
Expr::Constant(_) => parametrize_name_element_ranges(argnames, content),
Expr::List(list) => list
.elts
.iter()
.flat_map(|elt| parametrize_name_element_ranges(elt, content))
.collect(),
Expr::Tuple(tuple) => tuple
.elts
.iter()
.flat_map(|elt| parametrize_name_element_ranges(elt, content))
.collect(),
_ => vec![],
}
}
fn parametrize_name_element_ranges(
elt: &Expr,
content: &str,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
let Expr::Constant(c) = elt else {
return vec![];
};
if !matches!(c.value, rustpython_parser::ast::Constant::Str(_)) {
return vec![];
}
let start = c.range.start().to_usize();
let end = c.range.end().to_usize();
let Some(literal) = content.get(start..end) else {
return vec![];
};
split_argnames_from_source(literal, c.range.start())
}
pub fn extract_parametrize_indirect_names(
expr: &Expr,
argnames: &[String],
) -> std::collections::HashSet<String> {
use std::collections::HashSet;
let Expr::Call(call) = expr else {
return HashSet::new();
};
if !is_parametrize_decorator(&call.func) {
return HashSet::new();
}
let indirect = call
.keywords
.iter()
.find(|kw| kw.arg.as_ref().is_some_and(|a| a.as_str() == "indirect"))
.map(|kw| &kw.value)
.or_else(|| call.args.get(2));
let Some(indirect) = indirect else {
return HashSet::new();
};
match indirect {
Expr::Constant(c) if matches!(c.value, rustpython_parser::ast::Constant::Bool(true)) => {
argnames.iter().cloned().collect()
}
Expr::List(list) => collect_string_constants(&list.elts),
Expr::Tuple(tuple) => collect_string_constants(&tuple.elts),
_ => HashSet::new(),
}
}
fn collect_string_constants(elts: &[Expr]) -> std::collections::HashSet<String> {
elts.iter()
.filter_map(|elt| match elt {
Expr::Constant(c) => match &c.value {
rustpython_parser::ast::Constant::Str(s) => Some(s.to_string()),
_ => None,
},
_ => None,
})
.collect()
}
pub fn extract_parametrize_indirect_fixtures(
expr: &Expr,
) -> Vec<(String, rustpython_parser::text_size::TextRange)> {
let Expr::Call(call) = expr else {
return vec![];
};
if !is_parametrize_decorator(&call.func) {
return vec![];
}
let indirect_value = call.keywords.iter().find_map(|kw| {
if kw.arg.as_ref().is_some_and(|a| a.as_str() == "indirect") {
Some(&kw.value)
} else {
None
}
});
let Some(indirect) = indirect_value else {
return vec![];
};
let Some(first_arg) = call.args.first() else {
return vec![];
};
let Expr::Constant(param_const) = first_arg else {
return vec![];
};
let rustpython_parser::ast::Constant::Str(param_str) = ¶m_const.value else {
return vec![];
};
let param_names: Vec<&str> = param_str.split(',').map(|s| s.trim()).collect();
match indirect {
Expr::Constant(c) => {
if matches!(c.value, rustpython_parser::ast::Constant::Bool(true)) {
return param_names
.into_iter()
.map(|name| (name.to_string(), param_const.range))
.collect();
}
}
Expr::List(list) => {
return list
.elts
.iter()
.filter_map(|elt| {
if let Expr::Constant(c) = elt {
if let rustpython_parser::ast::Constant::Str(s) = &c.value {
if param_names.contains(&s.as_str()) {
return Some((s.to_string(), c.range));
}
}
}
None
})
.collect();
}
_ => {}
}
vec![]
}
pub fn extract_fixture_autouse(expr: &Expr) -> bool {
let Expr::Call(call) = expr else { return false };
if !is_fixture_decorator(&call.func) {
return false;
}
call.keywords
.iter()
.filter(|kw| kw.arg.as_ref().is_some_and(|a| a.as_str() == "autouse"))
.any(|kw| matches!(&kw.value, Expr::Constant(c) if matches!(c.value, rustpython_parser::ast::Constant::Bool(true))))
}
pub fn extract_fixture_scope(expr: &Expr) -> Option<super::types::FixtureScope> {
let Expr::Call(call) = expr else { return None };
if !is_fixture_decorator(&call.func) {
return None;
}
call.keywords
.iter()
.filter(|kw| kw.arg.as_ref().is_some_and(|a| a.as_str() == "scope"))
.find_map(|kw| match &kw.value {
Expr::Constant(c) => match &c.value {
rustpython_parser::ast::Constant::Str(s) => super::types::FixtureScope::parse(s),
_ => None,
},
_ => None,
})
}