fallow-cli 2.91.0

CLI for fallow, Rust-native codebase intelligence for TypeScript and JavaScript
Documentation
use crate::health_types::{CoverageGapSummary, CoverageGaps, UntestedExport, UntestedFile};

pub(super) struct CoverageGapData {
    pub report: CoverageGaps,
    pub runtime_paths: Vec<std::path::PathBuf>,
}

pub(super) fn build_coverage_summary(
    runtime_files: usize,
    covered_files: usize,
    untested_files: usize,
    untested_exports: usize,
) -> CoverageGapSummary {
    let file_coverage_pct = if runtime_files == 0 {
        100.0
    } else {
        ((covered_files as f64 / runtime_files as f64) * 1000.0).round() / 10.0
    };

    CoverageGapSummary {
        runtime_files,
        covered_files,
        file_coverage_pct,
        untested_files,
        untested_exports,
    }
}

pub(super) fn compute_coverage_gaps(
    graph: &fallow_core::graph::ModuleGraph,
    file_paths: &rustc_hash::FxHashMap<fallow_core::discover::FileId, &std::path::PathBuf>,
    module_by_id: &rustc_hash::FxHashMap<
        fallow_core::discover::FileId,
        &fallow_core::extract::ModuleInfo,
    >,
    unused_exports: &rustc_hash::FxHashSet<(&std::path::Path, String)>,
    root: &std::path::Path,
) -> CoverageGapData {
    let mut runtime_files = 0usize;
    let mut covered_files = 0usize;
    let mut runtime_paths = Vec::new();
    let mut files: Vec<UntestedFile> = Vec::new();
    let mut exports: Vec<UntestedExport> = Vec::new();

    for node in &graph.modules {
        if !node.is_runtime_reachable() {
            continue;
        }

        let Some(path) = file_paths.get(&node.file_id) else {
            continue;
        };

        if path
            .extension()
            .and_then(|e| e.to_str())
            .is_some_and(|ext| matches!(ext, "css" | "scss" | "less" | "sass"))
        {
            continue;
        }

        let module = module_by_id.get(&node.file_id);
        if module.is_some_and(|m| {
            fallow_core::suppress::is_file_suppressed(
                &m.suppressions,
                fallow_types::suppress::IssueKind::CoverageGaps,
            )
        }) {
            continue;
        }

        runtime_paths.push((*path).clone());

        runtime_files += 1;
        if node.is_test_reachable() {
            covered_files += 1;
        } else {
            files.push(UntestedFile {
                path: (*path).clone(),
                value_export_count: node.exports.iter().filter(|e| !e.is_type_only).count(),
            });
        }

        let Some(module) = module else {
            continue;
        };

        for export in &node.exports {
            if export.is_type_only {
                continue;
            }
            if unused_exports.contains(&(path.as_path(), export.name.to_string())) {
                continue;
            }

            let has_test_dependency = export.references.iter().any(|reference| {
                graph
                    .modules
                    .get(reference.from_file.0 as usize)
                    .is_some_and(|module| module.is_test_reachable())
            });
            if has_test_dependency {
                continue;
            }

            let (line, col) = fallow_types::extract::byte_offset_to_line_col(
                &module.line_offsets,
                export.span.start,
            );
            exports.push(UntestedExport {
                path: (*path).clone(),
                export_name: export.name.to_string(),
                line,
                col,
            });
        }
    }

    files.sort_by(|a, b| a.path.cmp(&b.path));
    exports.sort_by(|a, b| {
        a.path
            .cmp(&b.path)
            .then_with(|| a.export_name.cmp(&b.export_name))
            .then_with(|| a.line.cmp(&b.line))
    });

    let untested_file_count = files.len();
    let untested_export_count = exports.len();
    let wrapped_files: Vec<crate::health_types::UntestedFileFinding> = files
        .into_iter()
        .map(|file| crate::health_types::UntestedFileFinding::with_actions(file, root))
        .collect();
    let wrapped_exports: Vec<crate::health_types::UntestedExportFinding> = exports
        .into_iter()
        .map(|export| crate::health_types::UntestedExportFinding::with_actions(export, root))
        .collect();

    CoverageGapData {
        report: CoverageGaps {
            summary: build_coverage_summary(
                runtime_files,
                covered_files,
                untested_file_count,
                untested_export_count,
            ),
            files: wrapped_files,
            exports: wrapped_exports,
        },
        runtime_paths,
    }
}