gobby-code 1.3.2

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use crate::models::ImportRelation;

use super::super::context::{
    ExternalImportBinding, ExternalRootBinding, ExtractedImports, ImportResolutionContext,
};
use super::super::helpers::{
    extract_quoted_string, go_default_package_alias, rust_join_use_path, split_alias,
    split_rust_use_group, split_top_level,
};
use super::super::predicates::{is_external_go_module, is_external_rust_root};

pub(crate) fn parse_go_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) -> anyhow::Result<()> {
    let trimmed = text.trim();
    let Some(rest) = trimmed.strip_prefix("import") else {
        anyhow::bail!("expected Go import statement, got `{trimmed}`");
    };
    if rest
        .chars()
        .next()
        .is_some_and(|ch| !ch.is_whitespace() && ch != '(')
    {
        anyhow::bail!("expected Go import statement, got `{trimmed}`");
    }

    let rest = rest.trim();
    if rest.starts_with('(') {
        let block = rest.trim_start_matches('(').trim_end_matches(')');
        for line in block.lines() {
            parse_go_import_spec(line.trim(), rel_path, import_context, extracted);
        }
    } else {
        parse_go_import_spec(rest, rel_path, import_context, extracted);
    }
    Ok(())
}

fn parse_go_import_spec(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let text = text.split("//").next().unwrap_or(text).trim();
    if text.is_empty() {
        return;
    }
    let Some(module) = extract_quoted_string(text) else {
        return;
    };

    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: module.clone(),
    });

    let alias = text[..text.find(['"', '`']).unwrap_or(0)].trim();
    // `_` blank imports run init side effects only and `.` dot imports merge the
    // package namespace; neither binds a selector alias we can resolve here.
    if matches!(alias, "_" | ".") {
        return;
    }
    let local_alias = if alias.is_empty() {
        go_default_package_alias(&module)
    } else {
        alias.to_string()
    };
    if local_alias.is_empty() {
        return;
    }

    if is_external_go_module(&module, import_context) {
        extracted.bindings.member.insert(local_alias, module);
        return;
    }

    // Local (same-module) package import: bind the package alias to the
    // package directory's Go files. A selector call `alias.Fn()` then resolves
    // `Fn` against any file in that directory (Go packages are directory-
    // granular). The post-write DB pass narrows the candidates to a real
    // indexed symbol id, or degrades the call to unresolved.
    let candidate_files = import_context.go_candidate_files(&module);
    if candidate_files.is_empty() {
        return;
    }
    extracted.bindings.bare.remove(&local_alias);
    extracted.bindings.member.remove(&local_alias);
    extracted
        .bindings
        .local_member
        .insert(local_alias, candidate_files);
}

pub(crate) fn parse_rust_import_statement(
    text: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let trimmed = text.trim();
    let Some(rest) = trimmed.strip_prefix("use ") else {
        return;
    };
    let rest = rest.trim().trim_end_matches(';').trim();
    extracted.imports.push(ImportRelation {
        file_path: rel_path.to_string(),
        module_name: rest.to_string(),
    });

    if let Some((prefix, group)) = split_rust_use_group(rest) {
        register_rust_group_imports(prefix, group, rel_path, import_context, extracted);
        return;
    }

    if rest.contains('*') {
        // Glob imports are intentionally not expanded because exported names are unknown here.
        return;
    }

    register_rust_path_import(rest, rel_path, import_context, extracted);
}

fn register_rust_group_imports(
    prefix: &str,
    group: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let Ok(items) = split_top_level(group, ',') else {
        return;
    };
    for item in items {
        if item.is_empty() {
            continue;
        }
        if let Some((nested_prefix, nested_group)) = split_rust_use_group(item) {
            let Some(full_prefix) = rust_join_use_path(prefix, nested_prefix) else {
                continue;
            };
            register_rust_group_imports(
                &full_prefix,
                nested_group,
                rel_path,
                import_context,
                extracted,
            );
            continue;
        }
        if item.contains('*') {
            continue;
        }
        let Some(path) = rust_join_use_path(prefix, item) else {
            continue;
        };
        register_rust_path_import(&path, rel_path, import_context, extracted);
    }
}

fn register_rust_path_import(
    path_and_alias: &str,
    rel_path: &str,
    import_context: &ImportResolutionContext,
    extracted: &mut ExtractedImports,
) {
    let normalized = path_and_alias.trim();
    if normalized.is_empty() {
        return;
    }
    let (path, alias) = split_alias(normalized);
    if let Some(local_target) = import_context.rust_import_candidate(rel_path, path) {
        let local_alias = alias
            .map(ToOwned::to_owned)
            .unwrap_or_else(|| local_target.callee_name.clone());
        if !local_alias.is_empty() {
            extracted.bindings.bare.remove(&local_alias);
            extracted
                .bindings
                .local_bare
                .insert(local_alias, local_target);
        }
        return;
    }

    let segments: Vec<&str> = path.split("::").filter(|part| !part.is_empty()).collect();
    let Some(root) = segments.first().copied() else {
        return;
    };
    if !is_external_rust_root(root, import_context) {
        return;
    }

    extracted.bindings.external_roots.insert(
        root.to_string(),
        ExternalRootBinding {
            module: root.to_string(),
            module_from_qualifier: true,
        },
    );

    let Some(imported_name) = segments.last().copied() else {
        return;
    };
    let local_alias = alias.unwrap_or(imported_name);
    if local_alias.is_empty() {
        return;
    }

    let module = if segments.len() > 1 {
        segments[..segments.len() - 1].join("::")
    } else {
        root.to_string()
    };
    extracted.bindings.bare.insert(
        local_alias.to_string(),
        ExternalImportBinding {
            module: module.clone(),
            callee_name: imported_name.to_string(),
        },
    );
    extracted
        .bindings
        .member
        .insert(local_alias.to_string(), path.to_string());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn non_import_go_statement_does_not_record_raw_import() {
        let mut extracted = ExtractedImports::default();
        let result = parse_go_import_statement(
            "package main",
            "main.go",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert!(result.is_err());
        assert!(extracted.imports.is_empty());
    }

    #[test]
    fn non_use_rust_statement_does_not_record_raw_import() {
        let mut extracted = ExtractedImports::default();
        parse_rust_import_statement(
            "mod tests;",
            "lib.rs",
            &ImportResolutionContext::default(),
            &mut extracted,
        );

        assert!(extracted.imports.is_empty());
    }
}