1pub mod calls;
14pub mod expr;
15pub mod mutation;
16pub mod risk;
17
18use std::collections::HashSet;
19
20use crate::coverage;
21use crate::functions::{FnBody, FnUnit};
22use crate::imports::Imports;
23use crate::source::SpanIndex;
24use fxrank_core::confidence::function_confidence;
25use fxrank_core::effect::{RiskFeature, RiskKind, Tier};
26use fxrank_core::model::Hotspot;
27use fxrank_core::score::{
28 BoundaryCoverage, apply_boundary_discount, max_class, own_score, weight_for_class,
29};
30
31use libcst_native::{
32 Assert, AssignTargetExpression, Call, CompoundStatement, Decorator, Element, Expression,
33 FormattedStringContent, Parameters, Raise, SmallStatement, Statement, Suite,
34};
35
36pub trait EffectSink {
39 fn on_call(&mut self, call: &Call);
41 fn on_assert(&mut self, assert: &Assert);
43 fn on_raise(&mut self, raise: &Raise);
45 fn on_assign_target(&mut self, target: &AssignTargetExpression, is_aug: bool);
52 fn on_attribute_read(&mut self, _attr: &Expression) {}
55}
56
57pub fn walk_own_body<'a>(unit: &FnUnit<'a>, sink: &mut dyn EffectSink) {
79 match &unit.body {
80 FnBody::Suite(suite) => walk_suite(suite, sink),
81 FnBody::Expr(expr) => walk_expr(expr, sink),
82 }
83}
84
85fn walk_nested_def_header(def: &libcst_native::FunctionDef, sink: &mut dyn EffectSink) {
90 for dec in &def.decorators {
91 walk_decorator(dec, sink);
92 }
93 walk_param_defaults(&def.params, sink);
94}
95
96fn walk_decorator(dec: &Decorator, sink: &mut dyn EffectSink) {
97 walk_expr(&dec.decorator, sink);
98}
99
100fn walk_param_defaults(params: &Parameters, sink: &mut dyn EffectSink) {
101 let all = params
102 .posonly_params
103 .iter()
104 .chain(¶ms.params)
105 .chain(¶ms.kwonly_params);
106 for p in all {
107 if let Some(default) = &p.default {
108 walk_expr(default, sink);
109 }
110 }
111 if let Some(libcst_native::StarArg::Param(p)) = ¶ms.star_arg
113 && let Some(default) = &p.default
114 {
115 walk_expr(default, sink);
116 }
117 if let Some(p) = ¶ms.star_kwarg
118 && let Some(default) = &p.default
119 {
120 walk_expr(default, sink);
121 }
122}
123
124fn walk_suite(suite: &Suite, sink: &mut dyn EffectSink) {
127 match suite {
128 Suite::IndentedBlock(b) => {
129 for stmt in &b.body {
130 walk_statement(stmt, sink);
131 }
132 }
133 Suite::SimpleStatementSuite(s) => {
134 for small in &s.body {
135 walk_small(small, sink);
136 }
137 }
138 }
139}
140
141fn walk_statement(stmt: &Statement, sink: &mut dyn EffectSink) {
142 match stmt {
143 Statement::Simple(line) => {
144 for small in &line.body {
145 walk_small(small, sink);
146 }
147 }
148 Statement::Compound(c) => walk_compound(c, sink),
149 }
150}
151
152fn walk_compound(compound: &CompoundStatement, sink: &mut dyn EffectSink) {
153 match compound {
154 CompoundStatement::FunctionDef(d) => walk_nested_def_header(d, sink),
158 CompoundStatement::ClassDef(_) => {}
160 CompoundStatement::If(i) => {
161 walk_expr(&i.test, sink);
162 walk_suite(&i.body, sink);
163 if let Some(orelse) = &i.orelse {
164 walk_or_else(orelse, sink);
165 }
166 }
167 CompoundStatement::For(f) => {
168 walk_expr(&f.iter, sink);
169 walk_suite(&f.body, sink);
170 if let Some(orelse) = &f.orelse {
171 walk_suite(&orelse.body, sink);
172 }
173 }
174 CompoundStatement::While(w) => {
175 walk_expr(&w.test, sink);
176 walk_suite(&w.body, sink);
177 if let Some(orelse) = &w.orelse {
178 walk_suite(&orelse.body, sink);
179 }
180 }
181 CompoundStatement::Try(t) => {
182 walk_suite(&t.body, sink);
183 for handler in &t.handlers {
184 walk_suite(&handler.body, sink);
185 }
186 if let Some(orelse) = &t.orelse {
187 walk_suite(&orelse.body, sink);
188 }
189 if let Some(finalbody) = &t.finalbody {
190 walk_suite(&finalbody.body, sink);
191 }
192 }
193 CompoundStatement::TryStar(t) => {
194 walk_suite(&t.body, sink);
195 for handler in &t.handlers {
196 walk_suite(&handler.body, sink);
197 }
198 if let Some(orelse) = &t.orelse {
199 walk_suite(&orelse.body, sink);
200 }
201 if let Some(finalbody) = &t.finalbody {
202 walk_suite(&finalbody.body, sink);
203 }
204 }
205 CompoundStatement::With(w) => {
206 for item in &w.items {
209 walk_expr(&item.item, sink);
210 }
211 walk_suite(&w.body, sink);
212 }
213 CompoundStatement::Match(m) => {
214 walk_expr(&m.subject, sink);
215 for case in &m.cases {
216 walk_suite(&case.body, sink);
217 }
218 }
219 }
220}
221
222fn walk_or_else(orelse: &libcst_native::OrElse, sink: &mut dyn EffectSink) {
223 match orelse {
224 libcst_native::OrElse::Elif(elif) => {
225 walk_expr(&elif.test, sink);
226 walk_suite(&elif.body, sink);
227 if let Some(inner) = &elif.orelse {
228 walk_or_else(inner, sink);
229 }
230 }
231 libcst_native::OrElse::Else(e) => {
232 walk_suite(&e.body, sink);
233 }
234 }
235}
236
237fn walk_small(small: &SmallStatement, sink: &mut dyn EffectSink) {
238 match small {
239 SmallStatement::Expr(e) => walk_expr(&e.value, sink),
240 SmallStatement::Return(r) => {
241 if let Some(v) = &r.value {
242 walk_expr(v, sink);
243 }
244 }
245 SmallStatement::Assign(a) => {
246 for target in &a.targets {
247 sink.on_assign_target(&target.target, false);
248 walk_assign_target_subexprs(&target.target, sink);
249 }
250 walk_expr(&a.value, sink);
251 }
252 SmallStatement::AnnAssign(a) => {
253 sink.on_assign_target(&a.target, false);
255 walk_assign_target_subexprs(&a.target, sink);
256 if let Some(v) = &a.value {
257 walk_expr(v, sink);
258 }
259 }
260 SmallStatement::AugAssign(a) => {
261 sink.on_assign_target(&a.target, true);
262 walk_assign_target_subexprs(&a.target, sink);
263 walk_expr(&a.value, sink);
264 }
265 SmallStatement::Assert(a) => {
266 sink.on_assert(a);
267 walk_expr(&a.test, sink);
268 if let Some(msg) = &a.msg {
269 walk_expr(msg, sink);
270 }
271 }
272 SmallStatement::Raise(r) => {
273 sink.on_raise(r);
274 if let Some(exc) = &r.exc {
275 walk_expr(exc, sink);
276 }
277 }
278 _ => {}
281 }
282}
283
284fn walk_assign_target_subexprs(target: &AssignTargetExpression, sink: &mut dyn EffectSink) {
297 match target {
298 AssignTargetExpression::Name(_) => {}
300 AssignTargetExpression::Attribute(a) => walk_expr(&a.value, sink),
304 AssignTargetExpression::Subscript(s) => {
308 walk_expr(&s.value, sink);
309 for element in &s.slice {
310 walk_base_slice(&element.slice, sink);
311 }
312 }
313 AssignTargetExpression::Tuple(t) => {
315 for el in &t.elements {
316 walk_target_element(el, sink);
317 }
318 }
319 AssignTargetExpression::List(l) => {
320 for el in &l.elements {
321 walk_target_element(el, sink);
322 }
323 }
324 AssignTargetExpression::StarredElement(s) => walk_target_value(&s.value, sink),
325 }
326}
327
328fn walk_target_element(el: &Element, sink: &mut dyn EffectSink) {
331 match el {
332 Element::Simple { value, .. } => walk_target_value(value, sink),
333 Element::Starred(s) => walk_target_value(&s.value, sink),
334 }
335}
336
337fn walk_target_value(expr: &Expression, sink: &mut dyn EffectSink) {
341 match expr {
342 Expression::Name(_) => {}
343 Expression::Attribute(a) => walk_expr(&a.value, sink),
344 Expression::Subscript(s) => {
345 walk_expr(&s.value, sink);
346 for element in &s.slice {
347 walk_base_slice(&element.slice, sink);
348 }
349 }
350 Expression::Tuple(t) => {
351 for el in &t.elements {
352 walk_target_element(el, sink);
353 }
354 }
355 Expression::List(l) => {
356 for el in &l.elements {
357 walk_target_element(el, sink);
358 }
359 }
360 Expression::StarredElement(s) => walk_target_value(&s.value, sink),
361 _ => {}
362 }
363}
364
365fn walk_expr(expr: &Expression, sink: &mut dyn EffectSink) {
368 match expr {
369 Expression::Call(c) => {
370 sink.on_call(c);
371 walk_expr(&c.func, sink);
372 for arg in &c.args {
373 walk_expr(&arg.value, sink);
374 }
375 }
376 Expression::Lambda(l) => walk_param_defaults(&l.params, sink),
381
382 Expression::Attribute(a) => {
383 sink.on_attribute_read(expr);
384 walk_expr(&a.value, sink);
385 }
386 Expression::Subscript(s) => {
387 walk_expr(&s.value, sink);
391 for element in &s.slice {
394 walk_base_slice(&element.slice, sink);
395 }
396 }
397 Expression::BinaryOperation(b) => {
398 walk_expr(&b.left, sink);
399 walk_expr(&b.right, sink);
400 }
401 Expression::BooleanOperation(b) => {
402 walk_expr(&b.left, sink);
403 walk_expr(&b.right, sink);
404 }
405 Expression::UnaryOperation(u) => walk_expr(&u.expression, sink),
406 Expression::Comparison(c) => {
407 walk_expr(&c.left, sink);
408 for comp in &c.comparisons {
409 walk_expr(&comp.comparator, sink);
410 }
411 }
412 Expression::IfExp(i) => {
413 walk_expr(&i.test, sink);
414 walk_expr(&i.body, sink);
415 walk_expr(&i.orelse, sink);
416 }
417 Expression::Tuple(t) => {
418 for el in &t.elements {
419 walk_element(el, sink);
420 }
421 }
422 Expression::List(l) => {
423 for el in &l.elements {
424 walk_element(el, sink);
425 }
426 }
427 Expression::Set(s) => {
428 for el in &s.elements {
429 walk_element(el, sink);
430 }
431 }
432 Expression::Dict(d) => {
433 for el in &d.elements {
434 match el {
435 libcst_native::DictElement::Simple { key, value, .. } => {
436 walk_expr(key, sink);
437 walk_expr(value, sink);
438 }
439 libcst_native::DictElement::Starred(s) => walk_expr(&s.value, sink),
440 }
441 }
442 }
443 Expression::ListComp(l) => {
446 walk_expr(&l.elt, sink);
447 walk_comp_for(&l.for_in, sink, true);
448 }
449 Expression::SetComp(s) => {
450 walk_expr(&s.elt, sink);
451 walk_comp_for(&s.for_in, sink, true);
452 }
453 Expression::DictComp(d) => {
454 walk_expr(&d.key, sink);
455 walk_expr(&d.value, sink);
456 walk_comp_for(&d.for_in, sink, true);
457 }
458 Expression::GeneratorExp(g) => {
462 walk_comp_for(&g.for_in, sink, false);
463 }
464 Expression::FormattedString(fs) => {
465 for part in &fs.parts {
466 if let FormattedStringContent::Expression(e) = part {
467 walk_expr(&e.expression, sink);
468 if let Some(spec_parts) = &e.format_spec {
471 for sp in spec_parts {
472 if let FormattedStringContent::Expression(se) = sp {
473 walk_expr(&se.expression, sink);
474 }
475 }
476 }
477 }
478 }
479 }
480 Expression::Yield(y) => {
481 if let Some(v) = &y.value {
482 match &**v {
483 libcst_native::YieldValue::Expression(e) => walk_expr(e, sink),
484 libcst_native::YieldValue::From(f) => walk_expr(&f.item, sink),
485 }
486 }
487 }
488 Expression::Await(a) => walk_expr(&a.expression, sink),
489 Expression::NamedExpr(n) => walk_expr(&n.value, sink),
490 Expression::StarredElement(s) => walk_expr(&s.value, sink),
491
492 _ => {}
494 }
495}
496
497fn walk_comp_for(comp: &libcst_native::CompFor, sink: &mut dyn EffectSink, eager: bool) {
505 walk_expr(&comp.iter, sink);
507 if eager {
508 for cond in &comp.ifs {
509 walk_expr(&cond.test, sink);
510 }
511 if let Some(inner) = &comp.inner_for_in {
512 walk_comp_for(inner, sink, true);
513 }
514 }
515}
516
517fn walk_element(el: &Element, sink: &mut dyn EffectSink) {
518 match el {
519 Element::Simple { value, .. } => walk_expr(value, sink),
520 Element::Starred(s) => walk_expr(&s.value, sink),
521 }
522}
523
524fn walk_base_slice(slice: &libcst_native::BaseSlice, sink: &mut dyn EffectSink) {
526 match slice {
527 libcst_native::BaseSlice::Index(i) => walk_expr(&i.value, sink),
528 libcst_native::BaseSlice::Slice(s) => {
529 if let Some(lower) = &s.lower {
530 walk_expr(lower, sink);
531 }
532 if let Some(upper) = &s.upper {
533 walk_expr(upper, sink);
534 }
535 if let Some(step) = &s.step {
536 walk_expr(step, sink);
537 }
538 }
539 }
540}
541
542fn count_awaits(unit: &FnUnit) -> usize {
550 fn count_in_body(body: &FnBody) -> usize {
551 match body {
552 FnBody::Suite(suite) => count_in_suite(suite),
553 FnBody::Expr(expr) => count_in_expr(expr),
554 }
555 }
556
557 fn count_in_suite(suite: &libcst_native::Suite) -> usize {
558 match suite {
559 libcst_native::Suite::IndentedBlock(b) => b.body.iter().map(count_in_stmt).sum(),
560 libcst_native::Suite::SimpleStatementSuite(s) => {
561 s.body.iter().map(count_in_small).sum()
562 }
563 }
564 }
565
566 fn count_in_stmt(stmt: &libcst_native::Statement) -> usize {
567 match stmt {
568 libcst_native::Statement::Simple(line) => line.body.iter().map(count_in_small).sum(),
569 libcst_native::Statement::Compound(c) => count_in_compound(c),
570 }
571 }
572
573 fn count_in_compound(c: &libcst_native::CompoundStatement) -> usize {
574 match c {
575 libcst_native::CompoundStatement::FunctionDef(d) => count_in_def_header(d),
579 libcst_native::CompoundStatement::ClassDef(_) => 0,
580 libcst_native::CompoundStatement::If(i) => {
581 count_in_expr(&i.test)
582 + count_in_suite(&i.body)
583 + i.orelse.as_ref().map_or(0, |o| count_in_orelse(o))
584 }
585 libcst_native::CompoundStatement::For(f) => {
586 count_in_expr(&f.iter)
587 + count_in_suite(&f.body)
588 + f.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
589 }
590 libcst_native::CompoundStatement::While(w) => {
591 count_in_expr(&w.test)
592 + count_in_suite(&w.body)
593 + w.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
594 }
595 libcst_native::CompoundStatement::Try(t) => {
596 count_in_suite(&t.body)
597 + t.handlers
598 .iter()
599 .map(|h| count_in_suite(&h.body))
600 .sum::<usize>()
601 + t.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
602 + t.finalbody.as_ref().map_or(0, |e| count_in_suite(&e.body))
603 }
604 libcst_native::CompoundStatement::TryStar(t) => {
605 count_in_suite(&t.body)
606 + t.handlers
607 .iter()
608 .map(|h| count_in_suite(&h.body))
609 .sum::<usize>()
610 + t.orelse.as_ref().map_or(0, |e| count_in_suite(&e.body))
611 + t.finalbody.as_ref().map_or(0, |e| count_in_suite(&e.body))
612 }
613 libcst_native::CompoundStatement::With(w) => {
614 w.items
615 .iter()
616 .map(|item| count_in_expr(&item.item))
617 .sum::<usize>()
618 + count_in_suite(&w.body)
619 }
620 libcst_native::CompoundStatement::Match(m) => {
621 count_in_expr(&m.subject)
622 + m.cases
623 .iter()
624 .map(|case| count_in_suite(&case.body))
625 .sum::<usize>()
626 }
627 }
628 }
629
630 fn count_in_orelse(orelse: &libcst_native::OrElse) -> usize {
631 match orelse {
632 libcst_native::OrElse::Elif(elif) => {
633 count_in_expr(&elif.test)
634 + count_in_suite(&elif.body)
635 + elif.orelse.as_ref().map_or(0, |o| count_in_orelse(o))
636 }
637 libcst_native::OrElse::Else(e) => count_in_suite(&e.body),
638 }
639 }
640
641 fn count_in_small(small: &libcst_native::SmallStatement) -> usize {
642 match small {
643 libcst_native::SmallStatement::Expr(e) => count_in_expr(&e.value),
644 libcst_native::SmallStatement::Return(r) => r.value.as_ref().map_or(0, count_in_expr),
645 libcst_native::SmallStatement::Assign(a) => {
646 a.targets
647 .iter()
648 .map(|t| count_in_assign_target(&t.target))
649 .sum::<usize>()
650 + count_in_expr(&a.value)
651 }
652 libcst_native::SmallStatement::AnnAssign(a) => {
653 count_in_assign_target(&a.target) + a.value.as_ref().map_or(0, count_in_expr)
654 }
655 libcst_native::SmallStatement::AugAssign(a) => {
656 count_in_assign_target(&a.target) + count_in_expr(&a.value)
657 }
658 libcst_native::SmallStatement::Assert(a) => {
659 count_in_expr(&a.test) + a.msg.as_ref().map_or(0, count_in_expr)
660 }
661 libcst_native::SmallStatement::Raise(r) => r.exc.as_ref().map_or(0, count_in_expr),
662 _ => 0,
663 }
664 }
665
666 fn count_in_expr(expr: &libcst_native::Expression) -> usize {
667 match expr {
668 libcst_native::Expression::Await(a) => {
669 1 + count_in_expr(&a.expression)
672 }
673 libcst_native::Expression::Lambda(l) => count_in_params_defaults(&l.params),
676 libcst_native::Expression::Call(c) => {
677 count_in_expr(&c.func)
678 + c.args
679 .iter()
680 .map(|a| count_in_expr(&a.value))
681 .sum::<usize>()
682 }
683 libcst_native::Expression::Attribute(a) => count_in_expr(&a.value),
684 libcst_native::Expression::Subscript(s) => {
685 count_in_expr(&s.value)
686 + s.slice
687 .iter()
688 .map(|e| count_in_base_slice(&e.slice))
689 .sum::<usize>()
690 }
691 libcst_native::Expression::BinaryOperation(b) => {
692 count_in_expr(&b.left) + count_in_expr(&b.right)
693 }
694 libcst_native::Expression::BooleanOperation(b) => {
695 count_in_expr(&b.left) + count_in_expr(&b.right)
696 }
697 libcst_native::Expression::UnaryOperation(u) => count_in_expr(&u.expression),
698 libcst_native::Expression::Comparison(c) => {
699 count_in_expr(&c.left)
700 + c.comparisons
701 .iter()
702 .map(|comp| count_in_expr(&comp.comparator))
703 .sum::<usize>()
704 }
705 libcst_native::Expression::IfExp(i) => {
706 count_in_expr(&i.test) + count_in_expr(&i.body) + count_in_expr(&i.orelse)
707 }
708 libcst_native::Expression::Tuple(t) => t.elements.iter().map(count_in_element).sum(),
709 libcst_native::Expression::List(l) => l.elements.iter().map(count_in_element).sum(),
710 libcst_native::Expression::Set(s) => s.elements.iter().map(count_in_element).sum(),
711 libcst_native::Expression::Dict(d) => d
712 .elements
713 .iter()
714 .map(|el| match el {
715 libcst_native::DictElement::Simple { key, value, .. } => {
716 count_in_expr(key) + count_in_expr(value)
717 }
718 libcst_native::DictElement::Starred(s) => count_in_expr(&s.value),
719 })
720 .sum(),
721 libcst_native::Expression::ListComp(l) => {
722 count_in_expr(&l.elt) + count_in_comp_for(&l.for_in)
723 }
724 libcst_native::Expression::SetComp(s) => {
725 count_in_expr(&s.elt) + count_in_comp_for(&s.for_in)
726 }
727 libcst_native::Expression::DictComp(d) => {
728 count_in_expr(&d.key) + count_in_expr(&d.value) + count_in_comp_for(&d.for_in)
729 }
730 libcst_native::Expression::GeneratorExp(g) => count_in_expr(&g.for_in.iter),
736 libcst_native::Expression::FormattedString(fs) => fs
737 .parts
738 .iter()
739 .map(|p| {
740 if let libcst_native::FormattedStringContent::Expression(e) = p {
741 let in_expr = count_in_expr(&e.expression);
742 let in_spec = e
744 .format_spec
745 .as_deref()
746 .unwrap_or(&[])
747 .iter()
748 .map(|sp| {
749 if let libcst_native::FormattedStringContent::Expression(se) = sp {
750 count_in_expr(&se.expression)
751 } else {
752 0
753 }
754 })
755 .sum::<usize>();
756 in_expr + in_spec
757 } else {
758 0
759 }
760 })
761 .sum(),
762 libcst_native::Expression::Yield(y) => {
763 y.value.as_ref().map_or(0, |v| match v.as_ref() {
764 libcst_native::YieldValue::Expression(e) => count_in_expr(e),
765 libcst_native::YieldValue::From(f) => count_in_expr(&f.item),
766 })
767 }
768 libcst_native::Expression::NamedExpr(n) => count_in_expr(&n.value),
769 libcst_native::Expression::StarredElement(s) => count_in_expr(&s.value),
770 _ => 0,
771 }
772 }
773
774 fn count_in_def_header(def: &libcst_native::FunctionDef) -> usize {
777 def.decorators
778 .iter()
779 .map(|dec| count_in_expr(&dec.decorator))
780 .sum::<usize>()
781 + count_in_params_defaults(&def.params)
782 }
783
784 fn count_in_params_defaults(params: &libcst_native::Parameters) -> usize {
786 let mut n = 0;
787 let all = params
788 .posonly_params
789 .iter()
790 .chain(¶ms.params)
791 .chain(¶ms.kwonly_params);
792 for p in all {
793 if let Some(default) = &p.default {
794 n += count_in_expr(default);
795 }
796 }
797 if let Some(libcst_native::StarArg::Param(p)) = ¶ms.star_arg
798 && let Some(default) = &p.default
799 {
800 n += count_in_expr(default);
801 }
802 if let Some(p) = ¶ms.star_kwarg
803 && let Some(default) = &p.default
804 {
805 n += count_in_expr(default);
806 }
807 n
808 }
809
810 fn count_in_comp_for(comp: &libcst_native::CompFor) -> usize {
811 count_in_expr(&comp.iter)
812 + comp
813 .ifs
814 .iter()
815 .map(|c| count_in_expr(&c.test))
816 .sum::<usize>()
817 + comp
818 .inner_for_in
819 .as_ref()
820 .map_or(0, |inner| count_in_comp_for(inner))
821 }
822
823 fn count_in_assign_target(target: &libcst_native::AssignTargetExpression) -> usize {
827 use libcst_native::AssignTargetExpression as T;
828 match target {
829 T::Name(_) => 0,
830 T::Attribute(a) => count_in_expr(&a.value),
831 T::Subscript(s) => {
832 count_in_expr(&s.value)
833 + s.slice
834 .iter()
835 .map(|e| count_in_base_slice(&e.slice))
836 .sum::<usize>()
837 }
838 T::Tuple(t) => t.elements.iter().map(count_in_target_element).sum(),
839 T::List(l) => l.elements.iter().map(count_in_target_element).sum(),
840 T::StarredElement(s) => count_in_target_value(&s.value),
841 }
842 }
843
844 fn count_in_target_element(el: &libcst_native::Element) -> usize {
846 match el {
847 libcst_native::Element::Simple { value, .. } => count_in_target_value(value),
848 libcst_native::Element::Starred(s) => count_in_target_value(&s.value),
849 }
850 }
851
852 fn count_in_target_value(expr: &libcst_native::Expression) -> usize {
854 match expr {
855 libcst_native::Expression::Name(_) => 0,
856 libcst_native::Expression::Attribute(a) => count_in_expr(&a.value),
857 libcst_native::Expression::Subscript(s) => {
858 count_in_expr(&s.value)
859 + s.slice
860 .iter()
861 .map(|e| count_in_base_slice(&e.slice))
862 .sum::<usize>()
863 }
864 libcst_native::Expression::Tuple(t) => {
865 t.elements.iter().map(count_in_target_element).sum()
866 }
867 libcst_native::Expression::List(l) => {
868 l.elements.iter().map(count_in_target_element).sum()
869 }
870 libcst_native::Expression::StarredElement(s) => count_in_target_value(&s.value),
871 _ => 0,
872 }
873 }
874
875 fn count_in_base_slice(slice: &libcst_native::BaseSlice) -> usize {
876 match slice {
877 libcst_native::BaseSlice::Index(i) => count_in_expr(&i.value),
878 libcst_native::BaseSlice::Slice(s) => {
879 s.lower.as_ref().map_or(0, count_in_expr)
880 + s.upper.as_ref().map_or(0, count_in_expr)
881 + s.step.as_ref().map_or(0, count_in_expr)
882 }
883 }
884 }
885
886 fn count_in_element(el: &libcst_native::Element) -> usize {
887 match el {
888 libcst_native::Element::Simple { value, .. } => count_in_expr(value),
889 libcst_native::Element::Starred(s) => count_in_expr(&s.value),
890 }
891 }
892
893 count_in_body(&unit.body)
894}
895
896pub fn analyze_unit(
908 unit: &FnUnit,
909 path: &str,
910 imports: &Imports,
911 module_bindings: &HashSet<String>,
912 span: &SpanIndex,
913) -> Hotspot {
914 let mut effects = calls::detect(unit, imports, span);
916
917 let cov = coverage::of(unit, imports);
919
920 let discount_coverage = if cov.any_in_body {
926 BoundaryCoverage::None
927 } else {
928 cov.boundary
929 };
930 let mut_pairs = mutation::detect(unit, imports, module_bindings, span);
931 effects.extend(mut_pairs.into_iter().map(|(mut e, contained)| {
932 if contained && discount_coverage != BoundaryCoverage::None {
937 e.discounted_to = Some(apply_boundary_discount(e.class, discount_coverage, true));
938 e.discount = Some(
939 match discount_coverage {
940 BoundaryCoverage::Full => "contained, Full-typed boundary",
941 BoundaryCoverage::Partial => "contained, Partial-typed boundary",
942 BoundaryCoverage::None => unreachable!("guarded above"),
943 }
944 .to_string(),
945 );
946 e.sync_weight();
947 }
948 e
949 }));
950 let mut risks: Vec<RiskFeature> = Vec::new();
955
956 risks.extend(risk::detect(unit, imports, span, path));
958 if cov.any_in_signature || cov.any_in_body {
959 let class = RiskKind::TypeEscape.class();
960 risks.push(RiskFeature {
961 kind: RiskKind::TypeEscape,
962 class,
963 weight: weight_for_class(class),
964 path: path.into(),
965 line: unit.line,
966 evidence: "explicit Any (signature or body) — type-escape hatch".into(),
967 tier: Tier::Exact,
968 });
969 }
970
971 let await_count = count_awaits(unit);
972 let async_boundary = unit.is_async || await_count > 0;
973
974 let weights: Vec<u32> = effects.iter().map(|e| e.weight).collect();
976 let classes: Vec<u8> = effects.iter().map(|e| e.effective_class()).collect();
977
978 let mut confidences: Vec<f64> = effects.iter().map(|e| e.confidence).collect();
986 if await_count > 0 {
987 confidences.push(0.8);
988 }
989 if cov.unknown_decorator {
990 confidences.push(0.8);
991 }
992
993 let risk_class = risks.iter().map(|r| r.class).max().unwrap_or(0);
996 let risk_weight = if risks.is_empty() {
997 0
998 } else {
999 weight_for_class(risk_class)
1000 };
1001
1002 Hotspot {
1003 id: format!("{}:{}:{}:{}", path, unit.line, unit.col, unit.symbol),
1004 symbol: unit.symbol.clone(),
1005 path: path.into(),
1006 line: unit.line,
1007 max_class: max_class(&classes, risk_class),
1008 own_score: own_score(&weights),
1009 risk_weight,
1010 confidence: function_confidence(&confidences),
1011 async_boundary,
1012 await_count,
1013 effects,
1014 risk_features: risks,
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use fxrank_core::model::Hotspot;
1022
1023 fn scan_fixture_hotspots(name: &str) -> Vec<Hotspot> {
1028 let src = std::fs::read_to_string(format!("tests/fixtures/{name}.py")).unwrap();
1029 let module = libcst_native::parse_module(&src, None).unwrap();
1030 let imports = Imports::build(&module);
1031 let module_bindings = crate::imports::module_bindings(&module);
1032 let span = SpanIndex::new(&src);
1033 let anchors = crate::source::lambda_anchors(&src).expect("tokenize must succeed");
1034 let (units, _) = crate::functions::collect(&module, &src, &span, &anchors);
1035 units
1036 .iter()
1037 .map(|unit| {
1038 analyze_unit(
1039 unit,
1040 &format!("tests/fixtures/{name}.py"),
1041 &imports,
1042 &module_bindings,
1043 &span,
1044 )
1045 })
1046 .collect()
1047 }
1048
1049 #[test]
1053 fn def_header_defaults_charge_to_enclosing_scope() {
1054 let h = scan_fixture_hotspots("attribution");
1055 let net = |sym: &str| {
1056 h.iter()
1057 .find(|x| x.symbol == sym)
1058 .unwrap_or_else(|| panic!("symbol {sym} not found"))
1059 .effects
1060 .iter()
1061 .any(|e| e.kind.wire() == "net.fs.db")
1062 };
1063 assert!(
1065 net("outer"),
1066 "open(p) default must be charged to enclosing outer"
1067 );
1068 assert!(
1069 !net("inner"),
1070 "open(p) must NOT be charged to nested inner (its default runs in outer)"
1071 );
1072 assert!(
1075 !net("top_default"),
1076 "a top-level def's own param default is module-time → uncounted on itself"
1077 );
1078 }
1079
1080 #[test]
1083 fn subscript_index_expression_is_traversed() {
1084 let h = scan_fixture_hotspots("attribution");
1085 let si = h.iter().find(|x| x.symbol == "subscript_index").unwrap();
1086 assert!(
1087 si.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1088 "subscript index requests.get(u) must surface net.fs.db, got: {:?}",
1089 si.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1090 );
1091 }
1092
1093 #[test]
1103 fn assign_target_subexprs_are_traversed_without_double_counting() {
1104 let h = scan_fixture_hotspots("attribution");
1105
1106 let s = h
1108 .iter()
1109 .find(|x| x.symbol == "assign_target_subscript_index")
1110 .unwrap();
1111 let net_count = s
1112 .effects
1113 .iter()
1114 .filter(|e| e.kind.wire() == "net.fs.db")
1115 .count();
1116 assert_eq!(
1117 net_count,
1118 1,
1119 "subscript-target index requests.get(u) must surface exactly one net.fs.db, got: {:?}",
1120 s.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1121 );
1122 let param_mut_count = s
1124 .effects
1125 .iter()
1126 .filter(|e| e.kind.wire() == "param.mutation")
1127 .count();
1128 assert_eq!(
1129 param_mut_count,
1130 1,
1131 "the subscript target `xs` must emit exactly ONE param.mutation (no double-count), got: {:?}",
1132 s.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1133 );
1134
1135 let a = h
1137 .iter()
1138 .find(|x| x.symbol == "assign_target_attr_base")
1139 .unwrap();
1140 assert!(
1141 a.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1142 "attribute-target base requests.get(u) must surface net.fs.db, got: {:?}",
1143 a.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1144 );
1145 }
1146
1147 #[test]
1149 fn subscript_index_await_counts() {
1150 let src = "async def f(xs):\n return xs[await key()]\n";
1151 let module = libcst_native::parse_module(src, None).unwrap();
1152 let imports = Imports::build(&module);
1153 let module_bindings = crate::imports::module_bindings(&module);
1154 let span = SpanIndex::new(src);
1155 let anchors = crate::source::lambda_anchors(src).unwrap();
1156 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1157 let f = units.iter().find(|u| u.symbol == "f").unwrap();
1158 let h = analyze_unit(f, "x.py", &imports, &module_bindings, &span);
1159 assert!(
1160 h.await_count >= 1,
1161 "await in subscript index must count, got await_count={}",
1162 h.await_count
1163 );
1164 }
1165
1166 #[test]
1171 fn assign_target_subscript_index_await_counts() {
1172 let src = "async def f(xs):\n xs[await key()] = 1\n";
1173 let module = libcst_native::parse_module(src, None).unwrap();
1174 let imports = Imports::build(&module);
1175 let module_bindings = crate::imports::module_bindings(&module);
1176 let span = SpanIndex::new(src);
1177 let anchors = crate::source::lambda_anchors(src).unwrap();
1178 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1179 let f = units.iter().find(|u| u.symbol == "f").unwrap();
1180 let h = analyze_unit(f, "x.py", &imports, &module_bindings, &span);
1181 assert!(
1182 h.await_count >= 1,
1183 "await in an assignment-target subscript index must count, got await_count={}",
1184 h.await_count
1185 );
1186 assert!(
1187 h.async_boundary,
1188 "await in an assignment-target subscript index must set async_boundary"
1189 );
1190 }
1191
1192 #[test]
1197 fn function_local_import_resolves_effect_and_risk() {
1198 let h = scan_fixture_hotspots("local_import");
1199 let f = h.iter().find(|x| x.symbol == "f").unwrap();
1200 assert!(
1201 f.effects.iter().any(|e| e.kind.wire() == "process.control"),
1202 "function-local import must resolve subprocess.run → process.control, got: {:?}",
1203 f.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1204 );
1205 assert!(
1206 f.risk_features
1207 .iter()
1208 .any(|r| r.kind.wire() == "dynamic.code"),
1209 "shell=True must emit dynamic.code once the local import resolves"
1210 );
1211 }
1212
1213 #[test]
1214 fn analyze_unit_scores_world_effects() {
1215 let h = scan_fixture_hotspots("calls");
1216 let io = h.iter().find(|x| x.symbol == "io_boundary").unwrap();
1217 assert_eq!(io.max_class, 7);
1221 assert!(
1222 io.own_score >= 21.0,
1223 "expected own_score >= 21.0, got {}",
1224 io.own_score
1225 );
1226 }
1227
1228 fn coverage_of_symbol(src: &str, symbol: &str) -> crate::coverage::Coverage {
1232 let module = libcst_native::parse_module(src, None).unwrap();
1233 let imports = Imports::build(&module);
1234 let span = SpanIndex::new(src);
1235 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1236 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1237 let unit = units
1238 .iter()
1239 .find(|u| u.symbol == symbol)
1240 .expect("unit not found");
1241 crate::coverage::of(unit, &imports)
1242 }
1243
1244 #[test]
1245 fn boundary_discount_zeros_contained_local_when_typed() {
1246 let h = scan_fixture_hotspots("coverage");
1247 let ft = h.iter().find(|x| x.symbol == "fully_typed").unwrap();
1248 assert_eq!(ft.own_score, 0.0); }
1250
1251 #[test]
1252 fn any_emits_type_escape_and_blocks_discount() {
1253 let h = scan_fixture_hotspots("coverage");
1254 let has_type_escape = h
1256 .iter()
1257 .find(|x| x.symbol == "has_any")
1258 .unwrap()
1259 .risk_features
1260 .iter()
1261 .any(|r| r.kind.wire() == "type.escape");
1262 assert!(has_type_escape); let ba = h.iter().find(|x| x.symbol == "body_any").unwrap();
1264 assert!(
1265 ba.risk_features
1266 .iter()
1267 .any(|r| r.kind.wire() == "type.escape")
1268 ); assert!(ba.own_score >= 1.0); }
1271
1272 #[test]
1277 fn body_any_in_eager_containers_emits_escape_and_voids_discount() {
1278 let h = scan_fixture_hotspots("coverage");
1279 for sym in [
1280 "body_any_in_list",
1281 "body_any_in_fstring",
1282 "body_any_in_comprehension",
1283 ] {
1284 let f = h.iter().find(|x| x.symbol == sym).unwrap();
1285 assert!(
1286 f.risk_features
1287 .iter()
1288 .any(|r| r.kind.wire() == "type.escape"),
1289 "{sym}: body Any in an eager container must emit type.escape"
1290 );
1291 assert!(
1292 f.own_score >= 1.0,
1293 "{sym}: body Any must void the discount (local.mutation stays class 1), \
1294 got own_score={}",
1295 f.own_score
1296 );
1297 }
1298 }
1299
1300 #[test]
1303 fn discounted_effect_sets_rationale_string() {
1304 let h = scan_fixture_hotspots("coverage");
1305 let ft = h.iter().find(|x| x.symbol == "fully_typed").unwrap();
1306 let lm = ft
1307 .effects
1308 .iter()
1309 .find(|e| e.kind.wire() == "local.mutation")
1310 .expect("fully_typed must have a local.mutation effect");
1311 assert_eq!(
1312 lm.discount.as_deref(),
1313 Some("contained, Full-typed boundary"),
1314 "discounted effect must carry the Full-boundary rationale"
1315 );
1316 }
1317
1318 #[test]
1319 fn coverage_tiers_and_decorator_confidence() {
1320 let h = scan_fixture_hotspots("coverage");
1321 let score = |s: &str| h.iter().find(|x| x.symbol == s).unwrap().own_score;
1322 assert_eq!(score("untyped"), 1.0); assert_eq!(score("partial"), 0.0); let dec = h.iter().find(|x| x.symbol == "decorated").unwrap();
1325 assert!(dec.confidence < 1.0); }
1327
1328 #[test]
1329 fn coverage_excludes_self_and_degrades_untyped_star_args() {
1330 use fxrank_core::score::BoundaryCoverage;
1331 let src = "class C:\n def m(self, x: int) -> int:\n return x\ndef v(*args) -> int:\n return 0\n";
1332 let cov_m = coverage_of_symbol(src, "m");
1333 assert_eq!(cov_m.boundary, BoundaryCoverage::Full); let cov_v = coverage_of_symbol(src, "v");
1335 assert_ne!(cov_v.boundary, BoundaryCoverage::Full); }
1337
1338 #[test]
1354 fn count_awaits_genexp_if_and_nested_for_are_lazy_outermost_iterable_is_eager() {
1355 let h = scan_fixture_hotspots("genexp_await");
1356 let find = |sym: &str| {
1357 h.iter()
1358 .find(|x| x.symbol == sym)
1359 .unwrap_or_else(|| panic!("symbol {sym} not found in hotspots"))
1360 };
1361
1362 let lazy_if = find("genexp_await_in_if_condition");
1367 assert_eq!(
1368 lazy_if.await_count, 0,
1369 "genexp `if` condition await must NOT count toward enclosing await_count; \
1370 got await_count={} for genexp_await_in_if_condition",
1371 lazy_if.await_count
1372 );
1373
1374 let eager_listcomp = find("listcomp_await_in_if_condition");
1377 assert!(
1378 eager_listcomp.await_count >= 1,
1379 "list-comp `if` condition await IS eager and MUST count toward await_count; \
1380 got await_count={} for listcomp_await_in_if_condition",
1381 eager_listcomp.await_count
1382 );
1383 assert!(
1384 eager_listcomp.async_boundary,
1385 "list-comp `if` condition await must set async_boundary; \
1386 got async_boundary={} for listcomp_await_in_if_condition",
1387 eager_listcomp.async_boundary
1388 );
1389
1390 let lazy_nested = find("genexp_await_in_nested_for_iterable");
1394 assert_eq!(
1395 lazy_nested.await_count, 0,
1396 "genexp nested-for iterable await must NOT count toward enclosing await_count; \
1397 got await_count={} for genexp_await_in_nested_for_iterable",
1398 lazy_nested.await_count
1399 );
1400
1401 let eager_iterable = find("genexp_await_in_outermost_iterable");
1404 assert!(
1405 eager_iterable.await_count >= 1,
1406 "genexp outermost-iterable await IS eager and MUST count; \
1407 got await_count={} for genexp_await_in_outermost_iterable",
1408 eager_iterable.await_count
1409 );
1410 }
1411
1412 #[test]
1415 fn fstring_format_spec_walk_expr_charges_effects() {
1416 let src = "import requests\ndef f(x, u):\n return f\"{x:{requests.get(u)}}\"\n";
1417 let module = libcst_native::parse_module(src, None).unwrap();
1418 let imports = Imports::build(&module);
1419 let module_bindings = crate::imports::module_bindings(&module);
1420 let span = SpanIndex::new(src);
1421 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1422 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1423 let unit = units.iter().find(|u| u.symbol == "f").unwrap();
1424 let h = analyze_unit(unit, "x.py", &imports, &module_bindings, &span);
1425 assert!(
1426 h.effects.iter().any(|e| e.kind.wire() == "net.fs.db"),
1427 "requests.get(u) inside f-string format_spec must emit net.fs.db; got: {:?}",
1428 h.effects.iter().map(|e| e.kind.wire()).collect::<Vec<_>>()
1429 );
1430 }
1431
1432 #[test]
1435 fn fstring_format_spec_await_counts() {
1436 let src = "async def f(x):\n async def w(): ...\n return f\"{x:{await w()}}\"\n";
1437 let module = libcst_native::parse_module(src, None).unwrap();
1438 let imports = Imports::build(&module);
1439 let module_bindings = crate::imports::module_bindings(&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 outer = units.iter().find(|u| u.symbol == "f").unwrap();
1444 let h = analyze_unit(outer, "x.py", &imports, &module_bindings, &span);
1445 assert!(
1446 h.await_count >= 1,
1447 "await inside f-string format_spec must count; got await_count={}",
1448 h.await_count
1449 );
1450 assert!(
1451 h.async_boundary,
1452 "await inside f-string format_spec must set async_boundary"
1453 );
1454 }
1455
1456 #[test]
1459 fn fstring_format_spec_body_any_emits_type_escape() {
1460 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";
1461 let module = libcst_native::parse_module(src, None).unwrap();
1462 let imports = Imports::build(&module);
1463 let module_bindings = crate::imports::module_bindings(&module);
1464 let span = SpanIndex::new(src);
1465 let anchors = crate::source::lambda_anchors(src).expect("tokenize must succeed");
1466 let (units, _) = crate::functions::collect(&module, src, &span, &anchors);
1467 let unit = units.iter().find(|u| u.symbol == "f").unwrap();
1468 let h = analyze_unit(unit, "x.py", &imports, &module_bindings, &span);
1469 assert!(
1470 h.risk_features
1471 .iter()
1472 .any(|r| r.kind.wire() == "type.escape"),
1473 "cast(Any, …) inside f-string format_spec must emit type.escape; got: {:?}",
1474 h.risk_features
1475 .iter()
1476 .map(|r| r.kind.wire())
1477 .collect::<Vec<_>>()
1478 );
1479 }
1480}