relay-knowledge 1.1.9

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

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

pub(in crate::code::identity) fn resolve_import(
    import: &CodeImportRecord,
    context: &ImportContext<'_>,
) -> Option<(ImportResolution, Option<String>)> {
    let requests = RustImportRequest::parse(&import.path, &import.module)?;
    let mut target_hint = None;
    let resolution = combined_resolution(requests.iter().map(|request| match request {
        RustImportRequest::Module { candidates } => {
            let output =
                module_file_resolution(context.resolve_first_module_file(candidates, true));
            if requests.len() == 1 {
                target_hint = output.1.clone();
            }
            output.0
        }
        RustImportRequest::Symbol {
            name,
            module_candidates,
        } => {
            let file_resolution = context.resolve_first_module_file(module_candidates, true);
            match file_resolution {
                ModuleFileResolution::Resolved(path) => {
                    let resolution =
                        context.resolve_name_in_paths(name, std::slice::from_ref(&path));
                    if requests.len() == 1 {
                        target_hint = Some(path);
                    }
                    resolution
                }
                ModuleFileResolution::Ambiguous => ImportResolution::Ambiguous,
                ModuleFileResolution::Unresolved => {
                    context.resolve_name_in_paths(name, module_candidates)
                }
            }
        }
    }));

    Some((resolution, target_hint))
}

enum RustImportRequest {
    Module {
        candidates: Vec<String>,
    },
    Symbol {
        name: String,
        module_candidates: Vec<String>,
    },
}

impl RustImportRequest {
    fn parse(import_path: &str, statement: &str) -> Option<Vec<Self>> {
        let statement = statement.trim().trim_end_matches(';').trim();
        if let Some(body) = statement.strip_prefix("mod ") {
            let name = body.split_whitespace().next()?.trim();
            return Some(vec![Self::Module {
                candidates: rust_module_candidates(parent_dir(import_path), name),
            }]);
        }
        let path = rust_use_path(statement)?;
        let requests = expand_use_tree(path)
            .into_iter()
            .filter_map(|path| Self::parse_use_path(import_path, &path))
            .collect::<Vec<_>>();

        (!requests.is_empty()).then_some(requests)
    }

    fn parse_use_path(import_path: &str, path: &str) -> Option<Self> {
        let normalized = normalize_use_path(import_path, path)?;
        let Some((module_path, name)) = normalized.rsplit_once("::") else {
            return Some(Self::Module {
                candidates: rust_module_candidates("", &normalized),
            });
        };
        if name == "*" {
            return Some(Self::Module {
                candidates: rust_module_candidates("", &module_path.replace("::", "/")),
            });
        }

        Some(Self::Symbol {
            name: name.to_owned(),
            module_candidates: rust_module_candidates("", &module_path.replace("::", "/")),
        })
    }
}

fn rust_use_path(statement: &str) -> Option<&str> {
    statement
        .strip_prefix("use ")
        .or_else(|| statement.strip_prefix("pub use "))
        .or_else(|| {
            statement
                .strip_prefix("pub(")
                .and_then(|rest| rest.split_once(") use ").map(|(_, body)| body))
        })
        .map(str::trim)
}

fn expand_use_tree(path: &str) -> Vec<String> {
    expand_rust_use_tree(path.trim())
}

fn expand_rust_use_tree(path: &str) -> Vec<String> {
    let Some((prefix, selectors)) = path.split_once("::{") else {
        return vec![strip_rust_alias(path).to_owned()];
    };
    let Some(selectors) = selectors.strip_suffix('}') else {
        return vec![strip_rust_alias(path).to_owned()];
    };

    split_rust_selectors(selectors)
        .into_iter()
        .filter_map(|selector| {
            let selector = selector.trim();
            let selector_without_alias = strip_rust_alias(selector);
            if selector_without_alias == "self" {
                return Some(prefix.to_owned());
            }
            if selector.contains("::{") {
                return Some(format!("{prefix}::{selector}"));
            }
            (!selector_without_alias.is_empty())
                .then(|| format!("{prefix}::{selector_without_alias}"))
        })
        .flat_map(|path| expand_rust_use_tree(&path))
        .collect()
}

fn strip_rust_alias(path: &str) -> &str {
    path.split_once(" as ")
        .map_or(path, |(path, _)| path)
        .trim()
}

fn split_rust_selectors(selectors: &str) -> Vec<&str> {
    let mut items = Vec::new();
    let mut brace_depth = 0usize;
    let mut start = 0usize;
    for (index, character) in selectors.char_indices() {
        match character {
            '{' => brace_depth += 1,
            '}' => brace_depth = brace_depth.saturating_sub(1),
            ',' if brace_depth == 0 => {
                items.push(selectors[start..index].trim());
                start = index + character.len_utf8();
            }
            _ => {}
        }
    }
    items.push(selectors[start..].trim());

    items
}

fn normalize_use_path(import_path: &str, path: &str) -> Option<String> {
    if path.starts_with("crate::") {
        return Some(path.trim_start_matches("crate::").to_owned());
    }
    if let Some(stripped) = path.strip_prefix("self::") {
        let joined = normalize_join(parent_dir(import_path), &stripped.replace("::", "/"))?;
        return Some(joined.replace('/', "::"));
    }
    if let Some(stripped) = path.strip_prefix("super::") {
        let joined = normalize_join(
            rust_super_base_dir(import_path),
            &stripped.replace("::", "/"),
        )?;
        return Some(joined.replace('/', "::"));
    }
    if rust_crate_root_file(import_path) {
        return Some(path.to_owned());
    }

    None
}

fn rust_crate_root_file(path: &str) -> bool {
    matches!(path, "src/lib.rs" | "src/main.rs" | "lib.rs" | "main.rs")
}

fn rust_super_base_dir(import_path: &str) -> &str {
    if import_path.ends_with("/mod.rs") {
        parent_dir(parent_dir(import_path))
    } else {
        parent_dir(import_path)
    }
}

fn rust_module_candidates(parent: &str, module_path: &str) -> Vec<String> {
    let base = if parent.is_empty() {
        module_path.to_owned()
    } else {
        match normalize_join(parent, module_path) {
            Some(value) => value,
            None => return Vec::new(),
        }
    };
    vec![format!("{base}.rs"), format!("{base}/mod.rs")]
}