1pub mod calls;
14pub mod expr;
15pub mod mutation;
16pub mod refs;
17pub mod risk;
18
19use std::collections::HashSet;
20
21use crate::coverage;
22use crate::functions::{FnBody, FnUnit};
23use crate::imports::Imports;
24use crate::source::SpanIndex;
25use fxrank_core::confidence::function_confidence;
26use fxrank_core::effect::{RiskFeature, RiskKind, Tier};
27use fxrank_core::model::Hotspot;
28use fxrank_core::score::{
29 BoundaryCoverage, apply_boundary_discount, max_class, own_score, weight_for_class,
30};
31
32use libcst_native::{
33 Assert, AssignTargetExpression, Call, CompoundStatement, Decorator, Element, Expression,
34 FormattedStringContent, Parameters, Raise, SmallStatement, Statement, Suite,
35};
36
37pub trait EffectSink {
40 fn on_call(&mut self, call: &Call);
42 fn on_assert(&mut self, assert: &Assert);
44 fn on_raise(&mut self, raise: &Raise);
46 fn on_assign_target(&mut self, target: &AssignTargetExpression, is_aug: bool);
53 fn on_attribute_read(&mut self, _attr: &Expression) {}
56}
57
58pub fn walk_own_body<'a>(unit: &FnUnit<'a>, sink: &mut dyn EffectSink) {
80 match &unit.body {
81 FnBody::Suite(suite) => walk_suite(suite, sink),
82 FnBody::Expr(expr) => walk_expr(expr, sink),
83 FnBody::Module(stmts) => {
96 for stmt in *stmts {
97 match stmt {
98 Statement::Compound(CompoundStatement::ClassDef(class_def)) => {
99 walk_suite(&class_def.body, sink);
103 walk_class_header(class_def, sink);
111 }
112 other => walk_statement(other, sink),
113 }
114 }
115 }
116 }
117}
118
119fn walk_nested_def_header(def: &libcst_native::FunctionDef, sink: &mut dyn EffectSink) {
124 for dec in &def.decorators {
125 walk_decorator(dec, sink);
126 }
127 walk_param_defaults(&def.params, sink);
128}
129
130fn walk_class_header(class_def: &libcst_native::ClassDef, sink: &mut dyn EffectSink) {
137 for dec in &class_def.decorators {
138 walk_decorator(dec, sink);
139 }
140 for arg in class_def.bases.iter().chain(class_def.keywords.iter()) {
144 walk_expr(&arg.value, sink);
145 }
146}
147
148fn walk_decorator(dec: &Decorator, sink: &mut dyn EffectSink) {
149 walk_expr(&dec.decorator, sink);
150}
151
152fn walk_param_defaults(params: &Parameters, sink: &mut dyn EffectSink) {
153 let all = params
154 .posonly_params
155 .iter()
156 .chain(¶ms.params)
157 .chain(¶ms.kwonly_params);
158 for p in all {
159 if let Some(default) = &p.default {
160 walk_expr(default, sink);
161 }
162 }
163 if let Some(libcst_native::StarArg::Param(p)) = ¶ms.star_arg
165 && let Some(default) = &p.default
166 {
167 walk_expr(default, sink);
168 }
169 if let Some(p) = ¶ms.star_kwarg
170 && let Some(default) = &p.default
171 {
172 walk_expr(default, sink);
173 }
174}
175
176fn walk_suite(suite: &Suite, sink: &mut dyn EffectSink) {
179 match suite {
180 Suite::IndentedBlock(b) => {
181 for stmt in &b.body {
182 walk_statement(stmt, sink);
183 }
184 }
185 Suite::SimpleStatementSuite(s) => {
186 for small in &s.body {
187 walk_small(small, sink);
188 }
189 }
190 }
191}
192
193fn walk_statement(stmt: &Statement, sink: &mut dyn EffectSink) {
194 match stmt {
195 Statement::Simple(line) => {
196 for small in &line.body {
197 walk_small(small, sink);
198 }
199 }
200 Statement::Compound(c) => walk_compound(c, sink),
201 }
202}
203
204fn walk_compound(compound: &CompoundStatement, sink: &mut dyn EffectSink) {
205 match compound {
206 CompoundStatement::FunctionDef(d) => walk_nested_def_header(d, sink),
210 CompoundStatement::ClassDef(_) => {}
212 CompoundStatement::If(i) => {
213 walk_expr(&i.test, sink);
214 walk_suite(&i.body, sink);
215 if let Some(orelse) = &i.orelse {
216 walk_or_else(orelse, sink);
217 }
218 }
219 CompoundStatement::For(f) => {
220 walk_expr(&f.iter, sink);
221 walk_suite(&f.body, sink);
222 if let Some(orelse) = &f.orelse {
223 walk_suite(&orelse.body, sink);
224 }
225 }
226 CompoundStatement::While(w) => {
227 walk_expr(&w.test, sink);
228 walk_suite(&w.body, sink);
229 if let Some(orelse) = &w.orelse {
230 walk_suite(&orelse.body, sink);
231 }
232 }
233 CompoundStatement::Try(t) => {
234 walk_suite(&t.body, sink);
235 for handler in &t.handlers {
236 walk_suite(&handler.body, sink);
237 }
238 if let Some(orelse) = &t.orelse {
239 walk_suite(&orelse.body, sink);
240 }
241 if let Some(finalbody) = &t.finalbody {
242 walk_suite(&finalbody.body, sink);
243 }
244 }
245 CompoundStatement::TryStar(t) => {
246 walk_suite(&t.body, sink);
247 for handler in &t.handlers {
248 walk_suite(&handler.body, sink);
249 }
250 if let Some(orelse) = &t.orelse {
251 walk_suite(&orelse.body, sink);
252 }
253 if let Some(finalbody) = &t.finalbody {
254 walk_suite(&finalbody.body, sink);
255 }
256 }
257 CompoundStatement::With(w) => {
258 for item in &w.items {
261 walk_expr(&item.item, sink);
262 }
263 walk_suite(&w.body, sink);
264 }
265 CompoundStatement::Match(m) => {
266 walk_expr(&m.subject, sink);
267 for case in &m.cases {
268 walk_suite(&case.body, sink);
269 }
270 }
271 }
272}
273
274fn walk_or_else(orelse: &libcst_native::OrElse, sink: &mut dyn EffectSink) {
275 match orelse {
276 libcst_native::OrElse::Elif(elif) => {
277 walk_expr(&elif.test, sink);
278 walk_suite(&elif.body, sink);
279 if let Some(inner) = &elif.orelse {
280 walk_or_else(inner, sink);
281 }
282 }
283 libcst_native::OrElse::Else(e) => {
284 walk_suite(&e.body, sink);
285 }
286 }
287}
288
289fn walk_small(small: &SmallStatement, sink: &mut dyn EffectSink) {
290 match small {
291 SmallStatement::Expr(e) => walk_expr(&e.value, sink),
292 SmallStatement::Return(r) => {
293 if let Some(v) = &r.value {
294 walk_expr(v, sink);
295 }
296 }
297 SmallStatement::Assign(a) => {
298 for target in &a.targets {
299 sink.on_assign_target(&target.target, false);
300 walk_assign_target_subexprs(&target.target, sink);
301 }
302 walk_expr(&a.value, sink);
303 }
304 SmallStatement::AnnAssign(a) => {
305 sink.on_assign_target(&a.target, false);
307 walk_assign_target_subexprs(&a.target, sink);
308 if let Some(v) = &a.value {
309 walk_expr(v, sink);
310 }
311 }
312 SmallStatement::AugAssign(a) => {
313 sink.on_assign_target(&a.target, true);
314 walk_assign_target_subexprs(&a.target, sink);
315 walk_expr(&a.value, sink);
316 }
317 SmallStatement::Assert(a) => {
318 sink.on_assert(a);
319 walk_expr(&a.test, sink);
320 if let Some(msg) = &a.msg {
321 walk_expr(msg, sink);
322 }
323 }
324 SmallStatement::Raise(r) => {
325 sink.on_raise(r);
326 if let Some(exc) = &r.exc {
327 walk_expr(exc, sink);
328 }
329 }
330 _ => {}
333 }
334}
335
336fn walk_assign_target_subexprs(target: &AssignTargetExpression, sink: &mut dyn EffectSink) {
349 match target {
350 AssignTargetExpression::Name(_) => {}
352 AssignTargetExpression::Attribute(a) => walk_expr(&a.value, sink),
356 AssignTargetExpression::Subscript(s) => {
360 walk_expr(&s.value, sink);
361 for element in &s.slice {
362 walk_base_slice(&element.slice, sink);
363 }
364 }
365 AssignTargetExpression::Tuple(t) => {
367 for el in &t.elements {
368 walk_target_element(el, sink);
369 }
370 }
371 AssignTargetExpression::List(l) => {
372 for el in &l.elements {
373 walk_target_element(el, sink);
374 }
375 }
376 AssignTargetExpression::StarredElement(s) => walk_target_value(&s.value, sink),
377 }
378}
379
380fn walk_target_element(el: &Element, sink: &mut dyn EffectSink) {
383 match el {
384 Element::Simple { value, .. } => walk_target_value(value, sink),
385 Element::Starred(s) => walk_target_value(&s.value, sink),
386 }
387}
388
389fn walk_target_value(expr: &Expression, sink: &mut dyn EffectSink) {
393 match expr {
394 Expression::Name(_) => {}
395 Expression::Attribute(a) => walk_expr(&a.value, sink),
396 Expression::Subscript(s) => {
397 walk_expr(&s.value, sink);
398 for element in &s.slice {
399 walk_base_slice(&element.slice, sink);
400 }
401 }
402 Expression::Tuple(t) => {
403 for el in &t.elements {
404 walk_target_element(el, sink);
405 }
406 }
407 Expression::List(l) => {
408 for el in &l.elements {
409 walk_target_element(el, sink);
410 }
411 }
412 Expression::StarredElement(s) => walk_target_value(&s.value, sink),
413 _ => {}
414 }
415}
416
417fn walk_expr(expr: &Expression, sink: &mut dyn EffectSink) {
420 match expr {
421 Expression::Call(c) => {
422 sink.on_call(c);
423 walk_expr(&c.func, sink);
424 for arg in &c.args {
425 walk_expr(&arg.value, sink);
426 }
427 }
428 Expression::Lambda(l) => walk_param_defaults(&l.params, sink),
433
434 Expression::Attribute(a) => {
435 sink.on_attribute_read(expr);
436 walk_expr(&a.value, sink);
437 }
438 Expression::Subscript(s) => {
439 walk_expr(&s.value, sink);
443 for element in &s.slice {
446 walk_base_slice(&element.slice, sink);
447 }
448 }
449 Expression::BinaryOperation(b) => {
450 walk_expr(&b.left, sink);
451 walk_expr(&b.right, sink);
452 }
453 Expression::BooleanOperation(b) => {
454 walk_expr(&b.left, sink);
455 walk_expr(&b.right, sink);
456 }
457 Expression::UnaryOperation(u) => walk_expr(&u.expression, sink),
458 Expression::Comparison(c) => {
459 walk_expr(&c.left, sink);
460 for comp in &c.comparisons {
461 walk_expr(&comp.comparator, sink);
462 }
463 }
464 Expression::IfExp(i) => {
465 walk_expr(&i.test, sink);
466 walk_expr(&i.body, sink);
467 walk_expr(&i.orelse, sink);
468 }
469 Expression::Tuple(t) => {
470 for el in &t.elements {
471 walk_element(el, sink);
472 }
473 }
474 Expression::List(l) => {
475 for el in &l.elements {
476 walk_element(el, sink);
477 }
478 }
479 Expression::Set(s) => {
480 for el in &s.elements {
481 walk_element(el, sink);
482 }
483 }
484 Expression::Dict(d) => {
485 for el in &d.elements {
486 match el {
487 libcst_native::DictElement::Simple { key, value, .. } => {
488 walk_expr(key, sink);
489 walk_expr(value, sink);
490 }
491 libcst_native::DictElement::Starred(s) => walk_expr(&s.value, sink),
492 }
493 }
494 }
495 Expression::ListComp(l) => {
498 walk_expr(&l.elt, sink);
499 walk_comp_for(&l.for_in, sink, true);
500 }
501 Expression::SetComp(s) => {
502 walk_expr(&s.elt, sink);
503 walk_comp_for(&s.for_in, sink, true);
504 }
505 Expression::DictComp(d) => {
506 walk_expr(&d.key, sink);
507 walk_expr(&d.value, sink);
508 walk_comp_for(&d.for_in, sink, true);
509 }
510 Expression::GeneratorExp(g) => {
514 walk_comp_for(&g.for_in, sink, false);
515 }
516 Expression::FormattedString(fs) => {
517 for part in &fs.parts {
518 if let FormattedStringContent::Expression(e) = part {
519 walk_expr(&e.expression, sink);
520 if let Some(spec_parts) = &e.format_spec {
523 for sp in spec_parts {
524 if let FormattedStringContent::Expression(se) = sp {
525 walk_expr(&se.expression, sink);
526 }
527 }
528 }
529 }
530 }
531 }
532 Expression::Yield(y) => {
533 if let Some(v) = &y.value {
534 match &**v {
535 libcst_native::YieldValue::Expression(e) => walk_expr(e, sink),
536 libcst_native::YieldValue::From(f) => walk_expr(&f.item, sink),
537 }
538 }
539 }
540 Expression::Await(a) => walk_expr(&a.expression, sink),
541 Expression::NamedExpr(n) => walk_expr(&n.value, sink),
542 Expression::StarredElement(s) => walk_expr(&s.value, sink),
543
544 _ => {}
546 }
547}
548
549fn walk_comp_for(comp: &libcst_native::CompFor, sink: &mut dyn EffectSink, eager: bool) {
557 walk_expr(&comp.iter, sink);
559 if eager {
560 for cond in &comp.ifs {
561 walk_expr(&cond.test, sink);
562 }
563 if let Some(inner) = &comp.inner_for_in {
564 walk_comp_for(inner, sink, true);
565 }
566 }
567}
568
569fn walk_element(el: &Element, sink: &mut dyn EffectSink) {
570 match el {
571 Element::Simple { value, .. } => walk_expr(value, sink),
572 Element::Starred(s) => walk_expr(&s.value, sink),
573 }
574}
575
576fn walk_base_slice(slice: &libcst_native::BaseSlice, sink: &mut dyn EffectSink) {
578 match slice {
579 libcst_native::BaseSlice::Index(i) => walk_expr(&i.value, sink),
580 libcst_native::BaseSlice::Slice(s) => {
581 if let Some(lower) = &s.lower {
582 walk_expr(lower, sink);
583 }
584 if let Some(upper) = &s.upper {
585 walk_expr(upper, sink);
586 }
587 if let Some(step) = &s.step {
588 walk_expr(step, sink);
589 }
590 }
591 }
592}
593
594fn count_awaits(unit: &FnUnit) -> usize {
602 fn count_in_body(body: &FnBody) -> usize {
603 match body {
604 FnBody::Suite(suite) => count_in_suite(suite),
605 FnBody::Expr(expr) => count_in_expr(expr),
606 FnBody::Module(stmts) => stmts
611 .iter()
612 .map(|stmt| {
613 if let libcst_native::Statement::Compound(
614 libcst_native::CompoundStatement::ClassDef(c),
615 ) = stmt
616 {
617 count_in_stmt(stmt)
618 + c.decorators
619 .iter()
620 .map(|dec| count_in_expr(&dec.decorator))
621 .sum::<usize>()
622 + c.bases
623 .iter()
624 .chain(c.keywords.iter())
625 .map(|arg| count_in_expr(&arg.value))
626 .sum::<usize>()
627 } else {
628 count_in_stmt(stmt)
629 }
630 })
631 .sum(),
632 }
633 }
634
635 fn count_in_suite(suite: &libcst_native::Suite) -> usize {
636 match suite {
637 libcst_native::Suite::IndentedBlock(b) => b.body.iter().map(count_in_stmt).sum(),
638 libcst_native::Suite::SimpleStatementSuite(s) => {
639 s.body.iter().map(count_in_small).sum()
640 }
641 }
642 }
643
644 fn count_in_stmt(stmt: &libcst_native::Statement) -> usize {
645 match stmt {
646 libcst_native::Statement::Simple(line) => line.body.iter().map(count_in_small).sum(),
647 libcst_native::Statement::Compound(c) => count_in_compound(c),
648 }
649 }
650
651 fn count_in_compound(c: &libcst_native::CompoundStatement) -> usize {
652 match c {
653 libcst_native::CompoundStatement::FunctionDef(d) => count_in_def_header(d),
657 libcst_native::CompoundStatement::ClassDef(_) => 0,
658 libcst_native::CompoundStatement::If(i) => {
659 count_in_expr(&i.test)
660 + count_in_suite(&i.body)
661 + i.orelse.as_ref().map_or(0, |o| count_in_orelse(o))
662 }
663 libcst_native::CompoundStatement::For(f) => {
664 count_in_expr(&f.iter)
665 + count_in_suite(&f.body)
666 + f.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
667 }
668 libcst_native::CompoundStatement::While(w) => {
669 count_in_expr(&w.test)
670 + count_in_suite(&w.body)
671 + w.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
672 }
673 libcst_native::CompoundStatement::Try(t) => {
674 count_in_suite(&t.body)
675 + t.handlers
676 .iter()
677 .map(|h| count_in_suite(&h.body))
678 .sum::<usize>()
679 + t.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
680 + t.finalbody.as_ref().map_or(0, |e| count_in_suite(&e.body))
681 }
682 libcst_native::CompoundStatement::TryStar(t) => {
683 count_in_suite(&t.body)
684 + t.handlers
685 .iter()
686 .map(|h| count_in_suite(&h.body))
687 .sum::<usize>()
688 + t.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
689 + t.finalbody.as_ref().map_or(0, |e| count_in_suite(&e.body))
690 }
691 libcst_native::CompoundStatement::With(w) => {
692 w.items
693 .iter()
694 .map(|item| count_in_expr(&item.item))
695 .sum::<usize>()
696 + count_in_suite(&w.body)
697 }
698 libcst_native::CompoundStatement::Match(m) => {
699 count_in_expr(&m.subject)
700 + m.cases
701 .iter()
702 .map(|case| count_in_suite(&case.body))
703 .sum::<usize>()
704 }
705 }
706 }
707
708 fn count_in_orelse(orelse: &libcst_native::OrElse) -> usize {
709 match orelse {
710 libcst_native::OrElse::Elif(elif) => {
711 count_in_expr(&elif.test)
712 + count_in_suite(&elif.body)
713 + elif.orelse.as_ref().map_or(0, |o| count_in_orelse(o))
714 }
715 libcst_native::OrElse::Else(e) => count_in_suite(&e.body),
716 }
717 }
718
719 fn count_in_small(small: &libcst_native::SmallStatement) -> usize {
720 match small {
721 libcst_native::SmallStatement::Expr(e) => count_in_expr(&e.value),
722 libcst_native::SmallStatement::Return(r) => r.value.as_ref().map_or(0, count_in_expr),
723 libcst_native::SmallStatement::Assign(a) => {
724 a.targets
725 .iter()
726 .map(|t| count_in_assign_target(&t.target))
727 .sum::<usize>()
728 + count_in_expr(&a.value)
729 }
730 libcst_native::SmallStatement::AnnAssign(a) => {
731 count_in_assign_target(&a.target) + a.value.as_ref().map_or(0, count_in_expr)
732 }
733 libcst_native::SmallStatement::AugAssign(a) => {
734 count_in_assign_target(&a.target) + count_in_expr(&a.value)
735 }
736 libcst_native::SmallStatement::Assert(a) => {
737 count_in_expr(&a.test) + a.msg.as_ref().map_or(0, count_in_expr)
738 }
739 libcst_native::SmallStatement::Raise(r) => r.exc.as_ref().map_or(0, count_in_expr),
740 _ => 0,
741 }
742 }
743
744 fn count_in_expr(expr: &libcst_native::Expression) -> usize {
745 match expr {
746 libcst_native::Expression::Await(a) => {
747 1 + count_in_expr(&a.expression)
750 }
751 libcst_native::Expression::Lambda(l) => count_in_params_defaults(&l.params),
754 libcst_native::Expression::Call(c) => {
755 count_in_expr(&c.func)
756 + c.args
757 .iter()
758 .map(|a| count_in_expr(&a.value))
759 .sum::<usize>()
760 }
761 libcst_native::Expression::Attribute(a) => count_in_expr(&a.value),
762 libcst_native::Expression::Subscript(s) => {
763 count_in_expr(&s.value)
764 + s.slice
765 .iter()
766 .map(|e| count_in_base_slice(&e.slice))
767 .sum::<usize>()
768 }
769 libcst_native::Expression::BinaryOperation(b) => {
770 count_in_expr(&b.left) + count_in_expr(&b.right)
771 }
772 libcst_native::Expression::BooleanOperation(b) => {
773 count_in_expr(&b.left) + count_in_expr(&b.right)
774 }
775 libcst_native::Expression::UnaryOperation(u) => count_in_expr(&u.expression),
776 libcst_native::Expression::Comparison(c) => {
777 count_in_expr(&c.left)
778 + c.comparisons
779 .iter()
780 .map(|comp| count_in_expr(&comp.comparator))
781 .sum::<usize>()
782 }
783 libcst_native::Expression::IfExp(i) => {
784 count_in_expr(&i.test) + count_in_expr(&i.body) + count_in_expr(&i.orelse)
785 }
786 libcst_native::Expression::Tuple(t) => t.elements.iter().map(count_in_element).sum(),
787 libcst_native::Expression::List(l) => l.elements.iter().map(count_in_element).sum(),
788 libcst_native::Expression::Set(s) => s.elements.iter().map(count_in_element).sum(),
789 libcst_native::Expression::Dict(d) => d
790 .elements
791 .iter()
792 .map(|el| match el {
793 libcst_native::DictElement::Simple { key, value, .. } => {
794 count_in_expr(key) + count_in_expr(value)
795 }
796 libcst_native::DictElement::Starred(s) => count_in_expr(&s.value),
797 })
798 .sum(),
799 libcst_native::Expression::ListComp(l) => {
800 count_in_expr(&l.elt) + count_in_comp_for(&l.for_in)
801 }
802 libcst_native::Expression::SetComp(s) => {
803 count_in_expr(&s.elt) + count_in_comp_for(&s.for_in)
804 }
805 libcst_native::Expression::DictComp(d) => {
806 count_in_expr(&d.key) + count_in_expr(&d.value) + count_in_comp_for(&d.for_in)
807 }
808 libcst_native::Expression::GeneratorExp(g) => count_in_expr(&g.for_in.iter),
814 libcst_native::Expression::FormattedString(fs) => fs
815 .parts
816 .iter()
817 .map(|p| {
818 if let libcst_native::FormattedStringContent::Expression(e) = p {
819 let in_expr = count_in_expr(&e.expression);
820 let in_spec = e
822 .format_spec
823 .as_deref()
824 .unwrap_or(&[])
825 .iter()
826 .map(|sp| {
827 if let libcst_native::FormattedStringContent::Expression(se) = sp {
828 count_in_expr(&se.expression)
829 } else {
830 0
831 }
832 })
833 .sum::<usize>();
834 in_expr + in_spec
835 } else {
836 0
837 }
838 })
839 .sum(),
840 libcst_native::Expression::Yield(y) => {
841 y.value.as_ref().map_or(0, |v| match v.as_ref() {
842 libcst_native::YieldValue::Expression(e) => count_in_expr(e),
843 libcst_native::YieldValue::From(f) => count_in_expr(&f.item),
844 })
845 }
846 libcst_native::Expression::NamedExpr(n) => count_in_expr(&n.value),
847 libcst_native::Expression::StarredElement(s) => count_in_expr(&s.value),
848 _ => 0,
849 }
850 }
851
852 fn count_in_def_header(def: &libcst_native::FunctionDef) -> usize {
855 def.decorators
856 .iter()
857 .map(|dec| count_in_expr(&dec.decorator))
858 .sum::<usize>()
859 + count_in_params_defaults(&def.params)
860 }
861
862 fn count_in_params_defaults(params: &libcst_native::Parameters) -> usize {
864 let mut n = 0;
865 let all = params
866 .posonly_params
867 .iter()
868 .chain(¶ms.params)
869 .chain(¶ms.kwonly_params);
870 for p in all {
871 if let Some(default) = &p.default {
872 n += count_in_expr(default);
873 }
874 }
875 if let Some(libcst_native::StarArg::Param(p)) = ¶ms.star_arg
876 && let Some(default) = &p.default
877 {
878 n += count_in_expr(default);
879 }
880 if let Some(p) = ¶ms.star_kwarg
881 && let Some(default) = &p.default
882 {
883 n += count_in_expr(default);
884 }
885 n
886 }
887
888 fn count_in_comp_for(comp: &libcst_native::CompFor) -> usize {
889 count_in_expr(&comp.iter)
890 + comp
891 .ifs
892 .iter()
893 .map(|c| count_in_expr(&c.test))
894 .sum::<usize>()
895 + comp
896 .inner_for_in
897 .as_ref()
898 .map_or(0, |inner| count_in_comp_for(inner))
899 }
900
901 fn count_in_assign_target(target: &libcst_native::AssignTargetExpression) -> usize {
905 use libcst_native::AssignTargetExpression as T;
906 match target {
907 T::Name(_) => 0,
908 T::Attribute(a) => count_in_expr(&a.value),
909 T::Subscript(s) => {
910 count_in_expr(&s.value)
911 + s.slice
912 .iter()
913 .map(|e| count_in_base_slice(&e.slice))
914 .sum::<usize>()
915 }
916 T::Tuple(t) => t.elements.iter().map(count_in_target_element).sum(),
917 T::List(l) => l.elements.iter().map(count_in_target_element).sum(),
918 T::StarredElement(s) => count_in_target_value(&s.value),
919 }
920 }
921
922 fn count_in_target_element(el: &libcst_native::Element) -> usize {
924 match el {
925 libcst_native::Element::Simple { value, .. } => count_in_target_value(value),
926 libcst_native::Element::Starred(s) => count_in_target_value(&s.value),
927 }
928 }
929
930 fn count_in_target_value(expr: &libcst_native::Expression) -> usize {
932 match expr {
933 libcst_native::Expression::Name(_) => 0,
934 libcst_native::Expression::Attribute(a) => count_in_expr(&a.value),
935 libcst_native::Expression::Subscript(s) => {
936 count_in_expr(&s.value)
937 + s.slice
938 .iter()
939 .map(|e| count_in_base_slice(&e.slice))
940 .sum::<usize>()
941 }
942 libcst_native::Expression::Tuple(t) => {
943 t.elements.iter().map(count_in_target_element).sum()
944 }
945 libcst_native::Expression::List(l) => {
946 l.elements.iter().map(count_in_target_element).sum()
947 }
948 libcst_native::Expression::StarredElement(s) => count_in_target_value(&s.value),
949 _ => 0,
950 }
951 }
952
953 fn count_in_base_slice(slice: &libcst_native::BaseSlice) -> usize {
954 match slice {
955 libcst_native::BaseSlice::Index(i) => count_in_expr(&i.value),
956 libcst_native::BaseSlice::Slice(s) => {
957 s.lower.as_ref().map_or(0, count_in_expr)
958 + s.upper.as_ref().map_or(0, count_in_expr)
959 + s.step.as_ref().map_or(0, count_in_expr)
960 }
961 }
962 }
963
964 fn count_in_element(el: &libcst_native::Element) -> usize {
965 match el {
966 libcst_native::Element::Simple { value, .. } => count_in_expr(value),
967 libcst_native::Element::Starred(s) => count_in_expr(&s.value),
968 }
969 }
970
971 count_in_body(&unit.body)
972}
973
974type GatherOutput = (
988 Vec<fxrank_core::effect::Effect>,
989 Vec<RiskFeature>,
990 usize,
991 bool,
992 bool,
993);
994
995fn gather(
996 unit: &FnUnit,
997 path: &str,
998 imports: &Imports,
999 module_bindings: &HashSet<String>,
1000 span: &SpanIndex,
1001) -> GatherOutput {
1002 let mut effects = calls::detect(unit, imports, span);
1003
1004 let cov = coverage::of(unit, imports);
1006
1007 let discount_coverage = if cov.any_in_body {
1012 BoundaryCoverage::None
1013 } else {
1014 cov.boundary
1015 };
1016 let mut_pairs = mutation::detect(unit, imports, module_bindings, span);
1017 effects.extend(mut_pairs.into_iter().map(|(mut e, contained)| {
1018 e.contained = contained;
1021 if contained && discount_coverage != BoundaryCoverage::None {
1026 e.discounted_to = Some(apply_boundary_discount(e.class, discount_coverage, true));
1027 e.discount = Some(
1028 match discount_coverage {
1029 BoundaryCoverage::Full => "contained, Full-typed boundary",
1030 BoundaryCoverage::Partial => "contained, Partial-typed boundary",
1031 BoundaryCoverage::None => unreachable!("guarded above"),
1032 }
1033 .to_string(),
1034 );
1035 e.sync_weight();
1036 }
1037 e
1038 }));
1039
1040 let mut risks: Vec<RiskFeature> = risk::detect(unit, imports, span, path);
1044 if cov.any_in_signature || cov.any_in_body {
1045 let class = RiskKind::TypeEscape.class();
1046 risks.push(RiskFeature {
1047 kind: RiskKind::TypeEscape,
1048 class,
1049 weight: weight_for_class(class),
1050 path: path.into(),
1051 line: unit.line,
1052 col: unit.col,
1053 evidence: "explicit Any (signature or body) — type-escape hatch".into(),
1054 tier: Tier::Exact,
1055 });
1056 }
1057
1058 let await_count = count_awaits(unit);
1059 let async_boundary = unit.is_async || await_count > 0;
1060
1061 (
1062 effects,
1063 risks,
1064 await_count,
1065 async_boundary,
1066 cov.unknown_decorator,
1067 )
1068}
1069
1070pub fn analyze_unit(
1080 unit: &FnUnit,
1081 path: &str,
1082 imports: &Imports,
1083 module_bindings: &HashSet<String>,
1084 span: &SpanIndex,
1085) -> Hotspot {
1086 let (effects, risks, await_count, async_boundary, unknown_decorator) =
1088 gather(unit, path, imports, module_bindings, span);
1089
1090 let weights: Vec<u32> = effects.iter().map(|e| e.weight).collect();
1092 let classes: Vec<u8> = effects.iter().map(|e| e.effective_class()).collect();
1093
1094 let mut confidences: Vec<f64> = effects.iter().map(|e| e.confidence).collect();
1102 if await_count > 0 {
1103 confidences.push(0.8);
1104 }
1105 if unknown_decorator {
1106 confidences.push(0.8);
1107 }
1108
1109 let risk_class = risks.iter().map(|r| r.class).max().unwrap_or(0);
1112 let risk_weight = if risks.is_empty() {
1113 0
1114 } else {
1115 weight_for_class(risk_class)
1116 };
1117
1118 let mc = max_class(&classes, risk_class);
1119 let os = own_score(&weights);
1120 Hotspot {
1121 id: format!("{}:{}:{}:{}", path, unit.line, unit.col, unit.symbol),
1122 symbol: unit.symbol.clone(),
1123 path: path.into(),
1124 line: unit.line,
1125 risk_weight,
1126 confidence: function_confidence(&confidences),
1127 async_boundary,
1128 await_count,
1129 effects,
1130 risk_features: risks,
1131 ..Hotspot::own_seed(os, mc)
1133 }
1134}
1135
1136fn symbol_segments(symbol: &str) -> Option<Vec<String>> {
1144 if symbol.starts_with('<') {
1145 None
1146 } else {
1147 Some(vec![symbol.to_string()])
1148 }
1149}
1150
1151pub fn build_record(
1164 unit: &FnUnit,
1165 path: &str,
1166 imports: &Imports,
1167 module_bindings: &HashSet<String>,
1168 span: &SpanIndex,
1169 module_map: &crate::module_map::PyModuleMap,
1170) -> fxrank_core::record::UnitRecord {
1171 let (effects, risks, await_count, async_boundary, _unknown_decorator) =
1175 gather(unit, path, imports, module_bindings, span);
1176
1177 let canonical_path = if !unit.is_module_level {
1183 vec![]
1184 } else {
1185 match (module_map.module_of(path), symbol_segments(&unit.symbol)) {
1186 (Some(mut m), Some(seg)) => {
1187 m.extend(seg);
1188 m
1189 }
1190 _ => vec![], }
1192 };
1193
1194 let referencing_module = module_map.module_of(path).unwrap_or_default();
1196 let referencing_is_package = module_map.is_package(path);
1197 let call_refs = refs::extract(
1198 unit,
1199 imports,
1200 span,
1201 &referencing_module,
1202 referencing_is_package,
1203 module_map,
1204 );
1205
1206 fxrank_core::record::UnitRecord {
1207 unit_id: format!("{}:{}:{}:{}", path, unit.line, unit.col, unit.symbol),
1208 path: path.into(),
1209 line: unit.line,
1210 col: unit.col,
1211 symbol: unit.symbol.clone(),
1212 is_root: false,
1213 canonical_path,
1214 aliases: vec![],
1215 effects,
1216 risks,
1217 refs: call_refs,
1218 async_boundary,
1219 await_count,
1220 language: fxrank_core::frontend::Language::Python,
1221 }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use fxrank_core::model::Hotspot;
1228
1229 fn scan_fixture_hotspots(name: &str) -> Vec<Hotspot> {
1234 let src = std::fs::read_to_string(format!("tests/fixtures/{name}.py")).unwrap();
1235 let module = libcst_native::parse_module(&src, None).unwrap();
1236 let imports = Imports::build(&module);
1237 let module_bindings = crate::imports::module_bindings(&module);
1238 let span = SpanIndex::new(&src);
1239 let anchors = crate::source::lambda_anchors(&src).expect("tokenize must succeed");
1240 let (units, _) = crate::functions::collect(&module, &src, &span, &anchors);
1241 units
1242 .iter()
1243 .map(|unit| {
1244 analyze_unit(
1245 unit,
1246 &format!("tests/fixtures/{name}.py"),
1247 &imports,
1248 &module_bindings,
1249 &span,
1250 )
1251 })
1252 .collect()
1253 }
1254
1255 #[test]
1259 fn def_header_defaults_charge_to_enclosing_scope() {
1260 let h = scan_fixture_hotspots("attribution");
1261 let net = |sym: &str| {
1262 h.iter()
1263 .find(|x| x.symbol == sym)
1264 .unwrap_or_else(|| panic!("symbol {sym} not found"))
1265 .effects
1266 .iter()
1267 .any(|e| e.kind.wire() == "net.fs.db")
1268 };
1269 assert!(
1271 net("outer"),
1272 "open(p) default must be charged to enclosing outer"
1273 );
1274 assert!(
1275 !net("inner"),
1276 "open(p) must NOT be charged to nested inner (its default runs in outer)"
1277 );
1278 assert!(
1281 !net("top_default"),
1282 "a top-level def's own param default is module-time → uncounted on itself"
1283 );
1284 }
1285
1286 #[test]
1289 fn subscript_index_expression_is_traversed() {
1290 let h = scan_fixture_hotspots("attribution");
1291 let si = h.iter().find(|x| x.symbol == "subscript_index").unwrap();
1292 assert!(
1293 si.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1294 "subscript index requests.get(u) must surface net.fs.db, got: {:?}",
1295 si.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1296 );
1297 }
1298
1299 #[test]
1309 fn assign_target_subexprs_are_traversed_without_double_counting() {
1310 let h = scan_fixture_hotspots("attribution");
1311
1312 let s = h
1314 .iter()
1315 .find(|x| x.symbol == "assign_target_subscript_index")
1316 .unwrap();
1317 let net_count = s
1318 .effects
1319 .iter()
1320 .filter(|e| e.kind.wire() == "net.fs.db")
1321 .count();
1322 assert_eq!(
1323 net_count,
1324 1,
1325 "subscript-target index requests.get(u) must surface exactly one net.fs.db, got: {:?}",
1326 s.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1327 );
1328 let param_mut_count = s
1330 .effects
1331 .iter()
1332 .filter(|e| e.kind.wire() == "param.mutation")
1333 .count();
1334 assert_eq!(
1335 param_mut_count,
1336 1,
1337 "the subscript target `xs` must emit exactly ONE param.mutation (no double-count), got: {:?}",
1338 s.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1339 );
1340
1341 let a = h
1343 .iter()
1344 .find(|x| x.symbol == "assign_target_attr_base")
1345 .unwrap();
1346 assert!(
1347 a.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1348 "attribute-target base requests.get(u) must surface net.fs.db, got: {:?}",
1349 a.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1350 );
1351 }
1352
1353 #[test]
1355 fn subscript_index_await_counts() {
1356 let src = "async def f(xs):\n return xs[await key()]\n";
1357 let module = libcst_native::parse_module(src, None).unwrap();
1358 let imports = Imports::build(&module);
1359 let module_bindings = crate::imports::module_bindings(&module);
1360 let span = SpanIndex::new(src);
1361 let anchors = crate::source::lambda_anchors(src).unwrap();
1362 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1363 let f = units.iter().find(|u| u.symbol == "f").unwrap();
1364 let h = analyze_unit(f, "x.py", &imports, &module_bindings, &span);
1365 assert!(
1366 h.await_count >= 1,
1367 "await in subscript index must count, got await_count={}",
1368 h.await_count
1369 );
1370 }
1371
1372 #[test]
1377 fn assign_target_subscript_index_await_counts() {
1378 let src = "async def f(xs):\n xs[await key()] = 1\n";
1379 let module = libcst_native::parse_module(src, None).unwrap();
1380 let imports = Imports::build(&module);
1381 let module_bindings = crate::imports::module_bindings(&module);
1382 let span = SpanIndex::new(src);
1383 let anchors = crate::source::lambda_anchors(src).unwrap();
1384 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1385 let f = units.iter().find(|u| u.symbol == "f").unwrap();
1386 let h = analyze_unit(f, "x.py", &imports, &module_bindings, &span);
1387 assert!(
1388 h.await_count >= 1,
1389 "await in an assignment-target subscript index must count, got await_count={}",
1390 h.await_count
1391 );
1392 assert!(
1393 h.async_boundary,
1394 "await in an assignment-target subscript index must set async_boundary"
1395 );
1396 }
1397
1398 #[test]
1403 fn function_local_import_resolves_effect_and_risk() {
1404 let h = scan_fixture_hotspots("local_import");
1405 let f = h.iter().find(|x| x.symbol == "f").unwrap();
1406 assert!(
1407 f.effects.iter().any(|e| e.kind.wire() == "process.control"),
1408 "function-local import must resolve subprocess.run → process.control, got: {:?}",
1409 f.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1410 );
1411 assert!(
1412 f.risk_features
1413 .iter()
1414 .any(|r| r.kind.wire() == "dynamic.code"),
1415 "shell=True must emit dynamic.code once the local import resolves"
1416 );
1417 }
1418
1419 #[test]
1420 fn analyze_unit_scores_world_effects() {
1421 let h = scan_fixture_hotspots("calls");
1422 let io = h.iter().find(|x| x.symbol == "io_boundary").unwrap();
1423 assert_eq!(io.max_class, 7);
1427 assert!(
1428 io.own_score >= 21.0,
1429 "expected own_score >= 21.0, got {}",
1430 io.own_score
1431 );
1432 }
1433
1434 fn coverage_of_symbol(src: &str, symbol: &str) -> crate::coverage::Coverage {
1438 let module = libcst_native::parse_module(src, None).unwrap();
1439 let imports = Imports::build(&module);
1440 let span = SpanIndex::new(src);
1441 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1442 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1443 let unit = units
1444 .iter()
1445 .find(|u| u.symbol == symbol)
1446 .expect("unit not found");
1447 crate::coverage::of(unit, &imports)
1448 }
1449
1450 #[test]
1451 fn boundary_discount_zeros_contained_local_when_typed() {
1452 let h = scan_fixture_hotspots("coverage");
1453 let ft = h.iter().find(|x| x.symbol == "fully_typed").unwrap();
1454 assert_eq!(ft.own_score, 0.0); }
1456
1457 #[test]
1458 fn any_emits_type_escape_and_blocks_discount() {
1459 let h = scan_fixture_hotspots("coverage");
1460 let has_type_escape = h
1462 .iter()
1463 .find(|x| x.symbol == "has_any")
1464 .unwrap()
1465 .risk_features
1466 .iter()
1467 .any(|r| r.kind.wire() == "type.escape");
1468 assert!(has_type_escape); let ba = h.iter().find(|x| x.symbol == "body_any").unwrap();
1470 assert!(
1471 ba.risk_features
1472 .iter()
1473 .any(|r| r.kind.wire() == "type.escape")
1474 ); assert!(ba.own_score >= 1.0); }
1477
1478 #[test]
1483 fn body_any_in_eager_containers_emits_escape_and_voids_discount() {
1484 let h = scan_fixture_hotspots("coverage");
1485 for sym in [
1486 "body_any_in_list",
1487 "body_any_in_fstring",
1488 "body_any_in_comprehension",
1489 ] {
1490 let f = h.iter().find(|x| x.symbol == sym).unwrap();
1491 assert!(
1492 f.risk_features
1493 .iter()
1494 .any(|r| r.kind.wire() == "type.escape"),
1495 "{sym}: body Any in an eager container must emit type.escape"
1496 );
1497 assert!(
1498 f.own_score >= 1.0,
1499 "{sym}: body Any must void the discount (local.mutation stays class 1), \
1500 got own_score={}",
1501 f.own_score
1502 );
1503 }
1504 }
1505
1506 #[test]
1509 fn discounted_effect_sets_rationale_string() {
1510 let h = scan_fixture_hotspots("coverage");
1511 let ft = h.iter().find(|x| x.symbol == "fully_typed").unwrap();
1512 let lm = ft
1513 .effects
1514 .iter()
1515 .find(|e| e.kind.wire() == "local.mutation")
1516 .expect("fully_typed must have a local.mutation effect");
1517 assert_eq!(
1518 lm.discount.as_deref(),
1519 Some("contained, Full-typed boundary"),
1520 "discounted effect must carry the Full-boundary rationale"
1521 );
1522 }
1523
1524 #[test]
1525 fn coverage_tiers_and_decorator_confidence() {
1526 let h = scan_fixture_hotspots("coverage");
1527 let score = |s: &str| h.iter().find(|x| x.symbol == s).unwrap().own_score;
1528 assert_eq!(score("untyped"), 1.0); assert_eq!(score("partial"), 0.0); let dec = h.iter().find(|x| x.symbol == "decorated").unwrap();
1531 assert!(dec.confidence < 1.0); }
1533
1534 #[test]
1535 fn coverage_excludes_self_and_degrades_untyped_star_args() {
1536 use fxrank_core::score::BoundaryCoverage;
1537 let src = "class C:\n def m(self, x: int) -> int:\n return x\ndef v(*args) -> int:\n return 0\n";
1538 let cov_m = coverage_of_symbol(src, "m");
1539 assert_eq!(cov_m.boundary, BoundaryCoverage::Full); let cov_v = coverage_of_symbol(src, "v");
1541 assert_ne!(cov_v.boundary, BoundaryCoverage::Full); }
1543
1544 #[test]
1560 fn count_awaits_genexp_if_and_nested_for_are_lazy_outermost_iterable_is_eager() {
1561 let h = scan_fixture_hotspots("genexp_await");
1562 let find = |sym: &str| {
1563 h.iter()
1564 .find(|x| x.symbol == sym)
1565 .unwrap_or_else(|| panic!("symbol {sym} not found in hotspots"))
1566 };
1567
1568 let lazy_if = find("genexp_await_in_if_condition");
1573 assert_eq!(
1574 lazy_if.await_count, 0,
1575 "genexp `if` condition await must NOT count toward enclosing await_count; \
1576 got await_count={} for genexp_await_in_if_condition",
1577 lazy_if.await_count
1578 );
1579
1580 let eager_listcomp = find("listcomp_await_in_if_condition");
1583 assert!(
1584 eager_listcomp.await_count >= 1,
1585 "list-comp `if` condition await IS eager and MUST count toward await_count; \
1586 got await_count={} for listcomp_await_in_if_condition",
1587 eager_listcomp.await_count
1588 );
1589 assert!(
1590 eager_listcomp.async_boundary,
1591 "list-comp `if` condition await must set async_boundary; \
1592 got async_boundary={} for listcomp_await_in_if_condition",
1593 eager_listcomp.async_boundary
1594 );
1595
1596 let lazy_nested = find("genexp_await_in_nested_for_iterable");
1600 assert_eq!(
1601 lazy_nested.await_count, 0,
1602 "genexp nested-for iterable await must NOT count toward enclosing await_count; \
1603 got await_count={} for genexp_await_in_nested_for_iterable",
1604 lazy_nested.await_count
1605 );
1606
1607 let eager_iterable = find("genexp_await_in_outermost_iterable");
1610 assert!(
1611 eager_iterable.await_count >= 1,
1612 "genexp outermost-iterable await IS eager and MUST count; \
1613 got await_count={} for genexp_await_in_outermost_iterable",
1614 eager_iterable.await_count
1615 );
1616 }
1617
1618 #[test]
1621 fn fstring_format_spec_walk_expr_charges_effects() {
1622 let src = "import requests\ndef f(x, u):\n return f\"{x:{requests.get(u)}}\"\n";
1623 let module = libcst_native::parse_module(src, None).unwrap();
1624 let imports = Imports::build(&module);
1625 let module_bindings = crate::imports::module_bindings(&module);
1626 let span = SpanIndex::new(src);
1627 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1628 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1629 let unit = units.iter().find(|u| u.symbol == "f").unwrap();
1630 let h = analyze_unit(unit, "x.py", &imports, &module_bindings, &span);
1631 assert!(
1632 h.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1633 "requests.get(u) inside f-string format_spec must emit net.fs.db; got: {:?}",
1634 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1635 );
1636 }
1637
1638 #[test]
1641 fn fstring_format_spec_await_counts() {
1642 let src = "async def f(x):\n async def w(): ...\n return f\"{x:{await w()}}\"\n";
1643 let module = libcst_native::parse_module(src, None).unwrap();
1644 let imports = Imports::build(&module);
1645 let module_bindings = crate::imports::module_bindings(&module);
1646 let span = SpanIndex::new(src);
1647 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1648 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1649 let outer = units.iter().find(|u| u.symbol == "f").unwrap();
1650 let h = analyze_unit(outer, "x.py", &imports, &module_bindings, &span);
1651 assert!(
1652 h.await_count >= 1,
1653 "await inside f-string format_spec must count; got await_count={}",
1654 h.await_count
1655 );
1656 assert!(
1657 h.async_boundary,
1658 "await inside f-string format_spec must set async_boundary"
1659 );
1660 }
1661
1662 #[test]
1665 fn fstring_format_spec_body_any_emits_type_escape() {
1666 let src = "from typing import Any, cast\ndef f(x: int, y: int) -> int:\n acc: list[int] = []\n _ = f\"{x:{cast(Any, y)}}\"\n return x\n";
1667 let module = libcst_native::parse_module(src, None).unwrap();
1668 let imports = Imports::build(&module);
1669 let module_bindings = crate::imports::module_bindings(&module);
1670 let span = SpanIndex::new(src);
1671 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1672 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1673 let unit = units.iter().find(|u| u.symbol == "f").unwrap();
1674 let h = analyze_unit(unit, "x.py", &imports, &module_bindings, &span);
1675 assert!(
1676 h.risk_features
1677 .iter()
1678 .any(|r| r.kind.wire() == "type.escape"),
1679 "cast(Any, …) inside f-string format_spec must emit type.escape; got: {:?}",
1680 h.risk_features
1681 .iter()
1682 .map(|r| r.kind.wire())
1683 .collect::<Vec<_>>()
1684 );
1685 }
1686
1687 #[test]
1697 fn gather_sets_effect_contained_from_mutation_tuple() {
1698 let contained_src =
1700 "def builds_local():\n acc = []\n acc.append(1)\n return acc\n";
1701 let module = libcst_native::parse_module(contained_src, None).unwrap();
1702 let imports = Imports::build(&module);
1703 let module_bindings = crate::imports::module_bindings(&module);
1704 let span = SpanIndex::new(contained_src);
1705 let anchors = crate::source::lambda_anchors(contained_src).expect("tokenize");
1706 let (units, _) = crate::functions::collect(&module, contained_src, &span, &anchors);
1707 let unit = units
1708 .iter()
1709 .find(|u| u.symbol == "builds_local")
1710 .expect("builds_local not found");
1711 let h = analyze_unit(unit, "test.py", &imports, &module_bindings, &span);
1712 let local_mut = h
1713 .effects
1714 .iter()
1715 .find(|e| e.kind.wire() == "local.mutation")
1716 .expect("expected a local.mutation effect");
1717 assert!(
1718 local_mut.contained,
1719 "local.mutation from a body-local build must have contained=true, got: {:?}",
1720 local_mut
1721 );
1722 assert!(
1723 !local_mut.escapes(),
1724 "a contained local.mutation must not escape, got: {:?}",
1725 local_mut
1726 );
1727
1728 let escaping_src = "_g = 0\ndef uses_global():\n global _g\n _g += 1\n";
1730 let module2 = libcst_native::parse_module(escaping_src, None).unwrap();
1731 let imports2 = Imports::build(&module2);
1732 let module_bindings2 = crate::imports::module_bindings(&module2);
1733 let span2 = SpanIndex::new(escaping_src);
1734 let anchors2 = crate::source::lambda_anchors(escaping_src).expect("tokenize");
1735 let (units2, _) = crate::functions::collect(&module2, escaping_src, &span2, &anchors2);
1736 let unit2 = units2
1737 .iter()
1738 .find(|u| u.symbol == "uses_global")
1739 .expect("uses_global not found");
1740 let h2 = analyze_unit(unit2, "test.py", &imports2, &module_bindings2, &span2);
1741 let global_mut = h2
1742 .effects
1743 .iter()
1744 .find(|e| e.kind.wire() == "global.mutation")
1745 .expect("expected a global.mutation effect");
1746 assert!(
1747 !global_mut.contained,
1748 "global.mutation must have contained=false, got: {:?}",
1749 global_mut
1750 );
1751 assert!(
1752 global_mut.escapes(),
1753 "a non-contained global.mutation must escape, got: {:?}",
1754 global_mut
1755 );
1756 }
1757
1758 #[test]
1764 fn build_record_emits_record_for_python_unit() {
1765 use crate::module_map::PyModuleMap;
1766 use fxrank_core::frontend::SourceFile;
1767 let src = "import os\ndef writer():\n os.getcwd()\n";
1768 let module = libcst_native::parse_module(src, None).unwrap();
1769 let imports = Imports::build(&module);
1770 let module_bindings = crate::imports::module_bindings(&module);
1771 let span = SpanIndex::new(src);
1772 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1773 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1774 let unit = units
1775 .iter()
1776 .find(|u| u.symbol == "writer")
1777 .expect("writer unit not found");
1778 let mmap = PyModuleMap::build(&[SourceFile {
1779 path: "test.py".into(),
1780 text: String::new(),
1781 }]);
1782 let rec = build_record(unit, "test.py", &imports, &module_bindings, &span, &mmap);
1783
1784 assert_eq!(rec.symbol, "writer");
1785 assert!(
1786 rec.unit_id.ends_with(":writer"),
1787 "unit_id must end with ':writer', got: {}",
1788 rec.unit_id
1789 );
1790 assert_eq!(
1791 rec.language,
1792 fxrank_core::frontend::Language::Python,
1793 "language must be Python"
1794 );
1795 assert!(
1797 !rec.is_root,
1798 "frontend build_record must emit is_root=false (CLI sets the real value)"
1799 );
1800 let os_ref = rec
1801 .refs
1802 .iter()
1803 .find(|r| r.base == "os.getcwd")
1804 .expect("expected a ref with base 'os.getcwd'");
1805 assert!(
1806 os_ref.qualified,
1807 "os.getcwd ref must have qualified=true (os is imported)"
1808 );
1809 }
1810
1811 fn module_init_hotspot(src: &str) -> Option<Hotspot> {
1816 let module = libcst_native::parse_module(src, None).unwrap();
1817 let imports = Imports::build(&module);
1818 let module_bindings = crate::imports::module_bindings(&module);
1819 let span = SpanIndex::new(src);
1820 let unit = crate::functions::module_init_unit(&module)?;
1821 Some(analyze_unit(
1822 &unit,
1823 "test.py",
1824 &imports,
1825 &module_bindings,
1826 &span,
1827 ))
1828 }
1829
1830 #[test]
1845 fn module_init_captures_top_level_effects_not_nested_def_body() {
1846 let src = concat!(
1847 "import os\n",
1848 "CONFIG = os.environ[\"X\"]\n",
1849 "print(\"loading\")\n",
1850 "def impure():\n",
1851 " open(\"f\")\n",
1852 "def pure():\n",
1853 " return 1\n",
1854 );
1855 let h = module_init_hotspot(src).expect("<module> hotspot must exist for impure module");
1856
1857 assert_eq!(
1859 h.symbol, "<module>",
1860 "synthetic unit must have symbol '<module>'"
1861 );
1862
1863 assert!(
1866 !h.effects.is_empty(),
1867 "<module> must have ≥1 effect from top-level statements; got: {:?}",
1868 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1869 );
1870
1871 let module_effect_kinds: Vec<&str> = h.effects.iter().map(|e| e.kind.wire()).collect();
1876 assert!(
1877 !module_effect_kinds.contains(&"net.fs.db"),
1878 "impure()'s open('f') is inside a def body and must NOT surface on <module>; \
1879 got module effects: {:?}",
1880 module_effect_kinds
1881 );
1882
1883 let module2 = libcst_native::parse_module(src, None).unwrap();
1886 let span = SpanIndex::new(src);
1887 let anchors = crate::source::lambda_anchors(src).expect("tokenize");
1888 let imports = Imports::build(&module2);
1889 let module_bindings = crate::imports::module_bindings(&module2);
1890 let (units, _) = crate::functions::collect(&module2, src, &span, &anchors);
1891 let impure_h = analyze_unit(
1892 units
1893 .iter()
1894 .find(|u| u.symbol == "impure")
1895 .expect("impure unit"),
1896 "test.py",
1897 &imports,
1898 &module_bindings,
1899 &span,
1900 );
1901 assert!(
1902 impure_h
1903 .effects
1904 .iter()
1905 .any(|e| e.kind.wire() == "net.fs.db"),
1906 "impure must have its own net.fs.db from open('f'); got: {:?}",
1907 impure_h
1908 .effects
1909 .iter()
1910 .map(|e| e.kind.wire())
1911 .collect::<Vec<_>>()
1912 );
1913 }
1914
1915 #[test]
1918 fn pure_module_emits_no_module_init_hotspot() {
1919 let src = "import os\ndef f():\n return 1\n";
1920 assert!(
1921 module_init_hotspot(src).is_none(),
1922 "a pure module (import + def, no top-level effects) must not emit a <module> hotspot"
1923 );
1924 }
1925
1926 #[test]
1934 fn module_init_captures_class_decorator_effect() {
1935 let src = concat!("@open(\"y\")\n", "class C:\n", " pass\n",);
1936 let h = module_init_hotspot(src)
1937 .expect("<module> hotspot must exist when a class has an effectful decorator");
1938 assert!(
1939 h.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1940 "class decorator open(\"y\") must charge net.fs.db to <module>; got: {:?}",
1941 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1942 );
1943 }
1944
1945 #[test]
1948 fn module_init_captures_class_base_expr_effect() {
1949 let src = concat!("class C(open(\"y\")):\n", " pass\n",);
1950 let h = module_init_hotspot(src)
1951 .expect("<module> hotspot must exist when a class has an effectful base expression");
1952 assert!(
1953 h.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1954 "class base open(\"y\") must charge net.fs.db to <module>; got: {:?}",
1955 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1956 );
1957 }
1958
1959 #[test]
1966 fn module_init_class_header_captured_method_body_not() {
1967 let src = concat!(
1968 "class C(open(\"y\")):\n",
1969 " def m(self):\n",
1970 " open(\"z\")\n",
1971 );
1972 let h = module_init_hotspot(src)
1973 .expect("<module> hotspot must exist when a class has an effectful base expression");
1974
1975 assert!(
1977 h.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1978 "class base open(\"y\") must charge net.fs.db to <module>; got: {:?}",
1979 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1980 );
1981
1982 let net_count = h
1985 .effects
1986 .iter()
1987 .filter(|e| e.kind.wire() == "net.fs.db")
1988 .count();
1989 assert_eq!(
1990 net_count, 1,
1991 "method body open(\"z\") must NOT be double-counted on <module>; \
1992 expected exactly 1 net.fs.db effect, got {net_count}"
1993 );
1994 }
1995
1996 #[test]
2001 fn build_record_sets_canonical_path() {
2002 use crate::module_map::PyModuleMap;
2003 use fxrank_core::frontend::SourceFile;
2004 let mmap = PyModuleMap::build(&[
2006 SourceFile {
2007 path: "pkg/__init__.py".into(),
2008 text: String::new(),
2009 },
2010 SourceFile {
2011 path: "pkg/util.py".into(),
2012 text: String::new(),
2013 },
2014 ]);
2015 let src = "def write():\n pass\n";
2016 let module = libcst_native::parse_module(src, None).unwrap();
2017 let imports = Imports::build(&module);
2018 let module_bindings = crate::imports::module_bindings(&module);
2019 let span = SpanIndex::new(src);
2020 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
2021 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
2022 let unit = units
2023 .iter()
2024 .find(|u| u.symbol == "write")
2025 .expect("write unit not found");
2026 let rec = build_record(
2027 unit,
2028 "pkg/util.py",
2029 &imports,
2030 &module_bindings,
2031 &span,
2032 &mmap,
2033 );
2034 assert_eq!(
2035 rec.canonical_path,
2036 vec!["pkg".to_string(), "util".into(), "write".into()],
2037 "module-level write in pkg/util.py must get canonical_path [pkg, util, write]"
2038 );
2039 }
2040
2041 #[test]
2045 fn method_unit_gets_empty_canonical_path() {
2046 use crate::module_map::PyModuleMap;
2047 use fxrank_core::frontend::SourceFile;
2048 let mmap = PyModuleMap::build(&[
2049 SourceFile {
2050 path: "pkg/__init__.py".into(),
2051 text: String::new(),
2052 },
2053 SourceFile {
2054 path: "pkg/util.py".into(),
2055 text: String::new(),
2056 },
2057 ]);
2058 let src = "class C:\n def write(self):\n pass\n";
2059 let module = libcst_native::parse_module(src, None).unwrap();
2060 let imports = Imports::build(&module);
2061 let module_bindings = crate::imports::module_bindings(&module);
2062 let span = SpanIndex::new(src);
2063 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
2064 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
2065 let method_unit = units
2066 .iter()
2067 .find(|u| u.symbol == "write")
2068 .expect("write method unit not found");
2069 assert!(
2070 !method_unit.is_module_level,
2071 "a class method must have is_module_level=false, got true"
2072 );
2073 let rec = build_record(
2074 method_unit,
2075 "pkg/util.py",
2076 &imports,
2077 &module_bindings,
2078 &span,
2079 &mmap,
2080 );
2081 assert!(
2082 rec.canonical_path.is_empty(),
2083 "a method must not get an importable canonical_path; got: {:?}",
2084 rec.canonical_path
2085 );
2086 }
2087}