blast-radius 0.1.1

Analyze the transitive blast radius of code changes.
Documentation
use std::path::{Path, PathBuf};

use anyhow::Result;

use crate::parse::{ModuleFacts, parse_ruby_module};
use crate::resolve::{ResolveCtx, Resolution};

use super::LanguageAdapter;

pub(super) struct RubyAdapter;

impl LanguageAdapter for RubyAdapter {
    fn extensions(&self) -> &'static [&'static str] {
        &["rb"]
    }

    fn parse(&self, path: &Path, source: &str) -> Result<ModuleFacts> {
        parse_ruby_module(path, source)
    }

    fn resolve(&self, ctx: &ResolveCtx, importer: &Path, specifier: &str) -> Resolution {
        match resolve_ruby_import(ctx, importer, specifier) {
            Some(path) => Resolution::Resolved(path),
            None => Resolution::Unresolved,
        }
    }

    fn is_internal(&self, ctx: &ResolveCtx, importer: &Path, specifier: &str) -> bool {
        specifier.starts_with('.') || resolve_ruby_import(ctx, importer, specifier).is_some()
    }
}

fn resolve_ruby_import(ctx: &ResolveCtx, importer: &Path, specifier: &str) -> Option<PathBuf> {
    if specifier.starts_with('.') {
        let base = importer.parent().unwrap_or(&ctx.repo_root);
        return ctx.try_resolve_candidate(&base.join(specifier), &["rb"]);
    }

    for candidate in [
        ctx.repo_root.join(specifier),
        ctx.repo_root.join("lib").join(specifier),
        ctx.repo_root.join("app").join(specifier),
    ] {
        if let Some(path) = ctx.try_resolve_candidate(&candidate, &["rb"]) {
            return Some(path);
        }
    }

    ctx.suffix_index
        .get(&PathBuf::from(format!("{specifier}.rb")))
        .cloned()
}