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.panicked || !parsed.errors.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 },
4980 ),
4981 );
4982
4983 let output = crate::results::DeadCodeAnalysisArtifacts {
4984 results,
4985 timings: None,
4986 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
4987 modules: None,
4988 files: None,
4989 script_used_packages: rustc_hash::FxHashSet::default(),
4990 trace_provenance: crate::trace::TraceProvenance::default(),
4991 file_hashes: rustc_hash::FxHashMap::default(),
4992 };
4993
4994 let result = compute_file_scores_default(
4995 &modules,
4996 &file_paths,
4997 None,
4998 output,
4999 None,
5000 std::path::Path::new("/project"),
5001 )
5002 .unwrap();
5003 assert_eq!(result.analysis_counts.total_exports, 2);
5004 assert_eq!(result.analysis_counts.dead_exports, 2);
5005 assert_eq!(result.analysis_counts.unused_deps, 1);
5006 }
5007
5008 #[test]
5010 #[expect(
5011 clippy::too_many_lines,
5012 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
5013 )]
5014 fn total_exports_counts_graph_modules_not_extraction_modules() {
5015 let path_a = std::path::PathBuf::from("/src/a.ts");
5016 let files = vec![crate::discover::DiscoveredFile {
5017 id: crate::discover::FileId(0),
5018 path: path_a.clone(),
5019 size_bytes: 100,
5020 }];
5021
5022 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5023 file_id: crate::discover::FileId(0),
5024 path: path_a.clone(),
5025 exports: vec![
5026 fallow_types::extract::ExportInfo {
5027 name: crate::source::ExportName::Named("foo".into()),
5028 local_name: None,
5029 is_type_only: false,
5030 visibility: crate::source::VisibilityTag::None,
5031 expected_unused_reason: None,
5032 span: oxc_span::Span::empty(0),
5033 members: vec![],
5034 is_side_effect_used: false,
5035 super_class: None,
5036 deprecated: false,
5037 deprecated_reason: None,
5038 },
5039 fallow_types::extract::ExportInfo {
5040 name: crate::source::ExportName::Named("bar".into()),
5041 local_name: None,
5042 is_type_only: false,
5043 visibility: crate::source::VisibilityTag::None,
5044 expected_unused_reason: None,
5045 span: oxc_span::Span::empty(0),
5046 members: vec![],
5047 is_side_effect_used: false,
5048 super_class: None,
5049 deprecated: false,
5050 deprecated_reason: None,
5051 },
5052 fallow_types::extract::ExportInfo {
5053 name: crate::source::ExportName::Named("baz".into()),
5054 local_name: None,
5055 is_type_only: false,
5056 visibility: crate::source::VisibilityTag::None,
5057 expected_unused_reason: None,
5058 span: oxc_span::Span::new(0, 0),
5059 members: vec![],
5060 is_side_effect_used: false,
5061 super_class: None,
5062 deprecated: false,
5063 deprecated_reason: None,
5064 },
5065 ]
5066 .into(),
5067 ..Default::default()
5068 }];
5069
5070 let graph = build_test_graph(&files, &[], &resolved_modules);
5071
5072 let mut module = make_module_info(
5073 0,
5074 10,
5075 vec![fallow_types::extract::FunctionComplexity {
5076 name: "fn_a".into(),
5077 is_private_member: false,
5078 line: 1,
5079 col: 0,
5080 cyclomatic: 1,
5081 cognitive: 0,
5082 line_count: 10,
5083 param_count: 0,
5084 react_hook_count: 0,
5085 react_jsx_max_depth: 0,
5086 react_prop_count: 0,
5087 source_hash: None,
5088 contributions: Vec::new(),
5089 }],
5090 );
5091 module.exports = vec![
5092 fallow_types::extract::ExportInfo {
5093 name: crate::source::ExportName::Named("foo".into()),
5094 local_name: None,
5095 is_type_only: false,
5096 visibility: crate::source::VisibilityTag::None,
5097 expected_unused_reason: None,
5098 span: oxc_span::Span::empty(0),
5099 members: vec![],
5100 is_side_effect_used: false,
5101 super_class: None,
5102 deprecated: false,
5103 deprecated_reason: None,
5104 },
5105 fallow_types::extract::ExportInfo {
5106 name: crate::source::ExportName::Named("bar".into()),
5107 local_name: None,
5108 is_type_only: false,
5109 visibility: crate::source::VisibilityTag::None,
5110 expected_unused_reason: None,
5111 span: oxc_span::Span::empty(0),
5112 members: vec![],
5113 is_side_effect_used: false,
5114 super_class: None,
5115 deprecated: false,
5116 deprecated_reason: None,
5117 },
5118 ]
5119 .into();
5120 let modules = vec![module];
5121
5122 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5123 rustc_hash::FxHashMap::default();
5124 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5125
5126 let mut results = fallow_types::results::AnalysisResults::default();
5127 for name in ["foo", "bar", "baz"] {
5128 results.unused_exports.push(
5129 fallow_types::output_dead_code::UnusedExportFinding::with_actions(
5130 fallow_types::results::UnusedExport {
5131 path: path_a.clone(),
5132 export_name: name.into(),
5133 is_type_only: false,
5134 line: 1,
5135 col: 0,
5136 span_start: 0,
5137 is_re_export: name == "baz",
5138 deprecated: false,
5139 deprecated_reason: None,
5140 },
5141 ),
5142 );
5143 }
5144
5145 let output = crate::results::DeadCodeAnalysisArtifacts {
5146 results,
5147 timings: None,
5148 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5149 modules: None,
5150 files: None,
5151 script_used_packages: rustc_hash::FxHashSet::default(),
5152 trace_provenance: crate::trace::TraceProvenance::default(),
5153 file_hashes: rustc_hash::FxHashMap::default(),
5154 };
5155
5156 let result = compute_file_scores_default(
5157 &modules,
5158 &file_paths,
5159 None,
5160 output,
5161 None,
5162 std::path::Path::new("/project"),
5163 )
5164 .unwrap();
5165 assert_eq!(result.analysis_counts.total_exports, 3);
5166 assert_eq!(result.analysis_counts.dead_exports, 3);
5167 }
5168
5169 #[test]
5170 fn compute_file_scores_module_not_in_file_paths_skipped() {
5171 let path_a = std::path::PathBuf::from("/src/a.ts");
5172 let files = vec![crate::discover::DiscoveredFile {
5173 id: crate::discover::FileId(0),
5174 path: path_a.clone(),
5175 size_bytes: 100,
5176 }];
5177
5178 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5179 file_id: crate::discover::FileId(0),
5180 path: path_a,
5181 ..Default::default()
5182 }];
5183
5184 let graph = build_test_graph(&files, &[], &resolved_modules);
5185
5186 let modules = vec![make_module_info(
5187 0,
5188 10,
5189 vec![fallow_types::extract::FunctionComplexity {
5190 name: "fn_a".into(),
5191 is_private_member: false,
5192 line: 1,
5193 col: 0,
5194 cyclomatic: 2,
5195 cognitive: 1,
5196 line_count: 10,
5197 param_count: 0,
5198 react_hook_count: 0,
5199 react_jsx_max_depth: 0,
5200 react_prop_count: 0,
5201 source_hash: None,
5202 contributions: Vec::new(),
5203 }],
5204 )];
5205
5206 let file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5207 rustc_hash::FxHashMap::default();
5208
5209 let output = crate::results::DeadCodeAnalysisArtifacts {
5210 results: fallow_types::results::AnalysisResults::default(),
5211 timings: None,
5212 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5213 modules: None,
5214 files: None,
5215 script_used_packages: rustc_hash::FxHashSet::default(),
5216 trace_provenance: crate::trace::TraceProvenance::default(),
5217 file_hashes: rustc_hash::FxHashMap::default(),
5218 };
5219
5220 let result = compute_file_scores_default(
5221 &modules,
5222 &file_paths,
5223 None,
5224 output,
5225 None,
5226 std::path::Path::new("/project"),
5227 )
5228 .unwrap();
5229 assert!(result.scores.is_empty());
5230 }
5231
5232 #[test]
5233 fn compute_file_scores_mi_rounded_to_one_decimal() {
5234 let path_a = std::path::PathBuf::from("/src/a.ts");
5235 let files = vec![crate::discover::DiscoveredFile {
5236 id: crate::discover::FileId(0),
5237 path: path_a.clone(),
5238 size_bytes: 100,
5239 }];
5240
5241 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5242 file_id: crate::discover::FileId(0),
5243 path: path_a.clone(),
5244 ..Default::default()
5245 }];
5246
5247 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5248
5249 let modules = vec![make_module_info(
5250 0,
5251 100,
5252 vec![fallow_types::extract::FunctionComplexity {
5253 name: "fn".into(),
5254 is_private_member: false,
5255 line: 1,
5256 col: 0,
5257 cyclomatic: 7,
5258 cognitive: 3,
5259 line_count: 100,
5260 param_count: 0,
5261 react_hook_count: 0,
5262 react_jsx_max_depth: 0,
5263 react_prop_count: 0,
5264 source_hash: None,
5265 contributions: Vec::new(),
5266 }],
5267 )];
5268
5269 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5270 rustc_hash::FxHashMap::default();
5271 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5272
5273 let output = crate::results::DeadCodeAnalysisArtifacts {
5274 results: fallow_types::results::AnalysisResults::default(),
5275 timings: None,
5276 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5277 modules: None,
5278 files: None,
5279 script_used_packages: rustc_hash::FxHashSet::default(),
5280 trace_provenance: crate::trace::TraceProvenance::default(),
5281 file_hashes: rustc_hash::FxHashMap::default(),
5282 };
5283
5284 let result = compute_file_scores_default(
5285 &modules,
5286 &file_paths,
5287 None,
5288 output,
5289 None,
5290 std::path::Path::new("/project"),
5291 )
5292 .unwrap();
5293 let mi = result.scores[0].maintainability_index;
5294 let rounded = (mi * 10.0).round() / 10.0;
5295 assert!((mi - rounded).abs() < f64::EPSILON);
5296 }
5297
5298 #[test]
5299 fn compute_file_scores_value_export_counts_tracked() {
5300 let path_a = std::path::PathBuf::from("/src/a.ts");
5301 let files = vec![crate::discover::DiscoveredFile {
5302 id: crate::discover::FileId(0),
5303 path: path_a.clone(),
5304 size_bytes: 100,
5305 }];
5306
5307 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5308 file_id: crate::discover::FileId(0),
5309 path: path_a.clone(),
5310 exports: vec![
5311 fallow_types::extract::ExportInfo {
5312 name: crate::source::ExportName::Named("a".into()),
5313 local_name: None,
5314 is_type_only: false,
5315 visibility: crate::source::VisibilityTag::None,
5316 expected_unused_reason: None,
5317 span: oxc_span::Span::empty(0),
5318 members: vec![],
5319 is_side_effect_used: false,
5320 super_class: None,
5321 deprecated: false,
5322 deprecated_reason: None,
5323 },
5324 fallow_types::extract::ExportInfo {
5325 name: crate::source::ExportName::Named("b".into()),
5326 local_name: None,
5327 is_type_only: false,
5328 visibility: crate::source::VisibilityTag::None,
5329 expected_unused_reason: None,
5330 span: oxc_span::Span::empty(0),
5331 members: vec![],
5332 is_side_effect_used: false,
5333 super_class: None,
5334 deprecated: false,
5335 deprecated_reason: None,
5336 },
5337 fallow_types::extract::ExportInfo {
5338 name: crate::source::ExportName::Named("T".into()),
5339 local_name: None,
5340 is_type_only: true,
5341 visibility: crate::source::VisibilityTag::None,
5342 expected_unused_reason: None,
5343 span: oxc_span::Span::empty(0),
5344 members: vec![],
5345 is_side_effect_used: false,
5346 super_class: None,
5347 deprecated: false,
5348 deprecated_reason: None,
5349 },
5350 ]
5351 .into(),
5352 ..Default::default()
5353 }];
5354
5355 let graph = build_test_graph(&files, &[], &resolved_modules);
5356
5357 let modules = vec![make_module_info(
5358 0,
5359 10,
5360 vec![fallow_types::extract::FunctionComplexity {
5361 name: "fn_a".into(),
5362 is_private_member: false,
5363 line: 1,
5364 col: 0,
5365 cyclomatic: 2,
5366 cognitive: 1,
5367 line_count: 10,
5368 param_count: 0,
5369 react_hook_count: 0,
5370 react_jsx_max_depth: 0,
5371 react_prop_count: 0,
5372 source_hash: None,
5373 contributions: Vec::new(),
5374 }],
5375 )];
5376
5377 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5378 rustc_hash::FxHashMap::default();
5379 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5380
5381 let output = crate::results::DeadCodeAnalysisArtifacts {
5382 results: fallow_types::results::AnalysisResults::default(),
5383 timings: None,
5384 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5385 modules: None,
5386 files: None,
5387 script_used_packages: rustc_hash::FxHashSet::default(),
5388 trace_provenance: crate::trace::TraceProvenance::default(),
5389 file_hashes: rustc_hash::FxHashMap::default(),
5390 };
5391
5392 let result = compute_file_scores_default(
5393 &modules,
5394 &file_paths,
5395 None,
5396 output,
5397 None,
5398 std::path::Path::new("/project"),
5399 )
5400 .unwrap();
5401 assert_eq!(result.value_export_counts[&path_a], 2);
5402 }
5403
5404 #[test]
5405 fn compute_file_scores_top_complex_fns_zero_cognitive_excluded() {
5406 let path_a = std::path::PathBuf::from("/src/simple.ts");
5407 let files = vec![crate::discover::DiscoveredFile {
5408 id: crate::discover::FileId(0),
5409 path: path_a.clone(),
5410 size_bytes: 100,
5411 }];
5412
5413 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5414 file_id: crate::discover::FileId(0),
5415 path: path_a.clone(),
5416 ..Default::default()
5417 }];
5418
5419 let graph = build_test_graph(&files, &[], &resolved_modules);
5420
5421 let modules = vec![make_module_info(
5422 0,
5423 10,
5424 vec![fallow_types::extract::FunctionComplexity {
5425 name: "trivial".into(),
5426 is_private_member: false,
5427 line: 1,
5428 col: 0,
5429 cyclomatic: 1,
5430 cognitive: 0,
5431 line_count: 10,
5432 param_count: 0,
5433 react_hook_count: 0,
5434 react_jsx_max_depth: 0,
5435 react_prop_count: 0,
5436 source_hash: None,
5437 contributions: Vec::new(),
5438 }],
5439 )];
5440
5441 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5442 rustc_hash::FxHashMap::default();
5443 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5444
5445 let output = crate::results::DeadCodeAnalysisArtifacts {
5446 results: fallow_types::results::AnalysisResults::default(),
5447 timings: None,
5448 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5449 modules: None,
5450 files: None,
5451 script_used_packages: rustc_hash::FxHashSet::default(),
5452 trace_provenance: crate::trace::TraceProvenance::default(),
5453 file_hashes: rustc_hash::FxHashMap::default(),
5454 };
5455
5456 let result = compute_file_scores_default(
5457 &modules,
5458 &file_paths,
5459 None,
5460 output,
5461 None,
5462 std::path::Path::new("/project"),
5463 )
5464 .unwrap();
5465 assert!(!result.top_complex_fns.contains_key(&path_a));
5466 }
5467
5468 fn make_fn_complexity(cyclomatic: u16) -> fallow_types::extract::FunctionComplexity {
5469 fallow_types::extract::FunctionComplexity {
5470 name: "test_fn".into(),
5471 is_private_member: false,
5472 line: 1,
5473 col: 0,
5474 cyclomatic,
5475 cognitive: 0,
5476 line_count: 10,
5477 param_count: 0,
5478 react_hook_count: 0,
5479 react_jsx_max_depth: 0,
5480 react_prop_count: 0,
5481 source_hash: None,
5482 contributions: Vec::new(),
5483 }
5484 }
5485
5486 fn make_named_fn_complexity(
5487 name: &str,
5488 line: u32,
5489 cyclomatic: u16,
5490 ) -> fallow_types::extract::FunctionComplexity {
5491 fallow_types::extract::FunctionComplexity {
5492 name: name.into(),
5493 is_private_member: false,
5494 line,
5495 col: 0,
5496 cyclomatic,
5497 cognitive: 0,
5498 line_count: 10,
5499 param_count: 0,
5500 react_hook_count: 0,
5501 react_jsx_max_depth: 0,
5502 react_prop_count: 0,
5503 source_hash: None,
5504 contributions: Vec::new(),
5505 }
5506 }
5507
5508 fn crap_override_entry(
5509 files: &[&str],
5510 functions: &[&str],
5511 max_crap: Option<f64>,
5512 ) -> fallow_config::HealthThresholdOverride {
5513 fallow_config::HealthThresholdOverride {
5514 files: files.iter().map(ToString::to_string).collect(),
5515 functions: functions.iter().map(ToString::to_string).collect(),
5516 max_cyclomatic: None,
5517 max_cognitive: None,
5518 max_crap,
5519 max_unit_size: None,
5520 reason: Some("test override".into()),
5521 }
5522 }
5523
5524 fn estimated_signals_with(
5525 resolver: &ThresholdOverrideResolver,
5526 relative: &str,
5527 enforce_crap: bool,
5528 complexity: &[fallow_types::extract::FunctionComplexity],
5529 ) -> CrapThresholdSignals {
5530 let ceilings = CrapCeilingLookup::new(
5531 CrapScoreThresholds {
5532 resolver,
5533 enforce_crap,
5534 },
5535 std::path::Path::new(relative),
5536 );
5537 compute_crap_scores_estimated(
5538 complexity,
5539 &rustc_hash::FxHashSet::default(),
5540 false,
5541 fallow_output::CoverageSource::Estimated,
5542 &ceilings,
5543 )
5544 .signals
5545 }
5546
5547 #[test]
5548 fn crap_counting_exempts_functions_under_override_ceiling() {
5549 let resolver =
5552 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(500.0))]);
5553 let fns = vec![
5554 make_named_fn_complexity("a", 1, 10),
5555 make_named_fn_complexity("b", 12, 10),
5556 ];
5557
5558 let covered = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5559 assert_eq!(covered.above, 0);
5560 assert_eq!(covered.exempted, 2);
5561 assert_eq!(covered.min_ceiling, Some(500.0));
5562
5563 let elsewhere = estimated_signals_with(&resolver, "src/other.ts", true, &fns);
5564 assert_eq!(elsewhere.above, 2);
5565 assert_eq!(elsewhere.exempted, 0);
5566 assert_eq!(elsewhere.min_ceiling, Some(CRAP_THRESHOLD));
5567 }
5568
5569 #[test]
5570 fn crap_counting_insufficient_override_keeps_count() {
5571 let resolver =
5572 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(50.0))]);
5573 let fns = vec![
5574 make_named_fn_complexity("a", 1, 10),
5575 make_named_fn_complexity("b", 12, 10),
5576 ];
5577
5578 let signals = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5579 assert_eq!(signals.above, 2);
5580 assert_eq!(signals.exempted, 0);
5581 assert_eq!(signals.min_ceiling, Some(50.0));
5582 }
5583
5584 #[test]
5585 fn crap_counting_partial_function_override() {
5586 let resolver =
5589 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &["a"], Some(500.0))]);
5590 let fns = vec![
5591 make_named_fn_complexity("a", 1, 10),
5592 make_named_fn_complexity("b", 12, 10),
5593 ];
5594
5595 let signals = estimated_signals_with(&resolver, "src/legacy.ts", true, &fns);
5596 assert_eq!(signals.above, 1);
5597 assert_eq!(signals.exempted, 1);
5598 assert_eq!(signals.min_ceiling, Some(CRAP_THRESHOLD));
5599 }
5600
5601 #[test]
5602 fn crap_counting_disabled_enforcement_counts_baseline_exemptions() {
5603 let resolver = test_crap_resolver(0.0);
5606 let fns = vec![
5607 make_named_fn_complexity("a", 1, 10),
5608 make_named_fn_complexity("b", 12, 10),
5609 make_named_fn_complexity("tiny", 24, 1),
5610 ];
5611
5612 let signals = estimated_signals_with(&resolver, "src/any.ts", false, &fns);
5613 assert_eq!(signals.above, 0);
5614 assert_eq!(signals.exempted, 2);
5615 }
5616
5617 #[test]
5618 fn crap_counting_stricter_ceiling_never_counts_exempt() {
5619 let resolver = test_crap_resolver(10.0);
5622 let fns = vec![make_named_fn_complexity("a", 1, 4)]; let signals = estimated_signals_with(&resolver, "src/any.ts", true, &fns);
5625 assert_eq!(signals.above, 1);
5626 assert_eq!(signals.exempted, 0);
5627 }
5628
5629 #[test]
5630 fn crap_counting_uses_rounded_value_at_boundary() {
5631 let funcs = vec![make_fn_complexity(10)];
5636 let mut functions = rustc_hash::FxHashMap::default();
5637 functions.insert(("test_fn".to_string(), 1, 0), 41.56);
5638 let file_cov = test_istanbul_file_coverage(functions, false);
5639
5640 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5641 assert!((result.per_function[0].crap - 30.0).abs() < f64::EPSILON);
5642 assert_eq!(result.signals.above, 1);
5643 assert_eq!(result.signals.exempted, 0);
5644 }
5645
5646 #[test]
5647 fn compute_file_scores_discloses_override_exemption_on_row() {
5648 let path_a = std::path::PathBuf::from("/project/src/legacy.ts");
5649 let files = vec![crate::discover::DiscoveredFile {
5650 id: crate::discover::FileId(0),
5651 path: path_a.clone(),
5652 size_bytes: 100,
5653 }];
5654 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5655 file_id: crate::discover::FileId(0),
5656 path: path_a.clone(),
5657 ..Default::default()
5658 }];
5659 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5660 let modules = vec![make_module_info(
5661 0,
5662 26,
5663 vec![
5664 make_named_fn_complexity("a", 1, 10),
5665 make_named_fn_complexity("b", 12, 10),
5666 ],
5667 )];
5668 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5669 rustc_hash::FxHashMap::default();
5670 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5671 let output = crate::results::DeadCodeAnalysisArtifacts {
5672 results: fallow_types::results::AnalysisResults::default(),
5673 timings: None,
5674 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5675 modules: None,
5676 files: None,
5677 script_used_packages: rustc_hash::FxHashSet::default(),
5678 trace_provenance: crate::trace::TraceProvenance::default(),
5679 file_hashes: rustc_hash::FxHashMap::default(),
5680 };
5681
5682 let resolver =
5683 test_override_resolver(&[crap_override_entry(&["src/legacy.ts"], &[], Some(500.0))]);
5684 let result = compute_file_scores(
5685 FileScoreComputeInput {
5686 modules: &modules,
5687 file_paths: &file_paths,
5688 changed_files: None,
5689 istanbul_coverage: None,
5690 root: std::path::Path::new("/project"),
5691 crap_thresholds: CrapScoreThresholds {
5692 resolver: &resolver,
5693 enforce_crap: true,
5694 },
5695 },
5696 output,
5697 )
5698 .unwrap();
5699
5700 assert_eq!(result.scores.len(), 1);
5701 let score = &result.scores[0];
5702 assert!((score.crap_max - 110.0).abs() < f64::EPSILON);
5703 assert_eq!(score.crap_above_threshold, 0);
5704 assert_eq!(score.crap_exempted, 2);
5705 assert_eq!(score.crap_effective_threshold, Some(500.0));
5706 assert!(file_score_fully_crap_exempt(score, CRAP_THRESHOLD));
5707 assert_eq!(
5708 file_score_concern_axis(score, CRAP_THRESHOLD),
5709 FileScoreConcern::Structural
5710 );
5711 }
5712
5713 #[test]
5714 fn compute_file_scores_raised_global_omits_row_threshold() {
5715 let path_a = std::path::PathBuf::from("/project/src/legacy.ts");
5716 let files = vec![crate::discover::DiscoveredFile {
5717 id: crate::discover::FileId(0),
5718 path: path_a.clone(),
5719 size_bytes: 100,
5720 }];
5721 let resolved_modules = vec![fallow_graph::resolve::ResolvedModule {
5722 file_id: crate::discover::FileId(0),
5723 path: path_a.clone(),
5724 ..Default::default()
5725 }];
5726 let graph = build_test_graph(&files, std::slice::from_ref(&path_a), &resolved_modules);
5727 let modules = vec![make_module_info(
5728 0,
5729 26,
5730 vec![
5731 make_named_fn_complexity("a", 1, 10),
5732 make_named_fn_complexity("b", 12, 10),
5733 ],
5734 )];
5735 let mut file_paths: rustc_hash::FxHashMap<crate::discover::FileId, &std::path::PathBuf> =
5736 rustc_hash::FxHashMap::default();
5737 file_paths.insert(crate::discover::FileId(0), &files[0].path);
5738 let output = crate::results::DeadCodeAnalysisArtifacts {
5739 results: fallow_types::results::AnalysisResults::default(),
5740 timings: None,
5741 graph: Some(crate::module_graph::RetainedModuleGraph::from(graph)),
5742 modules: None,
5743 files: None,
5744 script_used_packages: rustc_hash::FxHashSet::default(),
5745 trace_provenance: crate::trace::TraceProvenance::default(),
5746 file_hashes: rustc_hash::FxHashMap::default(),
5747 };
5748
5749 let resolver = test_crap_resolver(5000.0);
5752 let result = compute_file_scores(
5753 FileScoreComputeInput {
5754 modules: &modules,
5755 file_paths: &file_paths,
5756 changed_files: None,
5757 istanbul_coverage: None,
5758 root: std::path::Path::new("/project"),
5759 crap_thresholds: CrapScoreThresholds {
5760 resolver: &resolver,
5761 enforce_crap: true,
5762 },
5763 },
5764 output,
5765 )
5766 .unwrap();
5767
5768 assert_eq!(result.scores.len(), 1);
5769 let score = &result.scores[0];
5770 assert_eq!(score.crap_above_threshold, 0);
5771 assert_eq!(score.crap_exempted, 2);
5772 assert_eq!(score.crap_effective_threshold, None);
5773 assert!(file_score_fully_crap_exempt(score, 5000.0));
5774 assert_eq!(
5775 file_score_concern_axis(score, 5000.0),
5776 FileScoreConcern::Structural
5777 );
5778 }
5779
5780 #[test]
5785 fn estimated_crap_untested_aggregates_over_real_units_only() {
5786 let funcs = vec![
5787 make_named_fn_complexity("below", 1, 4),
5788 make_named_fn_complexity("at_threshold", 2, 5),
5789 make_named_fn_complexity("above_threshold", 3, 8),
5790 make_named_fn_complexity("<template>", 4, 21),
5791 make_named_fn_complexity("<module>", 5, 21),
5792 ];
5793 let result = estimated_crap_default(
5794 &funcs,
5795 &rustc_hash::FxHashSet::default(),
5796 false,
5797 fallow_output::CoverageSource::Estimated,
5798 );
5799 assert!((result.max_crap - 72.0).abs() < f64::EPSILON, "{result:#?}");
5800 assert_eq!(result.signals.above, 2);
5801 assert_eq!(result.per_function.len(), 3);
5802 }
5803
5804 #[test]
5805 fn crap_formula_full_coverage() {
5806 let result = crap_formula(10.0, 100.0);
5807 assert!((result - 10.0).abs() < f64::EPSILON);
5808 }
5809
5810 #[test]
5811 fn crap_formula_zero_coverage() {
5812 let result = crap_formula(5.0, 0.0);
5813 assert!((result - 30.0).abs() < f64::EPSILON);
5814 }
5815
5816 #[test]
5817 fn crap_formula_partial_coverage() {
5818 let result = crap_formula(10.0, 50.0);
5819 assert!((result - 22.5).abs() < f64::EPSILON);
5820 }
5821
5822 #[test]
5823 fn crap_formula_high_coverage_low_complexity() {
5824 let result = crap_formula(2.0, 90.0);
5825 assert!((result - 2.004).abs() < 0.001);
5826 }
5827
5828 #[test]
5833 fn crap_default_gate_cyclomatic_boundaries_per_estimate_tier() {
5834 for (coverage_pct, gate_cc) in [(0.0, 5.0), (40.0, 10.0), (85.0, 28.0)] {
5835 assert!(
5836 crap_formula(gate_cc, coverage_pct) >= CRAP_THRESHOLD,
5837 "cyclomatic {gate_cc} at {coverage_pct}% must reach the gate"
5838 );
5839 assert!(
5840 crap_formula(gate_cc - 1.0, coverage_pct) < CRAP_THRESHOLD,
5841 "cyclomatic {} at {coverage_pct}% must stay under the gate",
5842 gate_cc - 1.0
5843 );
5844 }
5845 }
5846
5847 #[test]
5848 fn istanbul_crap_excludes_synthetic_template_units() {
5849 let funcs = vec![
5850 make_named_fn_complexity("<template>", 1, 21),
5851 make_named_fn_complexity("<snippet:rowBody>", 1, 16),
5852 make_fn_complexity(6),
5853 ];
5854 let result = istanbul_crap_default(&funcs, None, false);
5855 assert!((result.max_crap - 42.0).abs() < f64::EPSILON, "{result:#?}");
5856 assert_eq!(result.signals.above, 1);
5857 assert_eq!(
5858 result.total, 1,
5859 "template units must not count as unmatched"
5860 );
5861 assert_eq!(result.per_function.len(), 1);
5862 }
5863
5864 #[test]
5865 fn estimated_crap_excludes_synthetic_template_units() {
5866 let funcs = vec![
5867 make_named_fn_complexity("<template>", 1, 21),
5868 make_named_fn_complexity("<snippet:rowBody>", 1, 16),
5869 ];
5870 let result = estimated_crap_default(
5871 &funcs,
5872 &rustc_hash::FxHashSet::default(),
5873 false,
5874 fallow_output::CoverageSource::Estimated,
5875 );
5876 assert!(result.max_crap.abs() < f64::EPSILON, "{result:#?}");
5877 assert_eq!(result.signals.above, 0);
5878 assert!(result.per_function.is_empty());
5879 }
5880
5881 #[test]
5882 fn istanbul_crap_with_coverage_data() {
5883 let funcs = vec![make_fn_complexity(10)];
5884 let mut functions = rustc_hash::FxHashMap::default();
5885 functions.insert(("test_fn".to_string(), 1, 0), 80.0);
5886 let file_cov = test_istanbul_file_coverage(functions, false);
5887 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5888 assert!((result.max_crap - 10.8).abs() < 0.1);
5889 assert_eq!(result.signals.above, 0);
5890 }
5891
5892 #[test]
5893 fn istanbul_crap_falls_back_to_binary_when_no_match() {
5894 let funcs = vec![make_fn_complexity(6)];
5895 let file_cov = test_istanbul_file_coverage(rustc_hash::FxHashMap::default(), false);
5896 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5897 assert!((result.max_crap - 42.0).abs() < f64::EPSILON);
5898 assert_eq!(result.signals.above, 1);
5899 }
5900
5901 #[test]
5904 fn istanbul_crap_uses_the_static_estimate_when_no_file_coverage() {
5905 let funcs = vec![make_fn_complexity(5)];
5906 let result = istanbul_crap_default(&funcs, None, true);
5907 assert!((result.max_crap - 10.4).abs() < 1e-9);
5910 assert_eq!(result.signals.above, 0);
5911 }
5912
5913 #[test]
5914 fn istanbul_crap_zero_coverage_matches_binary_untested() {
5915 let funcs = vec![make_fn_complexity(5)];
5916 let mut functions = rustc_hash::FxHashMap::default();
5917 functions.insert(("test_fn".to_string(), 1, 0), 0.0);
5918 let file_cov = test_istanbul_file_coverage(functions, false);
5919 let result = istanbul_crap_default(&funcs, Some(&file_cov), false);
5920 assert!((result.max_crap - 30.0).abs() < f64::EPSILON);
5921 assert_eq!(result.signals.above, 1);
5922 }
5923
5924 #[test]
5925 fn estimated_crap_direct_test_reference() {
5926 let funcs = vec![make_fn_complexity(10)];
5927 let mut refs = rustc_hash::FxHashSet::default();
5928 refs.insert("test_fn".to_string());
5929 let result = estimated_crap_default(
5930 &funcs,
5931 &refs,
5932 true,
5933 fallow_output::CoverageSource::Estimated,
5934 );
5935 let (max, above) = (result.max_crap, result.signals.above);
5936 assert!((max - 10.3).abs() < 0.1);
5937 assert_eq!(above, 0);
5938 }
5939
5940 #[test]
5941 fn estimated_crap_indirect_test_reachable() {
5942 let funcs = vec![make_fn_complexity(10)];
5943 let refs = rustc_hash::FxHashSet::default();
5944 let result = estimated_crap_default(
5945 &funcs,
5946 &refs,
5947 true,
5948 fallow_output::CoverageSource::Estimated,
5949 );
5950 let (max, above) = (result.max_crap, result.signals.above);
5951 assert!((max - 31.6).abs() < 0.1);
5952 assert_eq!(above, 1);
5953 }
5954
5955 #[test]
5956 fn estimated_crap_untested_file() {
5957 let funcs = vec![make_fn_complexity(5)];
5958 let refs = rustc_hash::FxHashSet::default();
5959 let result = estimated_crap_default(
5960 &funcs,
5961 &refs,
5962 false,
5963 fallow_output::CoverageSource::Estimated,
5964 );
5965 let (max, above) = (result.max_crap, result.signals.above);
5966 assert!((max - 30.0).abs() < f64::EPSILON);
5967 assert_eq!(above, 1);
5968 }
5969
5970 #[test]
5971 fn estimated_crap_low_complexity_direct_ref() {
5972 let funcs = vec![make_fn_complexity(2)];
5973 let mut refs = rustc_hash::FxHashSet::default();
5974 refs.insert("test_fn".to_string());
5975 let result = estimated_crap_default(
5976 &funcs,
5977 &refs,
5978 true,
5979 fallow_output::CoverageSource::Estimated,
5980 );
5981 let (max, above) = (result.max_crap, result.signals.above);
5982 assert!(max < 3.0);
5983 assert_eq!(above, 0);
5984 }
5985
5986 #[test]
5987 fn estimated_crap_empty() {
5988 let refs = rustc_hash::FxHashSet::default();
5989 let result =
5990 estimated_crap_default(&[], &refs, true, fallow_output::CoverageSource::Estimated);
5991 let (max, above) = (result.max_crap, result.signals.above);
5992 assert!((max).abs() < f64::EPSILON);
5993 assert_eq!(above, 0);
5994 }
5995
5996 fn make_export(name: &str, is_type_only: bool) -> fallow_graph::graph::ExportSymbol {
5997 fallow_graph::graph::ExportSymbol {
5998 name: fallow_types::extract::ExportName::Named(name.into()),
5999 is_type_only,
6000 is_side_effect_used: false,
6001 visibility: crate::source::VisibilityTag::None,
6002 expected_unused_reason: None,
6003 span: oxc_span::Span::default(),
6004 references: vec![],
6005 reference_paths: Vec::new(),
6006 members: vec![],
6007 deprecated: false,
6008 deprecated_reason: None,
6009 }
6010 }
6011
6012 #[test]
6013 fn dead_code_ratio_type_only_exports_excluded_from_denominator() {
6014 let path = std::path::Path::new("src/types.ts");
6015 let exports = vec![
6016 make_export("MyInterface", true),
6017 make_export("MyType", true),
6018 make_export("myFunction", false),
6019 ];
6020 let unused_files = rustc_hash::FxHashSet::default();
6021 let mut unused_by_path = rustc_hash::FxHashMap::default();
6022 unused_by_path.insert(path, 1_usize); let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
6025 assert!((ratio - 1.0).abs() < f64::EPSILON);
6026 }
6027
6028 #[test]
6029 fn dead_code_ratio_only_type_exports_returns_zero() {
6030 let path = std::path::Path::new("src/types.ts");
6031 let exports = vec![
6032 make_export("MyInterface", true),
6033 make_export("MyType", true),
6034 ];
6035 let unused_files = rustc_hash::FxHashSet::default();
6036 let unused_by_path = rustc_hash::FxHashMap::default();
6037
6038 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
6039 assert!(ratio.abs() < f64::EPSILON);
6040 }
6041
6042 #[test]
6043 fn dead_code_ratio_mixed_exports_counts_only_values() {
6044 let path = std::path::Path::new("src/component.ts");
6045 let exports = vec![
6046 make_export("Props", true),
6047 make_export("State", true),
6048 make_export("Component", false),
6049 make_export("helper", false),
6050 ];
6051 let unused_files = rustc_hash::FxHashSet::default();
6052 let mut unused_by_path = rustc_hash::FxHashMap::default();
6053 unused_by_path.insert(path, 1_usize);
6054
6055 let ratio = compute_dead_code_ratio(path, &exports, &unused_files, &unused_by_path);
6056 assert!((ratio - 0.5).abs() < f64::EPSILON);
6057 }
6058
6059 fn write_single_file_istanbul_fixture(
6060 coverage_path: &std::path::Path,
6061 source_path: &std::path::Path,
6062 fn_map: &serde_json::Value,
6063 function_hits: &serde_json::Value,
6064 ) {
6065 write_single_file_istanbul_fixture_with_statements(
6066 coverage_path,
6067 source_path,
6068 fn_map,
6069 function_hits,
6070 &serde_json::json!({}),
6071 &serde_json::json!({}),
6072 );
6073 }
6074
6075 fn write_single_file_istanbul_fixture_with_statements(
6076 coverage_path: &std::path::Path,
6077 source_path: &std::path::Path,
6078 fn_map: &serde_json::Value,
6079 function_hits: &serde_json::Value,
6080 statement_map: &serde_json::Value,
6081 statement_hits: &serde_json::Value,
6082 ) {
6083 let mut root = serde_json::Map::new();
6084 root.insert(
6085 source_path.to_string_lossy().into_owned(),
6086 serde_json::json!({
6087 "path": source_path.to_string_lossy().into_owned(),
6088 "statementMap": statement_map,
6089 "fnMap": fn_map,
6090 "branchMap": {},
6091 "s": statement_hits,
6092 "f": function_hits,
6093 "b": {}
6094 }),
6095 );
6096
6097 std::fs::write(coverage_path, serde_json::to_string(&root).unwrap()).unwrap();
6098 }
6099
6100 #[test]
6106 fn a_negative_branch_coordinate_does_not_cost_the_whole_map() {
6107 let temp = tempfile::TempDir::new().unwrap();
6108 let source_path = temp.path().join("src/pick.ts");
6109 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6110 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
6111
6112 let coverage_path = temp.path().join("coverage-final.json");
6113 let source = source_path.to_string_lossy().into_owned();
6114 std::fs::write(
6115 &coverage_path,
6116 serde_json::to_string(&serde_json::json!({
6117 source.clone(): {
6118 "path": source,
6119 "statementMap": {},
6120 "fnMap": {
6121 "0": {
6122 "name": "pick",
6123 "line": 1,
6124 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 1, "column": 20 } },
6125 "loc": { "start": { "line": 1, "column": 41 }, "end": { "line": 6, "column": 1 } }
6126 }
6127 },
6128 "branchMap": {
6129 "0": {
6130 "type": "branch",
6131 "line": 5,
6132 "loc": {
6133 "start": { "line": 5, "column": -1 },
6134 "end": { "line": 6, "column": 0 }
6135 },
6136 "locations": [
6137 {
6138 "start": { "line": 5, "column": -1 },
6139 "end": { "line": 6, "column": 0 }
6140 }
6141 ]
6142 }
6143 },
6144 "s": {},
6145 "f": { "0": 2 },
6146 "b": { "0": [1, 0] }
6147 }
6148 }))
6149 .unwrap(),
6150 )
6151 .unwrap();
6152
6153 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6154 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6155 let file_coverage = coverage.get(&canonical_source).unwrap();
6156
6157 assert_eq!(file_coverage.lookup("pick", 1, 16), Some(100.0));
6158 }
6159
6160 #[test]
6164 fn an_accessor_answers_to_its_property_name() {
6165 let temp = tempfile::TempDir::new().unwrap();
6166 let source_path = temp.path().join("src/box.ts");
6167 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6168 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
6169
6170 let coverage_path = temp.path().join("coverage-final.json");
6171 write_single_file_istanbul_fixture(
6172 &coverage_path,
6173 &source_path,
6174 &serde_json::json!({
6175 "0": {
6176 "name": "get area",
6177 "line": 4,
6178 "decl": { "start": { "line": 4, "column": 6 }, "end": { "line": 4, "column": 10 } },
6179 "loc": { "start": { "line": 4, "column": 13 }, "end": { "line": 6, "column": 3 } }
6180 }
6181 }),
6182 &serde_json::json!({ "0": 0 }),
6183 );
6184
6185 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6186 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6187 let file_coverage = coverage.get(&canonical_source).unwrap();
6188
6189 assert_eq!(file_coverage.lookup("area", 4, 6), Some(0.0));
6191 assert_eq!(file_coverage.lookup("get area", 4, 6), Some(0.0));
6193 }
6194
6195 #[test]
6196 fn resolve_relative_to_root_joins_relative_with_project_root() {
6197 let resolved = resolve_relative_to_root(
6198 std::path::Path::new("coverage/coverage-final.json"),
6199 Some(std::path::Path::new("/work/my-app")),
6200 );
6201 assert_eq!(
6202 resolved,
6203 std::path::PathBuf::from("/work/my-app/coverage/coverage-final.json")
6204 );
6205 }
6206
6207 #[test]
6208 fn resolve_relative_to_root_returns_absolute_unchanged() {
6209 let resolved = resolve_relative_to_root(
6210 std::path::Path::new("/tmp/coverage-final.json"),
6211 Some(std::path::Path::new("/work/my-app")),
6212 );
6213 assert_eq!(
6214 resolved,
6215 std::path::PathBuf::from("/tmp/coverage-final.json")
6216 );
6217 }
6218
6219 #[test]
6220 fn resolve_relative_to_root_returns_windows_absolute_unchanged_on_any_host() {
6221 let path = std::path::Path::new(r"C:\coverage\coverage-final.json");
6222 let resolved = resolve_relative_to_root(path, Some(std::path::Path::new("/work/my-app")));
6223 assert_eq!(resolved, path);
6224 }
6225
6226 #[cfg(windows)]
6227 #[test]
6228 fn resolve_relative_to_root_returns_posix_rooted_path_unchanged_on_windows() {
6229 let path = std::path::Path::new(r"/ci/workspace/coverage-final.json");
6230 let resolved =
6231 resolve_relative_to_root(path, Some(std::path::Path::new(r"C:\work\my-app")));
6232 assert_eq!(resolved, path);
6233 }
6234
6235 #[test]
6236 fn resolve_relative_to_root_without_project_root_returns_relative_unchanged() {
6237 let resolved =
6238 resolve_relative_to_root(std::path::Path::new("coverage/coverage-final.json"), None);
6239 assert_eq!(
6240 resolved,
6241 std::path::PathBuf::from("coverage/coverage-final.json")
6242 );
6243 }
6244
6245 #[test]
6249 fn load_istanbul_coverage_resolves_relative_map_keys_against_project_root() {
6250 let temp = tempfile::TempDir::new().unwrap();
6251 let source_path = temp.path().join("src/index.ts");
6252 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6253 std::fs::write(&source_path, "export function f(){}").unwrap();
6254
6255 let coverage_path = temp.path().join("coverage-final.json");
6256 std::fs::write(
6257 &coverage_path,
6258 serde_json::to_string(&serde_json::json!({
6259 "src/index.ts": {
6260 "path": "src/index.ts",
6261 "statementMap": {},
6262 "fnMap": {
6263 "0": {
6264 "name": "f",
6265 "decl": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } },
6266 "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } }
6267 }
6268 },
6269 "branchMap": {},
6270 "s": {},
6271 "f": { "0": 2 },
6272 "b": {}
6273 }
6274 }))
6275 .unwrap(),
6276 )
6277 .unwrap();
6278
6279 let coverage =
6280 load_istanbul_coverage(&coverage_path, None, Some(temp.path()), false).unwrap();
6281 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6282 let file_coverage = coverage.get(&canonical_source).unwrap();
6283
6284 assert_eq!(file_coverage.lookup("f", 1, 0), Some(100.0));
6285 }
6286
6287 #[test]
6288 fn load_istanbul_coverage_resolves_relative_path_against_project_root() {
6289 let temp = tempfile::TempDir::new().unwrap();
6290 let source_path = temp.path().join("src/index.ts");
6291 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6292 std::fs::write(&source_path, "export function f(){}").unwrap();
6293
6294 let coverage_path = temp.path().join("coverage/coverage-final.json");
6295 std::fs::create_dir_all(coverage_path.parent().unwrap()).unwrap();
6296 write_single_file_istanbul_fixture(
6297 &coverage_path,
6298 &source_path,
6299 &serde_json::json!({
6300 "0": {
6301 "name": "f",
6302 "decl": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } },
6303 "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 1, "column": 21 } }
6304 }
6305 }),
6306 &serde_json::json!({ "0": 1 }),
6307 );
6308
6309 let coverage = load_istanbul_coverage(
6310 std::path::Path::new("coverage/coverage-final.json"),
6311 None,
6312 Some(temp.path()),
6313 false,
6314 )
6315 .expect("relative path must resolve against project_root");
6316 assert!(
6317 !coverage.files.is_empty(),
6318 "expected coverage to load via project_root resolution, got {} files",
6319 coverage.files.len()
6320 );
6321 }
6322
6323 #[test]
6324 fn load_istanbul_coverage_falls_back_to_decl_line_for_missing_fn_line() {
6325 let temp = tempfile::TempDir::new().unwrap();
6326 let source_path = temp.path().join("src/service.ts");
6327 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6328 std::fs::write(&source_path, "export class DataService {}\n").unwrap();
6329
6330 let coverage_path = temp.path().join("coverage-final.json");
6331 write_single_file_istanbul_fixture(
6332 &coverage_path,
6333 &source_path,
6334 &serde_json::json!({
6335 "0": {
6336 "name": "(anonymous_0)",
6337 "decl": {
6338 "start": { "line": 5, "column": 2 },
6339 "end": { "line": 5, "column": 13 }
6340 },
6341 "loc": {
6342 "start": { "line": 5, "column": 14 },
6343 "end": { "line": 11, "column": 3 }
6344 }
6345 },
6346 "1": {
6347 "name": "(anonymous_1)",
6348 "decl": {
6349 "start": { "line": 20, "column": 14 },
6350 "end": { "line": 20, "column": 25 }
6351 },
6352 "loc": {
6353 "start": { "line": 20, "column": 28 },
6354 "end": { "line": 22, "column": 2 }
6355 }
6356 }
6357 }),
6358 &serde_json::json!({
6359 "0": 1,
6360 "1": 0
6361 }),
6362 );
6363
6364 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6365 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6366 let file_coverage = coverage.get(&canonical_source).unwrap();
6367
6368 assert_eq!(file_coverage.lookup("processData", 5, 0), Some(100.0));
6369 assert_eq!(file_coverage.lookup("handleSpecial", 20, 0), Some(0.0));
6370 }
6371
6372 #[test]
6373 fn load_istanbul_coverage_indexes_explicit_and_decl_lines() {
6374 let temp = tempfile::TempDir::new().unwrap();
6375 let source_path = temp.path().join("src/handler.ts");
6376 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6377 std::fs::write(&source_path, "export const handleClick = () => {}\n").unwrap();
6378
6379 let coverage_path = temp.path().join("coverage-final.json");
6380 write_single_file_istanbul_fixture(
6381 &coverage_path,
6382 &source_path,
6383 &serde_json::json!({
6384 "0": {
6385 "name": "handleClick",
6386 "line": 40,
6387 "decl": {
6388 "start": { "line": 22, "column": 13 },
6389 "end": { "line": 22, "column": 24 }
6390 },
6391 "loc": {
6392 "start": { "line": 40, "column": 27 },
6393 "end": { "line": 42, "column": 1 }
6394 }
6395 }
6396 }),
6397 &serde_json::json!({
6398 "0": 1
6399 }),
6400 );
6401
6402 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6403 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6404 let file_coverage = coverage.get(&canonical_source).unwrap();
6405
6406 assert_eq!(file_coverage.lookup("handleClick", 40, 0), Some(100.0));
6407 assert_eq!(file_coverage.lookup("handleClick", 22, 13), Some(100.0));
6408 }
6409
6410 #[test]
6411 fn load_istanbul_coverage_indexes_valid_body_start_alias() {
6412 let temp = tempfile::TempDir::new().unwrap();
6413 let source_path = temp.path().join("src/handler.ts");
6414 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6415 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
6416
6417 let coverage_path = temp.path().join("coverage-final.json");
6418 write_single_file_istanbul_fixture(
6419 &coverage_path,
6420 &source_path,
6421 &serde_json::json!({
6422 "0": {
6423 "name": "(anonymous_0)",
6424 "line": 8,
6425 "decl": {
6426 "start": { "line": 8, "column": 14 },
6427 "end": { "line": 8, "column": 25 }
6428 },
6429 "loc": {
6430 "start": { "line": 20, "column": 6 },
6431 "end": { "line": 22, "column": 1 }
6432 }
6433 }
6434 }),
6435 &serde_json::json!({ "0": 1 }),
6436 );
6437
6438 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6439 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6440 let file_coverage = coverage.get(&canonical_source).unwrap();
6441
6442 assert_eq!(file_coverage.lookup("handler", 20, 6), Some(100.0));
6443
6444 let mut function = make_fn_complexity(4);
6445 function.name = "handler".to_string();
6446 function.line = 20;
6447 function.col = 6;
6448 let result = istanbul_crap_default(&[function], Some(file_coverage), false);
6449 assert_eq!(result.matched, 1);
6450 assert_eq!(result.total, 1);
6451 assert_eq!(
6452 result.per_function[0].coverage_source,
6453 fallow_output::CoverageSource::Istanbul
6454 );
6455 assert_eq!(result.per_function[0].coverage_pct, Some(100.0));
6456 }
6457
6458 #[test]
6465 fn curried_arrows_one_per_line_each_take_their_own_record() {
6466 let temp = tempfile::TempDir::new().unwrap();
6467 let source_path = temp.path().join("src/adjust.ts");
6468 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6469 std::fs::write(
6470 &source_path,
6471 "export const adjust = (base: number) =>\n (factor: number) =>\n (offset: number) =>\n base * factor + offset;\n",
6472 )
6473 .unwrap();
6474
6475 let coverage_path = temp.path().join("coverage-final.json");
6476 write_single_file_istanbul_fixture(
6477 &coverage_path,
6478 &source_path,
6479 &serde_json::json!({
6480 "0": {
6481 "name": "(anonymous_0)",
6482 "line": 2,
6483 "decl": { "start": { "line": 1, "column": 22 }, "end": { "line": 1, "column": 23 } },
6484 "loc": { "start": { "line": 2, "column": 2 }, "end": { "line": 4, "column": 26 } }
6485 },
6486 "1": {
6487 "name": "(anonymous_1)",
6488 "line": 3,
6489 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6490 "loc": { "start": { "line": 3, "column": 2 }, "end": { "line": 4, "column": 26 } }
6491 },
6492 "2": {
6493 "name": "(anonymous_2)",
6494 "line": 4,
6495 "decl": { "start": { "line": 3, "column": 2 }, "end": { "line": 3, "column": 3 } },
6496 "loc": { "start": { "line": 4, "column": 4 }, "end": { "line": 4, "column": 26 } }
6497 }
6498 }),
6499 &serde_json::json!({ "0": 2, "1": 1, "2": 0 }),
6500 );
6501
6502 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6503 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6504 let file_coverage = coverage.get(&canonical_source).unwrap();
6505
6506 assert_eq!(file_coverage.lookup("adjust", 1, 22), Some(100.0));
6507 assert_eq!(file_coverage.lookup("<arrow>", 2, 2), Some(100.0));
6508 assert_eq!(file_coverage.lookup("<arrow>", 3, 2), Some(0.0));
6510 }
6511
6512 #[test]
6519 fn a_default_value_keeps_its_own_record_inside_the_enclosing_signature() {
6520 let temp = tempfile::TempDir::new().unwrap();
6521 let source_path = temp.path().join("src/filter.ts");
6522 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6523 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
6524
6525 let coverage_path = temp.path().join("coverage-final.json");
6526 write_single_file_istanbul_fixture(
6527 &coverage_path,
6528 &source_path,
6529 &serde_json::json!({
6530 "0": {
6531 "name": "(anonymous_0)",
6532 "line": 173,
6533 "decl": { "start": { "line": 169, "column": 2 }, "end": { "line": 170, "column": 3 } },
6534 "loc": { "start": { "line": 173, "column": 4 }, "end": { "line": 182, "column": 3 } }
6535 },
6536 "1": {
6537 "name": "(anonymous_1)",
6538 "line": 171,
6539 "decl": { "start": { "line": 171, "column": 21 }, "end": { "line": 171, "column": 67 } },
6540 "loc": { "start": { "line": 171, "column": 67 }, "end": { "line": 171, "column": 76 } }
6541 }
6542 }),
6543 &serde_json::json!({ "0": 46, "1": 0 }),
6544 );
6545
6546 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6547 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6548 let file_coverage = coverage.get(&canonical_source).unwrap();
6549
6550 assert_eq!(file_coverage.lookup("<arrow>", 171, 61), Some(0.0));
6553 }
6554
6555 #[test]
6562 fn header_span_abstains_when_another_function_is_declared_inside_it() {
6563 let temp = tempfile::TempDir::new().unwrap();
6564 let source_path = temp.path().join("src/chart.ts");
6565 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6566 std::fs::write(
6567 &source_path,
6568 "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",
6569 )
6570 .unwrap();
6571
6572 let coverage_path = temp.path().join("coverage-final.json");
6573 write_single_file_istanbul_fixture(
6574 &coverage_path,
6575 &source_path,
6576 &serde_json::json!({
6577 "0": {
6578 "name": "(anonymous_0)",
6579 "line": 10,
6580 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6581 "loc": { "start": { "line": 10, "column": 4 }, "end": { "line": 12, "column": 3 } }
6582 },
6583 "1": {
6584 "name": "scale",
6585 "line": 7,
6586 "decl": { "start": { "line": 5, "column": 23 }, "end": { "line": 5, "column": 28 } },
6587 "loc": { "start": { "line": 7, "column": 6 }, "end": { "line": 9, "column": 5 } }
6588 }
6589 }),
6590 &serde_json::json!({ "0": 3, "1": 0 }),
6591 );
6592
6593 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6594 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6595 let coverage = load_istanbul_coverage_for_sources(
6596 &coverage_path,
6597 None,
6598 Some(temp.path()),
6599 Some(&discovered_sources),
6600 false,
6601 )
6602 .unwrap();
6603 let file_coverage = coverage.get(&canonical_source).unwrap();
6604
6605 assert_eq!(file_coverage.lookup("<anonymous>", 6, 6), None);
6608 assert_eq!(file_coverage.lookup("scale", 5, 14), Some(0.0));
6610 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
6611 }
6612
6613 #[test]
6617 fn same_named_function_in_signature_does_not_supply_member_coverage() {
6618 let temp = tempfile::TempDir::new().unwrap();
6619 let source_path = temp.path().join("src/chart.ts");
6620 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6621 std::fs::write(
6622 &source_path,
6623 "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",
6624 )
6625 .unwrap();
6626
6627 let coverage_path = temp.path().join("coverage-final.json");
6628 write_single_file_istanbul_fixture(
6629 &coverage_path,
6630 &source_path,
6631 &serde_json::json!({
6632 "0": {
6633 "name": "(anonymous_0)",
6634 "line": 10,
6635 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6636 "loc": { "start": { "line": 10, "column": 4 }, "end": { "line": 12, "column": 3 } }
6637 },
6638 "1": {
6639 "name": "render",
6640 "line": 7,
6641 "decl": { "start": { "line": 5, "column": 23 }, "end": { "line": 5, "column": 29 } },
6642 "loc": { "start": { "line": 7, "column": 6 }, "end": { "line": 9, "column": 5 } }
6643 }
6644 }),
6645 &serde_json::json!({ "0": 3, "1": 0 }),
6646 );
6647
6648 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
6649 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6650 let file_coverage = coverage.get(&canonical_source).unwrap();
6651
6652 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
6653 assert_eq!(file_coverage.lookup("render", 5, 23), Some(0.0));
6654 }
6655
6656 #[test]
6660 fn same_line_same_named_function_in_signature_does_not_supply_member_coverage() {
6661 let temp = tempfile::TempDir::new().unwrap();
6662 let source_path = temp.path().join("src/chart.ts");
6663 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6664 std::fs::write(
6665 &source_path,
6666 "export class Chart {\n @Watch(\"data\") render(x = function render() {}) {}\n}\n",
6667 )
6668 .unwrap();
6669
6670 let coverage_path = temp.path().join("coverage-final.json");
6671 write_single_file_istanbul_fixture(
6672 &coverage_path,
6673 &source_path,
6674 &serde_json::json!({
6675 "0": {
6676 "name": "(anonymous_0)",
6677 "line": 2,
6678 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6679 "loc": { "start": { "line": 2, "column": 52 }, "end": { "line": 2, "column": 54 } }
6680 },
6681 "1": {
6682 "name": "render",
6683 "line": 2,
6684 "decl": { "start": { "line": 2, "column": 39 }, "end": { "line": 2, "column": 45 } },
6685 "loc": { "start": { "line": 2, "column": 48 }, "end": { "line": 2, "column": 50 } }
6686 }
6687 }),
6688 &serde_json::json!({ "0": 3, "1": 0 }),
6689 );
6690
6691 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6692 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6693 let coverage = load_istanbul_coverage_for_sources(
6694 &coverage_path,
6695 None,
6696 Some(temp.path()),
6697 Some(&discovered_sources),
6698 false,
6699 )
6700 .unwrap();
6701 let file_coverage = coverage.get(&canonical_source).unwrap();
6702
6703 assert_eq!(file_coverage.lookup("render", 2, 23), Some(100.0));
6704 assert_eq!(file_coverage.lookup("render", 2, 29), Some(0.0));
6705 }
6706
6707 #[test]
6708 fn named_generator_alias_handles_trivia_and_utf16_columns() {
6709 let temp = tempfile::TempDir::new().unwrap();
6710 let source_path = temp.path().join("src/chart.ts");
6711 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6712 let source_line =
6713 " render(label = \"pi: π, mushroom: 🍄\", x = function/* gap */*render() {}) {}";
6714 std::fs::write(
6715 &source_path,
6716 format!("export class Chart {{\n{source_line}\n}}\n"),
6717 )
6718 .unwrap();
6719
6720 let utf16_column = |byte_column: usize| {
6721 u32::try_from(source_line[..byte_column].encode_utf16().count()).unwrap()
6722 };
6723 let outer_target_column = source_line.find("render(").unwrap() + "render".len();
6724 let syntax_column = source_line.find("function").unwrap();
6725 let name_column = source_line.find("*render").unwrap() + 1;
6726 let inner_body_column = source_line.find("{}").unwrap();
6727 let outer_body_column = source_line.rfind("{}").unwrap();
6728
6729 let coverage_path = temp.path().join("coverage-final.json");
6730 write_single_file_istanbul_fixture(
6731 &coverage_path,
6732 &source_path,
6733 &serde_json::json!({
6734 "0": {
6735 "name": "(anonymous_0)",
6736 "line": 2,
6737 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
6738 "loc": {
6739 "start": { "line": 2, "column": utf16_column(outer_body_column) },
6740 "end": { "line": 2, "column": utf16_column(outer_body_column + 2) }
6741 }
6742 },
6743 "1": {
6744 "name": "render",
6745 "line": 2,
6746 "decl": {
6747 "start": { "line": 2, "column": utf16_column(name_column) },
6748 "end": { "line": 2, "column": utf16_column(name_column + "render".len()) }
6749 },
6750 "loc": {
6751 "start": { "line": 2, "column": utf16_column(inner_body_column) },
6752 "end": { "line": 2, "column": utf16_column(inner_body_column + 2) }
6753 }
6754 }
6755 }),
6756 &serde_json::json!({ "0": 3, "1": 0 }),
6757 );
6758
6759 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6760 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6761 let coverage = load_istanbul_coverage_for_sources(
6762 &coverage_path,
6763 None,
6764 Some(temp.path()),
6765 Some(&discovered_sources),
6766 false,
6767 )
6768 .unwrap();
6769 let file_coverage = coverage.get(&canonical_source).unwrap();
6770
6771 assert_eq!(
6772 file_coverage.lookup("render", 2, u32::try_from(outer_target_column).unwrap()),
6773 Some(100.0)
6774 );
6775 assert_eq!(
6776 file_coverage.lookup("render", 2, u32::try_from(syntax_column).unwrap()),
6777 Some(0.0)
6778 );
6779 }
6780
6781 #[test]
6782 fn utf16_index_is_sparse_and_rejects_surrogate_boundaries() {
6783 let ascii_prefix = "a".repeat(4_096);
6784 let source = format!("{ascii_prefix}🍄{}", "b".repeat(4_096));
6785 let index = IstanbulSourceIndex::new(&source, std::path::Path::new("minified.js"));
6786 let line_index = &index.non_ascii_lines[&0];
6787
6788 assert_eq!(line_index.checkpoints.len(), 1);
6789 assert_eq!(
6790 index.byte_position(1, 4_096),
6791 Some(IstanbulPosition::new(1, 4_096))
6792 );
6793 assert_eq!(index.byte_position(1, 4_097), None);
6794 assert_eq!(
6795 index.byte_position(1, 4_098),
6796 Some(IstanbulPosition::new(1, 4_100))
6797 );
6798 assert_eq!(
6799 index.byte_position(1, 8_194),
6800 Some(IstanbulPosition::new(1, 8_196))
6801 );
6802 }
6803
6804 #[test]
6805 fn effective_alias_normalizes_against_its_own_unicode_line() {
6806 let temp = tempfile::TempDir::new().unwrap();
6807 let source_path = temp.path().join("src/render.ts");
6808 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6809 std::fs::write(
6810 &source_path,
6811 "/*🍄*/ const placeholder = 0;\n/*π*/ function render() {}\n",
6812 )
6813 .unwrap();
6814
6815 let coverage_path = temp.path().join("coverage-final.json");
6816 write_single_file_istanbul_fixture(
6817 &coverage_path,
6818 &source_path,
6819 &serde_json::json!({
6820 "0": {
6821 "name": "render",
6822 "line": 1,
6823 "decl": { "start": { "line": 2, "column": 15 }, "end": { "line": 2, "column": 21 } },
6824 "loc": { "start": { "line": 2, "column": 24 }, "end": { "line": 2, "column": 26 } }
6825 }
6826 }),
6827 &serde_json::json!({ "0": 1 }),
6828 );
6829
6830 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6831 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6832 let coverage = load_istanbul_coverage_for_sources(
6833 &coverage_path,
6834 None,
6835 Some(temp.path()),
6836 Some(&discovered_sources),
6837 false,
6838 )
6839 .unwrap();
6840 let function = &coverage.get(&canonical_source).unwrap().functions[0];
6841
6842 assert!(
6843 function
6844 .aliases
6845 .iter()
6846 .any(|alias| { alias.position == IstanbulPosition::new(1, 17) && alias.primary })
6847 );
6848 assert!(
6849 !function
6850 .aliases
6851 .iter()
6852 .any(|alias| alias.position == IstanbulPosition::new(1, 16))
6853 );
6854 }
6855
6856 #[test]
6857 fn stale_utf16_coordinates_are_rejected_with_trusted_source() {
6858 let temp = tempfile::TempDir::new().unwrap();
6859 let source_path = temp.path().join("src/render.ts");
6860 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6861 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6862
6863 let coverage_path = temp.path().join("coverage-final.json");
6864 write_single_file_istanbul_fixture(
6865 &coverage_path,
6866 &source_path,
6867 &serde_json::json!({
6868 "0": {
6869 "name": "render",
6870 "line": 1,
6871 "decl": { "start": { "line": 1, "column": 999 }, "end": { "line": 1, "column": 22 } },
6872 "loc": { "start": { "line": 1, "column": 25 }, "end": { "line": 1, "column": 27 } }
6873 }
6874 }),
6875 &serde_json::json!({ "0": 1 }),
6876 );
6877
6878 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6879 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6880 let coverage = load_istanbul_coverage_for_sources(
6881 &coverage_path,
6882 None,
6883 Some(temp.path()),
6884 Some(&discovered_sources),
6885 false,
6886 )
6887 .unwrap();
6888
6889 assert_eq!(
6890 coverage
6891 .get(&canonical_source)
6892 .unwrap()
6893 .lookup("render", 1, 7),
6894 None
6895 );
6896 }
6897
6898 #[test]
6899 fn invalid_optional_coordinates_preserve_valid_declaration_attribution() {
6900 let temp = tempfile::TempDir::new().unwrap();
6901 let source_path = temp.path().join("src/render.ts");
6902 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6903 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6904
6905 let coverage_path = temp.path().join("coverage-final.json");
6906 write_single_file_istanbul_fixture(
6907 &coverage_path,
6908 &source_path,
6909 &serde_json::json!({
6910 "0": {
6911 "name": "render",
6912 "line": 99,
6913 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 99, "column": 999 } },
6914 "loc": { "start": { "line": 1, "column": 999 }, "end": { "line": 99, "column": 999 } }
6915 }
6916 }),
6917 &serde_json::json!({ "0": 1 }),
6918 );
6919
6920 let canonical_source = dunce::canonicalize(&source_path).unwrap();
6921 let discovered_sources = rustc_hash::FxHashSet::from_iter([canonical_source.clone()]);
6922 let coverage = load_istanbul_coverage_for_sources(
6923 &coverage_path,
6924 None,
6925 Some(temp.path()),
6926 Some(&discovered_sources),
6927 false,
6928 )
6929 .unwrap();
6930 let file_coverage = coverage.get(&canonical_source).unwrap();
6931 let function = &file_coverage.functions[0];
6932
6933 assert_eq!(file_coverage.lookup("render", 1, 16), Some(100.0));
6934 assert!(function.body_span.is_none());
6935 assert!(function.header_span.is_none());
6936 assert!(
6937 !function
6938 .aliases
6939 .iter()
6940 .any(|alias| { alias.position.line == 99 || alias.position.col == 999 })
6941 );
6942 }
6943
6944 #[test]
6945 fn malformed_source_does_not_supply_named_function_provenance() {
6946 assert!(
6947 !IstanbulSourceIndex::new(
6948 "export function render() {}",
6949 std::path::Path::new("valid.ts"),
6950 )
6951 .named_function_starts
6952 .is_empty()
6953 );
6954 let index = IstanbulSourceIndex::new(
6955 "export function render() {} const broken = ;",
6956 std::path::Path::new("broken.ts"),
6957 );
6958
6959 assert!(index.named_function_starts.is_empty());
6960 }
6961
6962 #[test]
6963 fn javascript_with_jsx_uses_clean_jsx_provenance_parse() {
6964 let index = IstanbulSourceIndex::new(
6965 "export function render() { return <div />; }",
6966 std::path::Path::new("component.js"),
6967 );
6968
6969 assert!(!index.named_function_starts.is_empty());
6970 }
6971
6972 #[test]
6973 fn undiscovered_coverage_path_is_not_loaded_for_source_provenance() {
6974 let temp = tempfile::TempDir::new().unwrap();
6975 let source_path = temp.path().join("src/excluded.ts");
6976 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
6977 std::fs::write(&source_path, "export function render() {}\n").unwrap();
6978
6979 let coverage_path = temp.path().join("coverage-final.json");
6980 write_single_file_istanbul_fixture(
6981 &coverage_path,
6982 &source_path,
6983 &serde_json::json!({
6984 "0": {
6985 "name": "render",
6986 "line": 1,
6987 "decl": { "start": { "line": 1, "column": 16 }, "end": { "line": 1, "column": 22 } },
6988 "loc": { "start": { "line": 1, "column": 25 }, "end": { "line": 1, "column": 27 } }
6989 }
6990 }),
6991 &serde_json::json!({ "0": 1 }),
6992 );
6993
6994 let coverage = load_istanbul_coverage_for_sources(
6995 &coverage_path,
6996 None,
6997 Some(temp.path()),
6998 Some(&rustc_hash::FxHashSet::default()),
6999 false,
7000 )
7001 .unwrap();
7002 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7003 let function = &coverage.get(&canonical_source).unwrap().functions[0];
7004
7005 assert!(
7006 function
7007 .aliases
7008 .iter()
7009 .all(|alias| alias.position != IstanbulPosition::new(1, 7))
7010 );
7011 }
7012
7013 #[test]
7016 fn private_class_member_never_takes_enclosing_coverage() {
7017 let temp = tempfile::TempDir::new().unwrap();
7018 let source_path = temp.path().join("src/vault.js");
7019 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7020 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7021
7022 let coverage_path = temp.path().join("coverage-final.json");
7023 write_single_file_istanbul_fixture(
7024 &coverage_path,
7025 &source_path,
7026 &serde_json::json!({
7027 "0": {
7028 "name": "(anonymous_0)",
7029 "line": 7,
7030 "decl": { "start": { "line": 1, "column": 24 }, "end": { "line": 1, "column": 25 } },
7031 "loc": { "start": { "line": 7, "column": 5 }, "end": { "line": 7, "column": 20 } }
7032 }
7033 }),
7034 &serde_json::json!({ "0": 1 }),
7035 );
7036
7037 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7038 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7039 let file_coverage = coverage.get(&canonical_source).unwrap();
7040
7041 let mut private_member = make_fn_complexity(1);
7044 private_member.name = "#wipe".to_string();
7045 private_member.is_private_member = true;
7046 private_member.line = 3;
7047 private_member.col = 9;
7048 let result = istanbul_crap_default(&[private_member], Some(file_coverage), false);
7049 assert_eq!(result.matched, 0);
7050 assert_eq!(
7051 result.per_function[0].coverage_source,
7052 fallow_output::CoverageSource::Estimated
7053 );
7054 assert_eq!(file_coverage.lookup("<arrow>", 1, 24), Some(100.0));
7055 }
7056
7057 #[test]
7058 fn quoted_hash_method_keeps_exact_istanbul_coverage() {
7059 let temp = tempfile::TempDir::new().unwrap();
7060 let source_path = temp.path().join("src/vault.js");
7061 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7062 std::fs::write(&source_path, "class Vault { '#wipe'() {} }\n").unwrap();
7063
7064 let coverage_path = temp.path().join("coverage-final.json");
7065 write_single_file_istanbul_fixture(
7066 &coverage_path,
7067 &source_path,
7068 &serde_json::json!({
7069 "0": {
7070 "name": "#wipe",
7071 "line": 1,
7072 "decl": { "start": { "line": 1, "column": 14 }, "end": { "line": 1, "column": 21 } },
7073 "loc": { "start": { "line": 1, "column": 24 }, "end": { "line": 1, "column": 26 } }
7074 }
7075 }),
7076 &serde_json::json!({ "0": 1 }),
7077 );
7078
7079 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7080 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7081 let file_coverage = coverage.get(&canonical_source).unwrap();
7082
7083 let mut quoted_public_method = make_fn_complexity(1);
7084 quoted_public_method.name = "#wipe".to_string();
7085 quoted_public_method.line = 1;
7086 quoted_public_method.col = 14;
7087 let result = istanbul_crap_default(&[quoted_public_method], Some(file_coverage), false);
7088 assert_eq!(result.matched, 1);
7089 assert_eq!(
7090 result.per_function[0].coverage_source,
7091 fallow_output::CoverageSource::Istanbul
7092 );
7093 assert_eq!(result.per_function[0].coverage_pct, Some(100.0));
7094 }
7095
7096 #[test]
7105 fn decorated_member_matches_its_istanbul_header_span() {
7106 let temp = tempfile::TempDir::new().unwrap();
7107 let source_path = temp.path().join("src/users.controller.ts");
7108 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7109 std::fs::write(
7110 &source_path,
7111 "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",
7112 )
7113 .unwrap();
7114
7115 let coverage_path = temp.path().join("coverage-final.json");
7116 write_single_file_istanbul_fixture(
7117 &coverage_path,
7118 &source_path,
7119 &serde_json::json!({
7120 "0": {
7121 "name": "(anonymous_0)",
7122 "line": 6,
7123 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
7124 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
7125 }
7126 }),
7127 &serde_json::json!({ "0": 3 }),
7128 );
7129
7130 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7131 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7132 let file_coverage = coverage.get(&canonical_source).unwrap();
7133
7134 assert_eq!(
7136 file_coverage.lookup("findOneWithProfile", 3, 26),
7137 Some(100.0)
7138 );
7139 }
7140
7141 #[test]
7147 fn established_alias_wins_over_the_signature_that_contains_it() {
7148 let temp = tempfile::TempDir::new().unwrap();
7149 let source_path = temp.path().join("src/chart.ts");
7150 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7151 std::fs::write(
7152 &source_path,
7153 "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",
7154 )
7155 .unwrap();
7156
7157 let coverage_path = temp.path().join("coverage-final.json");
7158 write_single_file_istanbul_fixture(
7159 &coverage_path,
7160 &source_path,
7161 &serde_json::json!({
7162 "0": {
7163 "name": "(anonymous_0)",
7164 "line": 6,
7165 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
7166 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
7167 },
7168 "1": {
7169 "name": "(anonymous_1)",
7170 "line": 5,
7171 "decl": { "start": { "line": 5, "column": 14 }, "end": { "line": 5, "column": 15 } },
7172 "loc": { "start": { "line": 5, "column": 31 }, "end": { "line": 5, "column": 38 } }
7173 }
7174 }),
7175 &serde_json::json!({ "0": 3, "1": 0 }),
7176 );
7177
7178 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7179 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7180 let file_coverage = coverage.get(&canonical_source).unwrap();
7181
7182 assert_eq!(file_coverage.lookup("<arrow>", 5, 14), Some(0.0));
7184 assert_eq!(file_coverage.lookup("render", 3, 8), Some(100.0));
7186 }
7187
7188 #[test]
7196 fn signature_holding_a_function_abstains_instead_of_crediting_it() {
7197 let temp = tempfile::TempDir::new().unwrap();
7198 let source_path = temp.path().join("src/users.controller.ts");
7199 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7200 std::fs::write(
7201 &source_path,
7202 "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",
7203 )
7204 .unwrap();
7205
7206 let coverage_path = temp.path().join("coverage-final.json");
7207 write_single_file_istanbul_fixture(
7208 &coverage_path,
7209 &source_path,
7210 &serde_json::json!({
7211 "0": {
7212 "name": "(anonymous_0)",
7213 "line": 6,
7214 "decl": { "start": { "line": 2, "column": 2 }, "end": { "line": 2, "column": 3 } },
7215 "loc": { "start": { "line": 6, "column": 4 }, "end": { "line": 8, "column": 3 } }
7216 },
7217 "1": {
7218 "name": "(anonymous_1)",
7219 "line": 5,
7220 "decl": { "start": { "line": 5, "column": 16 }, "end": { "line": 5, "column": 17 } },
7221 "loc": { "start": { "line": 5, "column": 33 }, "end": { "line": 5, "column": 43 } }
7222 }
7223 }),
7224 &serde_json::json!({ "0": 3, "1": 0 }),
7225 );
7226
7227 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7228 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7229 let file_coverage = coverage.get(&canonical_source).unwrap();
7230
7231 assert_eq!(file_coverage.lookup("findOneWithProfile", 3, 26), None);
7234 assert_eq!(file_coverage.lookup("<arrow>", 5, 16), Some(0.0));
7236 }
7237
7238 #[test]
7239 fn anonymous_record_aliases_do_not_tie_with_their_own_identity() {
7240 let temp = tempfile::TempDir::new().unwrap();
7241 let source_path = temp.path().join("src/aliases.ts");
7242 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7243 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
7244
7245 let coverage_path = temp.path().join("coverage-final.json");
7246 write_single_file_istanbul_fixture(
7247 &coverage_path,
7248 &source_path,
7249 &serde_json::json!({
7250 "0": {
7251 "name": "(anonymous_0)",
7252 "line": 10,
7253 "decl": {
7254 "start": { "line": 10, "column": 8 },
7255 "end": { "line": 10, "column": 9 }
7256 },
7257 "loc": {
7258 "start": { "line": 12, "column": 8 },
7259 "end": { "line": 13, "column": 1 }
7260 }
7261 }
7262 }),
7263 &serde_json::json!({ "0": 1 }),
7264 );
7265
7266 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7267 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7268 let file_coverage = coverage.get(&canonical_source).unwrap();
7269
7270 assert_eq!(file_coverage.lookup("handler", 11, 8), Some(100.0));
7271 }
7272
7273 #[test]
7278 fn curried_arrow_one_liner_resolves_both_arrows() {
7279 let temp = tempfile::TempDir::new().unwrap();
7280 let source_path = temp.path().join("src/nested.ts");
7281 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7282 std::fs::write(&source_path, "export const nested = () => () => true;\n").unwrap();
7283
7284 let coverage_path = temp.path().join("coverage-final.json");
7285 write_single_file_istanbul_fixture(
7286 &coverage_path,
7287 &source_path,
7288 &serde_json::json!({
7289 "0": {
7290 "name": "(anonymous_0)",
7291 "line": 1,
7292 "decl": {
7293 "start": { "line": 1, "column": 22 },
7294 "end": { "line": 1, "column": 23 }
7295 },
7296 "loc": {
7297 "start": { "line": 1, "column": 28 },
7298 "end": { "line": 1, "column": 38 }
7299 }
7300 },
7301 "1": {
7302 "name": "(anonymous_1)",
7303 "line": 1,
7304 "decl": {
7305 "start": { "line": 1, "column": 28 },
7306 "end": { "line": 1, "column": 29 }
7307 },
7308 "loc": {
7309 "start": { "line": 1, "column": 34 },
7310 "end": { "line": 1, "column": 38 }
7311 }
7312 }
7313 }),
7314 &serde_json::json!({ "0": 1, "1": 0 }),
7315 );
7316
7317 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7318 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7319 let file_coverage = coverage.get(&canonical_source).unwrap();
7320
7321 assert_eq!(file_coverage.lookup("nested", 1, 22), Some(100.0));
7322 assert_eq!(file_coverage.lookup("<arrow>", 1, 28), Some(0.0));
7323 }
7324
7325 #[test]
7346 fn nested_function_statements_do_not_lower_the_outer_function() {
7347 let temp = tempfile::TempDir::new().unwrap();
7348 let source_path = temp.path().join("src/nested.js");
7349 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7350 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7351
7352 let coverage_path = temp.path().join("coverage-final.json");
7353 write_single_file_istanbul_fixture_with_statements(
7354 &coverage_path,
7355 &source_path,
7356 &nested_return_fn_map(),
7357 &serde_json::json!({ "0": 1, "1": 0 }),
7358 &nested_return_statement_map(),
7359 &serde_json::json!({ "0": 1, "1": 0, "2": 0, "3": 0, "4": 1 }),
7360 );
7361
7362 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7363 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7364 let file_coverage = coverage.get(&canonical_source).unwrap();
7365
7366 assert_eq!(file_coverage.lookup("outer", 1, 9), Some(100.0));
7367 assert_eq!(file_coverage.lookup("inner", 2, 18), Some(0.0));
7368 }
7369
7370 #[test]
7374 fn module_scope_statements_belong_to_no_function() {
7375 let temp = tempfile::TempDir::new().unwrap();
7376 let source_path = temp.path().join("src/nested.js");
7377 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7378 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7379
7380 let coverage_path = temp.path().join("coverage-final.json");
7381 write_single_file_istanbul_fixture_with_statements(
7382 &coverage_path,
7383 &source_path,
7384 &nested_return_fn_map(),
7385 &serde_json::json!({ "0": 1, "1": 0 }),
7386 &nested_return_statement_map(),
7387 &serde_json::json!({ "0": 1, "1": 0, "2": 0, "3": 0, "4": 0 }),
7388 );
7389
7390 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7391 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7392 let file_coverage = coverage.get(&canonical_source).unwrap();
7393
7394 assert_eq!(file_coverage.lookup("outer", 1, 9), Some(100.0));
7395 assert_eq!(file_coverage.lookup("inner", 2, 18), Some(0.0));
7396 }
7397
7398 #[test]
7416 fn an_outer_function_with_only_a_nested_body_falls_back_to_the_hit_count() {
7417 let temp = tempfile::TempDir::new().unwrap();
7418 let source_path = temp.path().join("src/decl-nested.js");
7419 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7420 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7421
7422 let coverage_path = temp.path().join("coverage-final.json");
7423 write_single_file_istanbul_fixture_with_statements(
7424 &coverage_path,
7425 &source_path,
7426 &serde_json::json!({
7427 "0": {
7428 "name": "outer",
7429 "line": 1,
7430 "decl": {
7431 "start": { "line": 1, "column": 9 },
7432 "end": { "line": 1, "column": 14 }
7433 },
7434 "loc": {
7435 "start": { "line": 1, "column": 17 },
7436 "end": { "line": 6, "column": 1 }
7437 }
7438 },
7439 "1": {
7440 "name": "inner",
7441 "line": 2,
7442 "decl": {
7443 "start": { "line": 2, "column": 11 },
7444 "end": { "line": 2, "column": 16 }
7445 },
7446 "loc": {
7447 "start": { "line": 2, "column": 23 },
7448 "end": { "line": 5, "column": 3 }
7449 }
7450 }
7451 }),
7452 &serde_json::json!({ "0": 1, "1": 0 }),
7453 &serde_json::json!({
7454 "0": {
7455 "start": { "line": 3, "column": 4 },
7456 "end": { "line": 3, "column": 27 }
7457 },
7458 "1": {
7459 "start": { "line": 3, "column": 14 },
7460 "end": { "line": 3, "column": 27 }
7461 },
7462 "2": {
7463 "start": { "line": 4, "column": 4 },
7464 "end": { "line": 4, "column": 16 }
7465 },
7466 "3": {
7467 "start": { "line": 8, "column": 0 },
7468 "end": { "line": 8, "column": 27 }
7469 }
7470 }),
7471 &serde_json::json!({ "0": 0, "1": 0, "2": 0, "3": 1 }),
7472 );
7473
7474 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7475 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7476 let file_coverage = coverage.get(&canonical_source).unwrap();
7477
7478 assert_eq!(file_coverage.lookup("outer", 1, 9), Some(100.0));
7479 assert_eq!(file_coverage.lookup("inner", 2, 11), Some(0.0));
7480 }
7481
7482 #[test]
7487 fn sibling_functions_keep_independent_statement_coverage() {
7488 let temp = tempfile::TempDir::new().unwrap();
7489 let source_path = temp.path().join("src/sibling.js");
7490 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7491 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7492
7493 let coverage_path = temp.path().join("coverage-final.json");
7494 write_single_file_istanbul_fixture_with_statements(
7495 &coverage_path,
7496 &source_path,
7497 &serde_json::json!({
7498 "0": {
7499 "name": "inner",
7500 "line": 1,
7501 "decl": {
7502 "start": { "line": 1, "column": 9 },
7503 "end": { "line": 1, "column": 14 }
7504 },
7505 "loc": {
7506 "start": { "line": 1, "column": 21 },
7507 "end": { "line": 4, "column": 1 }
7508 }
7509 },
7510 "1": {
7511 "name": "outer",
7512 "line": 6,
7513 "decl": {
7514 "start": { "line": 6, "column": 9 },
7515 "end": { "line": 6, "column": 14 }
7516 },
7517 "loc": {
7518 "start": { "line": 6, "column": 17 },
7519 "end": { "line": 8, "column": 1 }
7520 }
7521 }
7522 }),
7523 &serde_json::json!({ "0": 0, "1": 1 }),
7524 &serde_json::json!({
7525 "0": {
7526 "start": { "line": 2, "column": 2 },
7527 "end": { "line": 2, "column": 25 }
7528 },
7529 "1": {
7530 "start": { "line": 2, "column": 12 },
7531 "end": { "line": 2, "column": 25 }
7532 },
7533 "2": {
7534 "start": { "line": 3, "column": 2 },
7535 "end": { "line": 3, "column": 14 }
7536 },
7537 "3": {
7538 "start": { "line": 7, "column": 2 },
7539 "end": { "line": 7, "column": 15 }
7540 },
7541 "4": {
7542 "start": { "line": 10, "column": 0 },
7543 "end": { "line": 10, "column": 27 }
7544 }
7545 }),
7546 &serde_json::json!({ "0": 0, "1": 0, "2": 0, "3": 1, "4": 1 }),
7547 );
7548
7549 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7550 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7551 let file_coverage = coverage.get(&canonical_source).unwrap();
7552
7553 assert_eq!(file_coverage.lookup("outer", 6, 9), Some(100.0));
7554 assert_eq!(file_coverage.lookup("inner", 1, 9), Some(0.0));
7555 }
7556
7557 #[test]
7563 fn curried_arrow_statements_belong_to_the_innermost_arrow() {
7564 let temp = tempfile::TempDir::new().unwrap();
7565 let source_path = temp.path().join("src/curried.js");
7566 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7567 std::fs::write(&source_path, "// geometry fixture\n").unwrap();
7568
7569 let coverage_path = temp.path().join("coverage-final.json");
7570 write_single_file_istanbul_fixture_with_statements(
7571 &coverage_path,
7572 &source_path,
7573 &serde_json::json!({
7574 "0": {
7575 "name": "(anonymous_0)",
7576 "line": 1,
7577 "decl": {
7578 "start": { "line": 1, "column": 12 },
7579 "end": { "line": 1, "column": 13 }
7580 },
7581 "loc": {
7582 "start": { "line": 1, "column": 19 },
7583 "end": { "line": 1, "column": 31 }
7584 }
7585 },
7586 "1": {
7587 "name": "(anonymous_1)",
7588 "line": 1,
7589 "decl": {
7590 "start": { "line": 1, "column": 19 },
7591 "end": { "line": 1, "column": 20 }
7592 },
7593 "loc": {
7594 "start": { "line": 1, "column": 26 },
7595 "end": { "line": 1, "column": 31 }
7596 }
7597 }
7598 }),
7599 &serde_json::json!({ "0": 1, "1": 0 }),
7600 &serde_json::json!({
7601 "0": {
7602 "start": { "line": 1, "column": 12 },
7603 "end": { "line": 1, "column": 31 }
7604 },
7605 "1": {
7606 "start": { "line": 1, "column": 19 },
7607 "end": { "line": 1, "column": 31 }
7608 },
7609 "2": {
7610 "start": { "line": 1, "column": 26 },
7611 "end": { "line": 1, "column": 31 }
7612 },
7613 "3": {
7614 "start": { "line": 3, "column": 0 },
7615 "end": { "line": 3, "column": 25 }
7616 }
7617 }),
7618 &serde_json::json!({ "0": 1, "1": 1, "2": 0, "3": 1 }),
7619 );
7620
7621 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7622 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7623 let file_coverage = coverage.get(&canonical_source).unwrap();
7624
7625 assert_eq!(file_coverage.lookup("add", 1, 12), Some(100.0));
7626 assert_eq!(file_coverage.lookup("<arrow>", 1, 19), Some(0.0));
7627 }
7628
7629 fn nested_return_fn_map() -> serde_json::Value {
7633 serde_json::json!({
7634 "0": {
7635 "name": "outer",
7636 "line": 1,
7637 "decl": {
7638 "start": { "line": 1, "column": 9 },
7639 "end": { "line": 1, "column": 14 }
7640 },
7641 "loc": {
7642 "start": { "line": 1, "column": 17 },
7643 "end": { "line": 6, "column": 1 }
7644 }
7645 },
7646 "1": {
7647 "name": "inner",
7648 "line": 2,
7649 "decl": {
7650 "start": { "line": 2, "column": 18 },
7651 "end": { "line": 2, "column": 23 }
7652 },
7653 "loc": {
7654 "start": { "line": 2, "column": 30 },
7655 "end": { "line": 5, "column": 3 }
7656 }
7657 }
7658 })
7659 }
7660
7661 fn nested_return_statement_map() -> serde_json::Value {
7662 serde_json::json!({
7663 "0": {
7664 "start": { "line": 2, "column": 2 },
7665 "end": { "line": 5, "column": 4 }
7666 },
7667 "1": {
7668 "start": { "line": 3, "column": 4 },
7669 "end": { "line": 3, "column": 27 }
7670 },
7671 "2": {
7672 "start": { "line": 3, "column": 14 },
7673 "end": { "line": 3, "column": 27 }
7674 },
7675 "3": {
7676 "start": { "line": 4, "column": 4 },
7677 "end": { "line": 4, "column": 16 }
7678 },
7679 "4": {
7680 "start": { "line": 8, "column": 0 },
7681 "end": { "line": 8, "column": 27 }
7682 }
7683 })
7684 }
7685
7686 #[test]
7690 fn curried_arrow_multiline_hoc_resolves_both_arrows() {
7691 let temp = tempfile::TempDir::new().unwrap();
7692 let source_path = temp.path().join("src/with-auth.tsx");
7693 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7694 std::fs::write(
7695 &source_path,
7696 "export const withAuth = (Component) =>\n (props) => {\n return Component(props);\n };\n",
7697 )
7698 .unwrap();
7699
7700 let coverage_path = temp.path().join("coverage-final.json");
7701 write_single_file_istanbul_fixture(
7702 &coverage_path,
7703 &source_path,
7704 &serde_json::json!({
7705 "0": {
7706 "name": "(anonymous_0)",
7707 "line": 2,
7708 "decl": {
7709 "start": { "line": 1, "column": 24 },
7710 "end": { "line": 1, "column": 25 }
7711 },
7712 "loc": {
7713 "start": { "line": 2, "column": 2 },
7714 "end": { "line": 4, "column": 3 }
7715 }
7716 },
7717 "1": {
7718 "name": "(anonymous_1)",
7719 "line": 2,
7720 "decl": {
7721 "start": { "line": 2, "column": 2 },
7722 "end": { "line": 2, "column": 3 }
7723 },
7724 "loc": {
7725 "start": { "line": 2, "column": 13 },
7726 "end": { "line": 4, "column": 3 }
7727 }
7728 }
7729 }),
7730 &serde_json::json!({ "0": 1, "1": 0 }),
7731 );
7732
7733 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7734 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7735 let file_coverage = coverage.get(&canonical_source).unwrap();
7736
7737 assert_eq!(file_coverage.lookup("withAuth", 1, 24), Some(100.0));
7738 assert_eq!(file_coverage.lookup("<arrow>", 2, 2), Some(0.0));
7739 }
7740
7741 #[test]
7744 fn curried_arrow_depth_three_chain_resolves_every_arrow() {
7745 let temp = tempfile::TempDir::new().unwrap();
7746 let source_path = temp.path().join("src/logger.ts");
7747 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7748 std::fs::write(
7749 &source_path,
7750 "export const logger = (store) => (next) => (action) => {\n return next(action);\n};\n",
7751 )
7752 .unwrap();
7753
7754 let coverage_path = temp.path().join("coverage-final.json");
7755 write_single_file_istanbul_fixture(
7756 &coverage_path,
7757 &source_path,
7758 &serde_json::json!({
7759 "0": {
7760 "name": "(anonymous_0)",
7761 "line": 1,
7762 "decl": {
7763 "start": { "line": 1, "column": 22 },
7764 "end": { "line": 1, "column": 23 }
7765 },
7766 "loc": {
7767 "start": { "line": 1, "column": 33 },
7768 "end": { "line": 3, "column": 1 }
7769 }
7770 },
7771 "1": {
7772 "name": "(anonymous_1)",
7773 "line": 1,
7774 "decl": {
7775 "start": { "line": 1, "column": 33 },
7776 "end": { "line": 1, "column": 34 }
7777 },
7778 "loc": {
7779 "start": { "line": 1, "column": 43 },
7780 "end": { "line": 3, "column": 1 }
7781 }
7782 },
7783 "2": {
7784 "name": "(anonymous_2)",
7785 "line": 1,
7786 "decl": {
7787 "start": { "line": 1, "column": 43 },
7788 "end": { "line": 1, "column": 44 }
7789 },
7790 "loc": {
7791 "start": { "line": 1, "column": 55 },
7792 "end": { "line": 3, "column": 1 }
7793 }
7794 }
7795 }),
7796 &serde_json::json!({ "0": 0, "1": 1, "2": 0 }),
7797 );
7798
7799 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7800 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7801 let file_coverage = coverage.get(&canonical_source).unwrap();
7802
7803 assert_eq!(file_coverage.lookup("logger", 1, 22), Some(0.0));
7804 assert_eq!(file_coverage.lookup("<arrow>", 1, 33), Some(100.0));
7805 assert_eq!(file_coverage.lookup("<arrow>", 1, 43), Some(0.0));
7806 }
7807
7808 #[test]
7810 fn curried_class_property_arrow_resolves_both_arrows() {
7811 let temp = tempfile::TempDir::new().unwrap();
7812 let source_path = temp.path().join("src/store.ts");
7813 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7814 std::fs::write(
7815 &source_path,
7816 "export class Store {\n handle = (event) => (payload) => {\n return payload;\n };\n}\n",
7817 )
7818 .unwrap();
7819
7820 let coverage_path = temp.path().join("coverage-final.json");
7821 write_single_file_istanbul_fixture(
7822 &coverage_path,
7823 &source_path,
7824 &serde_json::json!({
7825 "0": {
7826 "name": "(anonymous_0)",
7827 "line": 2,
7828 "decl": {
7829 "start": { "line": 2, "column": 11 },
7830 "end": { "line": 2, "column": 12 }
7831 },
7832 "loc": {
7833 "start": { "line": 2, "column": 22 },
7834 "end": { "line": 4, "column": 3 }
7835 }
7836 },
7837 "1": {
7838 "name": "(anonymous_1)",
7839 "line": 2,
7840 "decl": {
7841 "start": { "line": 2, "column": 22 },
7842 "end": { "line": 2, "column": 23 }
7843 },
7844 "loc": {
7845 "start": { "line": 2, "column": 35 },
7846 "end": { "line": 4, "column": 3 }
7847 }
7848 }
7849 }),
7850 &serde_json::json!({ "0": 1, "1": 0 }),
7851 );
7852
7853 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7854 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7855 let file_coverage = coverage.get(&canonical_source).unwrap();
7856
7857 assert_eq!(file_coverage.lookup("handle", 2, 11), Some(100.0));
7858 assert_eq!(file_coverage.lookup("<arrow>", 2, 22), Some(0.0));
7859 }
7860
7861 #[test]
7865 fn anonymous_sibling_tie_outside_every_body_abstains() {
7866 let temp = tempfile::TempDir::new().unwrap();
7867 let source_path = temp.path().join("src/handlers.ts");
7868 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7869 std::fs::write(
7870 &source_path,
7871 "export const handlers = {\n a: () => true,\n\n b: () => false,\n};\n",
7872 )
7873 .unwrap();
7874
7875 let coverage_path = temp.path().join("coverage-final.json");
7876 write_single_file_istanbul_fixture(
7877 &coverage_path,
7878 &source_path,
7879 &serde_json::json!({
7880 "0": {
7881 "name": "(anonymous_0)",
7882 "line": 2,
7883 "decl": {
7884 "start": { "line": 2, "column": 5 },
7885 "end": { "line": 2, "column": 6 }
7886 },
7887 "loc": {
7888 "start": { "line": 2, "column": 11 },
7889 "end": { "line": 2, "column": 15 }
7890 }
7891 },
7892 "1": {
7893 "name": "(anonymous_1)",
7894 "line": 4,
7895 "decl": {
7896 "start": { "line": 4, "column": 5 },
7897 "end": { "line": 4, "column": 6 }
7898 },
7899 "loc": {
7900 "start": { "line": 4, "column": 11 },
7901 "end": { "line": 4, "column": 16 }
7902 }
7903 }
7904 }),
7905 &serde_json::json!({ "0": 1, "1": 0 }),
7906 );
7907
7908 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7909 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7910 let file_coverage = coverage.get(&canonical_source).unwrap();
7911
7912 assert_eq!(file_coverage.lookup("a", 2, 5), Some(100.0));
7913 assert_eq!(file_coverage.lookup("b", 4, 5), Some(0.0));
7914 assert!(file_coverage.lookup("<arrow>", 3, 5).is_none());
7915 }
7916
7917 #[test]
7923 fn anonymous_tie_selects_unique_strictly_innermost_containing_span() {
7924 let temp = tempfile::TempDir::new().unwrap();
7925 let source_path = temp.path().join("src/nested.ts");
7926 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
7927 std::fs::write(
7928 &source_path,
7929 "export const o = function () { const f = () => { return 1;\n }; return f; };\n",
7930 )
7931 .unwrap();
7932
7933 let coverage_path = temp.path().join("coverage-final.json");
7934 write_single_file_istanbul_fixture(
7935 &coverage_path,
7936 &source_path,
7937 &serde_json::json!({
7938 "0": {
7939 "name": "(anonymous_0)",
7940 "line": 1,
7941 "decl": {
7942 "start": { "line": 1, "column": 17 },
7943 "end": { "line": 1, "column": 18 }
7944 },
7945 "loc": {
7946 "start": { "line": 1, "column": 29 },
7947 "end": { "line": 2, "column": 53 }
7948 }
7949 },
7950 "1": {
7951 "name": "(anonymous_1)",
7952 "line": 1,
7953 "decl": {
7954 "start": { "line": 1, "column": 41 },
7955 "end": { "line": 1, "column": 42 }
7956 },
7957 "loc": {
7958 "start": { "line": 1, "column": 47 },
7959 "end": { "line": 2, "column": 40 }
7960 }
7961 }
7962 }),
7963 &serde_json::json!({ "0": 1, "1": 0 }),
7964 );
7965
7966 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
7967 let canonical_source = dunce::canonicalize(&source_path).unwrap();
7968 let file_coverage = coverage.get(&canonical_source).unwrap();
7969
7970 assert_eq!(file_coverage.lookup("<arrow>", 2, 35), Some(0.0));
7971 }
7972
7973 #[test]
7976 fn anonymous_tie_rejects_incomparable_containing_spans() {
7977 let file_coverage = IstanbulFileCoverage::new(
7978 vec![
7979 IstanbulFunctionCoverage {
7980 name: "(anonymous_0)".to_string(),
7981 coverage_pct: 100.0,
7982 aliases: vec![primary_alias(10, 8), secondary_alias(10, 14)],
7983 decl_start: IstanbulPosition::new(10, 8),
7984 header_holds_other_fn: false,
7985 header_span: None,
7986 body_span: Some(body_span((10, 14), (12, 30))),
7987 },
7988 IstanbulFunctionCoverage {
7989 name: "(anonymous_1)".to_string(),
7990 coverage_pct: 0.0,
7991 aliases: vec![primary_alias(10, 20), secondary_alias(11, 0)],
7992 decl_start: IstanbulPosition::new(10, 20),
7993 header_holds_other_fn: false,
7994 header_span: None,
7995 body_span: Some(body_span((11, 0), (14, 0))),
7996 },
7997 ],
7998 false,
7999 );
8000
8001 assert!(file_coverage.lookup("<arrow>", 12, 17).is_none());
8002 }
8003
8004 #[test]
8009 fn anonymous_shared_primary_alias_rejects_even_nested_spans() {
8010 let file_coverage = IstanbulFileCoverage::new(
8011 vec![
8012 IstanbulFunctionCoverage {
8013 name: "(anonymous_0)".to_string(),
8014 coverage_pct: 100.0,
8015 aliases: vec![primary_alias(4, 11), primary_alias(1, 11)],
8016 decl_start: IstanbulPosition::new(4, 11),
8017 header_holds_other_fn: false,
8018 header_span: None,
8019 body_span: Some(body_span((4, 11), (4, 23))),
8020 },
8021 IstanbulFunctionCoverage {
8022 name: "(anonymous_1)".to_string(),
8023 coverage_pct: 0.0,
8024 aliases: vec![primary_alias(4, 11), secondary_alias(4, 18)],
8025 decl_start: IstanbulPosition::new(4, 11),
8026 header_holds_other_fn: false,
8027 header_span: None,
8028 body_span: Some(body_span((4, 18), (4, 23))),
8029 },
8030 ],
8031 false,
8032 );
8033
8034 assert!(file_coverage.lookup("<arrow>", 4, 11).is_none());
8035 assert_eq!(file_coverage.lookup("aa", 1, 11), Some(100.0));
8036 }
8037
8038 #[test]
8039 fn colliding_anonymous_alias_uses_unique_safe_header_span() {
8040 let file_coverage = IstanbulFileCoverage::new(
8041 vec![
8042 IstanbulFunctionCoverage {
8043 name: "(anonymous_0)".to_string(),
8044 coverage_pct: 100.0,
8045 aliases: vec![primary_alias(1, 0), secondary_alias(3, 4)],
8046 decl_start: IstanbulPosition::new(1, 0),
8047 header_holds_other_fn: false,
8048 header_span: Some(body_span((1, 0), (5, 0))),
8049 body_span: Some(body_span((5, 0), (8, 0))),
8050 },
8051 IstanbulFunctionCoverage {
8052 name: "(anonymous_1)".to_string(),
8053 coverage_pct: 0.0,
8054 aliases: vec![primary_alias(10, 0), secondary_alias(3, 4)],
8055 decl_start: IstanbulPosition::new(10, 0),
8056 header_holds_other_fn: false,
8057 header_span: None,
8058 body_span: Some(body_span((10, 0), (12, 0))),
8059 },
8060 ],
8061 false,
8062 );
8063
8064 assert_eq!(file_coverage.lookup("<arrow>", 3, 4), Some(100.0));
8065 }
8066
8067 #[test]
8070 fn colliding_secondary_aliases_abstain_at_shared_position() {
8071 let file_coverage = IstanbulFileCoverage::new(
8072 vec![
8073 IstanbulFunctionCoverage {
8074 name: "(anonymous_0)".to_string(),
8075 coverage_pct: 100.0,
8076 aliases: vec![primary_alias(10, 0), secondary_alias(12, 4)],
8077 decl_start: IstanbulPosition::new(10, 0),
8078 header_holds_other_fn: false,
8079 header_span: None,
8080 body_span: Some(body_span((12, 4), (20, 0))),
8081 },
8082 IstanbulFunctionCoverage {
8083 name: "(anonymous_1)".to_string(),
8084 coverage_pct: 0.0,
8085 aliases: vec![primary_alias(11, 0), secondary_alias(12, 4)],
8086 decl_start: IstanbulPosition::new(11, 0),
8087 header_holds_other_fn: false,
8088 header_span: None,
8089 body_span: Some(body_span((12, 4), (18, 0))),
8090 },
8091 ],
8092 false,
8093 );
8094
8095 assert_eq!(file_coverage.lookup("first", 10, 0), Some(100.0));
8096 assert_eq!(file_coverage.lookup("second", 11, 0), Some(0.0));
8097 assert!(file_coverage.lookup("<arrow>", 12, 4).is_none());
8098 }
8099
8100 #[test]
8101 fn colliding_named_secondary_aliases_abstain_at_shared_position() {
8102 let file_coverage = IstanbulFileCoverage::new(
8103 vec![
8104 IstanbulFunctionCoverage {
8105 name: "handler".to_string(),
8106 coverage_pct: 100.0,
8107 aliases: vec![primary_alias(10, 0), secondary_alias(12, 4)],
8108 decl_start: IstanbulPosition::new(10, 0),
8109 header_holds_other_fn: false,
8110 header_span: None,
8111 body_span: Some(body_span((12, 4), (20, 0))),
8112 },
8113 IstanbulFunctionCoverage {
8114 name: "handler".to_string(),
8115 coverage_pct: 0.0,
8116 aliases: vec![primary_alias(11, 0), secondary_alias(12, 4)],
8117 decl_start: IstanbulPosition::new(11, 0),
8118 header_holds_other_fn: false,
8119 header_span: None,
8120 body_span: Some(body_span((12, 4), (18, 0))),
8121 },
8122 ],
8123 false,
8124 );
8125
8126 assert_eq!(file_coverage.lookup("handler", 10, 0), Some(100.0));
8127 assert_eq!(file_coverage.lookup("handler", 11, 0), Some(0.0));
8128 assert!(file_coverage.lookup("handler", 12, 4).is_none());
8129 }
8130
8131 #[test]
8132 fn invalid_body_location_does_not_create_an_alias() {
8133 let temp = tempfile::TempDir::new().unwrap();
8134 let source_path = temp.path().join("src/invalid-location.ts");
8135 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
8136 std::fs::write(&source_path, "export const handler = () => true;\n").unwrap();
8137
8138 let coverage_path = temp.path().join("coverage-final.json");
8139 write_single_file_istanbul_fixture(
8140 &coverage_path,
8141 &source_path,
8142 &serde_json::json!({
8143 "0": {
8144 "name": "(anonymous_0)",
8145 "line": 8,
8146 "decl": {
8147 "start": { "line": 8, "column": 14 },
8148 "end": { "line": 8, "column": 25 }
8149 },
8150 "loc": {
8151 "start": { "line": 22, "column": 1 },
8152 "end": { "line": 20, "column": 6 }
8153 }
8154 }
8155 }),
8156 &serde_json::json!({ "0": 1 }),
8157 );
8158
8159 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
8160 let canonical_source = dunce::canonicalize(&source_path).unwrap();
8161 let file_coverage = coverage.get(&canonical_source).unwrap();
8162
8163 assert!(file_coverage.lookup("handler", 22, 1).is_none());
8164 }
8165
8166 #[test]
8167 fn load_istanbul_coverage_matches_multiline_async_arrow_decl_alias() {
8168 let temp = tempfile::TempDir::new().unwrap();
8169 let source_path = temp.path().join("src/actor.ts");
8170 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
8171 std::fs::write(
8172 &source_path,
8173 "export const elementsFrom = async (\n locator: AnyLocator,\n options?: { missingAsEmpty?: boolean },\n): Promise<HTMLElement[]> => {\n return [];\n};\n",
8174 )
8175 .unwrap();
8176
8177 let coverage_path = temp.path().join("coverage-final.json");
8178 write_single_file_istanbul_fixture(
8179 &coverage_path,
8180 &source_path,
8181 &serde_json::json!({
8182 "0": {
8183 "name": "(anonymous_0)",
8184 "line": 4,
8185 "decl": {
8186 "start": { "line": 1, "column": 28 },
8187 "end": { "line": 4, "column": 26 }
8188 },
8189 "loc": {
8190 "start": { "line": 4, "column": 27 },
8191 "end": { "line": 6, "column": 1 }
8192 }
8193 }
8194 }),
8195 &serde_json::json!({
8196 "0": 642
8197 }),
8198 );
8199
8200 let coverage = load_istanbul_coverage(&coverage_path, None, None, false).unwrap();
8201 let canonical_source = dunce::canonicalize(&source_path).unwrap();
8202 let file_coverage = coverage.get(&canonical_source).unwrap();
8203
8204 assert_eq!(file_coverage.lookup("elementsFrom", 1, 28), Some(100.0));
8205 }
8206
8207 #[test]
8208 fn istanbul_lookup_exact_match() {
8209 let mut functions = rustc_hash::FxHashMap::default();
8210 functions.insert(("handleClick".to_string(), 10, 0), 85.0);
8211 let fc = test_istanbul_file_coverage(functions, false);
8212 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 85.0).abs() < f64::EPSILON);
8213 }
8214
8215 #[test]
8216 fn istanbul_lookup_fuzzy_match_within_offset() {
8217 let mut functions = rustc_hash::FxHashMap::default();
8218 functions.insert(("handleClick".to_string(), 10, 0), 72.0);
8219 let fc = test_istanbul_file_coverage(functions, false);
8220 assert!((fc.lookup("handleClick", 11, 0).unwrap() - 72.0).abs() < f64::EPSILON);
8221 assert!((fc.lookup("handleClick", 12, 0).unwrap() - 72.0).abs() < f64::EPSILON);
8222 }
8223
8224 #[test]
8225 fn istanbul_lookup_fuzzy_match_outside_offset() {
8226 let mut functions = rustc_hash::FxHashMap::default();
8227 functions.insert(("handleClick".to_string(), 10, 0), 72.0);
8228 let fc = test_istanbul_file_coverage(functions, false);
8229 assert!(fc.lookup("handleClick", 13, 0).is_none());
8230 }
8231
8232 #[test]
8233 fn istanbul_lookup_relocated_matches_unique_name_at_any_distance() {
8234 let mut functions = rustc_hash::FxHashMap::default();
8235 functions.insert(("handleClick".to_string(), 29, 0), 72.0);
8236 let fc = test_istanbul_file_coverage(functions, true);
8237 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
8238 }
8239
8240 #[test]
8241 fn istanbul_lookup_relocated_accepts_declaration_alias_pair() {
8242 let mut functions = rustc_hash::FxHashMap::default();
8243 functions.insert(("handleClick".to_string(), 29, 16), 72.0);
8244 functions.insert(("handleClick".to_string(), 29, 0), 72.0);
8245 let fc = test_istanbul_file_coverage(functions, true);
8246 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
8247 }
8248
8249 #[test]
8250 fn istanbul_lookup_relocated_bails_on_disagreeing_same_name_entries() {
8251 let mut functions = rustc_hash::FxHashMap::default();
8252 functions.insert(("render".to_string(), 29, 0), 72.0);
8253 functions.insert(("render".to_string(), 80, 0), 10.0);
8254 let fc = test_istanbul_file_coverage(functions, true);
8255 assert!(fc.lookup("render", 10, 0).is_none());
8256 }
8257
8258 #[test]
8259 fn istanbul_lookup_relocated_prefers_bounded_fuzzy_match() {
8260 let mut functions = rustc_hash::FxHashMap::default();
8261 functions.insert(("render".to_string(), 11, 0), 72.0);
8262 functions.insert(("render".to_string(), 80, 0), 10.0);
8263 let fc = test_istanbul_file_coverage(functions, true);
8264 assert!((fc.lookup("render", 10, 0).unwrap() - 72.0).abs() < f64::EPSILON);
8265 }
8266
8267 #[test]
8268 fn istanbul_lookup_name_mismatch() {
8269 let mut functions = rustc_hash::FxHashMap::default();
8270 functions.insert(("handleClick".to_string(), 10, 0), 85.0);
8271 let fc = test_istanbul_file_coverage(functions, false);
8272 assert!(fc.lookup("handleSubmit", 10, 0).is_none());
8273 }
8274
8275 #[test]
8276 fn istanbul_lookup_empty() {
8277 let fc = test_istanbul_file_coverage(rustc_hash::FxHashMap::default(), false);
8278 assert!(fc.lookup("anything", 1, 0).is_none());
8279 }
8280
8281 #[test]
8282 fn istanbul_lookup_fuzzy_picks_closest() {
8283 let mut functions = rustc_hash::FxHashMap::default();
8284 functions.insert(("render".to_string(), 8, 0), 60.0);
8285 functions.insert(("render".to_string(), 12, 0), 90.0);
8286 let fc = test_istanbul_file_coverage(functions, false);
8287 let result = fc.lookup("render", 10, 0);
8288 assert!(result.is_some());
8289 let pct = result.unwrap();
8290 assert!((pct - 60.0).abs() < f64::EPSILON || (pct - 90.0).abs() < f64::EPSILON);
8291 }
8292
8293 #[test]
8294 fn istanbul_lookup_anonymous_fallback_single_candidate() {
8295 let mut functions = rustc_hash::FxHashMap::default();
8296 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
8297 let fc = test_istanbul_file_coverage(functions, false);
8298 assert!((fc.lookup("myHandler", 28, 0).unwrap() - 75.0).abs() < f64::EPSILON);
8299 assert!((fc.lookup("myHandler", 30, 0).unwrap() - 75.0).abs() < f64::EPSILON);
8300 }
8301
8302 #[test]
8303 fn istanbul_lookup_anonymous_fallback_rejects_nearby_far_column() {
8304 let mut functions = rustc_hash::FxHashMap::default();
8305 functions.insert(("(anonymous_0)".to_string(), 4, 28), 75.0);
8306 let fc = test_istanbul_file_coverage(functions, false);
8307
8308 assert!(fc.lookup("declaredHelper", 3, 0).is_none());
8309 }
8310
8311 #[test]
8312 fn istanbul_lookup_anonymous_fallback_picks_closest_when_lines_differ() {
8313 let mut functions = rustc_hash::FxHashMap::default();
8314 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
8315 functions.insert(("(anonymous_1)".to_string(), 29, 0), 50.0);
8316 let fc = test_istanbul_file_coverage(functions, false);
8317 assert!((fc.lookup("myHandler", 28, 0).unwrap() - 75.0).abs() < f64::EPSILON);
8318 }
8319
8320 #[test]
8321 fn istanbul_lookup_anonymous_fallback_picks_closest_by_col_on_same_line() {
8322 let mut functions = rustc_hash::FxHashMap::default();
8323 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);
8326 assert!((fc.lookup("<arrow>", 1, 43).unwrap() - 10.0).abs() < f64::EPSILON);
8327 assert!((fc.lookup("<arrow>", 1, 23).unwrap() - 90.0).abs() < f64::EPSILON);
8328 }
8329
8330 #[test]
8331 fn istanbul_lookup_anonymous_fallback_bails_only_on_true_tie() {
8332 let mut functions = rustc_hash::FxHashMap::default();
8333 functions.insert(("(anonymous_0)".to_string(), 27, 0), 75.0);
8334 functions.insert(("(anonymous_1)".to_string(), 29, 0), 50.0);
8335 let fc = test_istanbul_file_coverage(functions, false);
8336 assert!(fc.lookup("myHandler", 28, 0).is_none());
8337 }
8338
8339 #[test]
8340 fn istanbul_lookup_anonymous_fallback_outside_offset() {
8341 let mut functions = rustc_hash::FxHashMap::default();
8342 functions.insert(("(anonymous_0)".to_string(), 28, 0), 75.0);
8343 let fc = test_istanbul_file_coverage(functions, false);
8344 assert!(fc.lookup("myHandler", 31, 0).is_none());
8345 }
8346
8347 #[test]
8348 fn istanbul_lookup_named_match_beats_nearby_anonymous() {
8349 let mut functions = rustc_hash::FxHashMap::default();
8350 functions.insert(("handleClick".to_string(), 10, 0), 90.0);
8351 functions.insert(("(anonymous_7)".to_string(), 11, 0), 10.0);
8352 let fc = test_istanbul_file_coverage(functions, false);
8353 assert!((fc.lookup("handleClick", 10, 0).unwrap() - 90.0).abs() < f64::EPSILON);
8354 }
8355
8356 #[test]
8357 fn build_test_refs_empty() {
8358 let exports: Vec<fallow_graph::graph::ExportSymbol> = vec![];
8359 let graph = fallow_graph::graph::ModuleGraph::build(&[], &[], &[]);
8360 let refs = build_test_referenced_exports(&exports, StaticTestCoverage::new(&graph));
8361 assert!(refs.is_empty());
8362 }
8363
8364 #[test]
8365 fn istanbul_crap_empty_complexity() {
8366 let result = istanbul_crap_default(&[], None, false);
8367 assert!((result.max_crap).abs() < f64::EPSILON);
8368 assert_eq!(result.signals.above, 0);
8369 assert_eq!(result.matched, 0);
8370 assert_eq!(result.total, 0);
8371 }
8372
8373 #[test]
8374 fn istanbul_crap_match_statistics() {
8375 let funcs = vec![make_fn_complexity(5), {
8376 let mut f = make_fn_complexity(3);
8377 f.name = "other_fn".into();
8378 f.line = 10;
8379 f
8380 }];
8381 let mut functions = rustc_hash::FxHashMap::default();
8382 functions.insert(("test_fn".to_string(), 1, 0), 80.0);
8383 let file_cov = test_istanbul_file_coverage(functions, false);
8384 let result = istanbul_crap_default(&funcs, Some(&file_cov), true);
8385 assert_eq!(result.matched, 1);
8386 assert_eq!(result.total, 2);
8387 }
8388
8389 #[test]
8390 fn estimated_crap_multiple_functions_mixed_coverage() {
8391 let funcs = vec![
8392 make_fn_complexity(10), {
8394 let mut f = make_fn_complexity(3);
8395 f.name = "helper".into();
8396 f.line = 20;
8397 f
8398 },
8399 ];
8400 let mut refs = rustc_hash::FxHashSet::default();
8401 refs.insert("test_fn".to_string());
8402 let result = estimated_crap_default(
8403 &funcs,
8404 &refs,
8405 true,
8406 fallow_output::CoverageSource::Estimated,
8407 );
8408 let (max, above) = (result.max_crap, result.signals.above);
8409 assert!(max > 10.0);
8410 assert_eq!(above, 0);
8411 }
8412}