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