fallow-core 2.48.1

Analysis orchestration for fallow codebase intelligence (dead code, duplication, plugins, cross-reference)
Documentation
use std::path::{Path, PathBuf};

use rustc_hash::{FxHashMap, FxHashSet};

use fallow_config::{ResolvedConfig, WorkspaceInfo};
use fallow_types::discover::{DiscoveredFile, FileId};
use oxc_span::Span;

use crate::extract::{ImportInfo, ImportedName, parse_from_content};
use crate::plugins::AggregatedPluginResult;
use crate::resolve::{
    ResolveResult, ResolvedImport, ResolvedModule, extract_package_name_from_node_modules_path,
    resolve_all_imports,
};

pub fn augment_external_style_package_usage(
    resolved_modules: &mut [ResolvedModule],
    config: &ResolvedConfig,
    workspaces: &[WorkspaceInfo],
    plugin_result: &AggregatedPluginResult,
) {
    let mut scanner = ExternalStylePackageScanner::new(config, workspaces, plugin_result);

    for module in resolved_modules {
        let mut synthetic_packages = FxHashSet::default();
        let existing_packages: FxHashSet<String> = module
            .resolved_imports
            .iter()
            .chain(module.resolved_dynamic_imports.iter())
            .filter_map(|import| match &import.target {
                ResolveResult::NpmPackage(name) => Some(name.clone()),
                _ => None,
            })
            .collect();

        for import in module
            .resolved_imports
            .iter()
            .chain(module.resolved_dynamic_imports.iter())
        {
            let ResolveResult::ExternalFile(path) = &import.target else {
                continue;
            };
            if !is_trackable_external_style_path(path) {
                continue;
            }

            synthetic_packages.extend(scanner.scan(path));
        }

        for package_name in synthetic_packages {
            if existing_packages.contains(package_name.as_str()) {
                continue;
            }
            module
                .resolved_imports
                .push(synthetic_package_import(package_name));
        }
    }
}

fn synthetic_package_import(package_name: String) -> ResolvedImport {
    ResolvedImport {
        info: ImportInfo {
            source: package_name.clone(),
            imported_name: ImportedName::SideEffect,
            local_name: String::new(),
            is_type_only: false,
            span: Span::default(),
            source_span: Span::default(),
        },
        target: ResolveResult::NpmPackage(package_name),
    }
}

fn is_trackable_external_style_path(path: &Path) -> bool {
    extract_package_name_from_node_modules_path(path).is_some()
        && path
            .extension()
            .and_then(|ext| ext.to_str())
            .is_some_and(|ext| matches!(ext, "css" | "scss" | "sass"))
}

struct ExternalStylePackageScanner<'a> {
    config: &'a ResolvedConfig,
    workspaces: &'a [WorkspaceInfo],
    plugin_result: &'a AggregatedPluginResult,
    memo: FxHashMap<PathBuf, FxHashSet<String>>,
    visiting: FxHashSet<PathBuf>,
}

impl<'a> ExternalStylePackageScanner<'a> {
    fn new(
        config: &'a ResolvedConfig,
        workspaces: &'a [WorkspaceInfo],
        plugin_result: &'a AggregatedPluginResult,
    ) -> Self {
        Self {
            config,
            workspaces,
            plugin_result,
            memo: FxHashMap::default(),
            visiting: FxHashSet::default(),
        }
    }

    fn scan(&mut self, path: &Path) -> FxHashSet<String> {
        let canonical = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
        if let Some(cached) = self.memo.get(&canonical) {
            return cached.clone();
        }
        if !self.visiting.insert(canonical.clone()) {
            return FxHashSet::default();
        }

        let mut packages = FxHashSet::default();
        if let Some(owner) = extract_package_name_from_node_modules_path(&canonical) {
            packages.insert(owner);
        }

        if !is_trackable_external_style_path(&canonical) {
            self.visiting.remove(&canonical);
            self.memo.insert(canonical.clone(), packages.clone());
            return packages;
        }

        let Ok(source) = std::fs::read_to_string(&canonical) else {
            self.visiting.remove(&canonical);
            self.memo.insert(canonical.clone(), packages.clone());
            return packages;
        };

        let file = DiscoveredFile {
            id: FileId(0),
            path: canonical.clone(),
            size_bytes: source.len() as u64,
        };
        let module = parse_from_content(FileId(0), &canonical, &source);
        let resolved = resolve_all_imports(
            &[module],
            &[file],
            self.workspaces,
            &self.plugin_result.active_plugins,
            &self.plugin_result.path_aliases,
            &self.plugin_result.scss_include_paths,
            &self.config.root,
            &self.config.resolve.conditions,
        );

        if let Some(resolved_module) = resolved.first() {
            for import in &resolved_module.resolved_imports {
                match &import.target {
                    ResolveResult::NpmPackage(name) => {
                        packages.insert(name.clone());
                    }
                    ResolveResult::ExternalFile(child) => {
                        if let Some(owner) = extract_package_name_from_node_modules_path(child) {
                            packages.insert(owner);
                        }
                        if is_trackable_external_style_path(child) {
                            packages.extend(self.scan(child));
                        }
                    }
                    ResolveResult::Unresolvable(_) => {
                        if let Some(child) = resolve_root_relative_style_import(
                            &self.config.root,
                            &import.info.source,
                        ) {
                            if let Some(owner) = extract_package_name_from_node_modules_path(&child)
                            {
                                packages.insert(owner);
                            }
                            if is_trackable_external_style_path(&child) {
                                packages.extend(self.scan(&child));
                            }
                        }
                    }
                    ResolveResult::InternalModule(_) => {}
                }
            }
        }

        self.visiting.remove(&canonical);
        self.memo.insert(canonical.clone(), packages.clone());
        packages
    }
}

fn resolve_root_relative_style_import(root: &Path, specifier: &str) -> Option<PathBuf> {
    let relative = specifier.strip_prefix('/')?;
    let candidate = root.join(relative);
    if candidate.is_file() {
        return Some(dunce::canonicalize(&candidate).unwrap_or(candidate));
    }

    if candidate.extension().is_some() {
        return None;
    }

    for ext in ["css", "scss", "sass"] {
        let candidate = candidate.with_extension(ext);
        if candidate.is_file() {
            return Some(dunce::canonicalize(&candidate).unwrap_or(candidate));
        }
    }

    None
}