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