use libcst_native::{
Annotation, Arg, ClassDef, CompoundStatement, Decorator, Expression, FunctionDef, Module,
Parameters, SmallStatement, Statement, Suite,
};
use crate::source::{SpanIndex, anchor_of_subslice};
pub enum FnBody<'a> {
Suite(&'a Suite<'a>),
Expr(&'a Expression<'a>),
Module(&'a [Statement<'a>]),
}
pub struct FnUnit<'a> {
pub symbol: String,
pub line: usize,
pub col: usize,
pub is_async: bool,
pub is_test_unit: bool,
pub is_module_level: bool,
pub body: FnBody<'a>,
pub params: &'a Parameters<'a>,
pub decorators: &'a [Decorator<'a>],
pub returns: Option<&'a Annotation<'a>>,
}
pub fn collect<'a>(
module: &'a Module<'a>,
src: &str,
span: &SpanIndex,
anchors: &[(usize, usize)],
) -> (Vec<FnUnit<'a>>, usize) {
let mut ctx = Ctx {
src,
span,
anchors,
lambda_idx: 0,
class_stack: Vec::new(),
def_depth: 0,
out: Vec::new(),
};
for stmt in &module.body {
collect_in_statement(stmt, &mut ctx);
}
debug_assert_eq!(
ctx.lambda_idx,
anchors.len(),
"lambda collection ({}) drifted from tokenizer lambda count ({}); \
a Lambda-bearing expression position is not visited by collect_in_expr",
ctx.lambda_idx,
anchors.len(),
);
let lambda_node_count = ctx.lambda_idx;
(ctx.out, lambda_node_count)
}
pub fn module_init_unit<'a>(module: &'a Module<'a>) -> Option<FnUnit<'a>> {
let has_executable = module.body.iter().any(|stmt| {
match stmt {
Statement::Simple(line) => line.body.iter().any(|small| {
!matches!(
small,
SmallStatement::Import(_)
| SmallStatement::ImportFrom(_)
| SmallStatement::Pass(_)
| SmallStatement::Global(_)
| SmallStatement::Nonlocal(_)
| SmallStatement::Break(_)
| SmallStatement::Continue(_)
)
}),
Statement::Compound(c) => {
!matches!(c, CompoundStatement::FunctionDef(_))
}
}
});
if !has_executable {
return None;
}
Some(FnUnit {
symbol: "<module>".to_owned(),
line: 1,
col: 1,
is_async: false,
is_test_unit: false,
is_module_level: false,
body: FnBody::Module(&module.body),
params: &EMPTY_PARAMS,
decorators: &[],
returns: None,
})
}
static EMPTY_PARAMS: std::sync::LazyLock<libcst_native::Parameters<'static>> =
std::sync::LazyLock::new(|| libcst_native::Parameters {
params: vec![],
posonly_params: vec![],
star_arg: None,
kwonly_params: vec![],
star_kwarg: None,
posonly_ind: None,
});
#[derive(Clone)]
struct ClassCtx {
is_test_class: bool,
}
struct Ctx<'a, 'b> {
src: &'b str,
span: &'b SpanIndex<'b>,
anchors: &'b [(usize, usize)],
lambda_idx: usize,
class_stack: Vec<ClassCtx>,
def_depth: usize,
out: Vec<FnUnit<'a>>,
}
fn collect_in_statement<'a>(stmt: &'a Statement<'a>, ctx: &mut Ctx<'a, '_>) {
match stmt {
Statement::Simple(line) => {
for small in &line.body {
collect_in_small(small, ctx);
}
}
Statement::Compound(compound) => {
collect_in_compound(compound, ctx);
}
}
}
fn collect_in_compound<'a>(compound: &'a CompoundStatement<'a>, ctx: &mut Ctx<'a, '_>) {
match compound {
CompoundStatement::FunctionDef(f) => {
collect_funcdef(f, ctx);
}
CompoundStatement::ClassDef(c) => {
collect_classdef(c, ctx);
}
CompoundStatement::If(i) => {
collect_in_expr(&i.test, ctx);
collect_in_suite(&i.body, ctx);
if let Some(orelse) = &i.orelse {
collect_in_or_else(orelse, ctx);
}
}
CompoundStatement::For(f) => {
collect_in_expr(&f.iter, ctx);
collect_in_suite(&f.body, ctx);
if let Some(orelse) = &f.orelse {
collect_in_suite(&orelse.body, ctx);
}
}
CompoundStatement::While(w) => {
collect_in_expr(&w.test, ctx);
collect_in_suite(&w.body, ctx);
if let Some(orelse) = &w.orelse {
collect_in_suite(&orelse.body, ctx);
}
}
CompoundStatement::Try(t) => {
collect_in_suite(&t.body, ctx);
for handler in &t.handlers {
collect_in_suite(&handler.body, ctx);
}
if let Some(orelse) = &t.orelse {
collect_in_suite(&orelse.body, ctx);
}
if let Some(finalbody) = &t.finalbody {
collect_in_suite(&finalbody.body, ctx);
}
}
CompoundStatement::TryStar(t) => {
collect_in_suite(&t.body, ctx);
for handler in &t.handlers {
collect_in_suite(&handler.body, ctx);
}
if let Some(orelse) = &t.orelse {
collect_in_suite(&orelse.body, ctx);
}
if let Some(finalbody) = &t.finalbody {
collect_in_suite(&finalbody.body, ctx);
}
}
CompoundStatement::With(w) => {
for item in &w.items {
collect_in_expr(&item.item, ctx);
}
collect_in_suite(&w.body, ctx);
}
CompoundStatement::Match(m) => {
collect_in_expr(&m.subject, ctx);
for case in &m.cases {
collect_in_suite(&case.body, ctx);
}
}
}
}
fn collect_in_or_else<'a>(orelse: &'a libcst_native::OrElse<'a>, ctx: &mut Ctx<'a, '_>) {
match orelse {
libcst_native::OrElse::Elif(elif) => {
collect_in_expr(&elif.test, ctx);
collect_in_suite(&elif.body, ctx);
if let Some(inner) = &elif.orelse {
collect_in_or_else(inner, ctx);
}
}
libcst_native::OrElse::Else(e) => {
collect_in_suite(&e.body, ctx);
}
}
}
fn collect_funcdef<'a>(f: &'a FunctionDef<'a>, ctx: &mut Ctx<'a, '_>) {
let off = anchor_of_subslice(ctx.src, f.name.value);
let (line, col) = ctx.span.line_col(off);
let in_test_class = ctx.class_stack.last().is_some_and(|c| c.is_test_class);
let is_test_unit = f.name.value.starts_with("test_") || in_test_class;
let is_module_level = ctx.def_depth == 0 && ctx.class_stack.is_empty();
ctx.out.push(FnUnit {
symbol: f.name.value.to_owned(),
line,
col,
is_async: f.asynchronous.is_some(),
is_test_unit,
is_module_level,
body: FnBody::Suite(&f.body),
params: &f.params,
decorators: &f.decorators,
returns: f.returns.as_ref(),
});
for dec in &f.decorators {
collect_in_expr(&dec.decorator, ctx);
}
collect_in_params(&f.params, ctx);
ctx.def_depth += 1;
collect_in_suite(&f.body, ctx);
ctx.def_depth -= 1;
}
fn collect_in_params<'a>(params: &'a Parameters<'a>, ctx: &mut Ctx<'a, '_>) {
let positional = params
.posonly_params
.iter()
.chain(¶ms.params)
.chain(¶ms.kwonly_params);
for p in positional {
if let Some(default) = &p.default {
collect_in_expr(default, ctx);
}
}
if let Some(libcst_native::StarArg::Param(p)) = ¶ms.star_arg
&& let Some(default) = &p.default
{
collect_in_expr(default, ctx);
}
if let Some(p) = ¶ms.star_kwarg
&& let Some(default) = &p.default
{
collect_in_expr(default, ctx);
}
}
fn collect_classdef<'a>(c: &'a ClassDef<'a>, ctx: &mut Ctx<'a, '_>) {
let is_test_class = class_name_is_test(c.name.value) || class_bases_are_test_case(&c.bases);
for dec in &c.decorators {
collect_in_expr(&dec.decorator, ctx);
}
for base in &c.bases {
collect_in_expr(&base.value, ctx);
}
for kw in &c.keywords {
collect_in_expr(&kw.value, ctx);
}
ctx.class_stack.push(ClassCtx { is_test_class });
collect_in_suite(&c.body, ctx);
ctx.class_stack.pop();
}
fn class_name_is_test(name: &str) -> bool {
name.starts_with("Test")
}
fn class_bases_are_test_case(bases: &[Arg<'_>]) -> bool {
bases.iter().any(|arg| match &arg.value {
Expression::Name(n) => n.value == "TestCase",
Expression::Attribute(a) => {
a.attr.value == "TestCase"
&& matches!(&*a.value, Expression::Name(n) if n.value == "unittest")
}
_ => false,
})
}
fn collect_in_suite<'a>(suite: &'a Suite<'a>, ctx: &mut Ctx<'a, '_>) {
let stmts: &[Statement<'a>] = match suite {
Suite::IndentedBlock(b) => &b.body,
Suite::SimpleStatementSuite(s) => {
for small in &s.body {
collect_in_small(small, ctx);
}
return;
}
};
for stmt in stmts {
collect_in_statement(stmt, ctx);
}
}
fn collect_in_small<'a>(small: &'a SmallStatement<'a>, ctx: &mut Ctx<'a, '_>) {
match small {
SmallStatement::Assign(a) => {
collect_in_expr(&a.value, ctx);
}
SmallStatement::AnnAssign(a) => {
if let Some(v) = &a.value {
collect_in_expr(v, ctx);
}
}
SmallStatement::AugAssign(a) => {
collect_in_expr(&a.value, ctx);
}
SmallStatement::Return(r) => {
if let Some(v) = &r.value {
collect_in_expr(v, ctx);
}
}
SmallStatement::Expr(e) => {
collect_in_expr(&e.value, ctx);
}
SmallStatement::Raise(r) => {
if let Some(exc) = &r.exc {
collect_in_expr(exc, ctx);
}
if let Some(from) = &r.cause {
collect_in_expr(&from.item, ctx);
}
}
SmallStatement::Assert(a) => {
collect_in_expr(&a.test, ctx);
if let Some(msg) = &a.msg {
collect_in_expr(msg, ctx);
}
}
SmallStatement::Del(d) => {
collect_in_del_target(&d.target, ctx);
}
_ => {}
}
}
fn collect_in_expr<'a>(expr: &'a Expression<'a>, ctx: &mut Ctx<'a, '_>) {
match expr {
Expression::Lambda(l) => {
if let Some(&(line, col)) = ctx.anchors.get(ctx.lambda_idx) {
ctx.out.push(FnUnit {
symbol: format!("<lambda@L{line}C{col}>"),
line,
col,
is_async: false,
is_test_unit: false,
is_module_level: false,
body: FnBody::Expr(&l.body),
params: &l.params,
decorators: &[],
returns: None,
});
}
ctx.lambda_idx += 1;
collect_in_params(&l.params, ctx);
collect_in_expr(&l.body, ctx);
}
Expression::BinaryOperation(b) => {
collect_in_expr(&b.left, ctx);
collect_in_expr(&b.right, ctx);
}
Expression::BooleanOperation(b) => {
collect_in_expr(&b.left, ctx);
collect_in_expr(&b.right, ctx);
}
Expression::UnaryOperation(u) => {
collect_in_expr(&u.expression, ctx);
}
Expression::Comparison(c) => {
collect_in_expr(&c.left, ctx);
for comp in &c.comparisons {
collect_in_expr(&comp.comparator, ctx);
}
}
Expression::IfExp(i) => {
collect_in_expr(&i.test, ctx);
collect_in_expr(&i.body, ctx);
collect_in_expr(&i.orelse, ctx);
}
Expression::Call(c) => {
collect_in_expr(&c.func, ctx);
for arg in &c.args {
collect_in_expr(&arg.value, ctx);
}
}
Expression::Attribute(a) => {
collect_in_expr(&a.value, ctx);
}
Expression::Subscript(s) => {
collect_in_expr(&s.value, ctx);
for element in &s.slice {
collect_in_base_slice(&element.slice, ctx);
}
}
Expression::Tuple(t) => {
for el in &t.elements {
collect_in_element(el, ctx);
}
}
Expression::List(l) => {
for el in &l.elements {
collect_in_element(el, ctx);
}
}
Expression::Set(s) => {
for el in &s.elements {
collect_in_element(el, ctx);
}
}
Expression::Dict(d) => {
for el in &d.elements {
match el {
libcst_native::DictElement::Simple { key, value, .. } => {
collect_in_expr(key, ctx);
collect_in_expr(value, ctx);
}
libcst_native::DictElement::Starred(s) => {
collect_in_expr(&s.value, ctx);
}
}
}
}
Expression::ListComp(l) => {
collect_in_expr(&l.elt, ctx);
collect_in_comp_for(&l.for_in, ctx);
}
Expression::SetComp(s) => {
collect_in_expr(&s.elt, ctx);
collect_in_comp_for(&s.for_in, ctx);
}
Expression::GeneratorExp(g) => {
collect_in_expr(&g.elt, ctx);
collect_in_comp_for(&g.for_in, ctx);
}
Expression::DictComp(d) => {
collect_in_expr(&d.key, ctx);
collect_in_expr(&d.value, ctx);
collect_in_comp_for(&d.for_in, ctx);
}
Expression::FormattedString(fs) => {
collect_in_fstring_parts(&fs.parts, ctx);
}
Expression::Yield(y) => {
if let Some(v) = &y.value {
match &**v {
libcst_native::YieldValue::Expression(e) => {
collect_in_expr(e, ctx);
}
libcst_native::YieldValue::From(f) => {
collect_in_expr(&f.item, ctx);
}
}
}
}
Expression::Await(a) => {
collect_in_expr(&a.expression, ctx);
}
Expression::NamedExpr(n) => {
collect_in_expr(&n.value, ctx);
}
Expression::StarredElement(s) => {
collect_in_expr(&s.value, ctx);
}
_ => {}
}
}
fn collect_in_comp_for<'a>(comp: &'a libcst_native::CompFor<'a>, ctx: &mut Ctx<'a, '_>) {
collect_in_expr(&comp.iter, ctx);
for cond in &comp.ifs {
collect_in_expr(&cond.test, ctx);
}
if let Some(inner) = &comp.inner_for_in {
collect_in_comp_for(inner, ctx);
}
}
fn collect_in_base_slice<'a>(slice: &'a libcst_native::BaseSlice<'a>, ctx: &mut Ctx<'a, '_>) {
match slice {
libcst_native::BaseSlice::Index(i) => collect_in_expr(&i.value, ctx),
libcst_native::BaseSlice::Slice(s) => {
if let Some(lower) = &s.lower {
collect_in_expr(lower, ctx);
}
if let Some(upper) = &s.upper {
collect_in_expr(upper, ctx);
}
if let Some(step) = &s.step {
collect_in_expr(step, ctx);
}
}
}
}
fn collect_in_fstring_parts<'a>(
parts: &'a [libcst_native::FormattedStringContent<'a>],
ctx: &mut Ctx<'a, '_>,
) {
for part in parts {
if let libcst_native::FormattedStringContent::Expression(e) = part {
collect_in_expr(&e.expression, ctx);
if let Some(spec) = &e.format_spec {
collect_in_fstring_parts(spec, ctx);
}
}
}
}
fn collect_in_del_target<'a>(
target: &'a libcst_native::DelTargetExpression<'a>,
ctx: &mut Ctx<'a, '_>,
) {
match target {
libcst_native::DelTargetExpression::Attribute(a) => collect_in_expr(&a.value, ctx),
libcst_native::DelTargetExpression::Subscript(s) => {
collect_in_expr(&s.value, ctx);
for element in &s.slice {
collect_in_base_slice(&element.slice, ctx);
}
}
libcst_native::DelTargetExpression::Tuple(t) => {
for el in &t.elements {
collect_in_element(el, ctx);
}
}
libcst_native::DelTargetExpression::List(l) => {
for el in &l.elements {
collect_in_element(el, ctx);
}
}
libcst_native::DelTargetExpression::Name(_) => {}
}
}
fn collect_in_element<'a>(el: &'a libcst_native::Element<'a>, ctx: &mut Ctx<'a, '_>) {
match el {
libcst_native::Element::Simple { value, .. } => collect_in_expr(value, ctx),
libcst_native::Element::Starred(s) => collect_in_expr(&s.value, ctx),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collects_all_named_and_lambda_units() {
let src = std::fs::read_to_string("tests/fixtures/functions.py").unwrap();
let module = libcst_native::parse_module(&src, None).unwrap();
let span = SpanIndex::new(&src);
let anchors = crate::source::lambda_anchors(&src).expect("tokenize must succeed");
let (units, lambda_node_count) = collect(&module, &src, &span, &anchors);
assert_eq!(
lambda_node_count,
anchors.len(),
"lambda node count must equal tokenizer anchor count"
);
let symbols: Vec<&str> = units.iter().map(|u| u.symbol.as_str()).collect();
assert!(symbols.contains(&"top"));
assert!(symbols.contains(&"method"));
assert!(symbols.contains(&"fetcher"));
assert!(units.iter().any(|u| u.symbol.starts_with("<lambda@L")));
assert!(
units
.iter()
.find(|u| u.symbol == "fetcher")
.unwrap()
.is_async
);
let mut lambdas: Vec<&str> = symbols
.iter()
.filter(|s| s.starts_with("<lambda@L"))
.cloned()
.collect();
assert_eq!(lambdas.len(), 4);
lambdas.sort();
lambdas.dedup();
assert_eq!(lambdas.len(), 4, "all lambda anchors distinct");
}
#[test]
fn lambda_collection_count_matches_tokenizer_and_trailing_anchor_correct() {
let src = std::fs::read_to_string("tests/fixtures/lambda_positions.py").unwrap();
let module = libcst_native::parse_module(&src, None).unwrap();
let span = SpanIndex::new(&src);
let anchors = crate::source::lambda_anchors(&src).expect("tokenize must succeed");
let (units, lambda_node_count) = collect(&module, &src, &span, &anchors);
let lambdas: Vec<&str> = units
.iter()
.map(|u| u.symbol.as_str())
.filter(|s| s.starts_with("<lambda@L"))
.collect();
let anchor_count = anchors.len();
assert_eq!(
lambda_node_count, anchor_count,
"lambda node count must equal tokenizer count; got {lambdas:?}"
);
let (line0, line_text) = src
.lines()
.enumerate()
.find(|(_, l)| l.starts_with("t = lambda"))
.expect("trailing lambda line present");
let line = line0 + 1;
let col = line_text.find("lambda").unwrap() + 1; let expected = format!("<lambda@L{line}C{col}>");
assert!(
lambdas.contains(&expected.as_str()),
"trailing lambda must anchor to {expected}; got {lambdas:?}"
);
}
}