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