1use fallow_output::{DirectCallerEvidence, DirectCallerSymbolEvidence, FileHealthScore};
2
3use crate::module_graph::StaticTestCoverage;
4
5use super::coverage_gaps::compute_coverage_gaps;
6pub(super) use super::coverage_gaps::{CoverageGapData, build_coverage_summary};
7use super::threshold_overrides::ThresholdOverrideResolver;
8
9pub struct FileScoreOutput {
11 pub(crate) scores: Vec<FileHealthScore>,
12 pub(crate) coverage: CoverageGapData,
14 pub(crate) circular_files: rustc_hash::FxHashSet<std::path::PathBuf>,
16 pub(crate) top_complex_fns: rustc_hash::FxHashMap<std::path::PathBuf, Vec<(String, u32, u16)>>,
18 pub(crate) entry_points: rustc_hash::FxHashSet<std::path::PathBuf>,
20 pub(crate) value_export_counts: rustc_hash::FxHashMap<std::path::PathBuf, usize>,
22 pub(crate) unused_export_names: rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>>,
24 pub(crate) cycle_members: rustc_hash::FxHashMap<std::path::PathBuf, Vec<std::path::PathBuf>>,
26 pub(crate) direct_callers: rustc_hash::FxHashMap<std::path::PathBuf, Vec<DirectCallerEvidence>>,
28 pub(crate) analysis_counts: crate::vital_signs::AnalysisCounts,
30 pub(crate) prop_drilling_chains: Vec<fallow_types::output_dead_code::PropDrillingChainFinding>,
35 pub(crate) render_fan_in: Option<fallow_types::results::RenderFanInMetric>,
41 pub(crate) analysis_snapshot: AnalysisCountsSnapshot,
45 pub(crate) istanbul_matched: usize,
47 pub(crate) istanbul_total: usize,
48 pub(crate) istanbul_files_joined: usize,
52 pub(crate) istanbul_files_total: usize,
54 pub(crate) per_function_crap: rustc_hash::FxHashMap<std::path::PathBuf, Vec<PerFunctionCrap>>,
58 pub(crate) template_inherit_provenance:
65 rustc_hash::FxHashMap<std::path::PathBuf, std::path::PathBuf>,
66}
67
68struct FileScoreOutputParts<'a> {
69 graph: &'a fallow_graph::graph::ModuleGraph,
70 file_paths: &'a rustc_hash::FxHashMap<crate::discover::FileId, &'a std::path::PathBuf>,
71 results: &'a crate::results::AnalysisResults,
72 scores: Vec<FileHealthScore>,
73 coverage: CoverageGapData,
74 circular_files: rustc_hash::FxHashSet<std::path::PathBuf>,
75 top_complex_fns: rustc_hash::FxHashMap<std::path::PathBuf, Vec<(String, u32, u16)>>,
76 entry_points: rustc_hash::FxHashSet<std::path::PathBuf>,
77 value_export_counts: rustc_hash::FxHashMap<std::path::PathBuf, usize>,
78 unused_export_names: rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>>,
79 cycle_members: rustc_hash::FxHashMap<std::path::PathBuf, Vec<std::path::PathBuf>>,
80 direct_callers: rustc_hash::FxHashMap<std::path::PathBuf, Vec<DirectCallerEvidence>>,
81 istanbul_matched: usize,
82 istanbul_total: usize,
83 istanbul_files_joined: usize,
84 istanbul_files_total: usize,
85 per_function_crap: rustc_hash::FxHashMap<std::path::PathBuf, Vec<PerFunctionCrap>>,
86 template_inherit: rustc_hash::FxHashMap<crate::discover::FileId, TemplateInheritContext>,
87}
88
89#[derive(Clone, Default)]
95pub struct AnalysisCountsSnapshot {
96 unused_file_paths: Vec<std::path::PathBuf>,
98 unused_export_paths: Vec<std::path::PathBuf>,
101 unused_dep_package_paths: Vec<std::path::PathBuf>,
105 circular_dep_groups: Vec<Vec<std::path::PathBuf>>,
108 module_export_counts: rustc_hash::FxHashMap<std::path::PathBuf, usize>,
111}
112
113impl AnalysisCountsSnapshot {
114 pub(crate) fn counts_for(
130 &self,
131 subset: &crate::health::SubsetFilter<'_>,
132 defaults: &crate::vital_signs::AnalysisCounts,
133 ) -> crate::vital_signs::AnalysisCounts {
134 if subset.is_full() {
135 return *defaults;
136 }
137 let dead_files = self
138 .unused_file_paths
139 .iter()
140 .filter(|p| subset.matches(p))
141 .count();
142 let dead_exports = self
143 .unused_export_paths
144 .iter()
145 .filter(|p| subset.matches(p))
146 .count();
147 let unused_deps = self
148 .unused_dep_package_paths
149 .iter()
150 .filter(|dep_path| dep_in_subset(subset, dep_path))
151 .count();
152 let circular_deps = self
153 .circular_dep_groups
154 .iter()
155 .filter(|cycle| cycle.iter().any(|p| subset.matches(p)))
156 .count();
157 let total_exports = self
158 .module_export_counts
159 .iter()
160 .filter(|(p, _)| subset.matches(p))
161 .map(|(_, n)| *n)
162 .sum();
163 crate::vital_signs::AnalysisCounts {
164 total_exports,
165 dead_files,
166 dead_exports,
167 unused_deps,
168 circular_deps,
169 total_deps: defaults.total_deps,
170 }
171 }
172}
173
174fn dep_in_subset(subset: &crate::health::SubsetFilter<'_>, dep_path: &std::path::Path) -> bool {
181 match subset {
182 crate::health::SubsetFilter::Full => true,
183 crate::health::SubsetFilter::Paths(set) => {
184 let Some(workspace_root) = dep_path.parent() else {
185 return false;
186 };
187 set.iter().any(|p| p.starts_with(workspace_root))
188 }
189 }
190}
191
192#[expect(
202 clippy::cast_possible_truncation,
203 reason = "line count is bounded by source file size"
204)]
205fn aggregate_complexity(module: &crate::source::ModuleInfo) -> (u32, u32, usize, u32) {
206 let cyc: u32 = module
207 .complexity
208 .iter()
209 .map(|f| u32::from(f.cyclomatic))
210 .sum();
211 let cog: u32 = module
212 .complexity
213 .iter()
214 .map(|f| u32::from(f.cognitive))
215 .sum();
216 let funcs = module.complexity.len();
217 let lines = module.line_offsets.len() as u32;
218 (cyc, cog, funcs, lines)
219}
220
221fn compute_dead_code_ratio(
229 path: &std::path::Path,
230 exports: &[fallow_graph::graph::ExportSymbol],
231 unused_files: &rustc_hash::FxHashSet<&std::path::Path>,
232 unused_exports_by_path: &rustc_hash::FxHashMap<&std::path::Path, usize>,
233) -> f64 {
234 if unused_files.contains(path) {
235 return 1.0;
236 }
237 let value_exports = exports.iter().filter(|e| !e.is_type_only).count();
238 if value_exports == 0 {
239 return 0.0;
240 }
241 let unused = unused_exports_by_path.get(path).copied().unwrap_or(0);
242 (unused as f64 / value_exports as f64).min(1.0)
243}
244
245fn compute_complexity_density(total_cyclomatic: u32, lines: u32) -> f64 {
249 if lines > 0 {
250 f64::from(total_cyclomatic) / f64::from(lines)
251 } else {
252 0.0
253 }
254}
255
256pub(super) const CRAP_THRESHOLD: f64 = 30.0;
259
260#[derive(Clone, Copy)]
263pub(super) struct CrapScoreThresholds<'a> {
264 pub(super) resolver: &'a ThresholdOverrideResolver,
265 pub(super) enforce_crap: bool,
266}
267
268pub(super) struct CrapCeilingLookup<'a> {
275 resolver: &'a ThresholdOverrideResolver,
276 relative: &'a std::path::Path,
277 enforce_crap: bool,
278}
279
280#[derive(Debug, Default)]
287struct CrapThresholdSignals {
288 above: usize,
290 exempted: usize,
294 min_ceiling: Option<f64>,
296}
297
298impl<'a> CrapCeilingLookup<'a> {
299 pub(super) fn new(thresholds: CrapScoreThresholds<'a>, relative: &'a std::path::Path) -> Self {
300 Self {
301 resolver: thresholds.resolver,
302 relative,
303 enforce_crap: thresholds.enforce_crap,
304 }
305 }
306
307 fn observe(&self, function: &str, crap_rounded: f64, signals: &mut CrapThresholdSignals) {
310 let ceiling = self.resolver.effective_max_crap(self.relative, function);
311 signals.min_ceiling = Some(signals.min_ceiling.map_or(ceiling, |m| m.min(ceiling)));
312 if !self.enforce_crap {
313 if crap_rounded >= CRAP_THRESHOLD {
314 signals.exempted += 1;
315 }
316 } else if crap_rounded >= ceiling {
317 signals.above += 1;
318 } else if crap_rounded >= CRAP_THRESHOLD {
319 signals.exempted += 1;
320 }
321 }
322}
323
324#[derive(Debug, Clone, Copy)]
326pub struct PerFunctionCrap {
327 pub(crate) line: u32,
329 pub(crate) col: u32,
335 pub(crate) crap: f64,
337 pub(crate) coverage_pct: Option<f64>,
340 pub(crate) coverage_tier: fallow_output::CoverageTier,
344 pub(crate) coverage_source: fallow_output::CoverageSource,
351}
352
353#[derive(Debug)]
355struct IstanbulCrapResult {
356 pub max_crap: f64,
357 pub signals: CrapThresholdSignals,
359 pub matched: usize,
361 pub total: usize,
363 pub per_function: Vec<PerFunctionCrap>,
365}
366
367fn compute_crap_scores_istanbul(
378 complexity: &[fallow_types::extract::FunctionComplexity],
379 file_coverage: Option<&IstanbulFileCoverage>,
380 is_test_reachable: bool,
381 ceilings: &CrapCeilingLookup<'_>,
382) -> IstanbulCrapResult {
383 if complexity.is_empty() {
384 return IstanbulCrapResult {
385 max_crap: 0.0,
386 signals: CrapThresholdSignals::default(),
387 matched: 0,
388 total: 0,
389 per_function: Vec::new(),
390 };
391 }
392 let mut max = 0.0_f64;
393 let mut signals = CrapThresholdSignals::default();
394 let mut matched = 0usize;
395 let mut total = 0usize;
396 let mut per_function = Vec::with_capacity(complexity.len());
397 for f in complexity {
398 if fallow_types::extract::is_synthetic_template_unit(&f.name)
407 || fallow_types::extract::is_synthetic_module_unit(&f.name)
408 {
409 continue;
410 }
411 total += 1;
412 let (crap, coverage_pct, tier, source) =
413 crap_for_function(f, file_coverage, is_test_reachable, &mut matched);
414 let crap_rounded = (crap * 10.0).round() / 10.0;
415 max = max.max(crap);
416 ceilings.observe(f.name.as_str(), crap_rounded, &mut signals);
417 per_function.push(PerFunctionCrap {
418 line: f.line,
419 col: f.col,
420 crap: crap_rounded,
421 coverage_pct,
422 coverage_tier: tier,
423 coverage_source: source,
424 });
425 }
426 IstanbulCrapResult {
427 max_crap: (max * 10.0).round() / 10.0,
428 signals,
429 matched,
430 total,
431 per_function,
432 }
433}
434
435#[expect(
439 clippy::suboptimal_flops,
440 reason = "cc * cc + cc matches the CRAP formula specification"
441)]
442fn crap_for_function(
443 f: &fallow_types::extract::FunctionComplexity,
444 file_coverage: Option<&IstanbulFileCoverage>,
445 is_test_reachable: bool,
446 matched: &mut usize,
447) -> (
448 f64,
449 Option<f64>,
450 fallow_output::CoverageTier,
451 fallow_output::CoverageSource,
452) {
453 let cc = f64::from(f.cyclomatic);
454 let lookup = file_coverage.and_then(|fc| fc.lookup_function(f));
455 if let Some(cov_pct) = lookup {
456 *matched += 1;
457 return (
458 crap_formula(cc, cov_pct),
459 Some(cov_pct),
460 fallow_output::CoverageTier::from_pct(cov_pct),
461 fallow_output::CoverageSource::Istanbul,
462 );
463 }
464 if is_test_reachable {
468 return (
469 crap_formula(cc, INDIRECT_TEST_COVERAGE_ESTIMATE),
470 None,
471 fallow_output::CoverageTier::from_pct(INDIRECT_TEST_COVERAGE_ESTIMATE),
472 fallow_output::CoverageSource::Estimated,
473 );
474 }
475 (
476 cc * cc + cc,
477 None,
478 fallow_output::CoverageTier::None,
479 fallow_output::CoverageSource::Estimated,
480 )
481}
482
483const DIRECT_TEST_COVERAGE_ESTIMATE: f64 = 85.0;
486
487const INDIRECT_TEST_COVERAGE_ESTIMATE: f64 = 40.0;
491const MAX_DIRECT_CALLER_EVIDENCE: usize = 5;
492
493#[derive(Debug)]
504struct EstimatedCrapResult {
505 pub max_crap: f64,
506 pub signals: CrapThresholdSignals,
508 pub per_function: Vec<PerFunctionCrap>,
509}
510
511fn compute_crap_scores_estimated(
512 complexity: &[fallow_types::extract::FunctionComplexity],
513 test_referenced_exports: &rustc_hash::FxHashSet<String>,
514 is_test_reachable: bool,
515 coverage_source: fallow_output::CoverageSource,
516 ceilings: &CrapCeilingLookup<'_>,
517) -> EstimatedCrapResult {
518 if complexity.is_empty() {
519 return EstimatedCrapResult {
520 max_crap: 0.0,
521 signals: CrapThresholdSignals::default(),
522 per_function: Vec::new(),
523 };
524 }
525 let mut max = 0.0_f64;
526 let mut signals = CrapThresholdSignals::default();
527 let mut per_function = Vec::with_capacity(complexity.len());
528 for f in complexity {
529 if fallow_types::extract::is_synthetic_template_unit(&f.name)
534 || fallow_types::extract::is_synthetic_module_unit(&f.name)
535 {
536 continue;
537 }
538 let cc = f64::from(f.cyclomatic);
539 let estimated_coverage = if test_referenced_exports.contains(f.name.as_str()) {
540 DIRECT_TEST_COVERAGE_ESTIMATE
541 } else if is_test_reachable {
542 INDIRECT_TEST_COVERAGE_ESTIMATE
543 } else {
544 0.0
545 };
546 let crap = crap_formula(cc, estimated_coverage);
547 let crap_rounded = (crap * 10.0).round() / 10.0;
548 max = max.max(crap);
549 ceilings.observe(f.name.as_str(), crap_rounded, &mut signals);
550 per_function.push(PerFunctionCrap {
551 line: f.line,
552 col: f.col,
553 crap: crap_rounded,
554 coverage_pct: None,
555 coverage_tier: fallow_output::CoverageTier::from_pct(estimated_coverage),
556 coverage_source,
557 });
558 }
559 EstimatedCrapResult {
560 max_crap: (max * 10.0).round() / 10.0,
561 signals,
562 per_function,
563 }
564}
565
566#[derive(Debug, Clone)]
580pub(super) struct TemplateInheritContext {
581 pub is_test_reachable: bool,
582 pub test_referenced_exports: rustc_hash::FxHashSet<String>,
583 pub provenance_owner: std::path::PathBuf,
588}
589
590fn build_template_inherit_contexts(
612 graph: &fallow_graph::graph::ModuleGraph,
613 test_coverage: StaticTestCoverage<'_>,
614 module_by_id: &rustc_hash::FxHashMap<crate::discover::FileId, &crate::source::ModuleInfo>,
615 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
616) -> rustc_hash::FxHashMap<crate::discover::FileId, TemplateInheritContext> {
617 let mut out = rustc_hash::FxHashMap::default();
618 for node in &graph.modules {
619 if let Some(context) =
620 template_inherit_context_for_node(node, graph, test_coverage, module_by_id, file_paths)
621 {
622 out.insert(node.file_id, context);
623 }
624 }
625 out
626}
627
628fn template_inherit_context_for_node(
629 node: &fallow_graph::graph::ModuleNode,
630 graph: &fallow_graph::graph::ModuleGraph,
631 test_coverage: StaticTestCoverage<'_>,
632 module_by_id: &rustc_hash::FxHashMap<crate::discover::FileId, &crate::source::ModuleInfo>,
633 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
634) -> Option<TemplateInheritContext> {
635 if !is_template_inherit_candidate(node, module_by_id, file_paths) {
636 return None;
637 }
638 let importers = graph.reverse_deps.get(node.file_id.0 as usize)?;
639 template_inherit_context_from_importers(
640 importers,
641 graph,
642 test_coverage,
643 module_by_id,
644 file_paths,
645 )
646}
647
648fn is_template_inherit_candidate(
649 node: &fallow_graph::graph::ModuleNode,
650 module_by_id: &rustc_hash::FxHashMap<crate::discover::FileId, &crate::source::ModuleInfo>,
651 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
652) -> bool {
653 let Some(path) = file_paths.get(&node.file_id) else {
654 return false;
655 };
656 if !path
657 .extension()
658 .and_then(|ext| ext.to_str())
659 .is_some_and(|ext| ext.eq_ignore_ascii_case("html"))
660 {
661 return false;
662 }
663 module_by_id.get(&node.file_id).is_some_and(|module| {
664 module
665 .complexity
666 .iter()
667 .any(|finding| finding.name.as_str() == "<template>")
668 })
669}
670
671fn template_inherit_context_from_importers(
672 importers: &[crate::discover::FileId],
673 graph: &fallow_graph::graph::ModuleGraph,
674 test_coverage: StaticTestCoverage<'_>,
675 module_by_id: &rustc_hash::FxHashMap<crate::discover::FileId, &crate::source::ModuleInfo>,
676 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
677) -> Option<TemplateInheritContext> {
678 let mut any_reachable = false;
679 let mut combined_refs = rustc_hash::FxHashSet::default();
680 let mut provenance: Option<std::path::PathBuf> = None;
681 let mut first_owner: Option<std::path::PathBuf> = None;
682
683 for &importer_id in importers {
684 let Some((owner_node, owner_path)) =
685 template_owner(importer_id, graph, module_by_id, file_paths)
686 else {
687 continue;
688 };
689 if first_owner.is_none() {
690 first_owner = Some((*owner_path).clone());
691 }
692 if test_coverage.covers_file(owner_node.file_id) {
693 any_reachable = true;
694 provenance.get_or_insert_with(|| (*owner_path).clone());
695 let refs = build_test_referenced_exports(&owner_node.exports, test_coverage);
696 combined_refs.extend(refs);
697 }
698 }
699
700 let provenance_owner = provenance.or(first_owner)?;
701 Some(TemplateInheritContext {
702 is_test_reachable: any_reachable,
703 test_referenced_exports: combined_refs,
704 provenance_owner,
705 })
706}
707
708fn template_owner<'a>(
709 importer_id: crate::discover::FileId,
710 graph: &'a fallow_graph::graph::ModuleGraph,
711 module_by_id: &rustc_hash::FxHashMap<crate::discover::FileId, &crate::source::ModuleInfo>,
712 file_paths: &'a rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
713) -> Option<(&'a fallow_graph::graph::ModuleNode, &'a std::path::PathBuf)> {
714 let owner_node = graph.modules.get(importer_id.0 as usize)?;
715 let owner_path = *file_paths.get(&importer_id)?;
716 if !is_template_owner_path(owner_path) || graph.test_entry_points.contains(&importer_id) {
717 return None;
718 }
719 let owner_has_component = module_by_id
720 .get(&importer_id)
721 .is_some_and(|module| module.has_angular_component_template_url);
722 owner_has_component.then_some((owner_node, owner_path))
723}
724
725fn is_template_owner_path(path: &std::path::Path) -> bool {
726 path.extension()
727 .and_then(|ext| ext.to_str())
728 .is_some_and(|ext| {
729 matches!(
730 ext.to_ascii_lowercase().as_str(),
731 "ts" | "tsx" | "mts" | "cts"
732 )
733 })
734}
735
736fn build_test_referenced_exports(
741 exports: &[fallow_graph::graph::ExportSymbol],
742 test_coverage: StaticTestCoverage<'_>,
743) -> rustc_hash::FxHashSet<String> {
744 let mut set = rustc_hash::FxHashSet::default();
745 for export in exports {
746 if export.is_type_only {
747 continue;
748 }
749 let has_test_ref = test_coverage.covers_any_reference(export);
750 if has_test_ref {
751 set.insert(export.name.to_string());
752 }
753 }
754 set
755}
756
757fn collect_direct_callers(
758 graph: &fallow_graph::graph::ModuleGraph,
759 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
760) -> rustc_hash::FxHashMap<std::path::PathBuf, Vec<DirectCallerEvidence>> {
761 let mut callers_by_target = rustc_hash::FxHashMap::default();
762 for node in &graph.modules {
763 let Some(target_path) = file_paths.get(&node.file_id) else {
764 continue;
765 };
766 let mut callers = graph
767 .direct_importer_summaries(node.file_id)
768 .into_iter()
769 .filter_map(|summary| {
770 file_paths
771 .get(&summary.source)
772 .map(|caller_path| DirectCallerEvidence {
773 path: (*caller_path).clone(),
774 symbols: summary
775 .symbols
776 .into_iter()
777 .map(|symbol| DirectCallerSymbolEvidence {
778 imported: symbol.imported,
779 local: symbol.local,
780 type_only: symbol.type_only,
781 })
782 .collect(),
783 })
784 })
785 .collect::<Vec<_>>();
786 callers.sort_by(|a, b| a.path.cmp(&b.path));
787 callers.truncate(MAX_DIRECT_CALLER_EVIDENCE);
788 if !callers.is_empty() {
789 callers_by_target.insert((*target_path).clone(), callers);
790 }
791 }
792 callers_by_target
793}
794
795#[expect(
798 clippy::suboptimal_flops,
799 reason = "explicit multiplication matches the CRAP formula specification"
800)]
801fn crap_formula(cc: f64, coverage_pct: f64) -> f64 {
802 let uncovered = 1.0 - coverage_pct / 100.0;
803 cc * cc * uncovered * uncovered * uncovered + cc
804}
805
806const ANONYMOUS_FALLBACK_MAX_COLUMN_DRIFT: u32 = 16;
812
813const ALIAS_FUZZ_MAX_LINE_DRIFT: u32 = 2;
818
819#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
820struct IstanbulPosition {
821 line: u32,
822 col: u32,
823}
824
825impl IstanbulPosition {
826 const fn new(line: u32, col: u32) -> Self {
827 Self { line, col }
828 }
829
830 const fn distance_from(self, target: Self) -> (u32, u32) {
831 (
832 self.line.abs_diff(target.line),
833 self.col.abs_diff(target.col),
834 )
835 }
836}
837
838#[derive(Clone, Copy, Debug, Eq, PartialEq)]
839struct IstanbulSpan {
840 start: IstanbulPosition,
841 end: IstanbulPosition,
842}
843
844impl IstanbulSpan {
845 fn from_entry(
846 fn_entry: &oxc_coverage_instrument::FnEntry,
847 source_index: Option<&IstanbulSourceIndex<'_>>,
848 ) -> Option<Self> {
849 let start = normalized_istanbul_position(
850 fn_entry.loc.start.line,
851 fn_entry.loc.start.column,
852 source_index,
853 )?;
854 let end = normalized_istanbul_position(
855 fn_entry.loc.end.line,
856 fn_entry.loc.end.column,
857 source_index,
858 )?;
859 (start.line > 0 && end.line > 0 && start < end).then_some(Self { start, end })
860 }
861
862 fn header_from_entry(
863 fn_entry: &oxc_coverage_instrument::FnEntry,
864 source_index: Option<&IstanbulSourceIndex<'_>>,
865 ) -> Option<Self> {
866 let start = normalized_istanbul_position(
867 fn_entry.decl.start.line,
868 fn_entry.decl.start.column,
869 source_index,
870 )?;
871 let end = normalized_istanbul_position(
872 fn_entry.loc.start.line,
873 fn_entry.loc.start.column,
874 source_index,
875 )?;
876 (start.line > 0 && end.line > 0 && start < end).then_some(Self { start, end })
877 }
878
879 fn contains(self, position: IstanbulPosition) -> bool {
881 self.start <= position && position < self.end
882 }
883
884 fn strictly_contains(self, other: Self) -> bool {
885 self != other && self.start <= other.start && other.end <= self.end
886 }
887}
888
889#[derive(Clone, Copy, Debug, Eq, PartialEq)]
898struct IstanbulAlias {
899 position: IstanbulPosition,
900 primary: bool,
901}
902
903#[derive(Clone, Copy, Debug, Default)]
906struct IstanbulAliasCounts {
907 primary: usize,
908 secondary: usize,
909}
910
911impl IstanbulAliasCounts {
912 fn add(&mut self, alias: IstanbulAlias) {
913 if alias.primary {
914 self.primary += 1;
915 } else {
916 self.secondary += 1;
917 }
918 }
919
920 const fn is_unique(self, alias: IstanbulAlias) -> bool {
923 if alias.primary {
924 self.primary == 1
925 } else {
926 self.primary == 0 && self.secondary == 1
927 }
928 }
929
930 const fn has_secondary_only_collision(self) -> bool {
931 self.primary == 0 && self.secondary > 1
932 }
933}
934
935fn is_anonymous_istanbul_name(name: &str) -> bool {
936 name.starts_with("(anonymous_")
937}
938
939struct IstanbulFunctionCoverage {
940 name: String,
941 coverage_pct: f64,
942 aliases: Vec<IstanbulAlias>,
943 decl_start: IstanbulPosition,
947 header_holds_other_fn: bool,
951 header_span: Option<IstanbulSpan>,
952 body_span: Option<IstanbulSpan>,
953}
954
955impl IstanbulFunctionCoverage {
956 fn answers_to(&self, name: &str) -> bool {
959 if self.name == name {
960 return true;
961 }
962 accessor_property_name(&self.name) == Some(name)
963 }
964
965 fn nearest_alias(
966 &self,
967 target: IstanbulPosition,
968 max_column_drift: Option<u32>,
969 ) -> Option<(u32, u32)> {
970 self.aliases
971 .iter()
972 .filter_map(|alias| {
973 let distance = alias.position.distance_from(target);
974 if distance.0 > ALIAS_FUZZ_MAX_LINE_DRIFT {
975 return None;
976 }
977 if distance.0 > 0 && max_column_drift.is_some_and(|maximum| distance.1 > maximum) {
978 return None;
979 }
980 Some(distance)
981 })
982 .min()
983 }
984}
985
986pub struct IstanbulFileCoverage {
989 functions: Vec<IstanbulFunctionCoverage>,
993 alias_index: rustc_hash::FxHashMap<(String, u32, u32), usize>,
998 ambiguous_aliases: rustc_hash::FxHashSet<(String, u32, u32)>,
1002 ambiguous_anonymous_aliases: rustc_hash::FxHashSet<IstanbulPosition>,
1006 alias_lines: Vec<(u32, usize)>,
1011 header_starts: Vec<(u32, usize)>,
1014 max_header_height: u32,
1018 relocated: bool,
1022}
1023
1024fn mark_headers_holding_other_fns(functions: &mut [IstanbulFunctionCoverage]) {
1030 let mut declarations: Vec<IstanbulPosition> = functions
1031 .iter()
1032 .map(|function| function.decl_start)
1033 .collect();
1034 declarations.sort_unstable();
1035 for function in functions {
1036 let Some(span) = function.header_span else {
1037 continue;
1038 };
1039 let first = declarations.partition_point(|position| *position < span.start);
1042 let past = declarations.partition_point(|position| *position < span.end);
1043 function.header_holds_other_fn = past - first > 1;
1044 }
1045}
1046
1047struct IstanbulLineIndexes {
1051 alias_lines: Vec<(u32, usize)>,
1052 header_starts: Vec<(u32, usize)>,
1053 max_header_height: u32,
1054}
1055
1056impl IstanbulLineIndexes {
1057 fn build(functions: &[IstanbulFunctionCoverage]) -> Self {
1058 let mut alias_lines: Vec<(u32, usize)> = functions
1059 .iter()
1060 .enumerate()
1061 .flat_map(|(function_index, function)| {
1062 function
1063 .aliases
1064 .iter()
1065 .map(move |alias| (alias.position.line, function_index))
1066 })
1067 .collect();
1068 alias_lines.sort_unstable();
1069
1070 let mut header_starts: Vec<(u32, usize)> = Vec::new();
1071 let mut max_header_height = 0;
1072 for (function_index, function) in functions.iter().enumerate() {
1073 if let Some(span) = function.header_span {
1074 header_starts.push((span.start.line, function_index));
1075 max_header_height = max_header_height.max(span.end.line - span.start.line);
1078 }
1079 }
1080 header_starts.sort_unstable();
1081
1082 Self {
1083 alias_lines,
1084 header_starts,
1085 max_header_height,
1086 }
1087 }
1088}
1089
1090impl IstanbulFileCoverage {
1091 pub fn lookup_function(
1097 &self,
1098 function: &fallow_types::extract::FunctionComplexity,
1099 ) -> Option<f64> {
1100 if function.is_private_member {
1101 return None;
1102 }
1103 self.lookup(function.name.as_str(), function.line, function.col)
1104 }
1105
1106 fn new(mut functions: Vec<IstanbulFunctionCoverage>, relocated: bool) -> Self {
1107 let mut named_alias_counts: rustc_hash::FxHashMap<
1108 (String, IstanbulPosition),
1109 IstanbulAliasCounts,
1110 > = rustc_hash::FxHashMap::default();
1111 let mut anonymous_alias_counts: rustc_hash::FxHashMap<
1112 IstanbulPosition,
1113 IstanbulAliasCounts,
1114 > = rustc_hash::FxHashMap::default();
1115 for function in &functions {
1116 let is_anonymous = is_anonymous_istanbul_name(&function.name);
1117 for alias in &function.aliases {
1118 named_alias_counts
1119 .entry((function.name.clone(), alias.position))
1120 .or_default()
1121 .add(*alias);
1122 if is_anonymous {
1123 anonymous_alias_counts
1124 .entry(alias.position)
1125 .or_default()
1126 .add(*alias);
1127 }
1128 }
1129 }
1130
1131 let mut ambiguous_aliases = rustc_hash::FxHashSet::default();
1136 let mut ambiguous_anonymous_aliases = rustc_hash::FxHashSet::default();
1137 for function in &mut functions {
1138 let name = function.name.clone();
1139 let is_anonymous = is_anonymous_istanbul_name(&name);
1140 function.aliases.retain(|alias| {
1141 let named = named_alias_counts
1142 .get(&(name.clone(), alias.position))
1143 .copied()
1144 .unwrap_or_default();
1145 let anonymous = is_anonymous
1146 .then(|| anonymous_alias_counts.get(&alias.position).copied())
1147 .flatten();
1148 let unique = named.is_unique(*alias)
1149 && anonymous.is_none_or(|counts| counts.is_unique(*alias));
1150 if unique {
1151 return true;
1152 }
1153 if alias.primary {
1154 ambiguous_aliases.insert((
1155 name.clone(),
1156 alias.position.line,
1157 alias.position.col,
1158 ));
1159 if anonymous.is_some_and(|counts| counts.primary > 1) {
1160 ambiguous_anonymous_aliases.insert(alias.position);
1161 }
1162 } else {
1163 if named.has_secondary_only_collision() {
1164 ambiguous_aliases.insert((
1165 name.clone(),
1166 alias.position.line,
1167 alias.position.col,
1168 ));
1169 }
1170 if anonymous.is_some_and(IstanbulAliasCounts::has_secondary_only_collision) {
1171 ambiguous_anonymous_aliases.insert(alias.position);
1172 }
1173 }
1174 false
1175 });
1176 }
1177
1178 let mut alias_index = rustc_hash::FxHashMap::default();
1179 for (function_index, function) in functions.iter().enumerate() {
1180 for alias in &function.aliases {
1181 alias_index.insert(
1182 (
1183 function.name.clone(),
1184 alias.position.line,
1185 alias.position.col,
1186 ),
1187 function_index,
1188 );
1189 if let Some(property) = accessor_property_name(&function.name) {
1192 alias_index
1193 .entry((
1194 property.to_string(),
1195 alias.position.line,
1196 alias.position.col,
1197 ))
1198 .or_insert(function_index);
1199 }
1200 }
1201 }
1202
1203 mark_headers_holding_other_fns(&mut functions);
1204 let indexes = IstanbulLineIndexes::build(&functions);
1205
1206 Self {
1207 functions,
1208 alias_index,
1209 ambiguous_aliases,
1210 ambiguous_anonymous_aliases,
1211 alias_lines: indexes.alias_lines,
1212 header_starts: indexes.header_starts,
1213 max_header_height: indexes.max_header_height,
1214 relocated,
1215 }
1216 }
1217
1218 fn fuzz_window(&self, target: IstanbulPosition) -> Vec<usize> {
1230 let low = target.line.saturating_sub(ALIAS_FUZZ_MAX_LINE_DRIFT);
1231 let high = target.line.saturating_add(ALIAS_FUZZ_MAX_LINE_DRIFT);
1232 let first = self.alias_lines.partition_point(|(line, _)| *line < low);
1233 let past = self.alias_lines.partition_point(|(line, _)| *line <= high);
1234 let mut window: Vec<usize> = self.alias_lines[first..past]
1235 .iter()
1236 .map(|(_, function_index)| *function_index)
1237 .collect();
1238 window.sort_unstable();
1239 window.dedup();
1240 window
1241 }
1242
1243 pub(crate) fn lookup(&self, name: &str, line: u32, col: u32) -> Option<f64> {
1276 let exact_key = (name.to_string(), line, col);
1277 if self.ambiguous_aliases.contains(&exact_key) {
1278 return None;
1279 }
1280 if let Some(&function_index) = self.alias_index.get(&exact_key) {
1281 return Some(self.functions[function_index].coverage_pct);
1282 }
1283
1284 let target = IstanbulPosition::new(line, col);
1285 let window = self.fuzz_window(target);
1286 let signature_owners = self.header_spans_containing(target);
1292 if let Some(function) = window
1293 .iter()
1294 .copied()
1295 .filter(|function_index| self.functions[*function_index].answers_to(name))
1296 .filter_map(|function_index| {
1297 let function = &self.functions[function_index];
1298 function
1299 .nearest_alias(target, None)
1300 .map(|distance| (distance, function_index))
1301 })
1302 .filter(|(distance, function_index)| {
1303 *distance == (0, 0)
1304 || !self.foreign_signature_blocks(&signature_owners, *function_index, target)
1305 })
1306 .min_by_key(|(distance, _)| *distance)
1307 .map(|(_, function_index)| &self.functions[function_index])
1308 {
1309 return Some(function.coverage_pct);
1310 }
1311 if self.relocated
1312 && let Some(pct) = self.unambiguous_named_pct(name)
1313 {
1314 return Some(pct);
1315 }
1316 if self.ambiguous_anonymous_aliases.contains(&target) {
1317 return self.unique_anonymous_header_match(&signature_owners);
1318 }
1319
1320 let mut nearest_distance: Option<(u32, u32)> = None;
1321 let mut nearest_functions = Vec::new();
1322 for function_index in window {
1323 let function = &self.functions[function_index];
1324 if !is_anonymous_istanbul_name(&function.name) {
1325 continue;
1326 }
1327 let Some(distance) =
1328 function.nearest_alias(target, Some(ANONYMOUS_FALLBACK_MAX_COLUMN_DRIFT))
1329 else {
1330 continue;
1331 };
1332 if distance != (0, 0)
1336 && self.foreign_signature_blocks(&signature_owners, function_index, target)
1337 {
1338 continue;
1339 }
1340 match nearest_distance {
1341 None => {
1342 nearest_distance = Some(distance);
1343 nearest_functions.push(function_index);
1344 }
1345 Some(previous) if distance < previous => {
1346 nearest_distance = Some(distance);
1347 nearest_functions.clear();
1348 nearest_functions.push(function_index);
1349 }
1350 Some(previous) if distance == previous => {
1351 nearest_functions.push(function_index);
1352 }
1353 Some(_) => {}
1354 }
1355 }
1356 let established_match = match nearest_functions.as_slice() {
1357 [] => None,
1358 [function_index] => Some(self.functions[*function_index].coverage_pct),
1359 tied => self.innermost_anonymous_match(tied, target),
1360 };
1361 if established_match.is_some() {
1362 return established_match;
1363 }
1364
1365 self.unique_anonymous_header_match(&signature_owners)
1366 }
1367
1368 fn header_spans_containing(&self, target: IstanbulPosition) -> Vec<usize> {
1373 let low = target.line.saturating_sub(self.max_header_height);
1374 let first = self.header_starts.partition_point(|(line, _)| *line < low);
1375 let past = self
1376 .header_starts
1377 .partition_point(|(line, _)| *line <= target.line);
1378 self.header_starts[first..past]
1379 .iter()
1380 .filter(|(_, function_index)| {
1381 self.functions[*function_index]
1382 .header_span
1383 .is_some_and(|span| span.contains(target))
1384 })
1385 .map(|(_, function_index)| *function_index)
1386 .collect()
1387 }
1388
1389 fn foreign_signature_blocks(
1400 &self,
1401 owners: &[usize],
1402 candidate: usize,
1403 target: IstanbulPosition,
1404 ) -> bool {
1405 let function = &self.functions[candidate];
1406 if function
1407 .header_span
1408 .is_some_and(|span| span.contains(target))
1409 || function.body_span.is_some_and(|span| span.contains(target))
1410 {
1411 return false;
1412 }
1413 owners.iter().any(|&owner| {
1414 owner != candidate
1415 && self.functions[owner]
1416 .header_span
1417 .is_some_and(|span| span.contains(function.decl_start))
1418 })
1419 }
1420
1421 fn unique_anonymous_header_match(&self, signature_owners: &[usize]) -> Option<f64> {
1433 let [function_index] = signature_owners else {
1434 return None;
1435 };
1436 let function = &self.functions[*function_index];
1437 if !is_anonymous_istanbul_name(&function.name) || function.header_holds_other_fn {
1438 return None;
1439 }
1440 Some(function.coverage_pct)
1441 }
1442
1443 fn innermost_anonymous_match(&self, tied: &[usize], target: IstanbulPosition) -> Option<f64> {
1444 let containing: Option<Vec<_>> = tied
1445 .iter()
1446 .map(|&function_index| {
1447 self.functions[function_index]
1448 .body_span
1449 .filter(|span| span.contains(target))
1450 .map(|span| (function_index, span))
1451 })
1452 .collect();
1453 let containing = containing?;
1454
1455 let mut winner = None;
1456 for &(function_index, candidate_span) in &containing {
1457 let is_strictly_innermost = containing.iter().all(|&(other_index, other_span)| {
1458 other_index == function_index || other_span.strictly_contains(candidate_span)
1459 });
1460 if !is_strictly_innermost {
1461 continue;
1462 }
1463 if winner.replace(function_index).is_some() {
1464 return None;
1465 }
1466 }
1467 winner.map(|function_index| self.functions[function_index].coverage_pct)
1468 }
1469
1470 fn unambiguous_named_pct(&self, name: &str) -> Option<f64> {
1475 let mut found: Option<f64> = None;
1476 for function in &self.functions {
1477 if !function.answers_to(name) {
1478 continue;
1479 }
1480 match found {
1481 None => found = Some(function.coverage_pct),
1482 Some(previous) if previous.to_bits() == function.coverage_pct.to_bits() => {}
1483 Some(_) => return None,
1484 }
1485 }
1486 found
1487 }
1488}
1489
1490pub struct IstanbulCoverage {
1492 files: rustc_hash::FxHashMap<std::path::PathBuf, IstanbulFileCoverage>,
1493}
1494
1495impl IstanbulCoverage {
1496 pub fn get(&self, path: &std::path::Path) -> Option<&IstanbulFileCoverage> {
1498 self.files.get(path)
1499 }
1500
1501 pub fn file_count(&self) -> usize {
1503 self.files.len()
1504 }
1505}
1506
1507enum CrapCoverageResolution<'a> {
1515 TemplateInherited(&'a TemplateInheritContext),
1516 Istanbul {
1517 file_coverage: Option<&'a IstanbulFileCoverage>,
1518 },
1519 StaticEstimated,
1520}
1521
1522fn resolve_crap_coverage<'a>(
1523 template_inherit: Option<&'a TemplateInheritContext>,
1524 istanbul_coverage: Option<&'a IstanbulCoverage>,
1525 path: &std::path::Path,
1526) -> CrapCoverageResolution<'a> {
1527 if let Some(inherit_ctx) = template_inherit {
1528 CrapCoverageResolution::TemplateInherited(inherit_ctx)
1529 } else if let Some(istanbul) = istanbul_coverage {
1530 let canonical = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1531 CrapCoverageResolution::Istanbul {
1532 file_coverage: istanbul.get(&canonical),
1533 }
1534 } else {
1535 CrapCoverageResolution::StaticEstimated
1536 }
1537}
1538
1539pub fn auto_detect_coverage(root: &std::path::Path) -> Option<std::path::PathBuf> {
1547 let candidates = [
1548 root.join("coverage/coverage-final.json"),
1549 root.join(".nyc_output/coverage-final.json"),
1550 ];
1551 candidates.into_iter().find(|p| p.is_file())
1552}
1553
1554pub fn resolve_relative_to_root(
1560 path: &std::path::Path,
1561 project_root: Option<&std::path::Path>,
1562) -> std::path::PathBuf {
1563 if fallow_types::path_util::is_absolute_path_any_platform(path) {
1564 return path.to_path_buf();
1565 }
1566 match project_root {
1567 Some(root) => root.join(path),
1568 None => path.to_path_buf(),
1569 }
1570}
1571
1572#[cfg(test)]
1587fn load_istanbul_coverage(
1588 path: &std::path::Path,
1589 coverage_root: Option<&std::path::Path>,
1590 project_root: Option<&std::path::Path>,
1591 relocated: bool,
1592) -> Result<IstanbulCoverage, String> {
1593 load_istanbul_coverage_for_sources(path, coverage_root, project_root, None, relocated)
1594}
1595
1596pub(super) fn load_istanbul_coverage_for_sources(
1597 path: &std::path::Path,
1598 coverage_root: Option<&std::path::Path>,
1599 project_root: Option<&std::path::Path>,
1600 discovered_sources: Option<&rustc_hash::FxHashSet<std::path::PathBuf>>,
1601 relocated: bool,
1602) -> Result<IstanbulCoverage, String> {
1603 super::validate_coverage_root_absolute(coverage_root)?;
1604 let resolved = resolve_relative_to_root(path, project_root);
1605 let file_path = if resolved.is_dir() {
1606 let candidate = resolved.join("coverage-final.json");
1607 if candidate.is_file() {
1608 candidate
1609 } else {
1610 return Err(format!(
1611 "no coverage-final.json found in {}",
1612 resolved.display()
1613 ));
1614 }
1615 } else {
1616 resolved
1617 };
1618
1619 let json = std::fs::read_to_string(&file_path)
1620 .map_err(|e| format!("failed to read coverage file {}: {e}", file_path.display()))?;
1621
1622 let raw: std::collections::BTreeMap<String, oxc_coverage_instrument::FileCoverage> =
1623 parse_coverage_map_tolerantly(&json).map_err(|e| {
1624 format!(
1625 "failed to parse coverage data from {}: {e}",
1626 file_path.display()
1627 )
1628 })?;
1629
1630 let mut files = rustc_hash::FxHashMap::default();
1631 for file_cov in raw.values() {
1632 let raw_path = resolve_relative_to_root(std::path::Path::new(&file_cov.path), project_root);
1637 let file_path = if let (Some(cov_root), Some(proj_root)) = (coverage_root, project_root) {
1638 rebase_coverage_path(raw_path, cov_root, proj_root)
1639 } else {
1640 raw_path
1641 };
1642 let canonical = dunce::canonicalize(&file_path).unwrap_or(file_path);
1643 let source = read_discovered_source(&canonical, discovered_sources, relocated);
1644 let source_index = source
1645 .as_deref()
1646 .map(|source| IstanbulSourceIndex::new(source, &canonical));
1647
1648 let mut functions = Vec::with_capacity(file_cov.fn_map.len());
1649 for (fn_id, fn_entry) in &file_cov.fn_map {
1650 let coverage_pct = compute_function_statement_coverage(file_cov, fn_id, fn_entry);
1651 if let Some(function) =
1652 istanbul_function_coverage(fn_entry, coverage_pct, source_index.as_ref())
1653 {
1654 functions.push(function);
1655 }
1656 }
1657
1658 files.insert(canonical, IstanbulFileCoverage::new(functions, relocated));
1659 }
1660
1661 Ok(IstanbulCoverage { files })
1662}
1663
1664#[expect(
1665 clippy::filetype_is_file,
1666 reason = "coverage provenance must admit regular files and reject every special file type"
1667)]
1668fn read_discovered_source(
1669 path: &std::path::Path,
1670 discovered_sources: Option<&rustc_hash::FxHashSet<std::path::PathBuf>>,
1671 relocated: bool,
1672) -> Option<String> {
1673 if relocated || !discovered_sources.is_some_and(|sources| sources.contains(path)) {
1674 return None;
1675 }
1676 if std::fs::symlink_metadata(path).ok()?.file_type().is_file() {
1677 std::fs::read_to_string(path).ok()
1678 } else {
1679 None
1680 }
1681}
1682
1683fn parse_coverage_map_tolerantly(
1692 json: &str,
1693) -> Result<std::collections::BTreeMap<String, oxc_coverage_instrument::FileCoverage>, String> {
1694 match oxc_coverage_instrument::parse_coverage_map(json) {
1695 Ok(raw) => Ok(raw),
1696 Err(strict_error) => {
1697 let mut value: serde_json::Value =
1698 serde_json::from_str(json).map_err(|_| strict_error.to_string())?;
1699 if !clamp_negative_positions(&mut value) {
1700 return Err(strict_error.to_string());
1701 }
1702 serde_json::from_value(value).map_err(|_| strict_error.to_string())
1703 }
1704 }
1705}
1706
1707fn clamp_negative_positions(value: &mut serde_json::Value) -> bool {
1711 match value {
1712 serde_json::Value::Object(entries) => {
1713 let mut clamped = false;
1714 for (key, child) in entries.iter_mut() {
1715 if matches!(key.as_str(), "line" | "column")
1716 && child.as_i64().is_some_and(|number| number < 0)
1717 {
1718 *child = serde_json::Value::from(0);
1719 clamped = true;
1720 continue;
1721 }
1722 clamped |= clamp_negative_positions(child);
1723 }
1724 clamped
1725 }
1726 serde_json::Value::Array(items) => items.iter_mut().fold(false, |clamped, item| {
1727 clamped | clamp_negative_positions(item)
1728 }),
1729 _ => false,
1730 }
1731}
1732
1733fn accessor_property_name(name: &str) -> Option<&str> {
1741 let property = name
1742 .strip_prefix("get ")
1743 .or_else(|| name.strip_prefix("set "))?;
1744 (!property.is_empty() && !property.contains(' ')).then_some(property)
1745}
1746
1747fn rebase_coverage_path(
1755 raw_path: std::path::PathBuf,
1756 coverage_root: &std::path::Path,
1757 project_root: &std::path::Path,
1758) -> std::path::PathBuf {
1759 if let Ok(rel) = raw_path.strip_prefix(coverage_root) {
1760 return project_root.join(rel);
1761 }
1762 if let Ok(canonical) = dunce::canonicalize(&raw_path)
1763 && let Ok(rel) = canonical.strip_prefix(coverage_root)
1764 {
1765 return project_root.join(rel);
1766 }
1767 raw_path
1768}
1769
1770fn istanbul_function_coverage(
1771 fn_entry: &oxc_coverage_instrument::FnEntry,
1772 coverage_pct: f64,
1773 source_index: Option<&IstanbulSourceIndex<'_>>,
1774) -> Option<IstanbulFunctionCoverage> {
1775 let body_span = IstanbulSpan::from_entry(fn_entry, source_index);
1776 let header_span = IstanbulSpan::header_from_entry(fn_entry, source_index);
1777 let decl_start = normalized_istanbul_position(
1778 fn_entry.decl.start.line,
1779 fn_entry.decl.start.column,
1780 source_index,
1781 )?;
1782 let effective_position = normalized_istanbul_position(
1783 effective_istanbul_fn_line(fn_entry),
1784 fn_entry.decl.start.column,
1785 source_index,
1786 );
1787 let candidates = [
1788 effective_position.map(|position| IstanbulAlias {
1789 position,
1790 primary: true,
1791 }),
1792 Some(IstanbulAlias {
1793 position: decl_start,
1794 primary: true,
1795 }),
1796 body_span.map(|span| IstanbulAlias {
1797 position: span.start,
1798 primary: false,
1799 }),
1800 named_function_syntax_alias(fn_entry, source_index),
1801 ];
1802 let mut aliases: Vec<IstanbulAlias> = Vec::with_capacity(candidates.len());
1803 for candidate in candidates.into_iter().flatten() {
1804 if !aliases
1805 .iter()
1806 .any(|alias| alias.position == candidate.position)
1807 {
1808 aliases.push(candidate);
1809 }
1810 }
1811
1812 Some(IstanbulFunctionCoverage {
1813 name: fn_entry.name.clone(),
1814 coverage_pct,
1815 aliases,
1816 decl_start,
1817 header_holds_other_fn: false,
1818 header_span,
1819 body_span,
1820 })
1821}
1822
1823struct IstanbulSourceIndex<'a> {
1826 source: &'a str,
1827 line_starts: Vec<usize>,
1828 non_ascii_lines: rustc_hash::FxHashMap<usize, Utf16LineIndex>,
1829 named_function_starts: rustc_hash::FxHashMap<usize, usize>,
1830}
1831
1832struct Utf16LineIndex {
1833 utf16_len: u32,
1834 byte_len: usize,
1835 checkpoints: Vec<Utf16Checkpoint>,
1836}
1837
1838struct Utf16Checkpoint {
1839 utf16_start: u32,
1840 utf16_end: u32,
1841 byte_end: usize,
1842}
1843
1844impl<'a> IstanbulSourceIndex<'a> {
1845 fn new(source: &'a str, path: &std::path::Path) -> Self {
1846 let mut line_starts = vec![0];
1847 line_starts.extend(
1848 source
1849 .bytes()
1850 .enumerate()
1851 .filter_map(|(index, byte)| (byte == b'\n').then_some(index + 1)),
1852 );
1853 let non_ascii_lines = utf16_line_indexes(source, &line_starts);
1854
1855 let named_function_starts = named_function_starts_from_clean_parse(source, path);
1856
1857 Self {
1858 source,
1859 line_starts,
1860 non_ascii_lines,
1861 named_function_starts,
1862 }
1863 }
1864
1865 fn byte_position(&self, line: u32, utf16_column: u32) -> Option<IstanbulPosition> {
1866 let line_index = usize::try_from(line.checked_sub(1)?).ok()?;
1867 let line_start = *self.line_starts.get(line_index)?;
1868 let line_end = self
1869 .line_starts
1870 .get(line_index + 1)
1871 .copied()
1872 .map_or(self.source.len(), |next_start| next_start - 1);
1873 let line_source = self.source.get(line_start..line_end)?;
1874 let byte_column = if let Some(index) = self.non_ascii_lines.get(&line_index) {
1875 index.byte_column(utf16_column)?
1876 } else {
1877 let byte_column = usize::try_from(utf16_column).ok()?;
1878 (byte_column <= line_source.len()).then_some(byte_column)?
1879 };
1880 Some(IstanbulPosition::new(
1881 line,
1882 u32::try_from(byte_column).ok()?,
1883 ))
1884 }
1885
1886 fn absolute_offset(&self, line: u32, utf16_column: u32) -> Option<usize> {
1887 let position = self.byte_position(line, utf16_column)?;
1888 let line_index = usize::try_from(position.line.checked_sub(1)?).ok()?;
1889 self.line_starts
1890 .get(line_index)?
1891 .checked_add(position.col as usize)
1892 }
1893
1894 fn position_at_offset(&self, offset: usize) -> Option<IstanbulPosition> {
1895 if offset > self.source.len() {
1896 return None;
1897 }
1898 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
1899 Some(IstanbulPosition::new(
1900 u32::try_from(line_index + 1).ok()?,
1901 u32::try_from(offset.checked_sub(self.line_starts[line_index])?).ok()?,
1902 ))
1903 }
1904
1905 fn named_function_start(
1906 &self,
1907 fn_entry: &oxc_coverage_instrument::FnEntry,
1908 ) -> Option<IstanbulPosition> {
1909 let declaration_offset =
1910 self.absolute_offset(fn_entry.decl.start.line, fn_entry.decl.start.column)?;
1911 let syntax_offset = *self.named_function_starts.get(&declaration_offset)?;
1912 self.position_at_offset(syntax_offset)
1913 }
1914}
1915
1916impl Utf16LineIndex {
1917 fn byte_column(&self, utf16_column: u32) -> Option<usize> {
1918 if utf16_column > self.utf16_len {
1919 return None;
1920 }
1921 let completed = self
1922 .checkpoints
1923 .partition_point(|checkpoint| checkpoint.utf16_end <= utf16_column);
1924 if let Some(next) = self.checkpoints.get(completed)
1925 && utf16_column > next.utf16_start
1926 {
1927 return None;
1928 }
1929 let (utf16_base, byte_base) = completed
1930 .checked_sub(1)
1931 .and_then(|index| self.checkpoints.get(index))
1932 .map_or((0, 0), |checkpoint| {
1933 (checkpoint.utf16_end, checkpoint.byte_end)
1934 });
1935 let ascii_width = usize::try_from(utf16_column.checked_sub(utf16_base)?).ok()?;
1936 let byte_column = byte_base.checked_add(ascii_width)?;
1937 (byte_column <= self.byte_len).then_some(byte_column)
1938 }
1939}
1940
1941fn utf16_line_indexes(
1942 source: &str,
1943 line_starts: &[usize],
1944) -> rustc_hash::FxHashMap<usize, Utf16LineIndex> {
1945 let mut indexes = rustc_hash::FxHashMap::default();
1946 for (line_index, &line_start) in line_starts.iter().enumerate() {
1947 let line_end = line_starts
1948 .get(line_index + 1)
1949 .copied()
1950 .map_or(source.len(), |next_start| next_start - 1);
1951 let Some(line) = source.get(line_start..line_end) else {
1952 continue;
1953 };
1954 if line.is_ascii() {
1955 continue;
1956 }
1957 let mut utf16_column = 0_u32;
1958 let mut checkpoints = Vec::new();
1959 for (byte_column, character) in line.char_indices() {
1960 let utf16_width = character.len_utf16() as u32;
1961 if !character.is_ascii() {
1962 checkpoints.push(Utf16Checkpoint {
1963 utf16_start: utf16_column,
1964 utf16_end: utf16_column.saturating_add(utf16_width),
1965 byte_end: byte_column.saturating_add(character.len_utf8()),
1966 });
1967 }
1968 utf16_column = utf16_column.saturating_add(utf16_width);
1969 }
1970 indexes.insert(
1971 line_index,
1972 Utf16LineIndex {
1973 utf16_len: utf16_column,
1974 byte_len: line.len(),
1975 checkpoints,
1976 },
1977 );
1978 }
1979 indexes
1980}
1981
1982fn named_function_starts_from_clean_parse(
1983 source: &str,
1984 path: &std::path::Path,
1985) -> rustc_hash::FxHashMap<usize, usize> {
1986 let source_type = match path.extension().and_then(|extension| extension.to_str()) {
1987 Some("gts") => oxc_span::SourceType::ts(),
1988 Some("gjs") => oxc_span::SourceType::mjs(),
1989 _ => oxc_span::SourceType::from_path(path).unwrap_or_default(),
1990 };
1991 if let Some(starts) = collect_named_function_starts(source, source_type) {
1992 return starts;
1993 }
1994 if source_type.is_jsx() {
1995 return rustc_hash::FxHashMap::default();
1996 }
1997 let jsx_source_type = if source_type.is_typescript() {
1998 oxc_span::SourceType::tsx()
1999 } else {
2000 oxc_span::SourceType::jsx()
2001 };
2002 collect_named_function_starts(source, jsx_source_type).unwrap_or_default()
2003}
2004
2005fn collect_named_function_starts(
2006 source: &str,
2007 source_type: oxc_span::SourceType,
2008) -> Option<rustc_hash::FxHashMap<usize, usize>> {
2009 let allocator = oxc_allocator::Allocator::default();
2010 let parsed = oxc_parser::Parser::new(&allocator, source, source_type).parse();
2011 if parsed.panicked || !parsed.errors.is_empty() {
2012 return None;
2013 }
2014 let mut starts = rustc_hash::FxHashMap::default();
2015 let mut collector = NamedFunctionSyntaxCollector {
2016 starts: &mut starts,
2017 };
2018 oxc_ast_visit::Visit::visit_program(&mut collector, &parsed.program);
2019 Some(starts)
2020}
2021
2022struct NamedFunctionSyntaxCollector<'a> {
2023 starts: &'a mut rustc_hash::FxHashMap<usize, usize>,
2024}
2025
2026impl<'ast> oxc_ast_visit::Visit<'ast> for NamedFunctionSyntaxCollector<'_> {
2027 fn visit_function(
2028 &mut self,
2029 function: &oxc_ast::ast::Function<'ast>,
2030 flags: oxc_syntax::scope::ScopeFlags,
2031 ) {
2032 if let Some(identifier) = &function.id
2033 && let (Ok(identifier_start), Ok(syntax_start)) = (
2034 usize::try_from(identifier.span.start),
2035 usize::try_from(function.span.start),
2036 )
2037 {
2038 self.starts.insert(identifier_start, syntax_start);
2039 }
2040 oxc_ast_visit::walk::walk_function(self, function, flags);
2041 }
2042}
2043
2044fn normalized_istanbul_position(
2045 line: u32,
2046 column: u32,
2047 source_index: Option<&IstanbulSourceIndex<'_>>,
2048) -> Option<IstanbulPosition> {
2049 match source_index {
2050 Some(index) => index.byte_position(line, column),
2051 None => Some(IstanbulPosition::new(line, column)),
2052 }
2053}
2054
2055fn named_function_syntax_alias(
2061 fn_entry: &oxc_coverage_instrument::FnEntry,
2062 source_index: Option<&IstanbulSourceIndex<'_>>,
2063) -> Option<IstanbulAlias> {
2064 if is_anonymous_istanbul_name(&fn_entry.name) {
2065 return None;
2066 }
2067 Some(IstanbulAlias {
2068 position: source_index?.named_function_start(fn_entry)?,
2069 primary: true,
2070 })
2071}
2072
2073fn effective_istanbul_fn_line(fn_entry: &oxc_coverage_instrument::FnEntry) -> u32 {
2074 if fn_entry.line > 0 {
2075 fn_entry.line
2076 } else {
2077 fn_entry.decl.start.line
2078 }
2079}
2080
2081fn compute_function_statement_coverage(
2088 file_cov: &oxc_coverage_instrument::FileCoverage,
2089 fn_id: &str,
2090 fn_entry: &oxc_coverage_instrument::FnEntry,
2091) -> f64 {
2092 let fn_start_line = fn_entry.loc.start.line;
2093 let fn_start_col = fn_entry.loc.start.column;
2094 let fn_end_line = fn_entry.loc.end.line;
2095 let fn_end_col = fn_entry.loc.end.column;
2096
2097 let mut total = 0u32;
2098 let mut covered = 0u32;
2099
2100 for (stmt_id, stmt_loc) in &file_cov.statement_map {
2101 let after_start = stmt_loc.start.line > fn_start_line
2102 || (stmt_loc.start.line == fn_start_line && stmt_loc.start.column >= fn_start_col);
2103 let before_end = stmt_loc.end.line < fn_end_line
2104 || (stmt_loc.end.line == fn_end_line && stmt_loc.end.column <= fn_end_col);
2105
2106 if after_start && before_end {
2107 total += 1;
2108 if file_cov.s.get(stmt_id).copied().unwrap_or(0) > 0 {
2109 covered += 1;
2110 }
2111 }
2112 }
2113
2114 if total == 0 {
2115 let hit = file_cov.f.get(fn_id).copied().unwrap_or(0);
2116 if hit > 0 { 100.0 } else { 0.0 }
2117 } else {
2118 f64::from(covered) / f64::from(total) * 100.0
2119 }
2120}
2121
2122fn count_unused_exports_by_path(
2127 unused_exports: &[crate::results::UnusedExportFinding],
2128) -> rustc_hash::FxHashMap<&std::path::Path, usize> {
2129 let mut map: rustc_hash::FxHashMap<&std::path::Path, usize> = rustc_hash::FxHashMap::default();
2130 for exp in unused_exports {
2131 *map.entry(exp.export.path.as_path()).or_default() += 1;
2132 }
2133 map
2134}
2135
2136fn compute_maintainability_index(
2156 complexity_density: f64,
2157 dead_code_ratio: f64,
2158 fan_out: usize,
2159 lines: u32,
2160) -> f64 {
2161 let dampening = (f64::from(lines) / fallow_output::MI_DENSITY_MIN_LINES).min(1.0);
2162 let fan_out_penalty = ((fan_out as f64).ln_1p() * 4.0).min(15.0);
2163 #[expect(
2164 clippy::suboptimal_flops,
2165 reason = "formula matches documented specification"
2166 )]
2167 let score = 100.0
2168 - (complexity_density * 30.0 * dampening)
2169 - (dead_code_ratio * 20.0)
2170 - fan_out_penalty;
2171 score.clamp(0.0, 100.0)
2172}
2173
2174fn file_score_structural_concern(score: &FileHealthScore) -> f64 {
2175 (100.0 - score.maintainability_index).clamp(0.0, 100.0)
2176}
2177
2178#[must_use]
2184pub fn file_score_fully_crap_exempt(score: &FileHealthScore, max_crap_threshold: f64) -> bool {
2185 max_crap_threshold <= 0.0 || (score.crap_above_threshold == 0 && score.crap_exempted > 0)
2186}
2187
2188fn file_score_crap_concern(score: &FileHealthScore, max_crap_threshold: f64) -> f64 {
2195 if file_score_fully_crap_exempt(score, max_crap_threshold) {
2196 return 0.0;
2197 }
2198 let crap_max = score.crap_max;
2199 let t = score.crap_effective_threshold.unwrap_or(max_crap_threshold);
2200 let half = t / 2.0;
2201 let saturation = t * 10.0 / 3.0;
2202 if crap_max <= 0.0 {
2203 0.0
2204 } else if crap_max < half {
2205 (crap_max / half) * 45.0
2206 } else if crap_max < t {
2207 ((crap_max - half) / half).mul_add(30.0, 45.0)
2208 } else if crap_max < saturation {
2209 ((crap_max - t) / (saturation - t)).mul_add(25.0, 75.0)
2210 } else {
2211 100.0
2212 }
2213}
2214
2215fn file_score_triage_concern(score: &FileHealthScore, max_crap_threshold: f64) -> f64 {
2216 file_score_structural_concern(score).max(file_score_crap_concern(score, max_crap_threshold))
2217}
2218
2219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2225pub enum FileScoreConcern {
2226 Structural,
2228 Risk,
2230}
2231
2232impl FileScoreConcern {
2233 pub const fn label(self) -> &'static str {
2235 match self {
2236 Self::Structural => "structure",
2237 Self::Risk => "risk",
2238 }
2239 }
2240}
2241
2242pub fn file_score_concern_axis(
2251 score: &FileHealthScore,
2252 max_crap_threshold: f64,
2253) -> FileScoreConcern {
2254 let crap_concern = file_score_crap_concern(score, max_crap_threshold);
2255 if crap_concern <= 0.0 {
2256 FileScoreConcern::Structural
2257 } else if crap_concern >= file_score_structural_concern(score) {
2258 FileScoreConcern::Risk
2259 } else {
2260 FileScoreConcern::Structural
2261 }
2262}
2263
2264fn compare_file_score_triage(
2265 a: &FileHealthScore,
2266 b: &FileHealthScore,
2267 max_crap_threshold: f64,
2268) -> std::cmp::Ordering {
2269 file_score_triage_concern(b, max_crap_threshold)
2270 .total_cmp(&file_score_triage_concern(a, max_crap_threshold))
2271 .then_with(|| b.crap_max.total_cmp(&a.crap_max))
2272 .then_with(|| a.maintainability_index.total_cmp(&b.maintainability_index))
2273 .then_with(|| a.path.cmp(&b.path))
2274}
2275
2276#[derive(Clone, Copy)]
2279pub(super) struct FileScoreComputeInput<'a> {
2280 pub(super) modules: &'a [crate::source::ModuleInfo],
2281 pub(super) file_paths:
2282 &'a rustc_hash::FxHashMap<crate::discover::FileId, &'a std::path::PathBuf>,
2283 pub(super) changed_files: Option<&'a rustc_hash::FxHashSet<std::path::PathBuf>>,
2284 pub(super) istanbul_coverage: Option<&'a IstanbulCoverage>,
2285 pub(super) root: &'a std::path::Path,
2286 pub(super) crap_thresholds: CrapScoreThresholds<'a>,
2287}
2288
2289pub(super) fn compute_file_scores(
2295 input: FileScoreComputeInput<'_>,
2296 analysis_output: crate::results::DeadCodeAnalysisArtifacts,
2297) -> Result<FileScoreOutput, String> {
2298 let FileScoreComputeInput {
2299 modules,
2300 file_paths,
2301 changed_files,
2302 istanbul_coverage,
2303 root,
2304 crap_thresholds,
2305 } = input;
2306 let retained_graph = analysis_output.graph.ok_or("graph not available")?;
2307 let test_coverage = retained_graph.static_test_coverage();
2308 let graph = retained_graph.as_graph();
2309 let results = &analysis_output.results;
2310
2311 let circular_files = collect_circular_files(results);
2312 let top_complex_fns = collect_top_complex_fns(modules, file_paths);
2313 let cycle_members = collect_cycle_members(results);
2314 let direct_callers = collect_direct_callers(graph, file_paths);
2315 let unused_export_names = collect_unused_export_names(results);
2316
2317 let unused_files: rustc_hash::FxHashSet<&std::path::Path> = results
2318 .unused_files
2319 .iter()
2320 .map(|f| f.file.path.as_path())
2321 .collect();
2322
2323 let unused_exports_by_path = count_unused_exports_by_path(&results.unused_exports);
2324
2325 let FileScoreCoverageSetup {
2326 module_by_id,
2327 coverage,
2328 } = prepare_file_score_coverage_setup(modules, file_paths, results, graph, test_coverage, root);
2329
2330 let template_inherit =
2331 build_template_inherit_contexts(graph, test_coverage, &module_by_id, file_paths);
2332
2333 let mut acc = accumulate_file_scores(
2334 unused_export_names,
2335 &FileScoreLoopCtx {
2336 graph,
2337 test_coverage,
2338 file_paths,
2339 module_by_id: &module_by_id,
2340 unused_files: &unused_files,
2341 unused_exports_by_path: &unused_exports_by_path,
2342 template_inherit: &template_inherit,
2343 istanbul_coverage,
2344 root,
2345 crap_thresholds,
2346 },
2347 );
2348 acc.scores = finalize_file_score_list(
2349 acc.scores,
2350 changed_files,
2351 crap_thresholds.resolver.global.crap,
2352 );
2353
2354 Ok(build_file_score_output(FileScoreOutputParts {
2355 graph,
2356 file_paths,
2357 results,
2358 scores: acc.scores,
2359 coverage,
2360 circular_files,
2361 top_complex_fns,
2362 entry_points: acc.entry_points,
2363 value_export_counts: acc.value_export_counts,
2364 unused_export_names: acc.unused_export_names,
2365 cycle_members,
2366 direct_callers,
2367 istanbul_matched: acc.istanbul_matched,
2368 istanbul_total: acc.istanbul_total,
2369 istanbul_files_joined: acc.istanbul_files_joined,
2370 istanbul_files_total: acc.istanbul_files_total,
2371 per_function_crap: acc.per_function_crap,
2372 template_inherit,
2373 }))
2374}
2375
2376struct FileScoreLoopCtx<'a> {
2378 graph: &'a fallow_graph::graph::ModuleGraph,
2379 test_coverage: StaticTestCoverage<'a>,
2380 file_paths: &'a rustc_hash::FxHashMap<crate::discover::FileId, &'a std::path::PathBuf>,
2381 module_by_id: &'a rustc_hash::FxHashMap<crate::discover::FileId, &'a crate::source::ModuleInfo>,
2382 unused_files: &'a rustc_hash::FxHashSet<&'a std::path::Path>,
2383 unused_exports_by_path: &'a rustc_hash::FxHashMap<&'a std::path::Path, usize>,
2384 template_inherit: &'a rustc_hash::FxHashMap<crate::discover::FileId, TemplateInheritContext>,
2385 istanbul_coverage: Option<&'a IstanbulCoverage>,
2386 root: &'a std::path::Path,
2389 crap_thresholds: CrapScoreThresholds<'a>,
2390}
2391
2392struct FileScoreAccumulator {
2394 scores: Vec<FileHealthScore>,
2395 entry_points: rustc_hash::FxHashSet<std::path::PathBuf>,
2396 value_export_counts: rustc_hash::FxHashMap<std::path::PathBuf, usize>,
2397 unused_export_names: rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>>,
2398 per_function_crap: rustc_hash::FxHashMap<std::path::PathBuf, Vec<PerFunctionCrap>>,
2399 istanbul_matched: usize,
2400 istanbul_files_joined: usize,
2401 istanbul_files_total: usize,
2402 istanbul_total: usize,
2403}
2404
2405impl FileScoreAccumulator {
2406 fn with_capacity(modules: usize) -> Self {
2408 FileScoreAccumulator {
2409 scores: Vec::with_capacity(modules),
2410 entry_points: rustc_hash::FxHashSet::default(),
2411 value_export_counts: rustc_hash::FxHashMap::default(),
2412 unused_export_names: rustc_hash::FxHashMap::default(),
2413 per_function_crap: rustc_hash::FxHashMap::default(),
2414 istanbul_matched: 0,
2415 istanbul_total: 0,
2416 istanbul_files_joined: 0,
2417 istanbul_files_total: 0,
2418 }
2419 }
2420}
2421
2422fn accumulate_file_scores(
2425 unused_export_names: rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>>,
2426 ctx: &FileScoreLoopCtx<'_>,
2427) -> FileScoreAccumulator {
2428 let mut acc = FileScoreAccumulator {
2429 unused_export_names,
2430 istanbul_files_total: ctx
2431 .istanbul_coverage
2432 .map_or(0, IstanbulCoverage::file_count),
2433 ..FileScoreAccumulator::with_capacity(ctx.graph.modules.len())
2434 };
2435 for node in &ctx.graph.modules {
2436 let Some(path) = ctx.file_paths.get(&node.file_id) else {
2437 continue;
2438 };
2439 record_entry_point(&mut acc.entry_points, node, path);
2440 let score = compute_one_file_score(&mut acc, ctx, node, path);
2441 acc.scores.push(score);
2442 }
2443 acc
2444}
2445
2446fn finalize_file_score_list(
2449 mut scores: Vec<FileHealthScore>,
2450 changed_files: Option<&rustc_hash::FxHashSet<std::path::PathBuf>>,
2451 max_crap_threshold: f64,
2452) -> Vec<FileHealthScore> {
2453 if let Some(changed) = changed_files {
2454 scores.retain(|s| changed.contains(&s.path));
2455 }
2456 scores.retain(|s| s.function_count > 0);
2457 scores.sort_by(|a, b| compare_file_score_triage(a, b, max_crap_threshold));
2458 scores
2459}
2460
2461fn compute_one_file_score(
2463 acc: &mut FileScoreAccumulator,
2464 ctx: &FileScoreLoopCtx<'_>,
2465 node: &fallow_graph::graph::ModuleNode,
2466 path: &std::path::Path,
2467) -> FileHealthScore {
2468 let fan_in = ctx
2469 .graph
2470 .reverse_deps
2471 .get(node.file_id.0 as usize)
2472 .map_or(0, Vec::len);
2473 let fan_out = node.edge_range.len();
2474
2475 let (total_cyclomatic, total_cognitive, function_count, lines) = ctx
2476 .module_by_id
2477 .get(&node.file_id)
2478 .map_or((0, 0, 0, 0), |module| aggregate_complexity(module));
2479
2480 let value_exports = node.exports.iter().filter(|e| !e.is_type_only).count();
2481 let path_owned = path.to_path_buf();
2482 acc.value_export_counts
2483 .insert(path_owned.clone(), value_exports);
2484 record_unused_file_export_names(
2485 path_owned.as_path(),
2486 &node.exports,
2487 ctx.unused_files,
2488 &mut acc.unused_export_names,
2489 );
2490
2491 let (dead_code_ratio_rounded, complexity_density_rounded, maintainability_index_rounded) =
2492 compute_file_score_metrics(node, &path_owned, ctx, total_cyclomatic, lines, fan_out);
2493
2494 let relative = path_owned.strip_prefix(ctx.root).unwrap_or(&path_owned);
2495 let ceilings = CrapCeilingLookup::new(ctx.crap_thresholds, relative);
2496 let crap = compute_file_score_crap(node, ctx, &path_owned, &ceilings);
2497 acc.istanbul_matched += crap.istanbul_matched;
2498 acc.istanbul_total += crap.istanbul_total;
2499 acc.istanbul_files_joined += usize::from(crap.coverage_file_joined);
2500 record_per_function_crap(&mut acc.per_function_crap, &path_owned, crap.per_function);
2501
2502 let global_crap = ctx.crap_thresholds.resolver.global.crap;
2507 let crap_effective_threshold = crap
2508 .signals
2509 .min_ceiling
2510 .filter(|ceiling| (*ceiling - global_crap).abs() > f64::EPSILON);
2511
2512 FileHealthScore {
2513 path: path_owned,
2514 fan_in,
2515 fan_out,
2516 dead_code_ratio: dead_code_ratio_rounded,
2517 complexity_density: complexity_density_rounded,
2518 maintainability_index: maintainability_index_rounded,
2519 total_cyclomatic,
2520 total_cognitive,
2521 function_count,
2522 lines,
2523 crap_max: crap.max,
2524 crap_above_threshold: crap.signals.above,
2525 crap_exempted: crap.signals.exempted,
2526 crap_effective_threshold,
2527 }
2528}
2529
2530fn compute_file_score_metrics(
2533 node: &fallow_graph::graph::ModuleNode,
2534 path: &std::path::Path,
2535 ctx: &FileScoreLoopCtx<'_>,
2536 total_cyclomatic: u32,
2537 lines: u32,
2538 fan_out: usize,
2539) -> (f64, f64, f64) {
2540 let dead_code_ratio = compute_dead_code_ratio(
2541 path,
2542 &node.exports,
2543 ctx.unused_files,
2544 ctx.unused_exports_by_path,
2545 );
2546 let complexity_density = compute_complexity_density(total_cyclomatic, lines);
2547
2548 let dead_code_ratio_rounded = (dead_code_ratio * 100.0).round() / 100.0;
2549 let complexity_density_rounded = (complexity_density * 100.0).round() / 100.0;
2550
2551 let maintainability_index = compute_maintainability_index(
2552 complexity_density_rounded,
2553 dead_code_ratio_rounded,
2554 fan_out,
2555 lines,
2556 );
2557 (
2558 dead_code_ratio_rounded,
2559 complexity_density_rounded,
2560 (maintainability_index * 10.0).round() / 10.0,
2561 )
2562}
2563
2564fn build_file_score_output(parts: FileScoreOutputParts<'_>) -> FileScoreOutput {
2565 let total_exports: usize = parts.graph.modules.iter().map(|m| m.exports.len()).sum();
2566 let unused_deps = parts.results.unused_dependencies.len()
2567 + parts.results.unused_dev_dependencies.len()
2568 + parts.results.unused_optional_dependencies.len();
2569 let analysis_snapshot =
2570 build_analysis_counts_snapshot(parts.graph, parts.file_paths, parts.results, unused_deps);
2571 let analysis_counts =
2572 build_file_score_analysis_counts(parts.results, total_exports, unused_deps);
2573 let template_inherit_provenance =
2574 build_template_inherit_provenance(parts.template_inherit, parts.file_paths);
2575
2576 FileScoreOutput {
2577 scores: parts.scores,
2578 coverage: parts.coverage,
2579 circular_files: parts.circular_files,
2580 top_complex_fns: parts.top_complex_fns,
2581 entry_points: parts.entry_points,
2582 value_export_counts: parts.value_export_counts,
2583 unused_export_names: parts.unused_export_names,
2584 cycle_members: parts.cycle_members,
2585 direct_callers: parts.direct_callers,
2586 analysis_counts,
2587 prop_drilling_chains: parts.results.prop_drilling_chains.clone(),
2588 render_fan_in: parts.results.render_fan_in.clone(),
2589 analysis_snapshot,
2590 istanbul_matched: parts.istanbul_matched,
2591 istanbul_total: parts.istanbul_total,
2592 istanbul_files_joined: parts.istanbul_files_joined,
2593 istanbul_files_total: parts.istanbul_files_total,
2594 per_function_crap: parts.per_function_crap,
2595 template_inherit_provenance,
2596 }
2597}
2598
2599fn build_file_score_analysis_counts(
2600 results: &crate::results::AnalysisResults,
2601 total_exports: usize,
2602 unused_deps: usize,
2603) -> crate::vital_signs::AnalysisCounts {
2604 crate::vital_signs::AnalysisCounts {
2605 total_exports,
2606 dead_files: results.unused_files.len(),
2607 dead_exports: results.unused_exports.len() + results.unused_types.len(),
2608 unused_deps,
2609 circular_deps: results.circular_dependencies.len(),
2610 total_deps: 0usize,
2611 }
2612}
2613
2614fn build_template_inherit_provenance(
2615 template_inherit: rustc_hash::FxHashMap<crate::discover::FileId, TemplateInheritContext>,
2616 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
2617) -> rustc_hash::FxHashMap<std::path::PathBuf, std::path::PathBuf> {
2618 template_inherit
2619 .into_iter()
2620 .filter_map(|(file_id, ctx)| {
2621 file_paths
2622 .get(&file_id)
2623 .map(|path| ((**path).clone(), ctx.provenance_owner))
2624 })
2625 .collect()
2626}
2627
2628fn record_entry_point(
2629 entry_points: &mut rustc_hash::FxHashSet<std::path::PathBuf>,
2630 node: &fallow_graph::graph::ModuleNode,
2631 path: &std::path::Path,
2632) {
2633 if node.is_entry_point() {
2634 entry_points.insert(path.to_path_buf());
2635 }
2636}
2637
2638fn record_unused_file_export_names(
2639 path: &std::path::Path,
2640 exports: &[fallow_graph::graph::ExportSymbol],
2641 unused_files: &rustc_hash::FxHashSet<&std::path::Path>,
2642 unused_export_names: &mut rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>>,
2643) {
2644 if !unused_files.contains(path) || unused_export_names.contains_key(path) {
2645 return;
2646 }
2647
2648 let names: Vec<String> = exports
2649 .iter()
2650 .filter(|export| !export.is_type_only)
2651 .map(|export| export.name.to_string())
2652 .collect();
2653 if !names.is_empty() {
2654 unused_export_names.insert(path.to_path_buf(), names);
2655 }
2656}
2657
2658struct FileScoreCrap {
2659 max: f64,
2660 signals: CrapThresholdSignals,
2661 per_function: Vec<PerFunctionCrap>,
2662 istanbul_matched: usize,
2663 istanbul_total: usize,
2664 coverage_file_joined: bool,
2667}
2668
2669impl FileScoreCrap {
2670 fn empty() -> Self {
2671 Self {
2672 max: 0.0,
2673 signals: CrapThresholdSignals::default(),
2674 per_function: Vec::new(),
2675 istanbul_matched: 0,
2676 istanbul_total: 0,
2677 coverage_file_joined: false,
2678 }
2679 }
2680
2681 fn estimated(result: EstimatedCrapResult) -> Self {
2682 Self {
2683 max: result.max_crap,
2684 signals: result.signals,
2685 per_function: result.per_function,
2686 istanbul_matched: 0,
2687 istanbul_total: 0,
2688 coverage_file_joined: false,
2689 }
2690 }
2691
2692 fn istanbul(result: IstanbulCrapResult, coverage_file_joined: bool) -> Self {
2693 Self {
2694 max: result.max_crap,
2695 signals: result.signals,
2696 per_function: result.per_function,
2697 istanbul_matched: result.matched,
2698 istanbul_total: result.total,
2699 coverage_file_joined,
2700 }
2701 }
2702}
2703
2704fn compute_file_score_crap(
2705 node: &fallow_graph::graph::ModuleNode,
2706 ctx: &FileScoreLoopCtx<'_>,
2707 path: &std::path::Path,
2708 ceilings: &CrapCeilingLookup<'_>,
2709) -> FileScoreCrap {
2710 let Some(module) = ctx.module_by_id.get(&node.file_id).copied() else {
2711 return FileScoreCrap::empty();
2712 };
2713
2714 let is_coverage_suppressed = crate::suppress::is_file_suppressed(
2715 &module.suppressions,
2716 fallow_types::suppress::IssueKind::CoverageGaps,
2717 );
2718 let is_test_reachable = ctx.test_coverage.covers_file(node.file_id) || is_coverage_suppressed;
2719 let resolution = resolve_crap_coverage(
2720 ctx.template_inherit.get(&node.file_id),
2721 ctx.istanbul_coverage,
2722 path,
2723 );
2724 match resolution {
2725 CrapCoverageResolution::TemplateInherited(inherit_ctx) => {
2726 compute_template_inherited_crap(module, inherit_ctx, ceilings)
2727 }
2728 CrapCoverageResolution::Istanbul { file_coverage } => {
2729 compute_istanbul_file_crap(module, file_coverage, is_test_reachable, ceilings)
2730 }
2731 CrapCoverageResolution::StaticEstimated => compute_static_file_crap(
2732 module,
2733 &node.exports,
2734 ctx.test_coverage,
2735 is_test_reachable,
2736 ceilings,
2737 ),
2738 }
2739}
2740
2741fn compute_template_inherited_crap(
2742 module: &crate::source::ModuleInfo,
2743 inherit_ctx: &TemplateInheritContext,
2744 ceilings: &CrapCeilingLookup<'_>,
2745) -> FileScoreCrap {
2746 FileScoreCrap::estimated(compute_crap_scores_estimated(
2747 &module.complexity,
2748 &inherit_ctx.test_referenced_exports,
2749 inherit_ctx.is_test_reachable,
2750 fallow_output::CoverageSource::EstimatedComponentInherited,
2751 ceilings,
2752 ))
2753}
2754
2755fn compute_istanbul_file_crap(
2756 module: &crate::source::ModuleInfo,
2757 file_coverage: Option<&IstanbulFileCoverage>,
2758 is_test_reachable: bool,
2759 ceilings: &CrapCeilingLookup<'_>,
2760) -> FileScoreCrap {
2761 FileScoreCrap::istanbul(
2762 compute_crap_scores_istanbul(
2763 &module.complexity,
2764 file_coverage,
2765 is_test_reachable,
2766 ceilings,
2767 ),
2768 file_coverage.is_some(),
2769 )
2770}
2771
2772fn compute_static_file_crap(
2773 module: &crate::source::ModuleInfo,
2774 exports: &[fallow_graph::graph::ExportSymbol],
2775 test_coverage: StaticTestCoverage<'_>,
2776 is_test_reachable: bool,
2777 ceilings: &CrapCeilingLookup<'_>,
2778) -> FileScoreCrap {
2779 let test_refs = build_test_referenced_exports(exports, test_coverage);
2780 FileScoreCrap::estimated(compute_crap_scores_estimated(
2781 &module.complexity,
2782 &test_refs,
2783 is_test_reachable,
2784 fallow_output::CoverageSource::Estimated,
2785 ceilings,
2786 ))
2787}
2788
2789fn record_per_function_crap(
2790 per_function_crap: &mut rustc_hash::FxHashMap<std::path::PathBuf, Vec<PerFunctionCrap>>,
2791 path: &std::path::Path,
2792 per_function: Vec<PerFunctionCrap>,
2793) {
2794 if !per_function.is_empty() {
2795 per_function_crap.insert(path.to_path_buf(), per_function);
2796 }
2797}
2798
2799struct FileScoreCoverageSetup<'a> {
2800 module_by_id: rustc_hash::FxHashMap<crate::discover::FileId, &'a crate::source::ModuleInfo>,
2801 coverage: CoverageGapData,
2802}
2803
2804fn prepare_file_score_coverage_setup<'a>(
2805 modules: &'a [crate::source::ModuleInfo],
2806 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
2807 results: &crate::results::AnalysisResults,
2808 graph: &fallow_graph::graph::ModuleGraph,
2809 test_coverage: StaticTestCoverage<'_>,
2810 root: &std::path::Path,
2811) -> FileScoreCoverageSetup<'a> {
2812 let module_by_id: rustc_hash::FxHashMap<_, _> =
2813 modules.iter().map(|m| (m.file_id, m)).collect();
2814 let unused_exports: rustc_hash::FxHashSet<(&std::path::Path, String)> = results
2815 .unused_exports
2816 .iter()
2817 .map(|export| {
2818 (
2819 export.export.path.as_path(),
2820 export.export.export_name.clone(),
2821 )
2822 })
2823 .collect();
2824 let coverage = compute_coverage_gaps(
2825 graph,
2826 test_coverage,
2827 file_paths,
2828 &module_by_id,
2829 &unused_exports,
2830 root,
2831 );
2832 FileScoreCoverageSetup {
2833 module_by_id,
2834 coverage,
2835 }
2836}
2837
2838fn collect_circular_files(
2839 results: &crate::results::AnalysisResults,
2840) -> rustc_hash::FxHashSet<std::path::PathBuf> {
2841 results
2842 .circular_dependencies
2843 .iter()
2844 .flat_map(|c| c.cycle.files.iter().cloned())
2845 .collect()
2846}
2847
2848fn collect_top_complex_fns(
2849 modules: &[crate::source::ModuleInfo],
2850 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
2851) -> rustc_hash::FxHashMap<std::path::PathBuf, Vec<(String, u32, u16)>> {
2852 let mut top_complex_fns = rustc_hash::FxHashMap::default();
2853 for module in modules {
2854 if module.complexity.is_empty() {
2855 continue;
2856 }
2857 let Some(path) = file_paths.get(&module.file_id) else {
2858 continue;
2859 };
2860 let mut funcs: Vec<(String, u32, u16)> = module
2865 .complexity
2866 .iter()
2867 .filter(|f| !fallow_types::extract::is_synthetic_module_unit(&f.name))
2868 .map(|f| (f.name.clone(), f.line, f.cognitive))
2869 .collect();
2870 funcs.sort_by_key(|f| std::cmp::Reverse(f.2));
2871 funcs.truncate(3);
2872 if funcs.first().is_some_and(|worst| worst.2 > 0) {
2873 top_complex_fns.insert((*path).clone(), funcs);
2874 }
2875 }
2876 top_complex_fns
2877}
2878
2879fn collect_cycle_members(
2880 results: &crate::results::AnalysisResults,
2881) -> rustc_hash::FxHashMap<std::path::PathBuf, Vec<std::path::PathBuf>> {
2882 let mut cycle_members: rustc_hash::FxHashMap<std::path::PathBuf, Vec<std::path::PathBuf>> =
2883 rustc_hash::FxHashMap::default();
2884 for cycle in &results.circular_dependencies {
2885 for file in &cycle.cycle.files {
2886 let others: Vec<std::path::PathBuf> = cycle
2887 .cycle
2888 .files
2889 .iter()
2890 .filter(|f| *f != file)
2891 .cloned()
2892 .collect();
2893 cycle_members
2894 .entry(file.clone())
2895 .or_default()
2896 .extend(others);
2897 }
2898 }
2899 for members in cycle_members.values_mut() {
2900 members.sort();
2901 members.dedup();
2902 }
2903 cycle_members
2904}
2905
2906fn collect_unused_export_names(
2907 results: &crate::results::AnalysisResults,
2908) -> rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>> {
2909 let mut unused_export_names: rustc_hash::FxHashMap<std::path::PathBuf, Vec<String>> =
2910 rustc_hash::FxHashMap::default();
2911 for exp in &results.unused_exports {
2912 unused_export_names
2913 .entry(exp.export.path.clone())
2914 .or_default()
2915 .push(exp.export.export_name.clone());
2916 }
2917 unused_export_names
2918}
2919
2920fn build_analysis_counts_snapshot(
2921 graph: &fallow_graph::graph::ModuleGraph,
2922 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
2923 results: &crate::results::AnalysisResults,
2924 unused_deps: usize,
2925) -> AnalysisCountsSnapshot {
2926 let mut module_export_counts = rustc_hash::FxHashMap::with_capacity_and_hasher(
2927 graph.modules.len(),
2928 rustc_hash::FxBuildHasher,
2929 );
2930 for module in &graph.modules {
2931 if let Some(path) = file_paths.get(&module.file_id) {
2932 module_export_counts.insert((*path).clone(), module.exports.len());
2933 }
2934 }
2935
2936 let mut unused_export_paths =
2937 Vec::with_capacity(results.unused_exports.len() + results.unused_types.len());
2938 unused_export_paths.extend(results.unused_exports.iter().map(|e| e.export.path.clone()));
2939 unused_export_paths.extend(results.unused_types.iter().map(|e| e.export.path.clone()));
2940
2941 let mut unused_dep_package_paths = Vec::with_capacity(unused_deps);
2942 unused_dep_package_paths.extend(
2943 results
2944 .unused_dependencies
2945 .iter()
2946 .map(|d| d.dep.path.clone()),
2947 );
2948 unused_dep_package_paths.extend(
2949 results
2950 .unused_dev_dependencies
2951 .iter()
2952 .map(|d| d.dep.path.clone()),
2953 );
2954 unused_dep_package_paths.extend(
2955 results
2956 .unused_optional_dependencies
2957 .iter()
2958 .map(|d| d.dep.path.clone()),
2959 );
2960
2961 AnalysisCountsSnapshot {
2962 unused_file_paths: results
2963 .unused_files
2964 .iter()
2965 .map(|f| f.file.path.clone())
2966 .collect(),
2967 unused_export_paths,
2968 unused_dep_package_paths,
2969 circular_dep_groups: results
2970 .circular_dependencies
2971 .iter()
2972 .map(|c| c.cycle.files.clone())
2973 .collect(),
2974 module_export_counts,
2975 }
2976}
2977
2978#[cfg(test)]
2979mod tests {
2980 use super::super::threshold_overrides::GlobalHealthThresholds;
2981 use super::*;
2982
2983 fn test_crap_resolver(crap: f64) -> ThresholdOverrideResolver {
2986 ThresholdOverrideResolver::new(
2987 &[],
2988 GlobalHealthThresholds {
2989 cyclomatic: 20,
2990 cognitive: 15,
2991 crap,
2992 unit_size: 120,
2993 },
2994 )
2995 }
2996
2997 fn test_override_resolver(
2999 overrides: &[fallow_config::HealthThresholdOverride],
3000 ) -> ThresholdOverrideResolver {
3001 ThresholdOverrideResolver::new(
3002 overrides,
3003 GlobalHealthThresholds {
3004 cyclomatic: 20,
3005 cognitive: 15,
3006 crap: CRAP_THRESHOLD,
3007 unit_size: 120,
3008 },
3009 )
3010 }
3011
3012 fn istanbul_crap_default(
3014 complexity: &[fallow_types::extract::FunctionComplexity],
3015 file_coverage: Option<&IstanbulFileCoverage>,
3016 is_test_reachable: bool,
3017 ) -> IstanbulCrapResult {
3018 let resolver = test_crap_resolver(CRAP_THRESHOLD);
3019 let ceilings = CrapCeilingLookup::new(
3020 CrapScoreThresholds {
3021 resolver: &resolver,
3022 enforce_crap: true,
3023 },
3024 std::path::Path::new("src/test.ts"),
3025 );
3026 compute_crap_scores_istanbul(complexity, file_coverage, is_test_reachable, &ceilings)
3027 }
3028
3029 #[test]
3034 fn an_unmatched_function_scores_the_same_with_and_without_a_coverage_map() {
3035 let temp = tempfile::TempDir::new().unwrap();
3036 let source_path = temp.path().join("src/grade.ts");
3037 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
3038 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
3039
3040 let coverage_path = temp.path().join("coverage-final.json");
3044 write_single_file_istanbul_fixture(
3045 &coverage_path,
3046 &source_path,
3047 &serde_json::json!({
3048 "0": {
3049 "name": "unrelated",
3050 "line": 40,
3051 "decl": { "start": { "line": 40, "column": 0 }, "end": { "line": 40, "column": 9 } },
3052 "loc": { "start": { "line": 40, "column": 20 }, "end": { "line": 44, "column": 1 } }
3053 }
3054 }),
3055 &serde_json::json!({ "0": 1 }),
3056 );
3057 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
3058 let canonical_source = dunce::canonicalize(&source_path).unwrap();
3059 let file_coverage = coverage.get(&canonical_source).unwrap();
3060
3061 let function = make_fn_complexity(10);
3062 let with_map =
3063 istanbul_crap_default(std::slice::from_ref(&function), Some(file_coverage), true);
3064 let estimated = compute_crap_scores_estimated(
3065 std::slice::from_ref(&function),
3066 &rustc_hash::FxHashSet::default(),
3067 true,
3068 fallow_output::CoverageSource::Estimated,
3069 &CrapCeilingLookup::new(
3070 CrapScoreThresholds {
3071 resolver: &test_crap_resolver(CRAP_THRESHOLD),
3072 enforce_crap: true,
3073 },
3074 std::path::Path::new("src/test.ts"),
3075 ),
3076 );
3077
3078 assert_eq!(with_map.matched, 0);
3079 assert!(
3080 (with_map.per_function[0].crap - estimated.per_function[0].crap).abs() < f64::EPSILON,
3081 "a map that attributes nothing must not change the score"
3082 );
3083 assert_eq!(with_map.per_function[0].coverage_pct, None);
3084 }
3085
3086 fn test_istanbul_file_coverage(
3087 functions: rustc_hash::FxHashMap<(String, u32, u32), f64>,
3088 relocated: bool,
3089 ) -> IstanbulFileCoverage {
3090 let functions = functions
3091 .into_iter()
3092 .map(
3093 |((name, line, col), coverage_pct)| IstanbulFunctionCoverage {
3094 name,
3095 coverage_pct,
3096 aliases: vec![primary_alias(line, col)],
3097 decl_start: IstanbulPosition::new(line, col),
3098 header_holds_other_fn: false,
3099 header_span: None,
3100 body_span: None,
3101 },
3102 )
3103 .collect();
3104 IstanbulFileCoverage::new(functions, relocated)
3105 }
3106
3107 fn primary_alias(line: u32, col: u32) -> IstanbulAlias {
3108 IstanbulAlias {
3109 position: IstanbulPosition::new(line, col),
3110 primary: true,
3111 }
3112 }
3113
3114 fn secondary_alias(line: u32, col: u32) -> IstanbulAlias {
3115 IstanbulAlias {
3116 position: IstanbulPosition::new(line, col),
3117 primary: false,
3118 }
3119 }
3120
3121 fn body_span(start: (u32, u32), end: (u32, u32)) -> IstanbulSpan {
3122 IstanbulSpan {
3123 start: IstanbulPosition::new(start.0, start.1),
3124 end: IstanbulPosition::new(end.0, end.1),
3125 }
3126 }
3127
3128 fn estimated_crap_default(
3130 complexity: &[fallow_types::extract::FunctionComplexity],
3131 test_referenced_exports: &rustc_hash::FxHashSet<String>,
3132 is_test_reachable: bool,
3133 coverage_source: fallow_output::CoverageSource,
3134 ) -> EstimatedCrapResult {
3135 let resolver = test_crap_resolver(CRAP_THRESHOLD);
3136 let ceilings = CrapCeilingLookup::new(
3137 CrapScoreThresholds {
3138 resolver: &resolver,
3139 enforce_crap: true,
3140 },
3141 std::path::Path::new("src/test.ts"),
3142 );
3143 compute_crap_scores_estimated(
3144 complexity,
3145 test_referenced_exports,
3146 is_test_reachable,
3147 coverage_source,
3148 &ceilings,
3149 )
3150 }
3151
3152 fn compute_file_scores_default(
3154 modules: &[crate::source::ModuleInfo],
3155 file_paths: &rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf>,
3156 changed_files: Option<&rustc_hash::FxHashSet<std::path::PathBuf>>,
3157 analysis_output: crate::results::DeadCodeAnalysisArtifacts,
3158 istanbul_coverage: Option<&IstanbulCoverage>,
3159 root: &std::path::Path,
3160 ) -> Result<FileScoreOutput, String> {
3161 let resolver = test_crap_resolver(CRAP_THRESHOLD);
3162 compute_file_scores(
3163 FileScoreComputeInput {
3164 modules,
3165 file_paths,
3166 changed_files,
3167 istanbul_coverage,
3168 root,
3169 crap_thresholds: CrapScoreThresholds {
3170 resolver: &resolver,
3171 enforce_crap: true,
3172 },
3173 },
3174 analysis_output,
3175 )
3176 }
3177
3178 #[test]
3179 fn maintainability_perfect_score() {
3180 assert!((compute_maintainability_index(0.0, 0.0, 0, 100) - 100.0).abs() < f64::EPSILON);
3181 }
3182
3183 #[test]
3184 fn crap_resolution_prefers_template_inheritance_over_istanbul() {
3185 let inherit_ctx = TemplateInheritContext {
3186 is_test_reachable: true,
3187 test_referenced_exports: rustc_hash::FxHashSet::default(),
3188 provenance_owner: std::path::PathBuf::from("/project/src/app.component.ts"),
3189 };
3190 let istanbul = IstanbulCoverage {
3191 files: rustc_hash::FxHashMap::default(),
3192 };
3193
3194 let resolution = resolve_crap_coverage(
3195 Some(&inherit_ctx),
3196 Some(&istanbul),
3197 std::path::Path::new("/project/src/app.component.html"),
3198 );
3199
3200 assert!(matches!(
3201 resolution,
3202 CrapCoverageResolution::TemplateInherited(_)
3203 ));
3204 }
3205
3206 #[test]
3207 fn crap_resolution_keeps_istanbul_when_file_is_missing() {
3208 let istanbul = IstanbulCoverage {
3209 files: rustc_hash::FxHashMap::default(),
3210 };
3211
3212 let resolution = resolve_crap_coverage(
3213 None,
3214 Some(&istanbul),
3215 std::path::Path::new("/project/src/missing.ts"),
3216 );
3217
3218 assert!(matches!(
3219 resolution,
3220 CrapCoverageResolution::Istanbul {
3221 file_coverage: None
3222 }
3223 ));
3224 }
3225
3226 #[test]
3227 fn maintainability_clamped_at_zero() {
3228 assert!((compute_maintainability_index(10.0, 1.0, 100, 200) - 0.0).abs() < f64::EPSILON);
3229 }
3230
3231 #[test]
3232 fn maintainability_formula_correct() {
3233 let result = compute_maintainability_index(0.5, 0.3, 10, 100);
3234 let expected = 11.0_f64.ln().mul_add(-4.0, 100.0 - 15.0 - 6.0);
3235 assert!((result - expected).abs() < 0.01);
3236 }
3237
3238 #[test]
3239 fn maintainability_dead_file_penalty() {
3240 let result = compute_maintainability_index(0.0, 1.0, 0, 100);
3241 assert!((result - 80.0).abs() < f64::EPSILON);
3242 }
3243
3244 #[test]
3245 fn maintainability_fan_out_is_logarithmic() {
3246 let result_10 = compute_maintainability_index(0.0, 0.0, 10, 100);
3247 let result_100 = compute_maintainability_index(0.0, 0.0, 100, 100);
3248 let result_200 = compute_maintainability_index(0.0, 0.0, 200, 100);
3249
3250 assert!(result_10 > 90.0); assert!(result_100 > 84.0); assert!((result_100 - result_200).abs() < f64::EPSILON);
3253 }
3254
3255 #[test]
3256 fn maintainability_fan_out_capped_at_15() {
3257 let result = compute_maintainability_index(0.0, 1.0, 1000, 100);
3258 assert!((result - 65.0).abs() < f64::EPSILON);
3259 }
3260
3261 #[test]
3262 fn maintainability_small_file_dampened() {
3263 let small = compute_maintainability_index(0.40, 0.0, 0, 5);
3264 assert!((small - 98.8).abs() < 0.01);
3265 }
3266
3267 #[test]
3268 fn maintainability_large_file_undampened() {
3269 let large = compute_maintainability_index(0.30, 0.0, 0, 192);
3270 assert!((large - 91.0).abs() < 0.01);
3271 }
3272
3273 #[test]
3274 fn maintainability_small_file_ranks_better_than_complex_large_file() {
3275 let trivial = compute_maintainability_index(0.40, 0.0, 0, 5);
3276 let nightmare = compute_maintainability_index(0.30, 0.0, 0, 192);
3277 assert!(
3278 trivial > nightmare,
3279 "trivial file ({trivial}) should rank better than nightmare ({nightmare})"
3280 );
3281 }
3282
3283 #[test]
3284 fn maintainability_at_dampening_boundary() {
3285 let at_boundary = compute_maintainability_index(0.5, 0.0, 0, 50);
3286 let above_boundary = compute_maintainability_index(0.5, 0.0, 0, 51);
3287 assert!((at_boundary - above_boundary).abs() < 0.01);
3288 }
3289
3290 #[test]
3291 fn maintainability_zero_lines_zero_density_penalty() {
3292 let result = compute_maintainability_index(5.0, 0.0, 0, 0);
3293 assert!((result - 100.0).abs() < f64::EPSILON);
3294 }
3295
3296 #[test]
3297 fn complexity_density_zero_lines() {
3298 assert!((compute_complexity_density(10, 0)).abs() < f64::EPSILON);
3299 }
3300
3301 #[test]
3302 fn complexity_density_normal() {
3303 let result = compute_complexity_density(10, 100);
3304 assert!((result - 0.1).abs() < f64::EPSILON);
3305 }
3306
3307 #[test]
3308 fn complexity_density_high() {
3309 let result = compute_complexity_density(50, 10);
3310 assert!((result - 5.0).abs() < f64::EPSILON);
3311 }
3312
3313 #[test]
3314 fn dead_code_ratio_no_exports() {
3315 let unused_files = rustc_hash::FxHashSet::default();
3316 let unused_map = rustc_hash::FxHashMap::default();
3317 let path = std::path::Path::new("/src/foo.ts");
3318 let exports: Vec<fallow_graph::graph::ExportSymbol> = vec![];
3319
3320 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3321 assert!((ratio).abs() < f64::EPSILON);
3322 }
3323
3324 #[test]
3325 fn dead_code_ratio_all_unused_file() {
3326 let mut unused_files: rustc_hash::FxHashSet<&std::path::Path> =
3327 rustc_hash::FxHashSet::default();
3328 let path = std::path::Path::new("/src/foo.ts");
3329 unused_files.insert(path);
3330 let unused_map = rustc_hash::FxHashMap::default();
3331 let exports: Vec<fallow_graph::graph::ExportSymbol> = vec![];
3332
3333 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3334 assert!((ratio - 1.0).abs() < f64::EPSILON);
3335 }
3336
3337 #[test]
3338 fn dead_code_ratio_mix() {
3339 let unused_files = rustc_hash::FxHashSet::default();
3340 let path = std::path::Path::new("/src/foo.ts");
3341
3342 let exports = vec![
3343 fallow_graph::graph::ExportSymbol {
3344 name: crate::source::ExportName::Named("a".into()),
3345 is_type_only: false,
3346 is_side_effect_used: false,
3347 visibility: crate::source::VisibilityTag::None,
3348 expected_unused_reason: None,
3349 span: oxc_span::Span::empty(0),
3350 references: vec![],
3351 reference_paths: Vec::new(),
3352 members: vec![],
3353 },
3354 fallow_graph::graph::ExportSymbol {
3355 name: crate::source::ExportName::Named("b".into()),
3356 is_type_only: false,
3357 is_side_effect_used: false,
3358 visibility: crate::source::VisibilityTag::None,
3359 expected_unused_reason: None,
3360 span: oxc_span::Span::empty(0),
3361 references: vec![],
3362 reference_paths: Vec::new(),
3363 members: vec![],
3364 },
3365 fallow_graph::graph::ExportSymbol {
3366 name: crate::source::ExportName::Named("c".into()),
3367 is_type_only: false,
3368 is_side_effect_used: false,
3369 visibility: crate::source::VisibilityTag::None,
3370 expected_unused_reason: None,
3371 span: oxc_span::Span::empty(0),
3372 references: vec![],
3373 reference_paths: Vec::new(),
3374 members: vec![],
3375 },
3376 fallow_graph::graph::ExportSymbol {
3377 name: crate::source::ExportName::Named("MyType".into()),
3378 is_type_only: true,
3379 is_side_effect_used: false,
3380 visibility: crate::source::VisibilityTag::None,
3381 expected_unused_reason: None,
3382 span: oxc_span::Span::empty(0),
3383 references: vec![],
3384 reference_paths: Vec::new(),
3385 members: vec![],
3386 },
3387 ];
3388
3389 let mut unused_map: rustc_hash::FxHashMap<&std::path::Path, usize> =
3390 rustc_hash::FxHashMap::default();
3391 unused_map.insert(path, 2);
3392
3393 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3394 assert!((ratio - 2.0 / 3.0).abs() < 1e-10);
3395 }
3396
3397 #[test]
3398 fn dead_code_ratio_all_type_only_exports() {
3399 let unused_files = rustc_hash::FxHashSet::default();
3400 let path = std::path::Path::new("/src/types.ts");
3401
3402 let exports = vec![fallow_graph::graph::ExportSymbol {
3403 name: crate::source::ExportName::Named("Foo".into()),
3404 is_type_only: true,
3405 is_side_effect_used: false,
3406 visibility: crate::source::VisibilityTag::None,
3407 expected_unused_reason: None,
3408 span: oxc_span::Span::empty(0),
3409 references: vec![],
3410 reference_paths: Vec::new(),
3411 members: vec![],
3412 }];
3413 let unused_map = rustc_hash::FxHashMap::default();
3414
3415 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3416 assert!((ratio).abs() < f64::EPSILON);
3417 }
3418
3419 #[test]
3420 fn aggregate_complexity_empty_module() {
3421 let module = crate::source::ModuleInfo::empty(crate::discover::FileId(0));
3422
3423 let (cyc, cog, funcs, lines) = aggregate_complexity(&module);
3424 assert_eq!(cyc, 0);
3425 assert_eq!(cog, 0);
3426 assert_eq!(funcs, 0);
3427 assert_eq!(lines, 0);
3428 }
3429
3430 #[test]
3431 fn aggregate_complexity_single_function() {
3432 let module = crate::source::ModuleInfo {
3433 line_offsets: vec![0, 10, 20, 30, 40], complexity: vec![fallow_types::extract::FunctionComplexity {
3435 name: "doStuff".into(),
3436 is_private_member: false,
3437 line: 1,
3438 col: 0,
3439 cyclomatic: 7,
3440 cognitive: 4,
3441 line_count: 5,
3442 param_count: 0,
3443 react_hook_count: 0,
3444 react_jsx_max_depth: 0,
3445 react_prop_count: 0,
3446 source_hash: None,
3447 contributions: Vec::new(),
3448 }],
3449 ..crate::source::ModuleInfo::empty(crate::discover::FileId(0))
3450 };
3451
3452 let (cyc, cog, funcs, lines) = aggregate_complexity(&module);
3453 assert_eq!(cyc, 7);
3454 assert_eq!(cog, 4);
3455 assert_eq!(funcs, 1);
3456 assert_eq!(lines, 5);
3457 }
3458
3459 #[test]
3460 fn aggregate_complexity_multiple_functions() {
3461 let module = crate::source::ModuleInfo {
3462 line_offsets: vec![0, 10, 20], complexity: vec![
3464 fallow_types::extract::FunctionComplexity {
3465 name: "a".into(),
3466 is_private_member: false,
3467 line: 1,
3468 col: 0,
3469 cyclomatic: 3,
3470 cognitive: 2,
3471 line_count: 1,
3472 param_count: 0,
3473 react_hook_count: 0,
3474 react_jsx_max_depth: 0,
3475 react_prop_count: 0,
3476 source_hash: None,
3477 contributions: Vec::new(),
3478 },
3479 fallow_types::extract::FunctionComplexity {
3480 name: "b".into(),
3481 is_private_member: false,
3482 line: 2,
3483 col: 0,
3484 cyclomatic: 5,
3485 cognitive: 8,
3486 line_count: 2,
3487 param_count: 0,
3488 react_hook_count: 0,
3489 react_jsx_max_depth: 0,
3490 react_prop_count: 0,
3491 source_hash: None,
3492 contributions: Vec::new(),
3493 },
3494 ],
3495 ..crate::source::ModuleInfo::empty(crate::discover::FileId(0))
3496 };
3497
3498 let (cyc, cog, funcs, lines) = aggregate_complexity(&module);
3499 assert_eq!(cyc, 8);
3500 assert_eq!(cog, 10);
3501 assert_eq!(funcs, 2);
3502 assert_eq!(lines, 3);
3503 }
3504
3505 #[test]
3506 fn count_unused_exports_empty() {
3507 let exports: Vec<crate::results::UnusedExportFinding> = vec![];
3508 let map = count_unused_exports_by_path(&exports);
3509 assert!(map.is_empty());
3510 }
3511
3512 #[test]
3513 fn count_unused_exports_groups_by_path() {
3514 let exports = vec![
3515 crate::results::UnusedExportFinding::with_actions(crate::results::UnusedExport {
3516 path: std::path::PathBuf::from("/src/a.ts"),
3517 export_name: "foo".into(),
3518 is_type_only: false,
3519 line: 1,
3520 col: 0,
3521 span_start: 0,
3522 is_re_export: false,
3523 }),
3524 crate::results::UnusedExportFinding::with_actions(crate::results::UnusedExport {
3525 path: std::path::PathBuf::from("/src/a.ts"),
3526 export_name: "bar".into(),
3527 is_type_only: false,
3528 line: 5,
3529 col: 0,
3530 span_start: 40,
3531 is_re_export: false,
3532 }),
3533 crate::results::UnusedExportFinding::with_actions(crate::results::UnusedExport {
3534 path: std::path::PathBuf::from("/src/b.ts"),
3535 export_name: "baz".into(),
3536 is_type_only: false,
3537 line: 1,
3538 col: 0,
3539 span_start: 0,
3540 is_re_export: false,
3541 }),
3542 ];
3543 let map = count_unused_exports_by_path(&exports);
3544 assert_eq!(map.get(std::path::Path::new("/src/a.ts")).copied(), Some(2));
3545 assert_eq!(map.get(std::path::Path::new("/src/b.ts")).copied(), Some(1));
3546 }
3547
3548 #[test]
3549 fn dead_code_ratio_all_value_exports_unused() {
3550 let unused_files = rustc_hash::FxHashSet::default();
3551 let path = std::path::Path::new("/src/foo.ts");
3552
3553 let exports = vec![
3554 fallow_graph::graph::ExportSymbol {
3555 name: crate::source::ExportName::Named("a".into()),
3556 is_type_only: false,
3557 is_side_effect_used: false,
3558 visibility: crate::source::VisibilityTag::None,
3559 expected_unused_reason: None,
3560 span: oxc_span::Span::empty(0),
3561 references: vec![],
3562 reference_paths: Vec::new(),
3563 members: vec![],
3564 },
3565 fallow_graph::graph::ExportSymbol {
3566 name: crate::source::ExportName::Named("b".into()),
3567 is_type_only: false,
3568 is_side_effect_used: false,
3569 visibility: crate::source::VisibilityTag::None,
3570 expected_unused_reason: None,
3571 span: oxc_span::Span::empty(0),
3572 references: vec![],
3573 reference_paths: Vec::new(),
3574 members: vec![],
3575 },
3576 ];
3577
3578 let mut unused_map: rustc_hash::FxHashMap<&std::path::Path, usize> =
3579 rustc_hash::FxHashMap::default();
3580 unused_map.insert(path, 2);
3581
3582 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3583 assert!((ratio - 1.0).abs() < f64::EPSILON);
3584 }
3585
3586 #[test]
3587 fn dead_code_ratio_clamped_when_unused_exceeds_value_exports() {
3588 let unused_files = rustc_hash::FxHashSet::default();
3589 let path = std::path::Path::new("/src/foo.ts");
3590
3591 let exports = vec![fallow_graph::graph::ExportSymbol {
3592 name: crate::source::ExportName::Named("a".into()),
3593 is_type_only: false,
3594 is_side_effect_used: false,
3595 visibility: crate::source::VisibilityTag::None,
3596 expected_unused_reason: None,
3597 span: oxc_span::Span::empty(0),
3598 references: vec![],
3599 reference_paths: Vec::new(),
3600 members: vec![],
3601 }];
3602
3603 let mut unused_map: rustc_hash::FxHashMap<&std::path::Path, usize> =
3604 rustc_hash::FxHashMap::default();
3605 unused_map.insert(path, 5);
3606
3607 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3608 assert!((ratio - 1.0).abs() < f64::EPSILON);
3609 }
3610
3611 #[test]
3612 fn dead_code_ratio_no_unused_exports_for_path() {
3613 let unused_files = rustc_hash::FxHashSet::default();
3614 let path = std::path::Path::new("/src/clean.ts");
3615
3616 let exports = vec![fallow_graph::graph::ExportSymbol {
3617 name: crate::source::ExportName::Named("used".into()),
3618 is_type_only: false,
3619 is_side_effect_used: false,
3620 visibility: crate::source::VisibilityTag::None,
3621 expected_unused_reason: None,
3622 span: oxc_span::Span::empty(0),
3623 references: vec![],
3624 reference_paths: Vec::new(),
3625 members: vec![],
3626 }];
3627
3628 let unused_map = rustc_hash::FxHashMap::default();
3629 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_map);
3630 assert!(ratio.abs() < f64::EPSILON);
3631 }
3632
3633 #[test]
3634 fn complexity_density_zero_cyclomatic_with_lines() {
3635 let result = compute_complexity_density(0, 100);
3636 assert!(result.abs() < f64::EPSILON);
3637 }
3638
3639 #[test]
3640 fn complexity_density_single_line() {
3641 let result = compute_complexity_density(1, 1);
3642 assert!((result - 1.0).abs() < f64::EPSILON);
3643 }
3644
3645 #[test]
3646 fn maintainability_only_complexity_penalty() {
3647 let result = compute_maintainability_index(3.0, 0.0, 0, 100);
3648 assert!((result - 10.0).abs() < f64::EPSILON);
3649 }
3650
3651 #[test]
3652 fn maintainability_only_dead_code_penalty() {
3653 let result = compute_maintainability_index(0.0, 0.5, 0, 100);
3654 assert!((result - 90.0).abs() < f64::EPSILON);
3655 }
3656
3657 #[test]
3658 fn maintainability_fan_out_one() {
3659 let result = compute_maintainability_index(0.0, 0.0, 1, 100);
3660 let expected = 2.0_f64.ln().mul_add(-4.0, 100.0);
3661 assert!((result - expected).abs() < 0.01);
3662 }
3663
3664 #[test]
3665 fn maintainability_all_penalties_maxed() {
3666 let result = compute_maintainability_index(10.0, 1.0, 1000, 200);
3667 assert!(result.abs() < f64::EPSILON);
3668 }
3669
3670 #[test]
3671 fn count_unused_exports_single_file_single_export() {
3672 let exports = vec![crate::results::UnusedExportFinding::with_actions(
3673 crate::results::UnusedExport {
3674 path: std::path::PathBuf::from("/src/only.ts"),
3675 export_name: "lonely".into(),
3676 is_type_only: false,
3677 line: 1,
3678 col: 0,
3679 span_start: 0,
3680 is_re_export: false,
3681 },
3682 )];
3683 let map = count_unused_exports_by_path(&exports);
3684 assert_eq!(map.len(), 1);
3685 assert_eq!(
3686 map.get(std::path::Path::new("/src/only.ts")).copied(),
3687 Some(1)
3688 );
3689 }
3690
3691 fn build_test_graph(
3693 files: &[crate::discover::DiscoveredFile],
3694 entry_point_paths: &[std::path::PathBuf],
3695 resolved_modules: &[fallow_graph::resolve::ResolvedModule],
3696 ) -> fallow_graph::graph::ModuleGraph {
3697 let entry_points: Vec<crate::discover::EntryPoint> = entry_point_paths
3698 .iter()
3699 .map(|p| crate::discover::EntryPoint {
3700 path: p.clone(),
3701 source: crate::discover::EntryPointSource::PackageJsonMain,
3702 })
3703 .collect();
3704 fallow_graph::graph::ModuleGraph::build(resolved_modules, &entry_points, files)
3705 }
3706
3707 fn make_module_info(
3709 file_id: u32,
3710 line_count: usize,
3711 functions: Vec<fallow_types::extract::FunctionComplexity>,
3712 ) -> crate::source::ModuleInfo {
3713 crate::source::ModuleInfo {
3714 line_offsets: (0..line_count).map(|i| (i * 10) as u32).collect(),
3715 complexity: functions,
3716 ..crate::source::ModuleInfo::empty(crate::discover::FileId(file_id))
3717 }
3718 }
3719
3720 fn make_file_score(path: &str, maintainability_index: f64, crap_max: f64) -> FileHealthScore {
3721 FileHealthScore {
3722 path: std::path::PathBuf::from(path),
3723 fan_in: 0,
3724 fan_out: 0,
3725 dead_code_ratio: 0.0,
3726 complexity_density: 0.0,
3727 maintainability_index,
3728 total_cyclomatic: 0,
3729 total_cognitive: 0,
3730 function_count: 1,
3731 lines: 1,
3732 crap_max,
3733 crap_above_threshold: usize::from(crap_max >= CRAP_THRESHOLD),
3734 crap_exempted: 0,
3735 crap_effective_threshold: None,
3736 }
3737 }
3738
3739 fn crap_concern_at_default(crap_max: f64) -> f64 {
3740 file_score_crap_concern(
3741 &make_file_score("/src/concern.ts", 100.0, crap_max),
3742 CRAP_THRESHOLD,
3743 )
3744 }
3745
3746 #[test]
3747 fn file_score_crap_concern_tracks_crap_risk_bands() {
3748 assert!((crap_concern_at_default(0.0) - 0.0).abs() < f64::EPSILON);
3749 assert!((crap_concern_at_default(15.0) - 45.0).abs() < f64::EPSILON);
3750 assert!((crap_concern_at_default(CRAP_THRESHOLD) - 75.0).abs() < f64::EPSILON);
3751 assert!((crap_concern_at_default(100.0) - 100.0).abs() < f64::EPSILON);
3752 assert!((crap_concern_at_default(552.0) - 100.0).abs() < f64::EPSILON);
3753 }
3754
3755 #[test]
3756 fn file_score_crap_concern_generalizes_bands_over_effective_ceiling() {
3757 let mut at_edge = make_file_score("/src/edge.ts", 100.0, 250.0);
3760 at_edge.crap_above_threshold = 1;
3761 at_edge.crap_effective_threshold = Some(500.0);
3762 assert!((file_score_crap_concern(&at_edge, CRAP_THRESHOLD) - 45.0).abs() < f64::EPSILON);
3763
3764 let mut at_ceiling = make_file_score("/src/ceiling.ts", 100.0, 500.0);
3765 at_ceiling.crap_above_threshold = 1;
3766 at_ceiling.crap_effective_threshold = Some(500.0);
3767 assert!((file_score_crap_concern(&at_ceiling, CRAP_THRESHOLD) - 75.0).abs() < f64::EPSILON);
3768 }
3769
3770 #[test]
3771 fn file_score_crap_concern_zeroes_fully_exempt_file() {
3772 let mut exempt = make_file_score("/src/legacy.ts", 88.0, 110.0);
3777 exempt.crap_above_threshold = 0;
3778 exempt.crap_exempted = 2;
3779 exempt.crap_effective_threshold = Some(500.0);
3780 assert!((file_score_crap_concern(&exempt, CRAP_THRESHOLD) - 0.0).abs() < f64::EPSILON);
3781 assert!(file_score_fully_crap_exempt(&exempt, CRAP_THRESHOLD));
3782 assert_eq!(
3783 file_score_concern_axis(&exempt, CRAP_THRESHOLD),
3784 FileScoreConcern::Structural
3785 );
3786 }
3787
3788 #[test]
3789 fn file_score_crap_concern_zeroes_when_enforcement_disabled() {
3790 let mut score = make_file_score("/src/any.ts", 88.0, 110.0);
3791 score.crap_above_threshold = 0;
3792 score.crap_exempted = 2;
3793 assert!((file_score_crap_concern(&score, 0.0) - 0.0).abs() < f64::EPSILON);
3794 assert!(file_score_fully_crap_exempt(&score, 0.0));
3795 assert_eq!(
3796 file_score_concern_axis(&score, 0.0),
3797 FileScoreConcern::Structural
3798 );
3799 }
3800
3801 #[test]
3802 fn file_score_partial_exemption_keeps_risk_axis() {
3803 let mut mixed = make_file_score("/src/mixed.ts", 88.0, 110.0);
3806 mixed.crap_above_threshold = 1;
3807 mixed.crap_exempted = 1;
3808 mixed.crap_effective_threshold = Some(30.0);
3809 assert!(!file_score_fully_crap_exempt(&mixed, CRAP_THRESHOLD));
3810 assert_eq!(
3811 file_score_concern_axis(&mixed, CRAP_THRESHOLD),
3812 FileScoreConcern::Risk
3813 );
3814 }
3815
3816 #[test]
3817 fn file_score_concern_axis_labels_dominant_signal() {
3818 let risk_driven = make_file_score("/src/risk.ts", 84.8, 552.0);
3819 assert_eq!(
3820 file_score_concern_axis(&risk_driven, CRAP_THRESHOLD),
3821 FileScoreConcern::Risk
3822 );
3823 assert_eq!(
3824 file_score_concern_axis(&risk_driven, CRAP_THRESHOLD).label(),
3825 "risk"
3826 );
3827
3828 let structure_driven = make_file_score("/src/structure.ts", 30.0, 8.0);
3829 assert_eq!(
3830 file_score_concern_axis(&structure_driven, CRAP_THRESHOLD),
3831 FileScoreConcern::Structural
3832 );
3833 assert_eq!(
3834 file_score_concern_axis(&structure_driven, CRAP_THRESHOLD).label(),
3835 "structure"
3836 );
3837
3838 let no_risk = make_file_score("/src/clean.ts", 100.0, 0.0);
3839 assert_eq!(
3840 file_score_concern_axis(&no_risk, CRAP_THRESHOLD),
3841 FileScoreConcern::Structural
3842 );
3843 }
3844
3845 #[test]
3846 fn file_score_triage_sort_prioritizes_high_crap_over_slightly_lower_mi() {
3847 let low_mi_low_risk = make_file_score("/src/low-mi-low-risk.ts", 81.7, 2.0);
3848 let higher_mi_high_risk = make_file_score("/src/higher-mi-high-risk.ts", 84.8, 552.0);
3849
3850 let mut scores = [low_mi_low_risk, higher_mi_high_risk];
3851 scores.sort_by(|a, b| compare_file_score_triage(a, b, CRAP_THRESHOLD));
3852
3853 assert_eq!(
3854 scores[0].path,
3855 std::path::Path::new("/src/higher-mi-high-risk.ts")
3856 );
3857 assert_eq!(
3858 scores[1].path,
3859 std::path::Path::new("/src/low-mi-low-risk.ts")
3860 );
3861 }
3862
3863 #[test]
3864 fn file_score_triage_sort_orders_saturated_crap_by_raw_crap_descending() {
3865 let lower_crap_worse_mi = make_file_score("/src/a.ts", 84.8, 106.0);
3866 let higher_crap_better_mi = make_file_score("/src/b.ts", 96.7, 552.0);
3867
3868 let mut scores = [lower_crap_worse_mi, higher_crap_better_mi];
3869 scores.sort_by(|a, b| compare_file_score_triage(a, b, CRAP_THRESHOLD));
3870
3871 assert_eq!(scores[0].path, std::path::Path::new("/src/b.ts"));
3872 assert_eq!(scores[1].path, std::path::Path::new("/src/a.ts"));
3873 }
3874
3875 #[test]
3876 fn file_score_triage_sort_uses_mi_crap_and_path_tie_breakers() {
3877 let mut scores = [
3878 make_file_score("/src/b.ts", 70.0, 1.0),
3879 make_file_score("/src/a.ts", 70.0, 1.0),
3880 make_file_score("/src/higher-crap.ts", 70.0, 2.0),
3881 make_file_score("/src/lower-concern.ts", 80.0, 1.0),
3882 ];
3883
3884 scores.sort_by(|a, b| compare_file_score_triage(a, b, CRAP_THRESHOLD));
3885
3886 let paths: Vec<_> = scores.iter().map(|score| score.path.as_path()).collect();
3887 assert_eq!(
3888 paths,
3889 vec![
3890 std::path::Path::new("/src/higher-crap.ts"),
3891 std::path::Path::new("/src/a.ts"),
3892 std::path::Path::new("/src/b.ts"),
3893 std::path::Path::new("/src/lower-concern.ts"),
3894 ]
3895 );
3896 }
3897
3898 #[test]
3899 fn compute_file_scores_empty_graph() {
3900 let files: Vec<crate::discover::DiscoveredFile> = vec![];
3901 let graph = build_test_graph(&files, &[], &[]);
3902 let modules: Vec<crate::source::ModuleInfo> = vec![];
3903 let file_paths = rustc_hash::FxHashMap::default();
3904
3905 let output = crate::results::DeadCodeAnalysisArtifacts {
3906 results: fallow_types::results::AnalysisResults::default(),
3907 timings: None,
3908 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
3909 modules: None,
3910 files: None,
3911 script_used_packages: rustc_hash::FxHashSet::default(),
3912 file_hashes: rustc_hash::FxHashMap::default(),
3913 };
3914
3915 let result = compute_file_scores_default(
3916 &modules,
3917 &file_paths,
3918 None,
3919 output,
3920 None,
3921 std::path::Path::new("/project"),
3922 )
3923 .unwrap();
3924 assert!(result.scores.is_empty());
3925 assert!(result.circular_files.is_empty());
3926 assert!(result.top_complex_fns.is_empty());
3927 assert!(result.entry_points.is_empty());
3928 assert_eq!(result.analysis_counts.total_exports, 0);
3929 assert_eq!(result.analysis_counts.dead_files, 0);
3930 }
3931
3932 #[test]
3933 fn compute_file_scores_no_graph_returns_error() {
3934 let modules: Vec<crate::source::ModuleInfo> = vec![];
3935 let file_paths = rustc_hash::FxHashMap::default();
3936
3937 let output = crate::results::DeadCodeAnalysisArtifacts {
3938 results: fallow_types::results::AnalysisResults::default(),
3939 timings: None,
3940 graph: None,
3941 modules: None,
3942 files: None,
3943 script_used_packages: rustc_hash::FxHashSet::default(),
3944 file_hashes: rustc_hash::FxHashMap::default(),
3945 };
3946
3947 let result = compute_file_scores_default(
3948 &modules,
3949 &file_paths,
3950 None,
3951 output,
3952 None,
3953 std::path::Path::new("/project"),
3954 );
3955 assert!(result.is_err());
3956 match result {
3957 Err(msg) => assert_eq!(msg, "graph not available"),
3958 Ok(_) => panic!("expected error"),
3959 }
3960 }
3961
3962 #[test]
3963 fn compute_file_scores_single_file_with_function() {
3964 let path_a = std::path::PathBuf::from("/src/a.ts");
3965 let files = vec![crate::discover::DiscoveredFile {
3966 id: crate::discover::FileId(0),
3967 path: path_a.clone(),
3968 size_bytes: 100,
3969 }];
3970
3971 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
3972 file_id: crate::discover::FileId(0),
3973 path: path_a.clone(),
3974 exports: vec![fallow_types::extract::ExportInfo {
3975 name: crate::source::ExportName::Named("foo".into()),
3976 local_name: None,
3977 is_type_only: false,
3978 visibility: crate::source::VisibilityTag::None,
3979 expected_unused_reason: None,
3980 span: oxc_span::Span::empty(0),
3981 members: vec![],
3982 is_side_effect_used: false,
3983 super_class: None,
3984 }]
3985 .into(),
3986 ..Default::default()
3987 }];
3988
3989 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
3990
3991 let modules = vec![make_module_info(
3992 0,
3993 10,
3994 vec![fallow_types::extract::FunctionComplexity {
3995 name: "foo".into(),
3996 is_private_member: false,
3997 line: 1,
3998 col: 0,
3999 cyclomatic: 5,
4000 cognitive: 3,
4001 line_count: 10,
4002 param_count: 0,
4003 react_hook_count: 0,
4004 react_jsx_max_depth: 0,
4005 react_prop_count: 0,
4006 source_hash: None,
4007 contributions: Vec::new(),
4008 }],
4009 )];
4010
4011 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4012 rustc_hash::FxHashMap::default();
4013 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4014
4015 let output = crate::results::DeadCodeAnalysisArtifacts {
4016 results: fallow_types::results::AnalysisResults::default(),
4017 timings: None,
4018 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4019 modules: None,
4020 files: None,
4021 script_used_packages: rustc_hash::FxHashSet::default(),
4022 file_hashes: rustc_hash::FxHashMap::default(),
4023 };
4024
4025 let result = compute_file_scores_default(
4026 &modules,
4027 &file_paths,
4028 None,
4029 output,
4030 None,
4031 std::path::Path::new("/project"),
4032 )
4033 .unwrap();
4034 assert_eq!(result.scores.len(), 1);
4035
4036 let score = &result.scores[0];
4037 assert_eq!(score.path, path_a);
4038 assert_eq!(score.total_cyclomatic, 5);
4039 assert_eq!(score.total_cognitive, 3);
4040 assert_eq!(score.function_count, 1);
4041 assert_eq!(score.lines, 10);
4042 assert!((score.complexity_density - 0.5).abs() < f64::EPSILON);
4043 assert!(score.dead_code_ratio.abs() < f64::EPSILON);
4044 assert!(result.entry_points.contains(&path_a));
4045 }
4046
4047 #[test]
4048 fn compute_file_scores_excludes_barrel_files() {
4049 let path_a = std::path::PathBuf::from("/src/index.ts");
4050 let files = vec![crate::discover::DiscoveredFile {
4051 id: crate::discover::FileId(0),
4052 path: path_a.clone(),
4053 size_bytes: 50,
4054 }];
4055
4056 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4057 file_id: crate::discover::FileId(0),
4058 path: path_a.clone(),
4059 ..Default::default()
4060 }];
4061
4062 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
4063
4064 let modules = vec![make_module_info(0, 5, vec![])];
4065
4066 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4067 rustc_hash::FxHashMap::default();
4068 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4069
4070 let output = crate::results::DeadCodeAnalysisArtifacts {
4071 results: fallow_types::results::AnalysisResults::default(),
4072 timings: None,
4073 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4074 modules: None,
4075 files: None,
4076 script_used_packages: rustc_hash::FxHashSet::default(),
4077 file_hashes: rustc_hash::FxHashMap::default(),
4078 };
4079
4080 let result = compute_file_scores_default(
4081 &modules,
4082 &file_paths,
4083 None,
4084 output,
4085 None,
4086 std::path::Path::new("/project"),
4087 )
4088 .unwrap();
4089 assert!(result.scores.is_empty());
4090 }
4091
4092 #[test]
4093 fn compute_file_scores_changed_since_filter() {
4094 let path_a = std::path::PathBuf::from("/src/a.ts");
4095 let path_b = std::path::PathBuf::from("/src/b.ts");
4096 let files = vec![
4097 crate::discover::DiscoveredFile {
4098 id: crate::discover::FileId(0),
4099 path: path_a.clone(),
4100 size_bytes: 100,
4101 },
4102 crate::discover::DiscoveredFile {
4103 id: crate::discover::FileId(1),
4104 path: path_b.clone(),
4105 size_bytes: 100,
4106 },
4107 ];
4108
4109 let resolved_modules = vec![
4110 fallow_graph::resolve::ResolvedModule {
4111 file_id: crate::discover::FileId(0),
4112 path: path_a,
4113 ..Default::default()
4114 },
4115 fallow_graph::resolve::ResolvedModule {
4116 file_id: crate::discover::FileId(1),
4117 path: path_b.clone(),
4118 ..Default::default()
4119 },
4120 ];
4121
4122 let graph = build_test_graph(&files, &[], &resolved_modules);
4123
4124 let modules = vec![
4125 make_module_info(
4126 0,
4127 10,
4128 vec![fallow_types::extract::FunctionComplexity {
4129 name: "fn_a".into(),
4130 is_private_member: false,
4131 line: 1,
4132 col: 0,
4133 cyclomatic: 2,
4134 cognitive: 1,
4135 line_count: 10,
4136 param_count: 0,
4137 react_hook_count: 0,
4138 react_jsx_max_depth: 0,
4139 react_prop_count: 0,
4140 source_hash: None,
4141 contributions: Vec::new(),
4142 }],
4143 ),
4144 make_module_info(
4145 1,
4146 10,
4147 vec![fallow_types::extract::FunctionComplexity {
4148 name: "fn_b".into(),
4149 is_private_member: false,
4150 line: 1,
4151 col: 0,
4152 cyclomatic: 3,
4153 cognitive: 2,
4154 line_count: 10,
4155 param_count: 0,
4156 react_hook_count: 0,
4157 react_jsx_max_depth: 0,
4158 react_prop_count: 0,
4159 source_hash: None,
4160 contributions: Vec::new(),
4161 }],
4162 ),
4163 ];
4164
4165 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4166 rustc_hash::FxHashMap::default();
4167 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4168 file_paths.insert(crate::discover::FileId(1), &files[1].path);
4169
4170 let path_b_check = std::path::PathBuf::from("/src/b.ts");
4171 let mut changed = rustc_hash::FxHashSet::default();
4172 changed.insert(path_b);
4173
4174 let output = crate::results::DeadCodeAnalysisArtifacts {
4175 results: fallow_types::results::AnalysisResults::default(),
4176 timings: None,
4177 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4178 modules: None,
4179 files: None,
4180 script_used_packages: rustc_hash::FxHashSet::default(),
4181 file_hashes: rustc_hash::FxHashMap::default(),
4182 };
4183
4184 let result = compute_file_scores_default(
4185 &modules,
4186 &file_paths,
4187 Some(&changed),
4188 output,
4189 None,
4190 std::path::Path::new("/project"),
4191 )
4192 .unwrap();
4193 assert_eq!(result.scores.len(), 1);
4194 assert_eq!(result.scores[0].path, path_b_check);
4195 }
4196
4197 #[test]
4198 fn compute_file_scores_sorted_by_triage_concern() {
4199 let path_a = std::path::PathBuf::from("/src/a.ts");
4200 let path_b = std::path::PathBuf::from("/src/b.ts");
4201 let files = vec![
4202 crate::discover::DiscoveredFile {
4203 id: crate::discover::FileId(0),
4204 path: path_a.clone(),
4205 size_bytes: 100,
4206 },
4207 crate::discover::DiscoveredFile {
4208 id: crate::discover::FileId(1),
4209 path: path_b.clone(),
4210 size_bytes: 100,
4211 },
4212 ];
4213
4214 let resolved_modules = vec![
4215 fallow_graph::resolve::ResolvedModule {
4216 file_id: crate::discover::FileId(0),
4217 path: path_a.clone(),
4218 ..Default::default()
4219 },
4220 fallow_graph::resolve::ResolvedModule {
4221 file_id: crate::discover::FileId(1),
4222 path: path_b,
4223 ..Default::default()
4224 },
4225 ];
4226
4227 let graph = build_test_graph(&files, &[], &resolved_modules);
4228
4229 let modules = vec![
4230 make_module_info(
4231 0,
4232 10,
4233 vec![fallow_types::extract::FunctionComplexity {
4234 name: "complex_fn".into(),
4235 is_private_member: false,
4236 line: 1,
4237 col: 0,
4238 cyclomatic: 30,
4239 cognitive: 20,
4240 line_count: 10,
4241 param_count: 0,
4242 react_hook_count: 0,
4243 react_jsx_max_depth: 0,
4244 react_prop_count: 0,
4245 source_hash: None,
4246 contributions: Vec::new(),
4247 }],
4248 ),
4249 make_module_info(
4250 1,
4251 100,
4252 vec![fallow_types::extract::FunctionComplexity {
4253 name: "simple_fn".into(),
4254 is_private_member: false,
4255 line: 1,
4256 col: 0,
4257 cyclomatic: 1,
4258 cognitive: 0,
4259 line_count: 100,
4260 param_count: 0,
4261 react_hook_count: 0,
4262 react_jsx_max_depth: 0,
4263 react_prop_count: 0,
4264 source_hash: None,
4265 contributions: Vec::new(),
4266 }],
4267 ),
4268 ];
4269
4270 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4271 rustc_hash::FxHashMap::default();
4272 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4273 file_paths.insert(crate::discover::FileId(1), &files[1].path);
4274
4275 let output = crate::results::DeadCodeAnalysisArtifacts {
4276 results: fallow_types::results::AnalysisResults::default(),
4277 timings: None,
4278 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4279 modules: None,
4280 files: None,
4281 script_used_packages: rustc_hash::FxHashSet::default(),
4282 file_hashes: rustc_hash::FxHashMap::default(),
4283 };
4284
4285 let result = compute_file_scores_default(
4286 &modules,
4287 &file_paths,
4288 None,
4289 output,
4290 None,
4291 std::path::Path::new("/project"),
4292 )
4293 .unwrap();
4294 assert_eq!(result.scores.len(), 2);
4295 assert!(result.scores[0].maintainability_index <= result.scores[1].maintainability_index);
4296 assert_eq!(result.scores[0].path, path_a);
4297 }
4298
4299 #[test]
4300 fn compute_file_scores_with_unused_file_populates_evidence() {
4301 let path_a = std::path::PathBuf::from("/src/unused.ts");
4302 let files = vec![crate::discover::DiscoveredFile {
4303 id: crate::discover::FileId(0),
4304 path: path_a.clone(),
4305 size_bytes: 100,
4306 }];
4307
4308 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4309 file_id: crate::discover::FileId(0),
4310 path: path_a.clone(),
4311 exports: vec![fallow_types::extract::ExportInfo {
4312 name: crate::source::ExportName::Named("orphan".into()),
4313 local_name: None,
4314 is_type_only: false,
4315 visibility: crate::source::VisibilityTag::None,
4316 expected_unused_reason: None,
4317 span: oxc_span::Span::empty(0),
4318 members: vec![],
4319 is_side_effect_used: false,
4320 super_class: None,
4321 }]
4322 .into(),
4323 ..Default::default()
4324 }];
4325
4326 let graph = build_test_graph(&files, &[], &resolved_modules);
4327
4328 let modules = vec![make_module_info(
4329 0,
4330 10,
4331 vec![fallow_types::extract::FunctionComplexity {
4332 name: "orphan".into(),
4333 is_private_member: false,
4334 line: 1,
4335 col: 0,
4336 cyclomatic: 1,
4337 cognitive: 0,
4338 line_count: 10,
4339 param_count: 0,
4340 react_hook_count: 0,
4341 react_jsx_max_depth: 0,
4342 react_prop_count: 0,
4343 source_hash: None,
4344 contributions: Vec::new(),
4345 }],
4346 )];
4347
4348 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4349 rustc_hash::FxHashMap::default();
4350 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4351
4352 let mut results = fallow_types::results::AnalysisResults::default();
4353 results.unused_files.push(
4354 fallow_types::output_dead_code::UnusedFileFinding::with_actions(
4355 fallow_types::results::UnusedFile {
4356 path: path_a.clone(),
4357 },
4358 ),
4359 );
4360
4361 let output = crate::results::DeadCodeAnalysisArtifacts {
4362 results,
4363 timings: None,
4364 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4365 modules: None,
4366 files: None,
4367 script_used_packages: rustc_hash::FxHashSet::default(),
4368 file_hashes: rustc_hash::FxHashMap::default(),
4369 };
4370
4371 let result = compute_file_scores_default(
4372 &modules,
4373 &file_paths,
4374 None,
4375 output,
4376 None,
4377 std::path::Path::new("/project"),
4378 )
4379 .unwrap();
4380 assert_eq!(result.scores.len(), 1);
4381 assert!((result.scores[0].dead_code_ratio - 1.0).abs() < f64::EPSILON);
4382 assert!(result.unused_export_names.contains_key(&path_a));
4383 let names = &result.unused_export_names[&path_a];
4384 assert_eq!(names, &["orphan"]);
4385 assert_eq!(result.analysis_counts.dead_files, 1);
4386 }
4387
4388 #[test]
4389 #[expect(
4390 clippy::too_many_lines,
4391 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
4392 )]
4393 fn compute_file_scores_tracks_top_complex_functions() {
4394 let path_a = std::path::PathBuf::from("/src/complex.ts");
4395 let files = vec![crate::discover::DiscoveredFile {
4396 id: crate::discover::FileId(0),
4397 path: path_a.clone(),
4398 size_bytes: 500,
4399 }];
4400
4401 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4402 file_id: crate::discover::FileId(0),
4403 path: path_a.clone(),
4404 ..Default::default()
4405 }];
4406
4407 let graph = build_test_graph(&files, &[], &resolved_modules);
4408
4409 let modules = vec![make_module_info(
4410 0,
4411 50,
4412 vec![
4413 fallow_types::extract::FunctionComplexity {
4414 name: "high".into(),
4415 is_private_member: false,
4416 line: 1,
4417 col: 0,
4418 cyclomatic: 10,
4419 cognitive: 20,
4420 line_count: 10,
4421 param_count: 0,
4422 react_hook_count: 0,
4423 react_jsx_max_depth: 0,
4424 react_prop_count: 0,
4425 source_hash: None,
4426 contributions: Vec::new(),
4427 },
4428 fallow_types::extract::FunctionComplexity {
4429 name: "medium".into(),
4430 is_private_member: false,
4431 line: 11,
4432 col: 0,
4433 cyclomatic: 5,
4434 cognitive: 10,
4435 line_count: 10,
4436 param_count: 0,
4437 react_hook_count: 0,
4438 react_jsx_max_depth: 0,
4439 react_prop_count: 0,
4440 source_hash: None,
4441 contributions: Vec::new(),
4442 },
4443 fallow_types::extract::FunctionComplexity {
4444 name: "low".into(),
4445 is_private_member: false,
4446 line: 21,
4447 col: 0,
4448 cyclomatic: 2,
4449 cognitive: 5,
4450 line_count: 10,
4451 param_count: 0,
4452 react_hook_count: 0,
4453 react_jsx_max_depth: 0,
4454 react_prop_count: 0,
4455 source_hash: None,
4456 contributions: Vec::new(),
4457 },
4458 fallow_types::extract::FunctionComplexity {
4459 name: "trivial".into(),
4460 is_private_member: false,
4461 line: 31,
4462 col: 0,
4463 cyclomatic: 1,
4464 cognitive: 1,
4465 line_count: 10,
4466 param_count: 0,
4467 react_hook_count: 0,
4468 react_jsx_max_depth: 0,
4469 react_prop_count: 0,
4470 source_hash: None,
4471 contributions: Vec::new(),
4472 },
4473 ],
4474 )];
4475
4476 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4477 rustc_hash::FxHashMap::default();
4478 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4479
4480 let output = crate::results::DeadCodeAnalysisArtifacts {
4481 results: fallow_types::results::AnalysisResults::default(),
4482 timings: None,
4483 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4484 modules: None,
4485 files: None,
4486 script_used_packages: rustc_hash::FxHashSet::default(),
4487 file_hashes: rustc_hash::FxHashMap::default(),
4488 };
4489
4490 let result = compute_file_scores_default(
4491 &modules,
4492 &file_paths,
4493 None,
4494 output,
4495 None,
4496 std::path::Path::new("/project"),
4497 )
4498 .unwrap();
4499 assert!(result.top_complex_fns.contains_key(&path_a));
4500 let top = &result.top_complex_fns[&path_a];
4501 assert_eq!(top.len(), 3);
4502 assert_eq!(top[0].0, "high");
4503 assert_eq!(top[0].2, 20);
4504 assert_eq!(top[1].0, "medium");
4505 assert_eq!(top[1].2, 10);
4506 assert_eq!(top[2].0, "low");
4507 assert_eq!(top[2].2, 5);
4508 }
4509
4510 #[test]
4511 #[expect(
4512 clippy::too_many_lines,
4513 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
4514 )]
4515 fn compute_file_scores_with_circular_deps() {
4516 let path_a = std::path::PathBuf::from("/src/a.ts");
4517 let path_b = std::path::PathBuf::from("/src/b.ts");
4518 let files = vec![
4519 crate::discover::DiscoveredFile {
4520 id: crate::discover::FileId(0),
4521 path: path_a.clone(),
4522 size_bytes: 100,
4523 },
4524 crate::discover::DiscoveredFile {
4525 id: crate::discover::FileId(1),
4526 path: path_b.clone(),
4527 size_bytes: 100,
4528 },
4529 ];
4530
4531 let resolved_modules = vec![
4532 fallow_graph::resolve::ResolvedModule {
4533 file_id: crate::discover::FileId(0),
4534 path: path_a.clone(),
4535 ..Default::default()
4536 },
4537 fallow_graph::resolve::ResolvedModule {
4538 file_id: crate::discover::FileId(1),
4539 path: path_b.clone(),
4540 ..Default::default()
4541 },
4542 ];
4543
4544 let graph = build_test_graph(&files, &[], &resolved_modules);
4545
4546 let modules = vec![
4547 make_module_info(
4548 0,
4549 10,
4550 vec![fallow_types::extract::FunctionComplexity {
4551 name: "fn_a".into(),
4552 is_private_member: false,
4553 line: 1,
4554 col: 0,
4555 cyclomatic: 2,
4556 cognitive: 1,
4557 line_count: 10,
4558 param_count: 0,
4559 react_hook_count: 0,
4560 react_jsx_max_depth: 0,
4561 react_prop_count: 0,
4562 source_hash: None,
4563 contributions: Vec::new(),
4564 }],
4565 ),
4566 make_module_info(
4567 1,
4568 10,
4569 vec![fallow_types::extract::FunctionComplexity {
4570 name: "fn_b".into(),
4571 is_private_member: false,
4572 line: 1,
4573 col: 0,
4574 cyclomatic: 3,
4575 cognitive: 2,
4576 line_count: 10,
4577 param_count: 0,
4578 react_hook_count: 0,
4579 react_jsx_max_depth: 0,
4580 react_prop_count: 0,
4581 source_hash: None,
4582 contributions: Vec::new(),
4583 }],
4584 ),
4585 ];
4586
4587 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4588 rustc_hash::FxHashMap::default();
4589 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4590 file_paths.insert(crate::discover::FileId(1), &files[1].path);
4591
4592 let mut results = fallow_types::results::AnalysisResults::default();
4593 results.circular_dependencies.push(
4594 fallow_types::output_dead_code::CircularDependencyFinding::with_actions(
4595 fallow_types::results::CircularDependency {
4596 files: vec![path_a.clone(), path_b.clone()],
4597 length: 2,
4598 line: 1,
4599 col: 0,
4600 edges: Vec::new(),
4601 is_cross_package: false,
4602 },
4603 ),
4604 );
4605
4606 let output = crate::results::DeadCodeAnalysisArtifacts {
4607 results,
4608 timings: None,
4609 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4610 modules: None,
4611 files: None,
4612 script_used_packages: rustc_hash::FxHashSet::default(),
4613 file_hashes: rustc_hash::FxHashMap::default(),
4614 };
4615
4616 let result = compute_file_scores_default(
4617 &modules,
4618 &file_paths,
4619 None,
4620 output,
4621 None,
4622 std::path::Path::new("/project"),
4623 )
4624 .unwrap();
4625 assert!(result.circular_files.contains(&path_a));
4626 assert!(result.circular_files.contains(&path_b));
4627 assert!(result.cycle_members.contains_key(&path_a));
4628 assert_eq!(result.cycle_members[&path_a], vec![path_b.clone()]);
4629 assert!(result.cycle_members.contains_key(&path_b));
4630 assert_eq!(result.cycle_members[&path_b], vec![path_a]);
4631 assert_eq!(result.analysis_counts.circular_deps, 1);
4632 }
4633
4634 #[test]
4635 #[expect(
4636 clippy::too_many_lines,
4637 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
4638 )]
4639 fn compute_file_scores_analysis_counts_unused_exports_and_types() {
4640 let path_a = std::path::PathBuf::from("/src/a.ts");
4641 let files = vec![crate::discover::DiscoveredFile {
4642 id: crate::discover::FileId(0),
4643 path: path_a.clone(),
4644 size_bytes: 100,
4645 }];
4646
4647 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4648 file_id: crate::discover::FileId(0),
4649 path: path_a.clone(),
4650 exports: vec![
4651 fallow_types::extract::ExportInfo {
4652 name: crate::source::ExportName::Named("foo".into()),
4653 local_name: None,
4654 is_type_only: false,
4655 visibility: crate::source::VisibilityTag::None,
4656 expected_unused_reason: None,
4657 span: oxc_span::Span::empty(0),
4658 members: vec![],
4659 is_side_effect_used: false,
4660 super_class: None,
4661 },
4662 fallow_types::extract::ExportInfo {
4663 name: crate::source::ExportName::Named("bar".into()),
4664 local_name: None,
4665 is_type_only: false,
4666 visibility: crate::source::VisibilityTag::None,
4667 expected_unused_reason: None,
4668 span: oxc_span::Span::empty(0),
4669 members: vec![],
4670 is_side_effect_used: false,
4671 super_class: None,
4672 },
4673 ]
4674 .into(),
4675 ..Default::default()
4676 }];
4677
4678 let graph = build_test_graph(&files, &[], &resolved_modules);
4679
4680 let mut module = make_module_info(
4681 0,
4682 10,
4683 vec![fallow_types::extract::FunctionComplexity {
4684 name: "fn_a".into(),
4685 is_private_member: false,
4686 line: 1,
4687 col: 0,
4688 cyclomatic: 1,
4689 cognitive: 0,
4690 line_count: 10,
4691 param_count: 0,
4692 react_hook_count: 0,
4693 react_jsx_max_depth: 0,
4694 react_prop_count: 0,
4695 source_hash: None,
4696 contributions: Vec::new(),
4697 }],
4698 );
4699 module.exports = vec![
4700 fallow_types::extract::ExportInfo {
4701 name: crate::source::ExportName::Named("foo".into()),
4702 local_name: None,
4703 is_type_only: false,
4704 visibility: crate::source::VisibilityTag::None,
4705 expected_unused_reason: None,
4706 span: oxc_span::Span::empty(0),
4707 members: vec![],
4708 is_side_effect_used: false,
4709 super_class: None,
4710 },
4711 fallow_types::extract::ExportInfo {
4712 name: crate::source::ExportName::Named("bar".into()),
4713 local_name: None,
4714 is_type_only: false,
4715 visibility: crate::source::VisibilityTag::None,
4716 expected_unused_reason: None,
4717 span: oxc_span::Span::empty(0),
4718 members: vec![],
4719 is_side_effect_used: false,
4720 super_class: None,
4721 },
4722 ]
4723 .into();
4724 let modules = vec![module];
4725
4726 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4727 rustc_hash::FxHashMap::default();
4728 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4729
4730 let mut results = fallow_types::results::AnalysisResults::default();
4731 results.unused_exports.push(
4732 fallow_types::output_dead_code::UnusedExportFinding::with_actions(
4733 fallow_types::results::UnusedExport {
4734 path: path_a.clone(),
4735 export_name: "foo".into(),
4736 is_type_only: false,
4737 line: 1,
4738 col: 0,
4739 span_start: 0,
4740 is_re_export: false,
4741 },
4742 ),
4743 );
4744 results.unused_types.push(
4745 fallow_types::output_dead_code::UnusedTypeFinding::with_actions(
4746 fallow_types::results::UnusedExport {
4747 path: path_a,
4748 export_name: "MyType".into(),
4749 is_type_only: true,
4750 line: 5,
4751 col: 0,
4752 span_start: 40,
4753 is_re_export: false,
4754 },
4755 ),
4756 );
4757 results.unused_dependencies.push(
4758 fallow_types::output_dead_code::UnusedDependencyFinding::with_actions(
4759 fallow_types::results::UnusedDependency {
4760 package_name: "lodash".into(),
4761 location: fallow_types::results::DependencyLocation::Dependencies,
4762 path: std::path::PathBuf::from("/package.json"),
4763 line: 1,
4764 used_in_workspaces: Vec::new(),
4765 },
4766 ),
4767 );
4768
4769 let output = crate::results::DeadCodeAnalysisArtifacts {
4770 results,
4771 timings: None,
4772 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4773 modules: None,
4774 files: None,
4775 script_used_packages: rustc_hash::FxHashSet::default(),
4776 file_hashes: rustc_hash::FxHashMap::default(),
4777 };
4778
4779 let result = compute_file_scores_default(
4780 &modules,
4781 &file_paths,
4782 None,
4783 output,
4784 None,
4785 std::path::Path::new("/project"),
4786 )
4787 .unwrap();
4788 assert_eq!(result.analysis_counts.total_exports, 2);
4789 assert_eq!(result.analysis_counts.dead_exports, 2);
4790 assert_eq!(result.analysis_counts.unused_deps, 1);
4791 }
4792
4793 #[test]
4795 #[expect(
4796 clippy::too_many_lines,
4797 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
4798 )]
4799 fn total_exports_counts_graph_modules_not_extraction_modules() {
4800 let path_a = std::path::PathBuf::from("/src/a.ts");
4801 let files = vec![crate::discover::DiscoveredFile {
4802 id: crate::discover::FileId(0),
4803 path: path_a.clone(),
4804 size_bytes: 100,
4805 }];
4806
4807 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4808 file_id: crate::discover::FileId(0),
4809 path: path_a.clone(),
4810 exports: vec![
4811 fallow_types::extract::ExportInfo {
4812 name: crate::source::ExportName::Named("foo".into()),
4813 local_name: None,
4814 is_type_only: false,
4815 visibility: crate::source::VisibilityTag::None,
4816 expected_unused_reason: None,
4817 span: oxc_span::Span::empty(0),
4818 members: vec![],
4819 is_side_effect_used: false,
4820 super_class: None,
4821 },
4822 fallow_types::extract::ExportInfo {
4823 name: crate::source::ExportName::Named("bar".into()),
4824 local_name: None,
4825 is_type_only: false,
4826 visibility: crate::source::VisibilityTag::None,
4827 expected_unused_reason: None,
4828 span: oxc_span::Span::empty(0),
4829 members: vec![],
4830 is_side_effect_used: false,
4831 super_class: None,
4832 },
4833 fallow_types::extract::ExportInfo {
4834 name: crate::source::ExportName::Named("baz".into()),
4835 local_name: None,
4836 is_type_only: false,
4837 visibility: crate::source::VisibilityTag::None,
4838 expected_unused_reason: None,
4839 span: oxc_span::Span::new(0, 0),
4840 members: vec![],
4841 is_side_effect_used: false,
4842 super_class: None,
4843 },
4844 ]
4845 .into(),
4846 ..Default::default()
4847 }];
4848
4849 let graph = build_test_graph(&files, &[], &resolved_modules);
4850
4851 let mut module = make_module_info(
4852 0,
4853 10,
4854 vec![fallow_types::extract::FunctionComplexity {
4855 name: "fn_a".into(),
4856 is_private_member: false,
4857 line: 1,
4858 col: 0,
4859 cyclomatic: 1,
4860 cognitive: 0,
4861 line_count: 10,
4862 param_count: 0,
4863 react_hook_count: 0,
4864 react_jsx_max_depth: 0,
4865 react_prop_count: 0,
4866 source_hash: None,
4867 contributions: Vec::new(),
4868 }],
4869 );
4870 module.exports = vec![
4871 fallow_types::extract::ExportInfo {
4872 name: crate::source::ExportName::Named("foo".into()),
4873 local_name: None,
4874 is_type_only: false,
4875 visibility: crate::source::VisibilityTag::None,
4876 expected_unused_reason: None,
4877 span: oxc_span::Span::empty(0),
4878 members: vec![],
4879 is_side_effect_used: false,
4880 super_class: None,
4881 },
4882 fallow_types::extract::ExportInfo {
4883 name: crate::source::ExportName::Named("bar".into()),
4884 local_name: None,
4885 is_type_only: false,
4886 visibility: crate::source::VisibilityTag::None,
4887 expected_unused_reason: None,
4888 span: oxc_span::Span::empty(0),
4889 members: vec![],
4890 is_side_effect_used: false,
4891 super_class: None,
4892 },
4893 ]
4894 .into();
4895 let modules = vec![module];
4896
4897 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4898 rustc_hash::FxHashMap::default();
4899 file_paths.insert(crate::discover::FileId(0), &files[0].path);
4900
4901 let mut results = fallow_types::results::AnalysisResults::default();
4902 for name in ["foo", "bar", "baz"] {
4903 results.unused_exports.push(
4904 fallow_types::output_dead_code::UnusedExportFinding::with_actions(
4905 fallow_types::results::UnusedExport {
4906 path: path_a.clone(),
4907 export_name: name.into(),
4908 is_type_only: false,
4909 line: 1,
4910 col: 0,
4911 span_start: 0,
4912 is_re_export: name == "baz",
4913 },
4914 ),
4915 );
4916 }
4917
4918 let output = crate::results::DeadCodeAnalysisArtifacts {
4919 results,
4920 timings: None,
4921 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4922 modules: None,
4923 files: None,
4924 script_used_packages: rustc_hash::FxHashSet::default(),
4925 file_hashes: rustc_hash::FxHashMap::default(),
4926 };
4927
4928 let result = compute_file_scores_default(
4929 &modules,
4930 &file_paths,
4931 None,
4932 output,
4933 None,
4934 std::path::Path::new("/project"),
4935 )
4936 .unwrap();
4937 assert_eq!(result.analysis_counts.total_exports, 3);
4938 assert_eq!(result.analysis_counts.dead_exports, 3);
4939 }
4940
4941 #[test]
4942 fn compute_file_scores_module_not_in_file_paths_skipped() {
4943 let path_a = std::path::PathBuf::from("/src/a.ts");
4944 let files = vec![crate::discover::DiscoveredFile {
4945 id: crate::discover::FileId(0),
4946 path: path_a.clone(),
4947 size_bytes: 100,
4948 }];
4949
4950 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
4951 file_id: crate::discover::FileId(0),
4952 path: path_a,
4953 ..Default::default()
4954 }];
4955
4956 let graph = build_test_graph(&files, &[], &resolved_modules);
4957
4958 let modules = vec![make_module_info(
4959 0,
4960 10,
4961 vec![fallow_types::extract::FunctionComplexity {
4962 name: "fn_a".into(),
4963 is_private_member: false,
4964 line: 1,
4965 col: 0,
4966 cyclomatic: 2,
4967 cognitive: 1,
4968 line_count: 10,
4969 param_count: 0,
4970 react_hook_count: 0,
4971 react_jsx_max_depth: 0,
4972 react_prop_count: 0,
4973 source_hash: None,
4974 contributions: Vec::new(),
4975 }],
4976 )];
4977
4978 let file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
4979 rustc_hash::FxHashMap::default();
4980
4981 let output = crate::results::DeadCodeAnalysisArtifacts {
4982 results: fallow_types::results::AnalysisResults::default(),
4983 timings: None,
4984 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4985 modules: None,
4986 files: None,
4987 script_used_packages: rustc_hash::FxHashSet::default(),
4988 file_hashes: rustc_hash::FxHashMap::default(),
4989 };
4990
4991 let result = compute_file_scores_default(
4992 &modules,
4993 &file_paths,
4994 None,
4995 output,
4996 None,
4997 std::path::Path::new("/project"),
4998 )
4999 .unwrap();
5000 assert!(result.scores.is_empty());
5001 }
5002
5003 #[test]
5004 fn compute_file_scores_mi_rounded_to_one_decimal() {
5005 let path_a = std::path::PathBuf::from("/src/a.ts");
5006 let files = vec![crate::discover::DiscoveredFile {
5007 id: crate::discover::FileId(0),
5008 path: path_a.clone(),
5009 size_bytes: 100,
5010 }];
5011
5012 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5013 file_id: crate::discover::FileId(0),
5014 path: path_a.clone(),
5015 ..Default::default()
5016 }];
5017
5018 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5019
5020 let modules = vec![make_module_info(
5021 0,
5022 100,
5023 vec![fallow_types::extract::FunctionComplexity {
5024 name: "fn".into(),
5025 is_private_member: false,
5026 line: 1,
5027 col: 0,
5028 cyclomatic: 7,
5029 cognitive: 3,
5030 line_count: 100,
5031 param_count: 0,
5032 react_hook_count: 0,
5033 react_jsx_max_depth: 0,
5034 react_prop_count: 0,
5035 source_hash: None,
5036 contributions: Vec::new(),
5037 }],
5038 )];
5039
5040 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5041 rustc_hash::FxHashMap::default();
5042 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5043
5044 let output = crate::results::DeadCodeAnalysisArtifacts {
5045 results: fallow_types::results::AnalysisResults::default(),
5046 timings: None,
5047 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5048 modules: None,
5049 files: None,
5050 script_used_packages: rustc_hash::FxHashSet::default(),
5051 file_hashes: rustc_hash::FxHashMap::default(),
5052 };
5053
5054 let result = compute_file_scores_default(
5055 &modules,
5056 &file_paths,
5057 None,
5058 output,
5059 None,
5060 std::path::Path::new("/project"),
5061 )
5062 .unwrap();
5063 let mi = result.scores[0].maintainability_index;
5064 let rounded = (mi * 10.0).round() / 10.0;
5065 assert!((mi - rounded).abs() < f64::EPSILON);
5066 }
5067
5068 #[test]
5069 fn compute_file_scores_value_export_counts_tracked() {
5070 let path_a = std::path::PathBuf::from("/src/a.ts");
5071 let files = vec![crate::discover::DiscoveredFile {
5072 id: crate::discover::FileId(0),
5073 path: path_a.clone(),
5074 size_bytes: 100,
5075 }];
5076
5077 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5078 file_id: crate::discover::FileId(0),
5079 path: path_a.clone(),
5080 exports: vec![
5081 fallow_types::extract::ExportInfo {
5082 name: crate::source::ExportName::Named("a".into()),
5083 local_name: None,
5084 is_type_only: false,
5085 visibility: crate::source::VisibilityTag::None,
5086 expected_unused_reason: None,
5087 span: oxc_span::Span::empty(0),
5088 members: vec![],
5089 is_side_effect_used: false,
5090 super_class: None,
5091 },
5092 fallow_types::extract::ExportInfo {
5093 name: crate::source::ExportName::Named("b".into()),
5094 local_name: None,
5095 is_type_only: false,
5096 visibility: crate::source::VisibilityTag::None,
5097 expected_unused_reason: None,
5098 span: oxc_span::Span::empty(0),
5099 members: vec![],
5100 is_side_effect_used: false,
5101 super_class: None,
5102 },
5103 fallow_types::extract::ExportInfo {
5104 name: crate::source::ExportName::Named("T".into()),
5105 local_name: None,
5106 is_type_only: true,
5107 visibility: crate::source::VisibilityTag::None,
5108 expected_unused_reason: None,
5109 span: oxc_span::Span::empty(0),
5110 members: vec![],
5111 is_side_effect_used: false,
5112 super_class: None,
5113 },
5114 ]
5115 .into(),
5116 ..Default::default()
5117 }];
5118
5119 let graph = build_test_graph(&files, &[], &resolved_modules);
5120
5121 let modules = vec![make_module_info(
5122 0,
5123 10,
5124 vec![fallow_types::extract::FunctionComplexity {
5125 name: "fn_a".into(),
5126 is_private_member: false,
5127 line: 1,
5128 col: 0,
5129 cyclomatic: 2,
5130 cognitive: 1,
5131 line_count: 10,
5132 param_count: 0,
5133 react_hook_count: 0,
5134 react_jsx_max_depth: 0,
5135 react_prop_count: 0,
5136 source_hash: None,
5137 contributions: Vec::new(),
5138 }],
5139 )];
5140
5141 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5142 rustc_hash::FxHashMap::default();
5143 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5144
5145 let output = crate::results::DeadCodeAnalysisArtifacts {
5146 results: fallow_types::results::AnalysisResults::default(),
5147 timings: None,
5148 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5149 modules: None,
5150 files: None,
5151 script_used_packages: rustc_hash::FxHashSet::default(),
5152 file_hashes: rustc_hash::FxHashMap::default(),
5153 };
5154
5155 let result = compute_file_scores_default(
5156 &modules,
5157 &file_paths,
5158 None,
5159 output,
5160 None,
5161 std::path::Path::new("/project"),
5162 )
5163 .unwrap();
5164 assert_eq!(result.value_export_counts[&path_a], 2);
5165 }
5166
5167 #[test]
5168 fn compute_file_scores_top_complex_fns_zero_cognitive_excluded() {
5169 let path_a = std::path::PathBuf::from("/src/simple.ts");
5170 let files = vec![crate::discover::DiscoveredFile {
5171 id: crate::discover::FileId(0),
5172 path: path_a.clone(),
5173 size_bytes: 100,
5174 }];
5175
5176 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5177 file_id: crate::discover::FileId(0),
5178 path: path_a.clone(),
5179 ..Default::default()
5180 }];
5181
5182 let graph = build_test_graph(&files, &[], &resolved_modules);
5183
5184 let modules = vec![make_module_info(
5185 0,
5186 10,
5187 vec![fallow_types::extract::FunctionComplexity {
5188 name: "trivial".into(),
5189 is_private_member: false,
5190 line: 1,
5191 col: 0,
5192 cyclomatic: 1,
5193 cognitive: 0,
5194 line_count: 10,
5195 param_count: 0,
5196 react_hook_count: 0,
5197 react_jsx_max_depth: 0,
5198 react_prop_count: 0,
5199 source_hash: None,
5200 contributions: Vec::new(),
5201 }],
5202 )];
5203
5204 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5205 rustc_hash::FxHashMap::default();
5206 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5207
5208 let output = crate::results::DeadCodeAnalysisArtifacts {
5209 results: fallow_types::results::AnalysisResults::default(),
5210 timings: None,
5211 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5212 modules: None,
5213 files: None,
5214 script_used_packages: rustc_hash::FxHashSet::default(),
5215 file_hashes: rustc_hash::FxHashMap::default(),
5216 };
5217
5218 let result = compute_file_scores_default(
5219 &modules,
5220 &file_paths,
5221 None,
5222 output,
5223 None,
5224 std::path::Path::new("/project"),
5225 )
5226 .unwrap();
5227 assert!(!result.top_complex_fns.contains_key(&path_a));
5228 }
5229
5230 fn make_fn_complexity(cyclomatic: u16) -> fallow_types::extract::FunctionComplexity {
5231 fallow_types::extract::FunctionComplexity {
5232 name: "test_fn".into(),
5233 is_private_member: false,
5234 line: 1,
5235 col: 0,
5236 cyclomatic,
5237 cognitive: 0,
5238 line_count: 10,
5239 param_count: 0,
5240 react_hook_count: 0,
5241 react_jsx_max_depth: 0,
5242 react_prop_count: 0,
5243 source_hash: None,
5244 contributions: Vec::new(),
5245 }
5246 }
5247
5248 fn make_named_fn_complexity(
5249 name: &str,
5250 line: u32,
5251 cyclomatic: u16,
5252 ) -> fallow_types::extract::FunctionComplexity {
5253 fallow_types::extract::FunctionComplexity {
5254 name: name.into(),
5255 is_private_member: false,
5256 line,
5257 col: 0,
5258 cyclomatic,
5259 cognitive: 0,
5260 line_count: 10,
5261 param_count: 0,
5262 react_hook_count: 0,
5263 react_jsx_max_depth: 0,
5264 react_prop_count: 0,
5265 source_hash: None,
5266 contributions: Vec::new(),
5267 }
5268 }
5269
5270 fn crap_override_entry(
5271 files: &[&str],
5272 functions: &[&str],
5273 max_crap: Option<f64>,
5274 ) -> fallow_config::HealthThresholdOverride {
5275 fallow_config::HealthThresholdOverride {
5276 files: files.iter().map(ToString::to_string).collect(),
5277 functions: functions.iter().map(ToString::to_string).collect(),
5278 max_cyclomatic: None,
5279 max_cognitive: None,
5280 max_crap,
5281 max_unit_size: None,
5282 reason: Some("test override".into()),
5283 }
5284 }
5285
5286 fn estimated_signals_with(
5287 resolver: &ThresholdOverrideResolver,
5288 relative: &str,
5289 enforce_crap: bool,
5290 complexity: &[fallow_types::extract::FunctionComplexity],
5291 ) -> CrapThresholdSignals {
5292 let ceilings = CrapCeilingLookup::new(
5293 CrapScoreThresholds {
5294 resolver,
5295 enforce_crap,
5296 },
5297 std::path::Path::new(relative),
5298 );
5299 compute_crap_scores_estimated(
5300 complexity,
5301 &rustc_hash::FxHashSet::default(),
5302 false,
5303 fallow_output::CoverageSource::Estimated,
5304 &ceilings,
5305 )
5306 .signals
5307 }
5308
5309 #[test]
5310 fn crap_counting_exempts_functions_under_override_ceiling() {
5311 let resolver =
5314 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(500.0))]);
5315 let fns = vec![
5316 make_named_fn_complexity("a", 1, 10),
5317 make_named_fn_complexity("b", 12, 10),
5318 ];
5319
5320 let covered = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5321 assert_eq!(covered.above, 0);
5322 assert_eq!(covered.exempted, 2);
5323 assert_eq!(covered.min_ceiling, Some(500.0));
5324
5325 let elsewhere = estimated_signals_with(&resolver, "src/other.ts", true, &fns);
5326 assert_eq!(elsewhere.above, 2);
5327 assert_eq!(elsewhere.exempted, 0);
5328 assert_eq!(elsewhere.min_ceiling, Some(CRAP_THRESHOLD));
5329 }
5330
5331 #[test]
5332 fn crap_counting_insufficient_override_keeps_count() {
5333 let resolver =
5334 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(50.0))]);
5335 let fns = vec![
5336 make_named_fn_complexity("a", 1, 10),
5337 make_named_fn_complexity("b", 12, 10),
5338 ];
5339
5340 let signals = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5341 assert_eq!(signals.above, 2);
5342 assert_eq!(signals.exempted, 0);
5343 assert_eq!(signals.min_ceiling, Some(50.0));
5344 }
5345
5346 #[test]
5347 fn crap_counting_partial_function_override() {
5348 let resolver =
5351 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &["a"], Some(500.0))]);
5352 let fns = vec![
5353 make_named_fn_complexity("a", 1, 10),
5354 make_named_fn_complexity("b", 12, 10),
5355 ];
5356
5357 let signals = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5358 assert_eq!(signals.above, 1);
5359 assert_eq!(signals.exempted, 1);
5360 assert_eq!(signals.min_ceiling, Some(CRAP_THRESHOLD));
5361 }
5362
5363 #[test]
5364 fn crap_counting_disabled_enforcement_counts_baseline_exemptions() {
5365 let resolver = test_crap_resolver(0.0);
5368 let fns = vec![
5369 make_named_fn_complexity("a", 1, 10),
5370 make_named_fn_complexity("b", 12, 10),
5371 make_named_fn_complexity("tiny", 24, 1),
5372 ];
5373
5374 let signals = estimated_signals_with(&resolver, "src/any.ts", false, &fns);
5375 assert_eq!(signals.above, 0);
5376 assert_eq!(signals.exempted, 2);
5377 }
5378
5379 #[test]
5380 fn crap_counting_stricter_ceiling_never_counts_exempt() {
5381 let resolver = test_crap_resolver(10.0);
5384 let fns = vec![make_named_fn_complexity("a", 1, 4)]; let signals = estimated_signals_with(&resolver, "src/any.ts", true, &fns);
5387 assert_eq!(signals.above, 1);
5388 assert_eq!(signals.exempted, 0);
5389 }
5390
5391 #[test]
5392 fn crap_counting_uses_rounded_value_at_boundary() {
5393 let funcs = vec![make_fn_complexity(10)];
5398 let mut functions = rustc_hash::FxHashMap::default();
5399 functions.insert(("test_fn".to_string(), 1, 0), 41.56);
5400 let file_cov = test_istanbul_file_coverage(functions, false);
5401
5402 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5403 assert!((result.per_function[0].crap - 30.0).abs() < f64::EPSILON);
5404 assert_eq!(result.signals.above, 1);
5405 assert_eq!(result.signals.exempted, 0);
5406 }
5407
5408 #[test]
5409 fn compute_file_scores_discloses_override_exemption_on_row() {
5410 let path_a = std::path::PathBuf::from("/project/src/legacy.ts");
5411 let files = vec![crate::discover::DiscoveredFile {
5412 id: crate::discover::FileId(0),
5413 path: path_a.clone(),
5414 size_bytes: 100,
5415 }];
5416 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5417 file_id: crate::discover::FileId(0),
5418 path: path_a.clone(),
5419 ..Default::default()
5420 }];
5421 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5422 let modules = vec![make_module_info(
5423 0,
5424 26,
5425 vec![
5426 make_named_fn_complexity("a", 1, 10),
5427 make_named_fn_complexity("b", 12, 10),
5428 ],
5429 )];
5430 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5431 rustc_hash::FxHashMap::default();
5432 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5433 let output = crate::results::DeadCodeAnalysisArtifacts {
5434 results: fallow_types::results::AnalysisResults::default(),
5435 timings: None,
5436 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5437 modules: None,
5438 files: None,
5439 script_used_packages: rustc_hash::FxHashSet::default(),
5440 file_hashes: rustc_hash::FxHashMap::default(),
5441 };
5442
5443 let resolver =
5444 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(500.0))]);
5445 let result = compute_file_scores(
5446 FileScoreComputeInput {
5447 modules: &modules,
5448 file_paths: &file_paths,
5449 changed_files: None,
5450 istanbul_coverage: None,
5451 root: std::path::Path::new("/project"),
5452 crap_thresholds: CrapScoreThresholds {
5453 resolver: &resolver,
5454 enforce_crap: true,
5455 },
5456 },
5457 output,
5458 )
5459 .unwrap();
5460
5461 assert_eq!(result.scores.len(), 1);
5462 let score = &result.scores[0];
5463 assert!((score.crap_max - 110.0).abs() < f64::EPSILON);
5464 assert_eq!(score.crap_above_threshold, 0);
5465 assert_eq!(score.crap_exempted, 2);
5466 assert_eq!(score.crap_effective_threshold, Some(500.0));
5467 assert!(file_score_fully_crap_exempt(score, CRAP_THRESHOLD));
5468 assert_eq!(
5469 file_score_concern_axis(score, CRAP_THRESHOLD),
5470 FileScoreConcern::Structural
5471 );
5472 }
5473
5474 #[test]
5475 fn compute_file_scores_raised_global_omits_row_threshold() {
5476 let path_a = std::path::PathBuf::from("/project/src/legacy.ts");
5477 let files = vec![crate::discover::DiscoveredFile {
5478 id: crate::discover::FileId(0),
5479 path: path_a.clone(),
5480 size_bytes: 100,
5481 }];
5482 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5483 file_id: crate::discover::FileId(0),
5484 path: path_a.clone(),
5485 ..Default::default()
5486 }];
5487 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5488 let modules = vec![make_module_info(
5489 0,
5490 26,
5491 vec![
5492 make_named_fn_complexity("a", 1, 10),
5493 make_named_fn_complexity("b", 12, 10),
5494 ],
5495 )];
5496 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5497 rustc_hash::FxHashMap::default();
5498 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5499 let output = crate::results::DeadCodeAnalysisArtifacts {
5500 results: fallow_types::results::AnalysisResults::default(),
5501 timings: None,
5502 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5503 modules: None,
5504 files: None,
5505 script_used_packages: rustc_hash::FxHashSet::default(),
5506 file_hashes: rustc_hash::FxHashMap::default(),
5507 };
5508
5509 let resolver = test_crap_resolver(5000.0);
5512 let result = compute_file_scores(
5513 FileScoreComputeInput {
5514 modules: &modules,
5515 file_paths: &file_paths,
5516 changed_files: None,
5517 istanbul_coverage: None,
5518 root: std::path::Path::new("/project"),
5519 crap_thresholds: CrapScoreThresholds {
5520 resolver: &resolver,
5521 enforce_crap: true,
5522 },
5523 },
5524 output,
5525 )
5526 .unwrap();
5527
5528 assert_eq!(result.scores.len(), 1);
5529 let score = &result.scores[0];
5530 assert_eq!(score.crap_above_threshold, 0);
5531 assert_eq!(score.crap_exempted, 2);
5532 assert_eq!(score.crap_effective_threshold, None);
5533 assert!(file_score_fully_crap_exempt(score, 5000.0));
5534 assert_eq!(
5535 file_score_concern_axis(score, 5000.0),
5536 FileScoreConcern::Structural
5537 );
5538 }
5539
5540 #[test]
5545 fn estimated_crap_untested_aggregates_over_real_units_only() {
5546 let funcs = vec![
5547 make_named_fn_complexity("below", 1, 4),
5548 make_named_fn_complexity("at_threshold", 2, 5),
5549 make_named_fn_complexity("above_threshold", 3, 8),
5550 make_named_fn_complexity("<template>", 4, 21),
5551 make_named_fn_complexity("<module>", 5, 21),
5552 ];
5553 let result = estimated_crap_default(
5554 &funcs,
5555 &rustc_hash::FxHashSet::default(),
5556 false,
5557 fallow_output::CoverageSource::Estimated,
5558 );
5559 assert!((result.max_crap - 72.0).abs() < f64::EPSILON, "{result:#?}");
5560 assert_eq!(result.signals.above, 2);
5561 assert_eq!(result.per_function.len(), 3);
5562 }
5563
5564 #[test]
5565 fn crap_formula_full_coverage() {
5566 let result = crap_formula(10.0, 100.0);
5567 assert!((result - 10.0).abs() < f64::EPSILON);
5568 }
5569
5570 #[test]
5571 fn crap_formula_zero_coverage() {
5572 let result = crap_formula(5.0, 0.0);
5573 assert!((result - 30.0).abs() < f64::EPSILON);
5574 }
5575
5576 #[test]
5577 fn crap_formula_partial_coverage() {
5578 let result = crap_formula(10.0, 50.0);
5579 assert!((result - 22.5).abs() < f64::EPSILON);
5580 }
5581
5582 #[test]
5583 fn crap_formula_high_coverage_low_complexity() {
5584 let result = crap_formula(2.0, 90.0);
5585 assert!((result - 2.004).abs() < 0.001);
5586 }
5587
5588 #[test]
5593 fn crap_default_gate_cyclomatic_boundaries_per_estimate_tier() {
5594 for (coverage_pct, gate_cc) in [(0.0, 5.0), (40.0, 10.0), (85.0, 28.0)] {
5595 assert!(
5596 crap_formula(gate_cc, coverage_pct) >= CRAP_THRESHOLD,
5597 "cyclomatic {gate_cc} at {coverage_pct}% must reach the gate"
5598 );
5599 assert!(
5600 crap_formula(gate_cc - 1.0, coverage_pct) < CRAP_THRESHOLD,
5601 "cyclomatic {} at {coverage_pct}% must stay under the gate",
5602 gate_cc - 1.0
5603 );
5604 }
5605 }
5606
5607 #[test]
5608 fn istanbul_crap_excludes_synthetic_template_units() {
5609 let funcs = vec![
5610 make_named_fn_complexity("<template>", 1, 21),
5611 make_named_fn_complexity("<snippet:rowBody>", 1, 16),
5612 make_fn_complexity(6),
5613 ];
5614 let result = istanbul_crap_default(&funcs, None, false);
5615 assert!((result.max_crap - 42.0).abs() < f64::EPSILON, "{result:#?}");
5616 assert_eq!(result.signals.above, 1);
5617 assert_eq!(
5618 result.total, 1,
5619 "template units must not count as unmatched"
5620 );
5621 assert_eq!(result.per_function.len(), 1);
5622 }
5623
5624 #[test]
5625 fn estimated_crap_excludes_synthetic_template_units() {
5626 let funcs = vec![
5627 make_named_fn_complexity("<template>", 1, 21),
5628 make_named_fn_complexity("<snippet:rowBody>", 1, 16),
5629 ];
5630 let result = estimated_crap_default(
5631 &funcs,
5632 &rustc_hash::FxHashSet::default(),
5633 false,
5634 fallow_output::CoverageSource::Estimated,
5635 );
5636 assert!(result.max_crap.abs() < f64::EPSILON, "{result:#?}");
5637 assert_eq!(result.signals.above, 0);
5638 assert!(result.per_function.is_empty());
5639 }
5640
5641 #[test]
5642 fn istanbul_crap_with_coverage_data() {
5643 let funcs = vec![make_fn_complexity(10)];
5644 let mut functions = rustc_hash::FxHashMap::default();
5645 functions.insert(("test_fn".to_string(), 1, 0), 80.0);
5646 let file_cov = test_istanbul_file_coverage(functions, false);
5647 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5648 assert!((result.max_crap - 10.8).abs() < 0.1);
5649 assert_eq!(result.signals.above, 0);
5650 }
5651
5652 #[test]
5653 fn istanbul_crap_falls_back_to_binary_when_no_match() {
5654 let funcs = vec![make_fn_complexity(6)];
5655 let file_cov = test_istanbul_file_coverage(rustc_hash::FxHashMap::default(), false);
5656 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5657 assert!((result.max_crap - 42.0).abs() < f64::EPSILON);
5658 assert_eq!(result.signals.above, 1);
5659 }
5660
5661 #[test]
5664 fn istanbul_crap_uses_the_static_estimate_when_no_file_coverage() {
5665 let funcs = vec![make_fn_complexity(5)];
5666 let result = istanbul_crap_default(&funcs, None, true);
5667 assert!((result.max_crap - 10.4).abs() < 1e-9);
5670 assert_eq!(result.signals.above, 0);
5671 }
5672
5673 #[test]
5674 fn istanbul_crap_zero_coverage_matches_binary_untested() {
5675 let funcs = vec![make_fn_complexity(5)];
5676 let mut functions = rustc_hash::FxHashMap::default();
5677 functions.insert(("test_fn".to_string(), 1, 0), 0.0);
5678 let file_cov = test_istanbul_file_coverage(functions, false);
5679 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5680 assert!((result.max_crap - 30.0).abs() < f64::EPSILON);
5681 assert_eq!(result.signals.above, 1);
5682 }
5683
5684 #[test]
5685 fn estimated_crap_direct_test_reference() {
5686 let funcs = vec![make_fn_complexity(10)];
5687 let mut refs = rustc_hash::FxHashSet::default();
5688 refs.insert("test_fn".to_string());
5689 let result = estimated_crap_default(
5690 &funcs,
5691 &refs,
5692 true,
5693 fallow_output::CoverageSource::Estimated,
5694 );
5695 let (max, above) = (result.max_crap, result.signals.above);
5696 assert!((max - 10.3).abs() < 0.1);
5697 assert_eq!(above, 0);
5698 }
5699
5700 #[test]
5701 fn estimated_crap_indirect_test_reachable() {
5702 let funcs = vec![make_fn_complexity(10)];
5703 let refs = rustc_hash::FxHashSet::default();
5704 let result = estimated_crap_default(
5705 &funcs,
5706 &refs,
5707 true,
5708 fallow_output::CoverageSource::Estimated,
5709 );
5710 let (max, above) = (result.max_crap, result.signals.above);
5711 assert!((max - 31.6).abs() < 0.1);
5712 assert_eq!(above, 1);
5713 }
5714
5715 #[test]
5716 fn estimated_crap_untested_file() {
5717 let funcs = vec![make_fn_complexity(5)];
5718 let refs = rustc_hash::FxHashSet::default();
5719 let result = estimated_crap_default(
5720 &funcs,
5721 &refs,
5722 false,
5723 fallow_output::CoverageSource::Estimated,
5724 );
5725 let (max, above) = (result.max_crap, result.signals.above);
5726 assert!((max - 30.0).abs() < f64::EPSILON);
5727 assert_eq!(above, 1);
5728 }
5729
5730 #[test]
5731 fn estimated_crap_low_complexity_direct_ref() {
5732 let funcs = vec![make_fn_complexity(2)];
5733 let mut refs = rustc_hash::FxHashSet::default();
5734 refs.insert("test_fn".to_string());
5735 let result = estimated_crap_default(
5736 &funcs,
5737 &refs,
5738 true,
5739 fallow_output::CoverageSource::Estimated,
5740 );
5741 let (max, above) = (result.max_crap, result.signals.above);
5742 assert!(max < 3.0);
5743 assert_eq!(above, 0);
5744 }
5745
5746 #[test]
5747 fn estimated_crap_empty() {
5748 let refs = rustc_hash::FxHashSet::default();
5749 let result =
5750 estimated_crap_default(&[], &refs, true, fallow_output::CoverageSource::Estimated);
5751 let (max, above) = (result.max_crap, result.signals.above);
5752 assert!((max).abs() < f64::EPSILON);
5753 assert_eq!(above, 0);
5754 }
5755
5756 fn make_export(name: &str, is_type_only: bool) -> fallow_graph::graph::ExportSymbol {
5757 fallow_graph::graph::ExportSymbol {
5758 name: fallow_types::extract::ExportName::Named(name.into()),
5759 is_type_only,
5760 is_side_effect_used: false,
5761 visibility: crate::source::VisibilityTag::None,
5762 expected_unused_reason: None,
5763 span: oxc_span::Span::default(),
5764 references: vec![],
5765 reference_paths: Vec::new(),
5766 members: vec![],
5767 }
5768 }
5769
5770 #[test]
5771 fn dead_code_ratio_type_only_exports_excluded_from_denominator() {
5772 let path = std::path::Path::new("src/types.ts");
5773 let exports = vec![
5774 make_export("MyInterface", true),
5775 make_export("MyType", true),
5776 make_export("myFunction", false),
5777 ];
5778 let unused_files = rustc_hash::FxHashSet::default();
5779 let mut unused_by_path = rustc_hash::FxHashMap::default();
5780 unused_by_path.insert(path, 1_usize); let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
5783 assert!((ratio - 1.0).abs() < f64::EPSILON);
5784 }
5785
5786 #[test]
5787 fn dead_code_ratio_only_type_exports_returns_zero() {
5788 let path = std::path::Path::new("src/types.ts");
5789 let exports = vec![
5790 make_export("MyInterface", true),
5791 make_export("MyType", true),
5792 ];
5793 let unused_files = rustc_hash::FxHashSet::default();
5794 let unused_by_path = rustc_hash::FxHashMap::default();
5795
5796 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
5797 assert!(ratio.abs() < f64::EPSILON);
5798 }
5799
5800 #[test]
5801 fn dead_code_ratio_mixed_exports_counts_only_values() {
5802 let path = std::path::Path::new("src/component.ts");
5803 let exports = vec![
5804 make_export("Props", true),
5805 make_export("State", true),
5806 make_export("Component", false),
5807 make_export("helper", false),
5808 ];
5809 let unused_files = rustc_hash::FxHashSet::default();
5810 let mut unused_by_path = rustc_hash::FxHashMap::default();
5811 unused_by_path.insert(path, 1_usize);
5812
5813 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
5814 assert!((ratio - 0.5).abs() < f64::EPSILON);
5815 }
5816
5817 fn write_single_file_istanbul_fixture(
5818 coverage_path: &std::path::Path,
5819 source_path: &std::path::Path,
5820 fn_map: &serde_json::Value,
5821 function_hits: &serde_json::Value,
5822 ) {
5823 let mut root = serde_json::Map::new();
5824 root.insert(
5825 source_path.to_string_lossy().into_owned(),
5826 serde_json::json!({
5827 "path": source_path.to_string_lossy().into_owned(),
5828 "statementMap": {},
5829 "fnMap": fn_map,
5830 "branchMap": {},
5831 "s": {},
5832 "f": function_hits,
5833 "b": {}
5834 }),
5835 );
5836
5837 std::fs::write(coverage_path, serde_json::to_string(&root).unwrap()).unwrap();
5838 }
5839
5840 #[test]
5846 fn a_negative_branch_coordinate_does_not_cost_the_whole_map() {
5847 let temp = tempfile::TempDir::new().unwrap();
5848 let source_path = temp.path().join("src/pick.ts");
5849 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
5850 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
5851
5852 let coverage_path = temp.path().join("coverage-final.json");
5853 let source = source_path.to_string_lossy().into_owned();
5854 std::fs::write(
5855 &coverage_path,
5856 serde_json::to_string(&serde_json::json!({
5857 source.clone(): {
5858 "path": source,
5859 "statementMap": {},
5860 "fnMap": {
5861 "0": {
5862 "name": "pick",
5863 "line": 1,
5864 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 1, "column": 20 } },
5865 "loc": { "start": { "line": 1, "column": 41 }, "end": { "line": 6, "column": 1 } }
5866 }
5867 },
5868 "branchMap": {
5869 "0": {
5870 "type": "branch",
5871 "line": 5,
5872 "loc": {
5873 "start": { "line": 5, "column": -1 },
5874 "end": { "line": 6, "column": 0 }
5875 },
5876 "locations": [
5877 {
5878 "start": { "line": 5, "column": -1 },
5879 "end": { "line": 6, "column": 0 }
5880 }
5881 ]
5882 }
5883 },
5884 "s": {},
5885 "f": { "0": 2 },
5886 "b": { "0": [1, 0] }
5887 }
5888 }))
5889 .unwrap(),
5890 )
5891 .unwrap();
5892
5893 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
5894 let canonical_source = dunce::canonicalize(&source_path).unwrap();
5895 let file_coverage = coverage.get(&canonical_source).unwrap();
5896
5897 assert_eq!(file_coverage.lookup("pick", 1, 16), Some(100.0));
5898 }
5899
5900 #[test]
5904 fn an_accessor_answers_to_its_property_name() {
5905 let temp = tempfile::TempDir::new().unwrap();
5906 let source_path = temp.path().join("src/box.ts");
5907 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
5908 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
5909
5910 let coverage_path = temp.path().join("coverage-final.json");
5911 write_single_file_istanbul_fixture(
5912 &coverage_path,
5913 &source_path,
5914 &serde_json::json!({
5915 "0": {
5916 "name": "get area",
5917 "line": 4,
5918 "decl": { "start": { "line": 4, "column": 6 }, "end": { "line": 4, "column": 10 } },
5919 "loc": { "start": { "line": 4, "column": 13 }, "end": { "line": 6, "column": 3 } }
5920 }
5921 }),
5922 &serde_json::json!({ "0": 0 }),
5923 );
5924
5925 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
5926 let canonical_source = dunce::canonicalize(&source_path).unwrap();
5927 let file_coverage = coverage.get(&canonical_source).unwrap();
5928
5929 assert_eq!(file_coverage.lookup("area", 4, 6), Some(0.0));
5931 assert_eq!(file_coverage.lookup("get area", 4, 6), Some(0.0));
5933 }
5934
5935 #[test]
5936 fn resolve_relative_to_root_joins_relative_with_project_root() {
5937 let resolved = resolve_relative_to_root(
5938 std::path::Path::new("coverage/coverage-final.json"),
5939 Some(std::path::Path::new("/work/my-app")),
5940 );
5941 assert_eq!(
5942 resolved,
5943 std::path::PathBuf::from("/work/my-app/coverage/coverage-final.json")
5944 );
5945 }
5946
5947 #[test]
5948 fn resolve_relative_to_root_returns_absolute_unchanged() {
5949 let resolved = resolve_relative_to_root(
5950 std::path::Path::new("/tmp/coverage-final.json"),
5951 Some(std::path::Path::new("/work/my-app")),
5952 );
5953 assert_eq!(
5954 resolved,
5955 std::path::PathBuf::from("/tmp/coverage-final.json")
5956 );
5957 }
5958
5959 #[test]
5960 fn resolve_relative_to_root_returns_windows_absolute_unchanged_on_any_host() {
5961 let path = std::path::Path::new(r"C:\coverage\coverage-final.json");
5962 let resolved = resolve_relative_to_root(path, Some(std::path::Path::new("/work/my-app")));
5963 assert_eq!(resolved, path);
5964 }
5965
5966 #[cfg(windows)]
5967 #[test]
5968 fn resolve_relative_to_root_returns_posix_rooted_path_unchanged_on_windows() {
5969 let path = std::path::Path::new(r"/ci/workspace/coverage-final.json");
5970 let resolved =
5971 resolve_relative_to_root(path, Some(std::path::Path::new(r"C:\work\my-app")));
5972 assert_eq!(resolved, path);
5973 }
5974
5975 #[test]
5976 fn resolve_relative_to_root_without_project_root_returns_relative_unchanged() {
5977 let resolved =
5978 resolve_relative_to_root(std::path::Path::new("coverage/coverage-final.json"), None);
5979 assert_eq!(
5980 resolved,
5981 std::path::PathBuf::from("coverage/coverage-final.json")
5982 );
5983 }
5984
5985 #[test]
5989 fn load_istanbul_coverage_resolves_relative_map_keys_against_project_root() {
5990 let temp = tempfile::TempDir::new().unwrap();
5991 let source_path = temp.path().join("src/index.ts");
5992 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
5993 std::fs::write(&source_path, "export function f(){}").unwrap();
5994
5995 let coverage_path = temp.path().join("coverage-final.json");
5996 std::fs::write(
5997 &coverage_path,
5998 serde_json::to_string(&serde_json::json!({
5999 "src/index.ts": {
6000 "path": "src/index.ts",
6001 "statementMap": {},
6002 "fnMap": {
6003 "0": {
6004 "name": "f",
6005 "decl": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } },
6006 "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } }
6007 }
6008 },
6009 "branchMap": {},
6010 "s": {},
6011 "f": { "0": 2 },
6012 "b": {}
6013 }
6014 }))
6015 .unwrap(),
6016 )
6017 .unwrap();
6018
6019 let coverage =
6020 load_istanbul_coverage(&coverage_path, None, Some(temp.path()), false).unwrap();
6021 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6022 let file_coverage = coverage.get(&canonical_source).unwrap();
6023
6024 assert_eq!(file_coverage.lookup("f", 1, 0), Some(100.0));
6025 }
6026
6027 #[test]
6028 fn load_istanbul_coverage_resolves_relative_path_against_project_root() {
6029 let temp = tempfile::TempDir::new().unwrap();
6030 let source_path = temp.path().join("src/index.ts");
6031 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6032 std::fs::write(&source_path, "export function f(){}").unwrap();
6033
6034 let coverage_path = temp.path().join("coverage/coverage-final.json");
6035 std::fs::create_dir_all(coverage_path.parent().unwrap()).unwrap();
6036 write_single_file_istanbul_fixture(
6037 &coverage_path,
6038 &source_path,
6039 &serde_json::json!({
6040 "0": {
6041 "name": "f",
6042 "decl": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } },
6043 "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } }
6044 }
6045 }),
6046 &serde_json::json!({ "0": 1 }),
6047 );
6048
6049 let coverage = load_istanbul_coverage(
6050 std::path::Path::new("coverage/coverage-final.json"),
6051 None,
6052 Some(temp.path()),
6053 false,
6054 )
6055 .expect("relative path must resolve against project_root");
6056 assert!(
6057 !coverage.files.is_empty(),
6058 "expected coverage to load via project_root resolution, got {} files",
6059 coverage.files.len()
6060 );
6061 }
6062
6063 #[test]
6064 fn load_istanbul_coverage_falls_back_to_decl_line_for_missing_fn_line() {
6065 let temp = tempfile::TempDir::new().unwrap();
6066 let source_path = temp.path().join("src/service.ts");
6067 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6068 std::fs::write(&source_path, "export class DataService {}\n").unwrap();
6069
6070 let coverage_path = temp.path().join("coverage-final.json");
6071 write_single_file_istanbul_fixture(
6072 &coverage_path,
6073 &source_path,
6074 &serde_json::json!({
6075 "0": {
6076 "name": "(anonymous_0)",
6077 "decl": {
6078 "start": { "line": 5, "column": 2 },
6079 "end": { "line": 5, "column": 13 }
6080 },
6081 "loc": {
6082 "start": { "line": 5, "column": 14 },
6083 "end": { "line": 11, "column": 3 }
6084 }
6085 },
6086 "1": {
6087 "name": "(anonymous_1)",
6088 "decl": {
6089 "start": { "line": 20, "column": 14 },
6090 "end": { "line": 20, "column": 25 }
6091 },
6092 "loc": {
6093 "start": { "line": 20, "column": 28 },
6094 "end": { "line": 22, "column": 2 }
6095 }
6096 }
6097 }),
6098 &serde_json::json!({
6099 "0": 1,
6100 "1": 0
6101 }),
6102 );
6103
6104 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6105 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6106 let file_coverage = coverage.get(&canonical_source).unwrap();
6107
6108 assert_eq!(file_coverage.lookup("processData", 5, 0), Some(100.0));
6109 assert_eq!(file_coverage.lookup("handleSpecial", 20, 0), Some(0.0));
6110 }
6111
6112 #[test]
6113 fn load_istanbul_coverage_indexes_explicit_and_decl_lines() {
6114 let temp = tempfile::TempDir::new().unwrap();
6115 let source_path = temp.path().join("src/handler.ts");
6116 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6117 std::fs::write(&source_path, "export const handleClick = () => {}\n").unwrap();
6118
6119 let coverage_path = temp.path().join("coverage-final.json");
6120 write_single_file_istanbul_fixture(
6121 &coverage_path,
6122 &source_path,
6123 &serde_json::json!({
6124 "0": {
6125 "name": "handleClick",
6126 "line": 40,
6127 "decl": {
6128 "start": { "line": 22, "column": 13 },
6129 "end": { "line": 22, "column": 24 }
6130 },
6131 "loc": {
6132 "start": { "line": 40, "column": 27 },
6133 "end": { "line": 42, "column": 1 }
6134 }
6135 }
6136 }),
6137 &serde_json::json!({
6138 "0": 1
6139 }),
6140 );
6141
6142 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6143 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6144 let file_coverage = coverage.get(&canonical_source).unwrap();
6145
6146 assert_eq!(file_coverage.lookup("handleClick", 40, 0), Some(100.0));
6147 assert_eq!(file_coverage.lookup("handleClick", 22, 13), Some(100.0));
6148 }
6149
6150 #[test]
6151 fn load_istanbul_coverage_indexes_valid_body_start_alias() {
6152 let temp = tempfile::TempDir::new().unwrap();
6153 let source_path = temp.path().join("src/handler.ts");
6154 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6155 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
6156
6157 let coverage_path = temp.path().join("coverage-final.json");
6158 write_single_file_istanbul_fixture(
6159 &coverage_path,
6160 &source_path,
6161 &serde_json::json!({
6162 "0": {
6163 "name": "(anonymous_0)",
6164 "line": 8,
6165 "decl": {
6166 "start": { "line": 8, "column": 14 },
6167 "end": { "line": 8, "column": 25 }
6168 },
6169 "loc": {
6170 "start": { "line": 20, "column": 6 },
6171 "end": { "line": 22, "column": 1 }
6172 }
6173 }
6174 }),
6175 &serde_json::json!({ "0": 1 }),
6176 );
6177
6178 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6179 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6180 let file_coverage = coverage.get(&canonical_source).unwrap();
6181
6182 assert_eq!(file_coverage.lookup("handler", 20, 6), Some(100.0));
6183
6184 let mut function = make_fn_complexity(4);
6185 function.name = "handler".to_string();
6186 function.line = 20;
6187 function.col = 6;
6188 let result = istanbul_crap_default(&[function], Some(file_coverage), false);
6189 assert_eq!(result.matched, 1);
6190 assert_eq!(result.total, 1);
6191 assert_eq!(
6192 result.per_function[0].coverage_source,
6193 fallow_output::CoverageSource::Istanbul
6194 );
6195 assert_eq!(result.per_function[0].coverage_pct, Some(100.0));
6196 }
6197
6198 #[test]
6205 fn curried_arrows_one_per_line_each_take_their_own_record() {
6206 let temp = tempfile::TempDir::new().unwrap();
6207 let source_path = temp.path().join("src/adjust.ts");
6208 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6209 std::fs::write(
6210 &source_path,
6211 "export const adjust = (base: number) =>\n (factor: number) =>\n (offset: number) =>\n base * factor + offset;\n",
6212 )
6213 .unwrap();
6214
6215 let coverage_path = temp.path().join("coverage-final.json");
6216 write_single_file_istanbul_fixture(
6217 &coverage_path,
6218 &source_path,
6219 &serde_json::json!({
6220 "0": {
6221 "name": "(anonymous_0)",
6222 "line": 2,
6223 "decl": { "start": { "line": 1, "column": 22 }, "end": { "line": 1, "column": 23 } },
6224 "loc": { "start": { "line": 2, "column": 2 }, "end": { "line": 4, "column": 26 } }
6225 },
6226 "1": {
6227 "name": "(anonymous_1)",
6228 "line": 3,
6229 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6230 "loc": { "start": { "line": 3, "column": 2 }, "end": { "line": 4, "column": 26 } }
6231 },
6232 "2": {
6233 "name": "(anonymous_2)",
6234 "line": 4,
6235 "decl": { "start": { "line": 3, "column": 2 }, "end": { "line": 3, "column": 3 } },
6236 "loc": { "start": { "line": 4, "column": 4 }, "end": { "line": 4, "column": 26 } }
6237 }
6238 }),
6239 &serde_json::json!({ "0": 2, "1": 1, "2": 0 }),
6240 );
6241
6242 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6243 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6244 let file_coverage = coverage.get(&canonical_source).unwrap();
6245
6246 assert_eq!(file_coverage.lookup("adjust", 1, 22), Some(100.0));
6247 assert_eq!(file_coverage.lookup("<arrow>", 2, 2), Some(100.0));
6248 assert_eq!(file_coverage.lookup("<arrow>", 3, 2), Some(0.0));
6250 }
6251
6252 #[test]
6259 fn a_default_value_keeps_its_own_record_inside_the_enclosing_signature() {
6260 let temp = tempfile::TempDir::new().unwrap();
6261 let source_path = temp.path().join("src/filter.ts");
6262 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6263 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
6264
6265 let coverage_path = temp.path().join("coverage-final.json");
6266 write_single_file_istanbul_fixture(
6267 &coverage_path,
6268 &source_path,
6269 &serde_json::json!({
6270 "0": {
6271 "name": "(anonymous_0)",
6272 "line": 173,
6273 "decl": { "start": { "line": 169, "column": 2 }, "end": { "line": 170, "column": 3 } },
6274 "loc": { "start": { "line": 173, "column": 4 }, "end": { "line": 182, "column": 3 } }
6275 },
6276 "1": {
6277 "name": "(anonymous_1)",
6278 "line": 171,
6279 "decl": { "start": { "line": 171, "column": 21 }, "end": { "line": 171, "column": 67 } },
6280 "loc": { "start": { "line": 171, "column": 67 }, "end": { "line": 171, "column": 76 } }
6281 }
6282 }),
6283 &serde_json::json!({ "0": 46, "1": 0 }),
6284 );
6285
6286 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6287 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6288 let file_coverage = coverage.get(&canonical_source).unwrap();
6289
6290 assert_eq!(file_coverage.lookup("<arrow>", 171, 61), Some(0.0));
6293 }
6294
6295 #[test]
6302 fn header_span_abstains_when_another_function_is_declared_inside_it() {
6303 let temp = tempfile::TempDir::new().unwrap();
6304 let source_path = temp.path().join("src/chart.ts");
6305 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6306 std::fs::write(
6307 &source_path,
6308 "export class Chart {\n @Watch(\"data\")\n render(\n rows: number[],\n project = function scale(\n row: number\n ) {\n return row * 2;\n }\n ) {\n return rows.map(project);\n }\n}\n",
6309 )
6310 .unwrap();
6311
6312 let coverage_path = temp.path().join("coverage-final.json");
6313 write_single_file_istanbul_fixture(
6314 &coverage_path,
6315 &source_path,
6316 &serde_json::json!({
6317 "0": {
6318 "name": "(anonymous_0)",
6319 "line": 10,
6320 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6321 "loc": { "start": { "line": 10, "column": 4 }, "end": { "line": 12, "column": 3 } }
6322 },
6323 "1": {
6324 "name": "scale",
6325 "line": 7,
6326 "decl": { "start": { "line": 5, "column": 23 }, "end": { "line": 5, "column": 28 } },
6327 "loc": { "start": { "line": 7, "column": 6 }, "end": { "line": 9, "column": 5 } }
6328 }
6329 }),
6330 &serde_json::json!({ "0": 3, "1": 0 }),
6331 );
6332
6333 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6334 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6335 let coverage = load_istanbul_coverage_for_sources(
6336 &coverage_path,
6337 None,
6338 Some(temp.path()),
6339 Some(&discovered_sources),
6340 false,
6341 )
6342 .unwrap();
6343 let file_coverage = coverage.get(&canonical_source).unwrap();
6344
6345 assert_eq!(file_coverage.lookup("<anonymous>", 6, 6), None);
6348 assert_eq!(file_coverage.lookup("scale", 5, 14), Some(0.0));
6350 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
6351 }
6352
6353 #[test]
6357 fn same_named_function_in_signature_does_not_supply_member_coverage() {
6358 let temp = tempfile::TempDir::new().unwrap();
6359 let source_path = temp.path().join("src/chart.ts");
6360 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6361 std::fs::write(
6362 &source_path,
6363 "export class Chart {\n @Watch(\"data\")\n render(\n rows: number[],\n project = function render(\n row: number\n ) {\n return row * 2;\n }\n ) {\n return rows.map(project);\n }\n}\n",
6364 )
6365 .unwrap();
6366
6367 let coverage_path = temp.path().join("coverage-final.json");
6368 write_single_file_istanbul_fixture(
6369 &coverage_path,
6370 &source_path,
6371 &serde_json::json!({
6372 "0": {
6373 "name": "(anonymous_0)",
6374 "line": 10,
6375 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6376 "loc": { "start": { "line": 10, "column": 4 }, "end": { "line": 12, "column": 3 } }
6377 },
6378 "1": {
6379 "name": "render",
6380 "line": 7,
6381 "decl": { "start": { "line": 5, "column": 23 }, "end": { "line": 5, "column": 29 } },
6382 "loc": { "start": { "line": 7, "column": 6 }, "end": { "line": 9, "column": 5 } }
6383 }
6384 }),
6385 &serde_json::json!({ "0": 3, "1": 0 }),
6386 );
6387
6388 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6389 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6390 let file_coverage = coverage.get(&canonical_source).unwrap();
6391
6392 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
6393 assert_eq!(file_coverage.lookup("render", 5, 23), Some(0.0));
6394 }
6395
6396 #[test]
6400 fn same_line_same_named_function_in_signature_does_not_supply_member_coverage() {
6401 let temp = tempfile::TempDir::new().unwrap();
6402 let source_path = temp.path().join("src/chart.ts");
6403 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6404 std::fs::write(
6405 &source_path,
6406 "export class Chart {\n @Watch(\"data\") render(x = function render() {}) {}\n}\n",
6407 )
6408 .unwrap();
6409
6410 let coverage_path = temp.path().join("coverage-final.json");
6411 write_single_file_istanbul_fixture(
6412 &coverage_path,
6413 &source_path,
6414 &serde_json::json!({
6415 "0": {
6416 "name": "(anonymous_0)",
6417 "line": 2,
6418 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6419 "loc": { "start": { "line": 2, "column": 52 }, "end": { "line": 2, "column": 54 } }
6420 },
6421 "1": {
6422 "name": "render",
6423 "line": 2,
6424 "decl": { "start": { "line": 2, "column": 39 }, "end": { "line": 2, "column": 45 } },
6425 "loc": { "start": { "line": 2, "column": 48 }, "end": { "line": 2, "column": 50 } }
6426 }
6427 }),
6428 &serde_json::json!({ "0": 3, "1": 0 }),
6429 );
6430
6431 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6432 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6433 let coverage = load_istanbul_coverage_for_sources(
6434 &coverage_path,
6435 None,
6436 Some(temp.path()),
6437 Some(&discovered_sources),
6438 false,
6439 )
6440 .unwrap();
6441 let file_coverage = coverage.get(&canonical_source).unwrap();
6442
6443 assert_eq!(file_coverage.lookup("render", 2, 23), Some(100.0));
6444 assert_eq!(file_coverage.lookup("render", 2, 29), Some(0.0));
6445 }
6446
6447 #[test]
6448 fn named_generator_alias_handles_trivia_and_utf16_columns() {
6449 let temp = tempfile::TempDir::new().unwrap();
6450 let source_path = temp.path().join("src/chart.ts");
6451 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6452 let source_line =
6453 " render(label = \"pi: π, mushroom: 🍄\", x = function/* gap */*render() {}) {}";
6454 std::fs::write(
6455 &source_path,
6456 format!("export class Chart {{\n{source_line}\n}}\n"),
6457 )
6458 .unwrap();
6459
6460 let utf16_column = |byte_column: usize| {
6461 u32::try_from(source_line[..byte_column].encode_utf16().count()).unwrap()
6462 };
6463 let outer_target_column = source_line.find("render(").unwrap() + "render".len();
6464 let syntax_column = source_line.find("function").unwrap();
6465 let name_column = source_line.find("*render").unwrap() + 1;
6466 let inner_body_column = source_line.find("{}").unwrap();
6467 let outer_body_column = source_line.rfind("{}").unwrap();
6468
6469 let coverage_path = temp.path().join("coverage-final.json");
6470 write_single_file_istanbul_fixture(
6471 &coverage_path,
6472 &source_path,
6473 &serde_json::json!({
6474 "0": {
6475 "name": "(anonymous_0)",
6476 "line": 2,
6477 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6478 "loc": {
6479 "start": { "line": 2, "column": utf16_column(outer_body_column) },
6480 "end": { "line": 2, "column": utf16_column(outer_body_column + 2) }
6481 }
6482 },
6483 "1": {
6484 "name": "render",
6485 "line": 2,
6486 "decl": {
6487 "start": { "line": 2, "column": utf16_column(name_column) },
6488 "end": { "line": 2, "column": utf16_column(name_column + "render".len()) }
6489 },
6490 "loc": {
6491 "start": { "line": 2, "column": utf16_column(inner_body_column) },
6492 "end": { "line": 2, "column": utf16_column(inner_body_column + 2) }
6493 }
6494 }
6495 }),
6496 &serde_json::json!({ "0": 3, "1": 0 }),
6497 );
6498
6499 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6500 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6501 let coverage = load_istanbul_coverage_for_sources(
6502 &coverage_path,
6503 None,
6504 Some(temp.path()),
6505 Some(&discovered_sources),
6506 false,
6507 )
6508 .unwrap();
6509 let file_coverage = coverage.get(&canonical_source).unwrap();
6510
6511 assert_eq!(
6512 file_coverage.lookup("render", 2, u32::try_from(outer_target_column).unwrap()),
6513 Some(100.0)
6514 );
6515 assert_eq!(
6516 file_coverage.lookup("render", 2, u32::try_from(syntax_column).unwrap()),
6517 Some(0.0)
6518 );
6519 }
6520
6521 #[test]
6522 fn utf16_index_is_sparse_and_rejects_surrogate_boundaries() {
6523 let ascii_prefix = "a".repeat(4_096);
6524 let source = format!("{ascii_prefix}🍄{}", "b".repeat(4_096));
6525 let index = IstanbulSourceIndex::new(&source, std::path::Path::new("minified.js"));
6526 let line_index = &index.non_ascii_lines[&0];
6527
6528 assert_eq!(line_index.checkpoints.len(), 1);
6529 assert_eq!(
6530 index.byte_position(1, 4_096),
6531 Some(IstanbulPosition::new(1, 4_096))
6532 );
6533 assert_eq!(index.byte_position(1, 4_097), None);
6534 assert_eq!(
6535 index.byte_position(1, 4_098),
6536 Some(IstanbulPosition::new(1, 4_100))
6537 );
6538 assert_eq!(
6539 index.byte_position(1, 8_194),
6540 Some(IstanbulPosition::new(1, 8_196))
6541 );
6542 }
6543
6544 #[test]
6545 fn effective_alias_normalizes_against_its_own_unicode_line() {
6546 let temp = tempfile::TempDir::new().unwrap();
6547 let source_path = temp.path().join("src/render.ts");
6548 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6549 std::fs::write(
6550 &source_path,
6551 "/*🍄*/ const placeholder = 0;\n/*π*/ function render() {}\n",
6552 )
6553 .unwrap();
6554
6555 let coverage_path = temp.path().join("coverage-final.json");
6556 write_single_file_istanbul_fixture(
6557 &coverage_path,
6558 &source_path,
6559 &serde_json::json!({
6560 "0": {
6561 "name": "render",
6562 "line": 1,
6563 "decl": { "start": { "line": 2, "column": 15 }, "end": { "line": 2, "column": 21 } },
6564 "loc": { "start": { "line": 2, "column": 24 }, "end": { "line": 2, "column": 26 } }
6565 }
6566 }),
6567 &serde_json::json!({ "0": 1 }),
6568 );
6569
6570 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6571 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6572 let coverage = load_istanbul_coverage_for_sources(
6573 &coverage_path,
6574 None,
6575 Some(temp.path()),
6576 Some(&discovered_sources),
6577 false,
6578 )
6579 .unwrap();
6580 let function = &coverage.get(&canonical_source).unwrap().functions[0];
6581
6582 assert!(
6583 function
6584 .aliases
6585 .iter()
6586 .any(|alias| { alias.position == IstanbulPosition::new(1, 17) && alias.primary })
6587 );
6588 assert!(
6589 !function
6590 .aliases
6591 .iter()
6592 .any(|alias| alias.position == IstanbulPosition::new(1, 16))
6593 );
6594 }
6595
6596 #[test]
6597 fn stale_utf16_coordinates_are_rejected_with_trusted_source() {
6598 let temp = tempfile::TempDir::new().unwrap();
6599 let source_path = temp.path().join("src/render.ts");
6600 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6601 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6602
6603 let coverage_path = temp.path().join("coverage-final.json");
6604 write_single_file_istanbul_fixture(
6605 &coverage_path,
6606 &source_path,
6607 &serde_json::json!({
6608 "0": {
6609 "name": "render",
6610 "line": 1,
6611 "decl": { "start": { "line": 1, "column": 999 }, "end": { "line": 1, "column": 22 } },
6612 "loc": { "start": { "line": 1, "column": 25 }, "end": { "line": 1, "column": 27 } }
6613 }
6614 }),
6615 &serde_json::json!({ "0": 1 }),
6616 );
6617
6618 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6619 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6620 let coverage = load_istanbul_coverage_for_sources(
6621 &coverage_path,
6622 None,
6623 Some(temp.path()),
6624 Some(&discovered_sources),
6625 false,
6626 )
6627 .unwrap();
6628
6629 assert_eq!(
6630 coverage
6631 .get(&canonical_source)
6632 .unwrap()
6633 .lookup("render", 1, 7),
6634 None
6635 );
6636 }
6637
6638 #[test]
6639 fn invalid_optional_coordinates_preserve_valid_declaration_attribution() {
6640 let temp = tempfile::TempDir::new().unwrap();
6641 let source_path = temp.path().join("src/render.ts");
6642 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6643 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6644
6645 let coverage_path = temp.path().join("coverage-final.json");
6646 write_single_file_istanbul_fixture(
6647 &coverage_path,
6648 &source_path,
6649 &serde_json::json!({
6650 "0": {
6651 "name": "render",
6652 "line": 99,
6653 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 99, "column": 999 } },
6654 "loc": { "start": { "line": 1, "column": 999 }, "end": { "line": 99, "column": 999 } }
6655 }
6656 }),
6657 &serde_json::json!({ "0": 1 }),
6658 );
6659
6660 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6661 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6662 let coverage = load_istanbul_coverage_for_sources(
6663 &coverage_path,
6664 None,
6665 Some(temp.path()),
6666 Some(&discovered_sources),
6667 false,
6668 )
6669 .unwrap();
6670 let file_coverage = coverage.get(&canonical_source).unwrap();
6671 let function = &file_coverage.functions[0];
6672
6673 assert_eq!(file_coverage.lookup("render", 1, 16), Some(100.0));
6674 assert!(function.body_span.is_none());
6675 assert!(function.header_span.is_none());
6676 assert!(
6677 !function
6678 .aliases
6679 .iter()
6680 .any(|alias| { alias.position.line == 99 || alias.position.col == 999 })
6681 );
6682 }
6683
6684 #[test]
6685 fn malformed_source_does_not_supply_named_function_provenance() {
6686 assert!(
6687 !IstanbulSourceIndex::new(
6688 "export function render() {}",
6689 std::path::Path::new("valid.ts"),
6690 )
6691 .named_function_starts
6692 .is_empty()
6693 );
6694 let index = IstanbulSourceIndex::new(
6695 "export function render() {} const broken = ;",
6696 std::path::Path::new("broken.ts"),
6697 );
6698
6699 assert!(index.named_function_starts.is_empty());
6700 }
6701
6702 #[test]
6703 fn javascript_with_jsx_uses_clean_jsx_provenance_parse() {
6704 let index = IstanbulSourceIndex::new(
6705 "export function render() { return <div />; }",
6706 std::path::Path::new("component.js"),
6707 );
6708
6709 assert!(!index.named_function_starts.is_empty());
6710 }
6711
6712 #[test]
6713 fn undiscovered_coverage_path_is_not_loaded_for_source_provenance() {
6714 let temp = tempfile::TempDir::new().unwrap();
6715 let source_path = temp.path().join("src/excluded.ts");
6716 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6717 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6718
6719 let coverage_path = temp.path().join("coverage-final.json");
6720 write_single_file_istanbul_fixture(
6721 &coverage_path,
6722 &source_path,
6723 &serde_json::json!({
6724 "0": {
6725 "name": "render",
6726 "line": 1,
6727 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 1, "column": 22 } },
6728 "loc": { "start": { "line": 1, "column": 25 }, "end": { "line": 1, "column": 27 } }
6729 }
6730 }),
6731 &serde_json::json!({ "0": 1 }),
6732 );
6733
6734 let coverage = load_istanbul_coverage_for_sources(
6735 &coverage_path,
6736 None,
6737 Some(temp.path()),
6738 Some(&rustc_hash::FxHashSet::default()),
6739 false,
6740 )
6741 .unwrap();
6742 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6743 let function = &coverage.get(&canonical_source).unwrap().functions[0];
6744
6745 assert!(
6746 function
6747 .aliases
6748 .iter()
6749 .all(|alias| alias.position != IstanbulPosition::new(1, 7))
6750 );
6751 }
6752
6753 #[test]
6756 fn private_class_member_never_takes_enclosing_coverage() {
6757 let temp = tempfile::TempDir::new().unwrap();
6758 let source_path = temp.path().join("src/vault.js");
6759 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6760 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
6761
6762 let coverage_path = temp.path().join("coverage-final.json");
6763 write_single_file_istanbul_fixture(
6764 &coverage_path,
6765 &source_path,
6766 &serde_json::json!({
6767 "0": {
6768 "name": "(anonymous_0)",
6769 "line": 7,
6770 "decl": { "start": { "line": 1, "column": 24 }, "end": { "line": 1, "column": 25 } },
6771 "loc": { "start": { "line": 7, "column": 5 }, "end": { "line": 7, "column": 20 } }
6772 }
6773 }),
6774 &serde_json::json!({ "0": 1 }),
6775 );
6776
6777 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6778 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6779 let file_coverage = coverage.get(&canonical_source).unwrap();
6780
6781 let mut private_member = make_fn_complexity(1);
6784 private_member.name = "#wipe".to_string();
6785 private_member.is_private_member = true;
6786 private_member.line = 3;
6787 private_member.col = 9;
6788 let result = istanbul_crap_default(&[private_member], Some(file_coverage), false);
6789 assert_eq!(result.matched, 0);
6790 assert_eq!(
6791 result.per_function[0].coverage_source,
6792 fallow_output::CoverageSource::Estimated
6793 );
6794 assert_eq!(file_coverage.lookup("<arrow>", 1, 24), Some(100.0));
6795 }
6796
6797 #[test]
6798 fn quoted_hash_method_keeps_exact_istanbul_coverage() {
6799 let temp = tempfile::TempDir::new().unwrap();
6800 let source_path = temp.path().join("src/vault.js");
6801 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6802 std::fs::write(&source_path, "class Vault { '#wipe'() {} }\n").unwrap();
6803
6804 let coverage_path = temp.path().join("coverage-final.json");
6805 write_single_file_istanbul_fixture(
6806 &coverage_path,
6807 &source_path,
6808 &serde_json::json!({
6809 "0": {
6810 "name": "#wipe",
6811 "line": 1,
6812 "decl": { "start": { "line": 1, "column": 14 }, "end": { "line": 1, "column": 21 } },
6813 "loc": { "start": { "line": 1, "column": 24 }, "end": { "line": 1, "column": 26 } }
6814 }
6815 }),
6816 &serde_json::json!({ "0": 1 }),
6817 );
6818
6819 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6820 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6821 let file_coverage = coverage.get(&canonical_source).unwrap();
6822
6823 let mut quoted_public_method = make_fn_complexity(1);
6824 quoted_public_method.name = "#wipe".to_string();
6825 quoted_public_method.line = 1;
6826 quoted_public_method.col = 14;
6827 let result = istanbul_crap_default(&[quoted_public_method], Some(file_coverage), false);
6828 assert_eq!(result.matched, 1);
6829 assert_eq!(
6830 result.per_function[0].coverage_source,
6831 fallow_output::CoverageSource::Istanbul
6832 );
6833 assert_eq!(result.per_function[0].coverage_pct, Some(100.0));
6834 }
6835
6836 #[test]
6845 fn decorated_member_matches_its_istanbul_header_span() {
6846 let temp = tempfile::TempDir::new().unwrap();
6847 let source_path = temp.path().join("src/users.controller.ts");
6848 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6849 std::fs::write(
6850 &source_path,
6851 "export class UserController {\n @Get(\":id\")\n async findOneWithProfile(\n id: string,\n include: string[]\n ) {\n return this.service.find(id, include);\n }\n}\n",
6852 )
6853 .unwrap();
6854
6855 let coverage_path = temp.path().join("coverage-final.json");
6856 write_single_file_istanbul_fixture(
6857 &coverage_path,
6858 &source_path,
6859 &serde_json::json!({
6860 "0": {
6861 "name": "(anonymous_0)",
6862 "line": 6,
6863 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6864 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
6865 }
6866 }),
6867 &serde_json::json!({ "0": 3 }),
6868 );
6869
6870 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6871 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6872 let file_coverage = coverage.get(&canonical_source).unwrap();
6873
6874 assert_eq!(
6876 file_coverage.lookup("findOneWithProfile", 3, 26),
6877 Some(100.0)
6878 );
6879 }
6880
6881 #[test]
6887 fn established_alias_wins_over_the_signature_that_contains_it() {
6888 let temp = tempfile::TempDir::new().unwrap();
6889 let source_path = temp.path().join("src/chart.ts");
6890 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6891 std::fs::write(
6892 &source_path,
6893 "export class Chart {\n @Watch(\"data\")\n render(\n rows: number[],\n project = (row: number) => row * 2\n ) {\n return rows.map(project);\n }\n}\n",
6894 )
6895 .unwrap();
6896
6897 let coverage_path = temp.path().join("coverage-final.json");
6898 write_single_file_istanbul_fixture(
6899 &coverage_path,
6900 &source_path,
6901 &serde_json::json!({
6902 "0": {
6903 "name": "(anonymous_0)",
6904 "line": 6,
6905 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6906 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
6907 },
6908 "1": {
6909 "name": "(anonymous_1)",
6910 "line": 5,
6911 "decl": { "start": { "line": 5, "column": 14 }, "end": { "line": 5, "column": 15 } },
6912 "loc": { "start": { "line": 5, "column": 31 }, "end": { "line": 5, "column": 38 } }
6913 }
6914 }),
6915 &serde_json::json!({ "0": 3, "1": 0 }),
6916 );
6917
6918 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6919 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6920 let file_coverage = coverage.get(&canonical_source).unwrap();
6921
6922 assert_eq!(file_coverage.lookup("<arrow>", 5, 14), Some(0.0));
6924 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
6926 }
6927
6928 #[test]
6936 fn signature_holding_a_function_abstains_instead_of_crediting_it() {
6937 let temp = tempfile::TempDir::new().unwrap();
6938 let source_path = temp.path().join("src/users.controller.ts");
6939 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6940 std::fs::write(
6941 &source_path,
6942 "export class UserController {\n @Get(\":id\")\n async findOneWithProfile(\n id: string,\n transform = (row: string) => row.trim()\n ) {\n return transform(id);\n }\n}\n",
6943 )
6944 .unwrap();
6945
6946 let coverage_path = temp.path().join("coverage-final.json");
6947 write_single_file_istanbul_fixture(
6948 &coverage_path,
6949 &source_path,
6950 &serde_json::json!({
6951 "0": {
6952 "name": "(anonymous_0)",
6953 "line": 6,
6954 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6955 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
6956 },
6957 "1": {
6958 "name": "(anonymous_1)",
6959 "line": 5,
6960 "decl": { "start": { "line": 5, "column": 16 }, "end": { "line": 5, "column": 17 } },
6961 "loc": { "start": { "line": 5, "column": 33 }, "end": { "line": 5, "column": 43 } }
6962 }
6963 }),
6964 &serde_json::json!({ "0": 3, "1": 0 }),
6965 );
6966
6967 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6968 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6969 let file_coverage = coverage.get(&canonical_source).unwrap();
6970
6971 assert_eq!(file_coverage.lookup("findOneWithProfile", 3, 26), None);
6974 assert_eq!(file_coverage.lookup("<arrow>", 5, 16), Some(0.0));
6976 }
6977
6978 #[test]
6979 fn anonymous_record_aliases_do_not_tie_with_their_own_identity() {
6980 let temp = tempfile::TempDir::new().unwrap();
6981 let source_path = temp.path().join("src/aliases.ts");
6982 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6983 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
6984
6985 let coverage_path = temp.path().join("coverage-final.json");
6986 write_single_file_istanbul_fixture(
6987 &coverage_path,
6988 &source_path,
6989 &serde_json::json!({
6990 "0": {
6991 "name": "(anonymous_0)",
6992 "line": 10,
6993 "decl": {
6994 "start": { "line": 10, "column": 8 },
6995 "end": { "line": 10, "column": 9 }
6996 },
6997 "loc": {
6998 "start": { "line": 12, "column": 8 },
6999 "end": { "line": 13, "column": 1 }
7000 }
7001 }
7002 }),
7003 &serde_json::json!({ "0": 1 }),
7004 );
7005
7006 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7007 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7008 let file_coverage = coverage.get(&canonical_source).unwrap();
7009
7010 assert_eq!(file_coverage.lookup("handler", 11, 8), Some(100.0));
7011 }
7012
7013 #[test]
7018 fn curried_arrow_one_liner_resolves_both_arrows() {
7019 let temp = tempfile::TempDir::new().unwrap();
7020 let source_path = temp.path().join("src/nested.ts");
7021 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7022 std::fs::write(&source_path, "export const nested = () => () => true;\n").unwrap();
7023
7024 let coverage_path = temp.path().join("coverage-final.json");
7025 write_single_file_istanbul_fixture(
7026 &coverage_path,
7027 &source_path,
7028 &serde_json::json!({
7029 "0": {
7030 "name": "(anonymous_0)",
7031 "line": 1,
7032 "decl": {
7033 "start": { "line": 1, "column": 22 },
7034 "end": { "line": 1, "column": 23 }
7035 },
7036 "loc": {
7037 "start": { "line": 1, "column": 28 },
7038 "end": { "line": 1, "column": 38 }
7039 }
7040 },
7041 "1": {
7042 "name": "(anonymous_1)",
7043 "line": 1,
7044 "decl": {
7045 "start": { "line": 1, "column": 28 },
7046 "end": { "line": 1, "column": 29 }
7047 },
7048 "loc": {
7049 "start": { "line": 1, "column": 34 },
7050 "end": { "line": 1, "column": 38 }
7051 }
7052 }
7053 }),
7054 &serde_json::json!({ "0": 1, "1": 0 }),
7055 );
7056
7057 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7058 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7059 let file_coverage = coverage.get(&canonical_source).unwrap();
7060
7061 assert_eq!(file_coverage.lookup("nested", 1, 22), Some(100.0));
7062 assert_eq!(file_coverage.lookup("<arrow>", 1, 28), Some(0.0));
7063 }
7064
7065 #[test]
7069 fn curried_arrow_multiline_hoc_resolves_both_arrows() {
7070 let temp = tempfile::TempDir::new().unwrap();
7071 let source_path = temp.path().join("src/with-auth.tsx");
7072 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7073 std::fs::write(
7074 &source_path,
7075 "export const withAuth = (Component) =>\n (props) => {\n return Component(props);\n };\n",
7076 )
7077 .unwrap();
7078
7079 let coverage_path = temp.path().join("coverage-final.json");
7080 write_single_file_istanbul_fixture(
7081 &coverage_path,
7082 &source_path,
7083 &serde_json::json!({
7084 "0": {
7085 "name": "(anonymous_0)",
7086 "line": 2,
7087 "decl": {
7088 "start": { "line": 1, "column": 24 },
7089 "end": { "line": 1, "column": 25 }
7090 },
7091 "loc": {
7092 "start": { "line": 2, "column": 2 },
7093 "end": { "line": 4, "column": 3 }
7094 }
7095 },
7096 "1": {
7097 "name": "(anonymous_1)",
7098 "line": 2,
7099 "decl": {
7100 "start": { "line": 2, "column": 2 },
7101 "end": { "line": 2, "column": 3 }
7102 },
7103 "loc": {
7104 "start": { "line": 2, "column": 13 },
7105 "end": { "line": 4, "column": 3 }
7106 }
7107 }
7108 }),
7109 &serde_json::json!({ "0": 1, "1": 0 }),
7110 );
7111
7112 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7113 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7114 let file_coverage = coverage.get(&canonical_source).unwrap();
7115
7116 assert_eq!(file_coverage.lookup("withAuth", 1, 24), Some(100.0));
7117 assert_eq!(file_coverage.lookup("<arrow>", 2, 2), Some(0.0));
7118 }
7119
7120 #[test]
7123 fn curried_arrow_depth_three_chain_resolves_every_arrow() {
7124 let temp = tempfile::TempDir::new().unwrap();
7125 let source_path = temp.path().join("src/logger.ts");
7126 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7127 std::fs::write(
7128 &source_path,
7129 "export const logger = (store) => (next) => (action) => {\n return next(action);\n};\n",
7130 )
7131 .unwrap();
7132
7133 let coverage_path = temp.path().join("coverage-final.json");
7134 write_single_file_istanbul_fixture(
7135 &coverage_path,
7136 &source_path,
7137 &serde_json::json!({
7138 "0": {
7139 "name": "(anonymous_0)",
7140 "line": 1,
7141 "decl": {
7142 "start": { "line": 1, "column": 22 },
7143 "end": { "line": 1, "column": 23 }
7144 },
7145 "loc": {
7146 "start": { "line": 1, "column": 33 },
7147 "end": { "line": 3, "column": 1 }
7148 }
7149 },
7150 "1": {
7151 "name": "(anonymous_1)",
7152 "line": 1,
7153 "decl": {
7154 "start": { "line": 1, "column": 33 },
7155 "end": { "line": 1, "column": 34 }
7156 },
7157 "loc": {
7158 "start": { "line": 1, "column": 43 },
7159 "end": { "line": 3, "column": 1 }
7160 }
7161 },
7162 "2": {
7163 "name": "(anonymous_2)",
7164 "line": 1,
7165 "decl": {
7166 "start": { "line": 1, "column": 43 },
7167 "end": { "line": 1, "column": 44 }
7168 },
7169 "loc": {
7170 "start": { "line": 1, "column": 55 },
7171 "end": { "line": 3, "column": 1 }
7172 }
7173 }
7174 }),
7175 &serde_json::json!({ "0": 0, "1": 1, "2": 0 }),
7176 );
7177
7178 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7179 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7180 let file_coverage = coverage.get(&canonical_source).unwrap();
7181
7182 assert_eq!(file_coverage.lookup("logger", 1, 22), Some(0.0));
7183 assert_eq!(file_coverage.lookup("<arrow>", 1, 33), Some(100.0));
7184 assert_eq!(file_coverage.lookup("<arrow>", 1, 43), Some(0.0));
7185 }
7186
7187 #[test]
7189 fn curried_class_property_arrow_resolves_both_arrows() {
7190 let temp = tempfile::TempDir::new().unwrap();
7191 let source_path = temp.path().join("src/store.ts");
7192 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7193 std::fs::write(
7194 &source_path,
7195 "export class Store {\n handle = (event) => (payload) => {\n return payload;\n };\n}\n",
7196 )
7197 .unwrap();
7198
7199 let coverage_path = temp.path().join("coverage-final.json");
7200 write_single_file_istanbul_fixture(
7201 &coverage_path,
7202 &source_path,
7203 &serde_json::json!({
7204 "0": {
7205 "name": "(anonymous_0)",
7206 "line": 2,
7207 "decl": {
7208 "start": { "line": 2, "column": 11 },
7209 "end": { "line": 2, "column": 12 }
7210 },
7211 "loc": {
7212 "start": { "line": 2, "column": 22 },
7213 "end": { "line": 4, "column": 3 }
7214 }
7215 },
7216 "1": {
7217 "name": "(anonymous_1)",
7218 "line": 2,
7219 "decl": {
7220 "start": { "line": 2, "column": 22 },
7221 "end": { "line": 2, "column": 23 }
7222 },
7223 "loc": {
7224 "start": { "line": 2, "column": 35 },
7225 "end": { "line": 4, "column": 3 }
7226 }
7227 }
7228 }),
7229 &serde_json::json!({ "0": 1, "1": 0 }),
7230 );
7231
7232 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7233 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7234 let file_coverage = coverage.get(&canonical_source).unwrap();
7235
7236 assert_eq!(file_coverage.lookup("handle", 2, 11), Some(100.0));
7237 assert_eq!(file_coverage.lookup("<arrow>", 2, 22), Some(0.0));
7238 }
7239
7240 #[test]
7244 fn anonymous_sibling_tie_outside_every_body_abstains() {
7245 let temp = tempfile::TempDir::new().unwrap();
7246 let source_path = temp.path().join("src/handlers.ts");
7247 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7248 std::fs::write(
7249 &source_path,
7250 "export const handlers = {\n a: () => true,\n\n b: () => false,\n};\n",
7251 )
7252 .unwrap();
7253
7254 let coverage_path = temp.path().join("coverage-final.json");
7255 write_single_file_istanbul_fixture(
7256 &coverage_path,
7257 &source_path,
7258 &serde_json::json!({
7259 "0": {
7260 "name": "(anonymous_0)",
7261 "line": 2,
7262 "decl": {
7263 "start": { "line": 2, "column": 5 },
7264 "end": { "line": 2, "column": 6 }
7265 },
7266 "loc": {
7267 "start": { "line": 2, "column": 11 },
7268 "end": { "line": 2, "column": 15 }
7269 }
7270 },
7271 "1": {
7272 "name": "(anonymous_1)",
7273 "line": 4,
7274 "decl": {
7275 "start": { "line": 4, "column": 5 },
7276 "end": { "line": 4, "column": 6 }
7277 },
7278 "loc": {
7279 "start": { "line": 4, "column": 11 },
7280 "end": { "line": 4, "column": 16 }
7281 }
7282 }
7283 }),
7284 &serde_json::json!({ "0": 1, "1": 0 }),
7285 );
7286
7287 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7288 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7289 let file_coverage = coverage.get(&canonical_source).unwrap();
7290
7291 assert_eq!(file_coverage.lookup("a", 2, 5), Some(100.0));
7292 assert_eq!(file_coverage.lookup("b", 4, 5), Some(0.0));
7293 assert!(file_coverage.lookup("<arrow>", 3, 5).is_none());
7294 }
7295
7296 #[test]
7302 fn anonymous_tie_selects_unique_strictly_innermost_containing_span() {
7303 let temp = tempfile::TempDir::new().unwrap();
7304 let source_path = temp.path().join("src/nested.ts");
7305 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7306 std::fs::write(
7307 &source_path,
7308 "export const o = function () { const f = () => { return 1;\n }; return f; };\n",
7309 )
7310 .unwrap();
7311
7312 let coverage_path = temp.path().join("coverage-final.json");
7313 write_single_file_istanbul_fixture(
7314 &coverage_path,
7315 &source_path,
7316 &serde_json::json!({
7317 "0": {
7318 "name": "(anonymous_0)",
7319 "line": 1,
7320 "decl": {
7321 "start": { "line": 1, "column": 17 },
7322 "end": { "line": 1, "column": 18 }
7323 },
7324 "loc": {
7325 "start": { "line": 1, "column": 29 },
7326 "end": { "line": 2, "column": 53 }
7327 }
7328 },
7329 "1": {
7330 "name": "(anonymous_1)",
7331 "line": 1,
7332 "decl": {
7333 "start": { "line": 1, "column": 41 },
7334 "end": { "line": 1, "column": 42 }
7335 },
7336 "loc": {
7337 "start": { "line": 1, "column": 47 },
7338 "end": { "line": 2, "column": 40 }
7339 }
7340 }
7341 }),
7342 &serde_json::json!({ "0": 1, "1": 0 }),
7343 );
7344
7345 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7346 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7347 let file_coverage = coverage.get(&canonical_source).unwrap();
7348
7349 assert_eq!(file_coverage.lookup("<arrow>", 2, 35), Some(0.0));
7350 }
7351
7352 #[test]
7355 fn anonymous_tie_rejects_incomparable_containing_spans() {
7356 let file_coverage = IstanbulFileCoverage::new(
7357 vec![
7358 IstanbulFunctionCoverage {
7359 name: "(anonymous_0)".to_string(),
7360 coverage_pct: 100.0,
7361 aliases: vec![primary_alias(10, 8), secondary_alias(10, 14)],
7362 decl_start: IstanbulPosition::new(10, 8),
7363 header_holds_other_fn: false,
7364 header_span: None,
7365 body_span: Some(body_span((10, 14), (12, 30))),
7366 },
7367 IstanbulFunctionCoverage {
7368 name: "(anonymous_1)".to_string(),
7369 coverage_pct: 0.0,
7370 aliases: vec![primary_alias(10, 20), secondary_alias(11, 0)],
7371 decl_start: IstanbulPosition::new(10, 20),
7372 header_holds_other_fn: false,
7373 header_span: None,
7374 body_span: Some(body_span((11, 0), (14, 0))),
7375 },
7376 ],
7377 false,
7378 );
7379
7380 assert!(file_coverage.lookup("<arrow>", 12, 17).is_none());
7381 }
7382
7383 #[test]
7388 fn anonymous_shared_primary_alias_rejects_even_nested_spans() {
7389 let file_coverage = IstanbulFileCoverage::new(
7390 vec![
7391 IstanbulFunctionCoverage {
7392 name: "(anonymous_0)".to_string(),
7393 coverage_pct: 100.0,
7394 aliases: vec![primary_alias(4, 11), primary_alias(1, 11)],
7395 decl_start: IstanbulPosition::new(4, 11),
7396 header_holds_other_fn: false,
7397 header_span: None,
7398 body_span: Some(body_span((4, 11), (4, 23))),
7399 },
7400 IstanbulFunctionCoverage {
7401 name: "(anonymous_1)".to_string(),
7402 coverage_pct: 0.0,
7403 aliases: vec![primary_alias(4, 11), secondary_alias(4, 18)],
7404 decl_start: IstanbulPosition::new(4, 11),
7405 header_holds_other_fn: false,
7406 header_span: None,
7407 body_span: Some(body_span((4, 18), (4, 23))),
7408 },
7409 ],
7410 false,
7411 );
7412
7413 assert!(file_coverage.lookup("<arrow>", 4, 11).is_none());
7414 assert_eq!(file_coverage.lookup("aa", 1, 11), Some(100.0));
7415 }
7416
7417 #[test]
7418 fn colliding_anonymous_alias_uses_unique_safe_header_span() {
7419 let file_coverage = IstanbulFileCoverage::new(
7420 vec![
7421 IstanbulFunctionCoverage {
7422 name: "(anonymous_0)".to_string(),
7423 coverage_pct: 100.0,
7424 aliases: vec![primary_alias(1, 0), secondary_alias(3, 4)],
7425 decl_start: IstanbulPosition::new(1, 0),
7426 header_holds_other_fn: false,
7427 header_span: Some(body_span((1, 0), (5, 0))),
7428 body_span: Some(body_span((5, 0), (8, 0))),
7429 },
7430 IstanbulFunctionCoverage {
7431 name: "(anonymous_1)".to_string(),
7432 coverage_pct: 0.0,
7433 aliases: vec![primary_alias(10, 0), secondary_alias(3, 4)],
7434 decl_start: IstanbulPosition::new(10, 0),
7435 header_holds_other_fn: false,
7436 header_span: None,
7437 body_span: Some(body_span((10, 0), (12, 0))),
7438 },
7439 ],
7440 false,
7441 );
7442
7443 assert_eq!(file_coverage.lookup("<arrow>", 3, 4), Some(100.0));
7444 }
7445
7446 #[test]
7449 fn colliding_secondary_aliases_abstain_at_shared_position() {
7450 let file_coverage = IstanbulFileCoverage::new(
7451 vec![
7452 IstanbulFunctionCoverage {
7453 name: "(anonymous_0)".to_string(),
7454 coverage_pct: 100.0,
7455 aliases: vec![primary_alias(10, 0), secondary_alias(12, 4)],
7456 decl_start: IstanbulPosition::new(10, 0),
7457 header_holds_other_fn: false,
7458 header_span: None,
7459 body_span: Some(body_span((12, 4), (20, 0))),
7460 },
7461 IstanbulFunctionCoverage {
7462 name: "(anonymous_1)".to_string(),
7463 coverage_pct: 0.0,
7464 aliases: vec![primary_alias(11, 0), secondary_alias(12, 4)],
7465 decl_start: IstanbulPosition::new(11, 0),
7466 header_holds_other_fn: false,
7467 header_span: None,
7468 body_span: Some(body_span((12, 4), (18, 0))),
7469 },
7470 ],
7471 false,
7472 );
7473
7474 assert_eq!(file_coverage.lookup("first", 10, 0), Some(100.0));
7475 assert_eq!(file_coverage.lookup("second", 11, 0), Some(0.0));
7476 assert!(file_coverage.lookup("<arrow>", 12, 4).is_none());
7477 }
7478
7479 #[test]
7480 fn colliding_named_secondary_aliases_abstain_at_shared_position() {
7481 let file_coverage = IstanbulFileCoverage::new(
7482 vec![
7483 IstanbulFunctionCoverage {
7484 name: "handler".to_string(),
7485 coverage_pct: 100.0,
7486 aliases: vec![primary_alias(10, 0), secondary_alias(12, 4)],
7487 decl_start: IstanbulPosition::new(10, 0),
7488 header_holds_other_fn: false,
7489 header_span: None,
7490 body_span: Some(body_span((12, 4), (20, 0))),
7491 },
7492 IstanbulFunctionCoverage {
7493 name: "handler".to_string(),
7494 coverage_pct: 0.0,
7495 aliases: vec![primary_alias(11, 0), secondary_alias(12, 4)],
7496 decl_start: IstanbulPosition::new(11, 0),
7497 header_holds_other_fn: false,
7498 header_span: None,
7499 body_span: Some(body_span((12, 4), (18, 0))),
7500 },
7501 ],
7502 false,
7503 );
7504
7505 assert_eq!(file_coverage.lookup("handler", 10, 0), Some(100.0));
7506 assert_eq!(file_coverage.lookup("handler", 11, 0), Some(0.0));
7507 assert!(file_coverage.lookup("handler", 12, 4).is_none());
7508 }
7509
7510 #[test]
7511 fn invalid_body_location_does_not_create_an_alias() {
7512 let temp = tempfile::TempDir::new().unwrap();
7513 let source_path = temp.path().join("src/invalid-location.ts");
7514 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7515 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
7516
7517 let coverage_path = temp.path().join("coverage-final.json");
7518 write_single_file_istanbul_fixture(
7519 &coverage_path,
7520 &source_path,
7521 &serde_json::json!({
7522 "0": {
7523 "name": "(anonymous_0)",
7524 "line": 8,
7525 "decl": {
7526 "start": { "line": 8, "column": 14 },
7527 "end": { "line": 8, "column": 25 }
7528 },
7529 "loc": {
7530 "start": { "line": 22, "column": 1 },
7531 "end": { "line": 20, "column": 6 }
7532 }
7533 }
7534 }),
7535 &serde_json::json!({ "0": 1 }),
7536 );
7537
7538 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7539 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7540 let file_coverage = coverage.get(&canonical_source).unwrap();
7541
7542 assert!(file_coverage.lookup("handler", 22, 1).is_none());
7543 }
7544
7545 #[test]
7546 fn load_istanbul_coverage_matches_multiline_async_arrow_decl_alias() {
7547 let temp = tempfile::TempDir::new().unwrap();
7548 let source_path = temp.path().join("src/actor.ts");
7549 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7550 std::fs::write(
7551 &source_path,
7552 "export const elementsFrom = async (\n locator: AnyLocator,\n options?: { missingAsEmpty?: boolean },\n): Promise<HTMLElement[]> => {\n return [];\n};\n",
7553 )
7554 .unwrap();
7555
7556 let coverage_path = temp.path().join("coverage-final.json");
7557 write_single_file_istanbul_fixture(
7558 &coverage_path,
7559 &source_path,
7560 &serde_json::json!({
7561 "0": {
7562 "name": "(anonymous_0)",
7563 "line": 4,
7564 "decl": {
7565 "start": { "line": 1, "column": 28 },
7566 "end": { "line": 4, "column": 26 }
7567 },
7568 "loc": {
7569 "start": { "line": 4, "column": 27 },
7570 "end": { "line": 6, "column": 1 }
7571 }
7572 }
7573 }),
7574 &serde_json::json!({
7575 "0": 642
7576 }),
7577 );
7578
7579 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7580 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7581 let file_coverage = coverage.get(&canonical_source).unwrap();
7582
7583 assert_eq!(file_coverage.lookup("elementsFrom", 1, 28), Some(100.0));
7584 }
7585
7586 #[test]
7587 fn istanbul_lookup_exact_match() {
7588 let mut functions = rustc_hash::FxHashMap::default();
7589 functions.insert(("handleClick".to_string(), 10, 0), 85.0);
7590 let fc = test_istanbul_file_coverage(functions, false);
7591 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 85.0).abs() < f64::EPSILON);
7592 }
7593
7594 #[test]
7595 fn istanbul_lookup_fuzzy_match_within_offset() {
7596 let mut functions = rustc_hash::FxHashMap::default();
7597 functions.insert(("handleClick".to_string(), 10, 0), 72.0);
7598 let fc = test_istanbul_file_coverage(functions, false);
7599 assert!((fc.lookup("handleClick", 11, 0).unwrap() - 72.0).abs() < f64::EPSILON);
7600 assert!((fc.lookup("handleClick", 12, 0).unwrap() - 72.0).abs() < f64::EPSILON);
7601 }
7602
7603 #[test]
7604 fn istanbul_lookup_fuzzy_match_outside_offset() {
7605 let mut functions = rustc_hash::FxHashMap::default();
7606 functions.insert(("handleClick".to_string(), 10, 0), 72.0);
7607 let fc = test_istanbul_file_coverage(functions, false);
7608 assert!(fc.lookup("handleClick", 13, 0).is_none());
7609 }
7610
7611 #[test]
7612 fn istanbul_lookup_relocated_matches_unique_name_at_any_distance() {
7613 let mut functions = rustc_hash::FxHashMap::default();
7614 functions.insert(("handleClick".to_string(), 29, 0), 72.0);
7615 let fc = test_istanbul_file_coverage(functions, true);
7616 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
7617 }
7618
7619 #[test]
7620 fn istanbul_lookup_relocated_accepts_declaration_alias_pair() {
7621 let mut functions = rustc_hash::FxHashMap::default();
7622 functions.insert(("handleClick".to_string(), 29, 16), 72.0);
7623 functions.insert(("handleClick".to_string(), 29, 0), 72.0);
7624 let fc = test_istanbul_file_coverage(functions, true);
7625 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
7626 }
7627
7628 #[test]
7629 fn istanbul_lookup_relocated_bails_on_disagreeing_same_name_entries() {
7630 let mut functions = rustc_hash::FxHashMap::default();
7631 functions.insert(("render".to_string(), 29, 0), 72.0);
7632 functions.insert(("render".to_string(), 80, 0), 10.0);
7633 let fc = test_istanbul_file_coverage(functions, true);
7634 assert!(fc.lookup("render", 10, 0).is_none());
7635 }
7636
7637 #[test]
7638 fn istanbul_lookup_relocated_prefers_bounded_fuzzy_match() {
7639 let mut functions = rustc_hash::FxHashMap::default();
7640 functions.insert(("render".to_string(), 11, 0), 72.0);
7641 functions.insert(("render".to_string(), 80, 0), 10.0);
7642 let fc = test_istanbul_file_coverage(functions, true);
7643 assert!((fc.lookup("render", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
7644 }
7645
7646 #[test]
7647 fn istanbul_lookup_name_mismatch() {
7648 let mut functions = rustc_hash::FxHashMap::default();
7649 functions.insert(("handleClick".to_string(), 10, 0), 85.0);
7650 let fc = test_istanbul_file_coverage(functions, false);
7651 assert!(fc.lookup("handleSubmit", 10, 0).is_none());
7652 }
7653
7654 #[test]
7655 fn istanbul_lookup_empty() {
7656 let fc = test_istanbul_file_coverage(rustc_hash::FxHashMap::default(), false);
7657 assert!(fc.lookup("anything", 1, 0).is_none());
7658 }
7659
7660 #[test]
7661 fn istanbul_lookup_fuzzy_picks_closest() {
7662 let mut functions = rustc_hash::FxHashMap::default();
7663 functions.insert(("render".to_string(), 8, 0), 60.0);
7664 functions.insert(("render".to_string(), 12, 0), 90.0);
7665 let fc = test_istanbul_file_coverage(functions, false);
7666 let result = fc.lookup("render", 10, 0);
7667 assert!(result.is_some());
7668 let pct = result.unwrap();
7669 assert!((pct - 60.0).abs() < f64::EPSILON || (pct - 90.0).abs() < f64::EPSILON);
7670 }
7671
7672 #[test]
7673 fn istanbul_lookup_anonymous_fallback_single_candidate() {
7674 let mut functions = rustc_hash::FxHashMap::default();
7675 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
7676 let fc = test_istanbul_file_coverage(functions, false);
7677 assert!((fc.lookup("myHandler", 28, 0).unwrap() - 75.0).abs() < f64::EPSILON);
7678 assert!((fc.lookup("myHandler", 30, 0).unwrap() - 75.0).abs() < f64::EPSILON);
7679 }
7680
7681 #[test]
7682 fn istanbul_lookup_anonymous_fallback_rejects_nearby_far_column() {
7683 let mut functions = rustc_hash::FxHashMap::default();
7684 functions.insert(("(anonymous_0)".to_string(), 4, 28), 75.0);
7685 let fc = test_istanbul_file_coverage(functions, false);
7686
7687 assert!(fc.lookup("declaredHelper", 3, 0).is_none());
7688 }
7689
7690 #[test]
7691 fn istanbul_lookup_anonymous_fallback_picks_closest_when_lines_differ() {
7692 let mut functions = rustc_hash::FxHashMap::default();
7693 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
7694 functions.insert(("(anonymous_1)".to_string(), 29, 0), 50.0);
7695 let fc = test_istanbul_file_coverage(functions, false);
7696 assert!((fc.lookup("myHandler", 28, 0).unwrap() - 75.0).abs() < f64::EPSILON);
7697 }
7698
7699 #[test]
7700 fn istanbul_lookup_anonymous_fallback_picks_closest_by_col_on_same_line() {
7701 let mut functions = rustc_hash::FxHashMap::default();
7702 functions.insert(("(anonymous_0)".to_string(), 1, 23), 90.0); functions.insert(("(anonymous_1)".to_string(), 1, 43), 10.0); let fc = test_istanbul_file_coverage(functions, false);
7705 assert!((fc.lookup("<arrow>", 1, 43).unwrap() - 10.0).abs() < f64::EPSILON);
7706 assert!((fc.lookup("<arrow>", 1, 23).unwrap() - 90.0).abs() < f64::EPSILON);
7707 }
7708
7709 #[test]
7710 fn istanbul_lookup_anonymous_fallback_bails_only_on_true_tie() {
7711 let mut functions = rustc_hash::FxHashMap::default();
7712 functions.insert(("(anonymous_0)".to_string(), 27, 0), 75.0);
7713 functions.insert(("(anonymous_1)".to_string(), 29, 0), 50.0);
7714 let fc = test_istanbul_file_coverage(functions, false);
7715 assert!(fc.lookup("myHandler", 28, 0).is_none());
7716 }
7717
7718 #[test]
7719 fn istanbul_lookup_anonymous_fallback_outside_offset() {
7720 let mut functions = rustc_hash::FxHashMap::default();
7721 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
7722 let fc = test_istanbul_file_coverage(functions, false);
7723 assert!(fc.lookup("myHandler", 31, 0).is_none());
7724 }
7725
7726 #[test]
7727 fn istanbul_lookup_named_match_beats_nearby_anonymous() {
7728 let mut functions = rustc_hash::FxHashMap::default();
7729 functions.insert(("handleClick".to_string(), 10, 0), 90.0);
7730 functions.insert(("(anonymous_7)".to_string(), 11, 0), 10.0);
7731 let fc = test_istanbul_file_coverage(functions, false);
7732 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 90.0).abs() < f64::EPSILON);
7733 }
7734
7735 #[test]
7736 fn build_test_refs_empty() {
7737 let exports: Vec<fallow_graph::graph::ExportSymbol> = vec![];
7738 let graph = fallow_graph::graph::ModuleGraph::build(&[], &[], &[]);
7739 let refs = build_test_referenced_exports(&exports, StaticTestCoverage::new(&graph));
7740 assert!(refs.is_empty());
7741 }
7742
7743 #[test]
7744 fn istanbul_crap_empty_complexity() {
7745 let result = istanbul_crap_default(&[], None, false);
7746 assert!((result.max_crap).abs() < f64::EPSILON);
7747 assert_eq!(result.signals.above, 0);
7748 assert_eq!(result.matched, 0);
7749 assert_eq!(result.total, 0);
7750 }
7751
7752 #[test]
7753 fn istanbul_crap_match_statistics() {
7754 let funcs = vec![make_fn_complexity(5), {
7755 let mut f = make_fn_complexity(3);
7756 f.name = "other_fn".into();
7757 f.line = 10;
7758 f
7759 }];
7760 let mut functions = rustc_hash::FxHashMap::default();
7761 functions.insert(("test_fn".to_string(), 1, 0), 80.0);
7762 let file_cov = test_istanbul_file_coverage(functions, false);
7763 let result = istanbul_crap_default(&funcs, Some(&file_cov), true);
7764 assert_eq!(result.matched, 1);
7765 assert_eq!(result.total, 2);
7766 }
7767
7768 #[test]
7769 fn estimated_crap_multiple_functions_mixed_coverage() {
7770 let funcs = vec![
7771 make_fn_complexity(10), {
7773 let mut f = make_fn_complexity(3);
7774 f.name = "helper".into();
7775 f.line = 20;
7776 f
7777 },
7778 ];
7779 let mut refs = rustc_hash::FxHashSet::default();
7780 refs.insert("test_fn".to_string());
7781 let result = estimated_crap_default(
7782 &funcs,
7783 &refs,
7784 true,
7785 fallow_output::CoverageSource::Estimated,
7786 );
7787 let (max, above) = (result.max_crap, result.signals.above);
7788 assert!(max > 10.0);
7789 assert_eq!(above, 0);
7790 }
7791}