relay-knowledge 1.1.5

Graph-database-based knowledge graph project.
Documentation
use crate::domain::CodeImportRecord;

use super::super::import_resolution::{
    ImportContext, ImportResolution, ModuleFileResolution, normalize_join, parent_dir,
    parse_quoted_specifier,
};

pub(in crate::code::identity) fn resolve_import(
    import: &CodeImportRecord,
    context: &ImportContext<'_>,
) -> Option<(ImportResolution, Option<String>)> {
    let statement = import.module.lines().find_map(ruby_import_statement)?;
    let specifier = parse_quoted_specifier(statement)?;
    let relative = ruby_keyword_statement(statement, "require_relative")
        || ruby_keyword_statement(statement, "load")
        || specifier.starts_with("./")
        || specifier.starts_with("../");
    let candidates = ruby_module_candidates(&import.path, specifier, relative);
    Some(resolve_module_candidates(context, &candidates))
}

fn ruby_import_statement(statement: &str) -> Option<&str> {
    let statement = statement.trim();
    let import_like = ["require", "require_relative", "load"]
        .iter()
        .any(|keyword| ruby_keyword_statement(statement, keyword));

    import_like.then_some(statement)
}

fn ruby_module_candidates(import_path: &str, specifier: &str, relative: bool) -> Vec<String> {
    let mut candidates = Vec::new();
    if relative {
        if let Some(candidate) = normalize_join(parent_dir(import_path), specifier) {
            push_module_file_candidates(&mut candidates, &candidate);
        }
    } else {
        push_module_file_candidates(&mut candidates, specifier);
        push_module_file_candidates(&mut candidates, &format!("lib/{specifier}"));
    }

    candidates
}

fn push_module_file_candidates(candidates: &mut Vec<String>, base_path: &str) {
    push_unique(candidates, base_path.to_owned());
    if base_path.rsplit_once('.').is_some() {
        return;
    }
    push_unique(candidates, format!("{base_path}.rb"));
}

fn resolve_module_candidates(
    context: &ImportContext<'_>,
    candidates: &[String],
) -> (ImportResolution, Option<String>) {
    match context.resolve_first_module_file(candidates, true) {
        ModuleFileResolution::Resolved(target_hint) => {
            (ImportResolution::Resolved, Some(target_hint))
        }
        ModuleFileResolution::Ambiguous => (ImportResolution::Ambiguous, None),
        ModuleFileResolution::Unresolved => (ImportResolution::Unresolved, None),
    }
}

fn ruby_keyword_statement(statement: &str, keyword: &str) -> bool {
    statement
        .strip_prefix(keyword)
        .is_some_and(|rest| rest.starts_with(char::is_whitespace) || rest.starts_with('('))
}

fn push_unique(values: &mut Vec<String>, value: String) {
    if !values.contains(&value) {
        values.push(value);
    }
}