codelens_engine/call_graph/
resolve.rs1use crate::import_graph::GraphCache;
2use crate::project::{ProjectRoot, collect_files};
3use anyhow::Result;
4use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use super::js_imports::{JSImportBindingIndex, is_import_sensitive_path};
9use super::language::{best_path_proximity_candidate, call_language_for_path, same_call_language};
10use super::types::CallEdge;
11
12fn symbol_defined_in(
13 symbol_index: &HashMap<String, Vec<String>>,
14 symbol_name: &str,
15 file: &str,
16) -> bool {
17 symbol_index
18 .get(symbol_name)
19 .map(|defs| defs.iter().any(|def| def == file))
20 .unwrap_or(false)
21}
22
23fn resolve_reexport_target(
24 import_bindings: Option<&JSImportBindingIndex>,
25 symbol_index: &HashMap<String, Vec<String>>,
26 resolved_file: &str,
27 canonical_name: &str,
28) -> Option<(String, String)> {
29 let reexport_binding = import_bindings
30 .and_then(|index| index.get(resolved_file))
31 .and_then(|bindings| bindings.get(canonical_name).or_else(|| bindings.get("*")))?;
32 let reexport_file = reexport_binding.resolved_file.as_ref()?;
33 let reexport_name = match reexport_binding.imported_name.as_deref() {
34 Some("*") => canonical_name,
35 Some(name) => name,
36 None => canonical_name,
37 };
38 if symbol_defined_in(symbol_index, reexport_name, reexport_file) {
39 Some((reexport_file.clone(), reexport_name.to_owned()))
40 } else {
41 None
42 }
43}
44
45fn resolve_namespace_reexport_target(
46 import_bindings: Option<&JSImportBindingIndex>,
47 symbol_index: &HashMap<String, Vec<String>>,
48 resolved_file: &str,
49 namespace_name: &str,
50 callee_name: &str,
51) -> Option<(String, String)> {
52 let namespace_binding = import_bindings
53 .and_then(|index| index.get(resolved_file))
54 .and_then(|bindings| bindings.get(namespace_name))?;
55 if namespace_binding.imported_name.as_deref() != Some("*") {
56 return None;
57 }
58 let namespace_file = namespace_binding.resolved_file.as_ref()?;
59 if symbol_defined_in(symbol_index, callee_name, namespace_file) {
60 return Some((namespace_file.clone(), callee_name.to_owned()));
61 }
62 resolve_reexport_target(import_bindings, symbol_index, namespace_file, callee_name)
63}
64
65pub(crate) fn collect_candidate_files(root: &Path) -> Result<Vec<PathBuf>> {
66 collect_files(root, |path| call_language_for_path(path).is_some())
67}
68pub(crate) fn maybe_import_graph(
69 project: &ProjectRoot,
70 files: &[PathBuf],
71 graph_cache: Option<&GraphCache>,
72) -> Option<Arc<HashMap<String, crate::import_graph::FileNode>>> {
73 let cache = graph_cache?;
74 let needs_import_graph = files.iter().any(|file| {
75 let relative = project.to_relative(file);
76 crate::import_graph::supports_import_graph(&relative)
77 });
78 if !needs_import_graph {
79 return None;
80 }
81 let mut graph = crate::import_graph::build_graph_pub(project, cache)
82 .map(|graph| (*graph).clone())
83 .unwrap_or_default();
84
85 for file in files {
86 let relative = project.to_relative(file);
87 if !crate::import_graph::supports_import_graph(&relative) {
88 continue;
89 }
90 let needs_patch = graph
91 .get(&relative)
92 .map(|node| node.imports.is_empty())
93 .unwrap_or(true);
94 if !needs_patch {
95 continue;
96 }
97
98 let imports: HashSet<String> = crate::import_graph::extract_imports_for_file(file)
99 .into_iter()
100 .filter_map(|module| {
101 crate::import_graph::resolve_module_for_file(project, file, &module)
102 })
103 .collect();
104 let entry =
105 graph
106 .entry(relative.clone())
107 .or_insert_with(|| crate::import_graph::FileNode {
108 imports: HashSet::new(),
109 imported_by: HashSet::new(),
110 });
111 entry.imports = imports.clone();
112
113 for imported_file in imports {
114 graph
115 .entry(imported_file)
116 .or_insert_with(|| crate::import_graph::FileNode {
117 imports: HashSet::new(),
118 imported_by: HashSet::new(),
119 })
120 .imported_by
121 .insert(relative.clone());
122 }
123 }
124
125 if graph.is_empty() {
126 None
127 } else {
128 Some(Arc::new(graph))
129 }
130}
131pub(crate) fn resolve_call_edges(
136 edges: &mut [CallEdge],
137 project: &ProjectRoot,
138 import_graph: Option<&HashMap<String, crate::import_graph::FileNode>>,
139 import_bindings: Option<&JSImportBindingIndex>,
140) {
141 let db_path = crate::db::index_db_path(project.as_path());
143 let symbol_index: HashMap<String, Vec<String>> = crate::db::IndexDb::open(&db_path)
144 .and_then(|db| {
145 let all = db.all_symbol_names()?;
146 let mut map: HashMap<String, Vec<String>> = HashMap::new();
147 for (name, _kind, file, _line, _signature, _name_path) in all {
148 map.entry(name).or_default().push(file);
149 }
150 Ok(map)
151 })
152 .unwrap_or_default();
153
154 for edge in edges.iter_mut() {
155 if edge.confidence > 0.0 {
156 continue; }
158
159 let callee = &edge.callee_name;
160 let caller_file = &edge.caller_file;
161 let has_imported_namespace_qualifier = edge
162 .callee_qualifier
163 .as_deref()
164 .and_then(|qualifier| {
165 import_bindings
166 .and_then(|index| index.get(caller_file))
167 .and_then(|bindings| bindings.get(qualifier))
168 })
169 .map(|binding| binding.imported_name.as_deref() == Some("*"))
170 .unwrap_or(false);
171
172 if !has_imported_namespace_qualifier
174 && symbol_defined_in(&symbol_index, callee, caller_file)
175 {
176 edge.resolved_file = Some(caller_file.clone());
177 edge.confidence = 0.90;
178 edge.resolution_strategy = Some("same_file");
179 continue;
180 }
181
182 if let Some(namespace_binding) = edge.callee_qualifier.as_deref().and_then(|qualifier| {
184 import_bindings
185 .and_then(|index| index.get(caller_file))
186 .and_then(|bindings| bindings.get(qualifier))
187 }) && let Some(resolved_file) = namespace_binding.resolved_file.as_ref()
188 {
189 match namespace_binding.imported_name.as_deref() {
190 Some("*") => {
191 if symbol_defined_in(&symbol_index, callee, resolved_file) {
192 edge.resolved_file = Some(resolved_file.clone());
193 edge.confidence = 0.95;
194 edge.resolution_strategy = Some("import_map");
195 edge.canonical_callee_name = Some(callee.clone());
196 continue;
197 }
198 if let Some((reexport_file, reexport_name)) = resolve_reexport_target(
199 import_bindings,
200 &symbol_index,
201 resolved_file,
202 callee,
203 ) {
204 edge.resolved_file = Some(reexport_file);
205 edge.confidence = 0.93;
206 edge.resolution_strategy = Some("import_reexport_map");
207 edge.canonical_callee_name = Some(reexport_name);
208 continue;
209 }
210 }
211 Some(namespace_name) => {
212 if let Some((reexport_file, reexport_name)) = resolve_namespace_reexport_target(
213 import_bindings,
214 &symbol_index,
215 resolved_file,
216 namespace_name,
217 callee,
218 ) {
219 edge.resolved_file = Some(reexport_file);
220 edge.confidence = 0.93;
221 edge.resolution_strategy = Some("import_reexport_map");
222 edge.canonical_callee_name = Some(reexport_name);
223 continue;
224 }
225 }
226 None => {}
227 }
228 }
229
230 if let Some(binding) = import_bindings
231 .and_then(|index| index.get(caller_file))
232 .and_then(|bindings| bindings.get(callee))
233 && let Some(resolved_file) = binding.resolved_file.as_ref()
234 {
235 let canonical_name = binding.imported_name.as_deref().unwrap_or(callee);
236 if symbol_defined_in(&symbol_index, canonical_name, resolved_file) {
237 edge.resolved_file = Some(resolved_file.clone());
238 edge.confidence = 0.95;
239 edge.resolution_strategy = Some("import_map");
240 edge.canonical_callee_name = Some(canonical_name.to_owned());
241 continue;
242 }
243 if let Some((reexport_file, reexport_name)) = resolve_reexport_target(
244 import_bindings,
245 &symbol_index,
246 resolved_file,
247 canonical_name,
248 ) {
249 edge.resolved_file = Some(reexport_file);
250 edge.confidence = 0.93;
251 edge.resolution_strategy = Some("import_reexport_map");
252 edge.canonical_callee_name = Some(reexport_name);
253 continue;
254 }
255 }
256
257 if let Some(graph) = import_graph
258 && let Some(node) = graph.get(caller_file)
259 {
260 for imported_file in &node.imports {
261 if let Some(defs) = symbol_index.get(callee)
263 && defs.iter().any(|f| f == imported_file)
264 {
265 edge.resolved_file = Some(imported_file.clone());
266 edge.confidence = 0.95;
267 edge.resolution_strategy = Some("import_map");
268 edge.canonical_callee_name = Some(callee.clone());
269 break;
270 }
271 }
272 }
273 if edge.confidence > 0.0 {
274 continue;
275 }
276
277 if let Some(graph) = import_graph
279 && let Some(node) = graph.get(caller_file)
280 && let Some(defs) = symbol_index.get(callee)
281 {
282 for def_file in defs {
284 if node.imports.iter().any(|imp| {
285 def_file.ends_with(imp)
287 || def_file.ends_with(&format!("/{imp}"))
288 || imp.ends_with(def_file)
289 || imp.ends_with(&format!("/{def_file}"))
290 }) {
291 edge.resolved_file = Some(def_file.clone());
292 edge.confidence = 0.70;
293 edge.resolution_strategy = Some("import_suffix");
294 edge.canonical_callee_name = Some(callee.clone());
295 break;
296 }
297 }
298 }
299 if edge.confidence > 0.0 {
300 continue;
301 }
302
303 if let Some(defs) = symbol_index.get(callee) {
306 let same_lang_defs: Vec<&String> = defs
307 .iter()
308 .filter(|def| same_call_language(caller_file, def))
309 .collect();
310 if same_lang_defs.len() == 1 {
311 let def = same_lang_defs[0];
312 edge.resolved_file = Some(def.clone());
313 if is_import_sensitive_path(caller_file) && def.as_str() != caller_file.as_str() {
314 edge.confidence = 0.50;
315 edge.resolution_strategy = Some("path_proximity");
316 } else {
317 edge.confidence = 0.65;
318 edge.resolution_strategy = Some("unique_name");
319 }
320 continue;
321 }
322 }
323
324 if let Some(defs) = symbol_index.get(callee)
326 && !defs.is_empty()
327 && let Some(best) = best_path_proximity_candidate(caller_file, defs)
328 {
329 edge.resolved_file = Some(best.clone());
330 edge.confidence = 0.50;
331 edge.resolution_strategy = Some("path_proximity");
332 continue;
333 }
334
335 edge.confidence = 0.25;
337 edge.resolution_strategy = Some("unresolved");
338 }
339}