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