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