Skip to main content

gobby_code/index/import_resolution/
context.rs

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use super::js_local::js_candidate_files;
5use super::predicates::ruby_require_root;
6use super::rust_local::{rust_candidate_files, rust_import_target, rust_qualified_call_target};
7
8mod apple;
9mod bindings;
10mod dotnet;
11mod elixir;
12mod jvm;
13mod package_metadata;
14mod python;
15mod scripting;
16
17pub(super) use bindings::JS_BUILTIN_MODULES;
18pub(crate) use bindings::{
19    ExternalCallTarget, ExternalImportBinding, ExternalRootBinding, ExtractedImports,
20    ImportBindings, LocalCallBinding,
21};
22pub(super) use package_metadata::{
23    build_go_package_files, load_dart_external_packages, load_dart_self_package_name,
24    load_go_module_path, load_js_external_packages, load_js_self_package_name,
25    load_rust_external_crates, load_rust_self_crate_name,
26};
27pub(super) use python::{build_python_module_index, python_candidate_files};
28
29pub(super) use apple::build_swift_module_files;
30use apple::{build_objc_indexes, objc_relative_import_file, swift_modules_for_rel};
31use dotnet::build_csharp_index;
32pub(super) use elixir::build_elixir_local_module_files;
33#[cfg(test)]
34pub(super) use elixir::load_elixir_dependency_names;
35use elixir::{build_elixir_local_module_roots, load_elixir_external_roots};
36use jvm::{build_java_class_index, build_kotlin_package_files, build_scala_package_files};
37pub(super) use scripting::build_php_symbol_files;
38use scripting::{build_lua_module_files, build_ruby_constant_files};
39
40#[derive(Debug, Clone, Default)]
41pub struct ImportResolutionContext {
42    pub(super) python_modules: HashSet<String>,
43    pub(super) js_external_packages: HashSet<String>,
44    pub(super) js_self_package_name: Option<String>,
45    pub(super) go_module_path: Option<String>,
46    /// Maps a project-relative package directory to the Go source files it
47    /// contains. A local selector call `pkg.Fn()` resolves `Fn` against any
48    /// file in the imported package's directory (Go packages are
49    /// directory-granular).
50    pub(super) go_package_files: HashMap<String, Vec<String>>,
51    pub(super) rust_external_crates: HashSet<String>,
52    pub(super) rust_self_crate_name: Option<String>,
53    pub(super) java_local_classes: HashSet<String>,
54    /// Maps a locally-declared fully-qualified class name (`pkg.Class`) to the
55    /// project-relative files that declare it. A local single-type import
56    /// `import pkg.Class;` resolves `Class.m()`/`new Class()` against these
57    /// files (Java classes are file-granular per the public-class convention).
58    pub(super) java_class_files: HashMap<String, Vec<String>>,
59    pub(super) csharp_local_roots: HashSet<String>,
60    /// Maps a locally-declared C# type's fully-qualified name (`Ns.Type`) to the
61    /// project-relative files declaring it. A member call resolves its type
62    /// against these files: a fully-qualified `Ns.Type.M()`, an aliased
63    /// `using X = Ns.Type;` then `X.M()`, or a namespace-imported `using Ns;`
64    /// then `Type.M()`. The post-write DB pass narrows to the real symbol.
65    pub(super) csharp_type_files: HashMap<String, Vec<String>>,
66    /// Maps a locally-declared Kotlin package (`com.example`) to the
67    /// project-relative files declaring it. A local import `import com.example.X`
68    /// resolves `X()`/`X.m()` against these files. Kotlin is package-granular:
69    /// file names need not match declared types, and top-level functions live at
70    /// package scope, so a name can be in any file of its package. The post-write
71    /// DB pass narrows to the real symbol.
72    pub(super) kotlin_package_files: HashMap<String, Vec<String>>,
73    /// Maps a locally-declared Scala package (`com.example`) to the
74    /// project-relative files declaring it. Scala imports name a package member
75    /// (`import com.example.Widget`) or selector group
76    /// (`import com.example.{render, Widget}`); either resolves against every
77    /// `.scala`/`.sc` file in the package, then the post-write DB pass narrows to
78    /// the real symbol.
79    pub(super) scala_package_files: HashMap<String, Vec<String>>,
80    /// Maps a local Lua module name (`foo.bar`) to project-relative `.lua` files
81    /// that may satisfy `require("foo.bar")`. Common source roots (`lua/`,
82    /// `src/`) and `init.lua` package modules are normalized into the same key.
83    /// Member calls through a require alias resolve against these files, then
84    /// the post-write DB pass narrows to the real symbol.
85    pub(super) lua_module_files: HashMap<String, Vec<String>>,
86    /// Maps Objective-C local import specifiers (`Widget.h`,
87    /// `Sources/App/Widget.h`) to project-relative `.h`/`.m`/`.mm` files. The
88    /// parser also resolves relative quoted imports directly from the caller's
89    /// path; this map covers project-style include paths and basename imports.
90    pub(super) objc_import_files: HashMap<String, Vec<String>>,
91    /// Maps project-relative Objective-C files to the class/protocol names they
92    /// declare. A `#import "Widget.h"` can then bind receiver type `Widget` to
93    /// that header's candidate file without guessing from the import basename.
94    pub(super) objc_file_types: HashMap<String, Vec<String>>,
95    /// Maps project-relative Objective-C files to C function names they declare
96    /// or define. A local `#import` seeds exact bare-call bindings from this
97    /// map, so unrelated imported headers do not become candidates.
98    pub(super) objc_file_functions: HashMap<String, Vec<String>>,
99    pub(super) php_local_symbols: HashSet<String>,
100    /// Maps a locally-declared PHP symbol name to the project-relative files that
101    /// declare it. Both the bare name (`Widget`, `helper`) and the
102    /// namespace-qualified name (`App\Widget`) are keyed, lowercased because PHP
103    /// class/function names are case-insensitive. A `use`-imported local class
104    /// resolves `Widget::m()` / `new Widget()` against these files, and a
105    /// fully-qualified `\Ns\Class::method()` resolves its class path here. The
106    /// post-write DB pass narrows the candidates to the real symbol.
107    pub(super) php_symbol_files: HashMap<String, Vec<String>>,
108    pub(super) ruby_local_constant_roots: HashSet<String>,
109    /// Maps a locally-declared Ruby constant root (`Widget`, the first `::`
110    /// segment of a `class`/`module` name) to the project-relative files that
111    /// declare it. A member call `Widget.build`/`Widget.new` resolves against
112    /// these files; the post-write DB pass narrows to the real symbol. Ruby
113    /// `require` does not import names, so resolution is constant-driven rather
114    /// than import-driven.
115    pub(super) ruby_constant_files: HashMap<String, Vec<String>>,
116    pub(super) ruby_require_root_overrides: HashMap<String, String>,
117    pub(super) swift_local_modules: HashSet<String>,
118    /// Maps a local Swift module name to the project-relative `.swift` files that
119    /// belong to it. Swift has whole-module scope: files in a module call each
120    /// other with no `import`, so an unqualified call resolves against every file
121    /// sharing the caller's module. The post-write DB pass narrows the callee
122    /// name to the real symbol. The keys are exactly `swift_local_modules`.
123    pub(super) swift_module_files: HashMap<String, Vec<String>>,
124    pub(super) dart_external_packages: HashSet<String>,
125    pub(super) dart_self_package_name: Option<String>,
126    pub(super) elixir_external_roots: HashMap<String, String>,
127    pub(super) elixir_external_root_overrides: HashMap<String, String>,
128    pub(super) elixir_local_module_roots: HashSet<String>,
129    /// Maps a locally-declared Elixir module's fully-qualified name (`App.Foo`)
130    /// to the project-relative `.ex`/`.exs` files that declare it (via
131    /// `defmodule`). A fully-qualified `App.Foo.func()` call resolves `App.Foo`
132    /// here, and a local `alias App.Foo` / `import App.Foo` seeds the alias and
133    /// bare channels from these files. Modules need not follow the
134    /// path-from-name convention, so the map is built by scanning `defmodule`
135    /// headers. The post-write DB pass narrows the candidates to the real
136    /// symbol.
137    pub(super) elixir_module_files: HashMap<String, Vec<String>>,
138}
139
140impl ImportResolutionContext {
141    /// Candidate target files for a JavaScript/TypeScript import `specifier`
142    /// resolved relative to `rel_path`, derived by pure path logic (no file
143    /// reads). The post-write pass narrows these against the indexed symbols.
144    pub(super) fn js_candidate_files(&self, rel_path: &str, specifier: &str) -> Vec<String> {
145        js_candidate_files(rel_path, self.js_self_package_name.as_deref(), specifier)
146    }
147
148    /// Local-call binding for a Rust `use` path import (e.g. `use crate::m::f`),
149    /// resolved to candidate module files without reading them. `None` when the
150    /// path does not name a local module.
151    pub(super) fn rust_import_candidate(
152        &self,
153        rel_path: &str,
154        path: &str,
155    ) -> Option<LocalCallBinding> {
156        let target = rust_import_target(
157            rel_path,
158            self.rust_self_crate_name.as_deref(),
159            &self.rust_external_crates,
160            path,
161        )?;
162        Some(LocalCallBinding::named(
163            rust_candidate_files(&target.source_root, &target.module),
164            target.name,
165        ))
166    }
167
168    /// Local-call binding for a path-qualified Rust call without a `use`
169    /// (e.g. `crate::m::f()`), resolved against the module tree by path logic.
170    pub(super) fn rust_qualified_candidate(
171        &self,
172        rel_path: &str,
173        qualifier_path: &str,
174        name: &str,
175    ) -> Option<LocalCallBinding> {
176        let target = rust_qualified_call_target(
177            rel_path,
178            self.rust_self_crate_name.as_deref(),
179            &self.rust_external_crates,
180            qualifier_path,
181            name,
182        )?;
183        Some(LocalCallBinding::named(
184            rust_candidate_files(&target.source_root, &target.module),
185            target.name,
186        ))
187    }
188
189    /// Candidate target files for a local Go package `module` (a self-module
190    /// import path), found by mapping the import path to its package directory.
191    /// Go packages are directory-granular, so this returns every indexed Go
192    /// file in that directory; the post-write pass narrows to the real symbol.
193    /// Empty when `module` is external or names no indexed package directory.
194    pub(super) fn go_candidate_files(&self, module: &str) -> Vec<String> {
195        let Some(self_module) = self.go_module_path.as_deref() else {
196            return Vec::new();
197        };
198        let dir = if module == self_module {
199            String::new()
200        } else if let Some(rest) = module.strip_prefix(&format!("{self_module}/")) {
201            rest.to_string()
202        } else {
203            return Vec::new();
204        };
205        self.go_package_files.get(&dir).cloned().unwrap_or_default()
206    }
207
208    /// Project-relative files declaring the local class named by `fqcn` (a
209    /// fully-qualified `pkg.Class`). Empty when no indexed local file declares
210    /// it (e.g. an external class, or a local simple-name collision whose
211    /// fully-qualified form is not actually declared here).
212    pub(super) fn java_candidate_files(&self, fqcn: &str) -> Vec<String> {
213        self.java_class_files.get(fqcn).cloned().unwrap_or_default()
214    }
215
216    /// Project-relative files declaring the local C# type named by `type_path`
217    /// (a fully-qualified `Ns.Type`). Empty when no indexed local file declares
218    /// it (e.g. an external type, or a namespace/type pair not actually present
219    /// here).
220    pub(super) fn csharp_type_files(&self, type_path: &str) -> Vec<String> {
221        self.csharp_type_files
222            .get(type_path)
223            .cloned()
224            .unwrap_or_default()
225    }
226
227    /// Project-relative files declaring the local Kotlin `package`. Empty when no
228    /// indexed local file declares that package (an external import, or an
229    /// unknown package — e.g. a `pkg.Type.member` import whose package portion
230    /// `pkg.Type` is a type, not a declared package).
231    pub(super) fn kotlin_package_files(&self, package: &str) -> Vec<String> {
232        self.kotlin_package_files
233            .get(package)
234            .cloned()
235            .unwrap_or_default()
236    }
237
238    /// Project-relative files declaring the local Scala `package`. Empty when no
239    /// indexed local file declares that package (external import or unknown
240    /// package).
241    pub(super) fn scala_package_files(&self, package: &str) -> Vec<String> {
242        self.scala_package_files
243            .get(package)
244            .cloned()
245            .unwrap_or_default()
246    }
247
248    pub(super) fn lua_module_files(&self, module: &str) -> Vec<String> {
249        self.lua_module_files
250            .get(module)
251            .cloned()
252            .unwrap_or_default()
253    }
254
255    pub(super) fn objc_import_candidate_files(
256        &self,
257        rel_path: &str,
258        import_path: &str,
259    ) -> Vec<String> {
260        let mut files = Vec::new();
261        if let Some(relative) = objc_relative_import_file(rel_path, import_path) {
262            files.push(relative);
263        }
264        if let Some(mapped) = self.objc_import_files.get(import_path) {
265            files.extend(mapped.iter().cloned());
266        }
267        if let Some(name) = Path::new(import_path)
268            .file_name()
269            .and_then(|name| name.to_str())
270            && let Some(mapped) = self.objc_import_files.get(name)
271        {
272            files.extend(mapped.iter().cloned());
273        }
274        files.sort();
275        files.dedup();
276        files
277    }
278
279    pub(super) fn objc_declared_types(&self, rel_path: &str) -> Vec<String> {
280        self.objc_file_types
281            .get(rel_path)
282            .cloned()
283            .unwrap_or_default()
284    }
285
286    pub(super) fn objc_declared_functions(&self, rel_path: &str) -> Vec<String> {
287        self.objc_file_functions
288            .get(rel_path)
289            .cloned()
290            .unwrap_or_default()
291    }
292
293    /// Project-relative files declaring the local PHP class/function named
294    /// `name`. Accepts a bare name or a namespace-qualified name, with or without
295    /// a leading `\`; matching is case-insensitive. Empty when no indexed local
296    /// file declares it (an external/vendor symbol, or an unknown name).
297    pub(super) fn php_candidate_files(&self, name: &str) -> Vec<String> {
298        self.php_symbol_files
299            .get(&name.trim_start_matches('\\').to_ascii_lowercase())
300            .cloned()
301            .unwrap_or_default()
302    }
303
304    /// Project-relative `.swift` files that share a Swift module with `rel_path`
305    /// (including `rel_path` itself). Swift's whole-module scope means an
306    /// unqualified call may target any file in the caller's module; the
307    /// post-write DB pass narrows the callee name to a single symbol. Empty when
308    /// the file belongs to no discovered module.
309    pub(super) fn swift_module_candidate_files(&self, rel_path: &str) -> Vec<String> {
310        let mut files = swift_modules_for_rel(Path::new(rel_path))
311            .iter()
312            .filter_map(|module| self.swift_module_files.get(module))
313            .flatten()
314            .cloned()
315            .collect::<Vec<_>>();
316        files.sort();
317        files.dedup();
318        files
319    }
320
321    pub(super) fn ruby_require_root(&self, required: &str) -> Option<&str> {
322        self.ruby_require_root_overrides
323            .get(required)
324            .map(String::as_str)
325            .or_else(|| ruby_require_root(required))
326    }
327
328    pub(super) fn ruby_constant_files(&self, root: &str) -> Vec<String> {
329        self.ruby_constant_files
330            .get(root)
331            .cloned()
332            .unwrap_or_default()
333    }
334
335    pub(super) fn elixir_external_root_module(&self, root: &str) -> Option<&str> {
336        self.elixir_external_root_overrides
337            .get(root)
338            .or_else(|| self.elixir_external_roots.get(root))
339            .map(String::as_str)
340    }
341
342    /// Project-relative files declaring the local Elixir module named `module`
343    /// (a fully-qualified `App.Foo`). Empty when no indexed local file declares
344    /// it (an external/dependency module, or an unknown name).
345    pub(super) fn elixir_module_files(&self, module: &str) -> Vec<String> {
346        self.elixir_module_files
347            .get(module)
348            .cloned()
349            .unwrap_or_default()
350    }
351}
352
353pub fn build_import_resolution_context(
354    root_path: &Path,
355    candidate_files: &[PathBuf],
356) -> ImportResolutionContext {
357    build_import_resolution_context_with_overrides(
358        root_path,
359        candidate_files,
360        HashMap::new(),
361        HashMap::new(),
362    )
363}
364
365pub fn build_import_resolution_context_with_overrides(
366    root_path: &Path,
367    candidate_files: &[PathBuf],
368    ruby_require_root_overrides: HashMap<String, String>,
369    elixir_external_root_overrides: HashMap<String, String>,
370) -> ImportResolutionContext {
371    let java_index = build_java_class_index(root_path, candidate_files);
372    let csharp_index = build_csharp_index(root_path, candidate_files);
373    let ruby_constant_files = build_ruby_constant_files(root_path, candidate_files);
374    let php_symbol_files = build_php_symbol_files(root_path, candidate_files);
375    let swift_module_files = build_swift_module_files(root_path, candidate_files);
376    let objc_index = build_objc_indexes(root_path, candidate_files);
377    ImportResolutionContext {
378        python_modules: build_python_module_index(root_path, candidate_files),
379        js_external_packages: load_js_external_packages(root_path),
380        js_self_package_name: load_js_self_package_name(root_path),
381        go_module_path: load_go_module_path(root_path),
382        go_package_files: build_go_package_files(root_path, candidate_files),
383        rust_external_crates: load_rust_external_crates(root_path),
384        rust_self_crate_name: load_rust_self_crate_name(root_path),
385        java_local_classes: java_index.local_classes,
386        java_class_files: java_index.class_files,
387        csharp_local_roots: csharp_index.local_roots,
388        csharp_type_files: csharp_index.type_files,
389        kotlin_package_files: build_kotlin_package_files(root_path, candidate_files),
390        scala_package_files: build_scala_package_files(root_path, candidate_files),
391        lua_module_files: build_lua_module_files(root_path, candidate_files),
392        objc_import_files: objc_index.import_files,
393        objc_file_types: objc_index.file_types,
394        objc_file_functions: objc_index.file_functions,
395        php_local_symbols: php_symbol_files.keys().cloned().collect(),
396        php_symbol_files,
397        ruby_local_constant_roots: ruby_constant_files.keys().cloned().collect(),
398        ruby_constant_files,
399        ruby_require_root_overrides,
400        swift_local_modules: swift_module_files.keys().cloned().collect(),
401        swift_module_files,
402        dart_external_packages: load_dart_external_packages(root_path),
403        dart_self_package_name: load_dart_self_package_name(root_path),
404        elixir_external_roots: load_elixir_external_roots(root_path),
405        elixir_external_root_overrides,
406        elixir_local_module_roots: build_elixir_local_module_roots(candidate_files),
407        elixir_module_files: build_elixir_local_module_files(root_path, candidate_files),
408    }
409}