1mod rust;
5mod scope;
6mod typescript;
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use objects::object::{
11 ContentHash, ImportBinding, ImportEntry, OccurrenceEntry, OccurrenceRole, ResolvedSemanticEdge,
12 SemanticEdgeKind, SemanticFileNode, SymbolKindTag, SymbolNamespace,
13};
14
15pub const RESOLVER_VERSION: u32 = 1;
17
18#[derive(Clone, Debug)]
20pub struct RepositorySemanticFile {
21 pub node_hash: ContentHash,
22 pub node: SemanticFileNode,
23}
24
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct FileResolution {
28 pub dependencies: BTreeSet<String>,
30 pub edges: Vec<ResolvedSemanticEdge>,
32}
33
34pub fn resolve_repository(
39 files: &BTreeMap<String, RepositorySemanticFile>,
40) -> BTreeMap<String, FileResolution> {
41 resolve_paths(files, files.keys().map(String::as_str))
42}
43
44pub fn resolve_paths<'a>(
47 files: &BTreeMap<String, RepositorySemanticFile>,
48 paths: impl IntoIterator<Item = &'a str>,
49) -> BTreeMap<String, FileResolution> {
50 paths
51 .into_iter()
52 .filter_map(|path| {
53 let file = files.get(path)?;
54 Some((path.to_string(), resolve_file(path, file, files)))
55 })
56 .collect()
57}
58
59fn resolve_file(
60 path: &str,
61 file: &RepositorySemanticFile,
62 files: &BTreeMap<String, RepositorySemanticFile>,
63) -> FileResolution {
64 let mut dependencies = dependencies_for(path, &file.node, files);
65 let mut edges = file
66 .node
67 .occurrences
68 .iter()
69 .filter(|occurrence| occurrence.role != OccurrenceRole::Definition)
70 .filter_map(|occurrence| resolve_occurrence(path, file, occurrence, files))
71 .collect::<Vec<_>>();
72 edges.sort();
73 edges.dedup();
74 dependencies.extend(
75 edges
76 .iter()
77 .filter(|edge| edge.target_path != *path)
78 .map(|edge| edge.target_path.clone()),
79 );
80 FileResolution {
81 dependencies,
82 edges,
83 }
84}
85
86fn dependencies_for(
87 source_path: &str,
88 source: &SemanticFileNode,
89 files: &BTreeMap<String, RepositorySemanticFile>,
90) -> BTreeSet<String> {
91 source
92 .imports
93 .iter()
94 .filter_map(|import| resolve_module(source_path, source, &import.module_specifier, files))
95 .collect()
96}
97
98fn resolve_occurrence(
99 source_path: &str,
100 source: &RepositorySemanticFile,
101 occurrence: &OccurrenceEntry,
102 files: &BTreeMap<String, RepositorySemanticFile>,
103) -> Option<ResolvedSemanticEdge> {
104 let target = if occurrence.qualifier.is_empty() {
105 definition(files, source_path, &occurrence.name, occurrence.namespace).or_else(|| {
106 if local_definition_shadows(&source.node, occurrence) {
107 None
108 } else {
109 resolve_imported_name(source_path, &source.node, occurrence, files)
110 }
111 })
112 } else {
113 resolve_qualified_name(source_path, &source.node, occurrence, files)
114 }?;
115 Some(ResolvedSemanticEdge {
116 source_occurrence: occurrence.local_id,
117 target_path: target.path,
118 target_file_node: target.file_node,
119 target_definition: target.definition,
120 kind: match occurrence.role {
121 OccurrenceRole::Call => SemanticEdgeKind::Calls,
122 OccurrenceRole::TypeReference => SemanticEdgeKind::TypeRef,
123 OccurrenceRole::Reference => SemanticEdgeKind::RefersTo,
124 OccurrenceRole::Definition => return None,
125 },
126 })
127}
128
129fn resolve_imported_name(
130 source_path: &str,
131 source: &SemanticFileNode,
132 occurrence: &OccurrenceEntry,
133 files: &BTreeMap<String, RepositorySemanticFile>,
134) -> Option<Target> {
135 let candidates = visible_bindings(source, occurrence, &occurrence.name)
136 .filter(|(_, binding)| binding.local == occurrence.name && binding.imported != "*")
137 .filter_map(|(import, binding)| {
138 let path = resolve_module(source_path, source, &import.module_specifier, files)?;
139 definition(files, &path, &binding.imported, occurrence.namespace)
140 })
141 .collect::<BTreeSet<_>>();
142 exactly_one(candidates)
143}
144
145fn resolve_qualified_name(
146 source_path: &str,
147 source: &SemanticFileNode,
148 occurrence: &OccurrenceEntry,
149 files: &BTreeMap<String, RepositorySemanticFile>,
150) -> Option<Target> {
151 let first = occurrence.qualifier.first()?;
152 let imported = visible_bindings(source, occurrence, first)
153 .filter(|(_, binding)| binding.local == *first)
154 .filter(|(_, binding)| binding.imported == "*" || source.language == "rust")
155 .filter_map(|(import, binding)| {
156 let specifier = if source.language == "rust" && binding.imported != "*" {
157 format!("{}::{}", import.module_specifier, binding.imported)
158 } else {
159 import.module_specifier.clone()
160 };
161 resolve_module(source_path, source, &specifier, files)
162 })
163 .collect::<BTreeSet<_>>();
164 let path = exactly_one(imported).or_else(|| {
165 (source.language == "rust" && matches!(first.as_str(), "crate" | "self" | "super"))
166 .then(|| occurrence.qualifier.join("::"))
167 .and_then(|qualifier| resolve_module(source_path, source, &qualifier, files))
168 })?;
169 definition(files, &path, &occurrence.name, occurrence.namespace)
170}
171
172fn visible_bindings<'a>(
173 source: &'a SemanticFileNode,
174 occurrence: &'a OccurrenceEntry,
175 local_name: &'a str,
176) -> impl Iterator<Item = (&'a ImportEntry, &'a ImportBinding)> {
177 let occurrence_namespace = occurrence.namespace;
178 let nearest = source
179 .imports
180 .iter()
181 .filter(|import| {
182 scope::contains(source, import.scope, occurrence.scope)
183 && import.bindings.iter().any(|binding| {
184 binding.local == local_name
185 && namespaces_overlap(binding.namespace, occurrence.namespace)
186 })
187 })
188 .map(|import| scope::depth(source, import.scope))
189 .max();
190 source
191 .imports
192 .iter()
193 .filter(move |import| {
194 nearest.is_some_and(|depth| {
195 scope::contains(source, import.scope, occurrence.scope)
196 && scope::depth(source, import.scope) == depth
197 })
198 })
199 .flat_map(move |import| {
200 import
201 .bindings
202 .iter()
203 .filter(move |binding| namespaces_overlap(binding.namespace, occurrence_namespace))
204 .map(move |binding| (import, binding))
205 })
206}
207
208fn local_definition_shadows(source: &SemanticFileNode, occurrence: &OccurrenceEntry) -> bool {
209 source.occurrences.iter().any(|candidate| {
210 candidate.role == OccurrenceRole::Definition
211 && candidate.name == occurrence.name
212 && namespaces_overlap(candidate.namespace, occurrence.namespace)
213 && candidate.span.start <= occurrence.span.start
214 && scope::contains(source, candidate.scope, occurrence.scope)
215 })
216}
217
218fn resolve_module(
219 source_path: &str,
220 source: &SemanticFileNode,
221 specifier: &str,
222 files: &BTreeMap<String, RepositorySemanticFile>,
223) -> Option<String> {
224 let candidates = match source.language.as_str() {
225 "rust" => rust::module_candidates(source_path, specifier),
226 "typescript" | "javascript" => typescript::module_candidates(source_path, specifier),
227 _ => Vec::new(),
228 };
229 candidates
230 .into_iter()
231 .find(|candidate| files.contains_key(candidate))
232}
233
234#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
235struct Target {
236 path: String,
237 file_node: ContentHash,
238 definition: u32,
239}
240
241fn definition(
242 files: &BTreeMap<String, RepositorySemanticFile>,
243 path: &str,
244 name: &str,
245 namespace: SymbolNamespace,
246) -> Option<Target> {
247 let file = files.get(path)?;
248 let candidates = file
249 .node
250 .symbols
251 .iter()
252 .enumerate()
253 .filter(|(_, symbol)| symbol.container_path.is_empty() && symbol.name == name)
254 .filter(|(_, symbol)| namespaces_overlap(symbol_namespace(symbol.kind), namespace))
255 .map(|(index, _)| Target {
256 path: path.to_string(),
257 file_node: file.node_hash,
258 definition: index as u32,
259 })
260 .collect::<BTreeSet<_>>();
261 exactly_one(candidates)
262}
263
264fn symbol_namespace(kind: SymbolKindTag) -> SymbolNamespace {
265 match kind {
266 SymbolKindTag::Function | SymbolKindTag::Const | SymbolKindTag::Other => {
267 SymbolNamespace::Value
268 }
269 SymbolKindTag::Type
270 | SymbolKindTag::Enum
271 | SymbolKindTag::Trait
272 | SymbolKindTag::Class
273 | SymbolKindTag::Interface
274 | SymbolKindTag::TypeAlias => SymbolNamespace::Type,
275 SymbolKindTag::Module => SymbolNamespace::Both,
276 }
277}
278
279fn namespaces_overlap(a: SymbolNamespace, b: SymbolNamespace) -> bool {
280 a == SymbolNamespace::Both || b == SymbolNamespace::Both || a == b
281}
282
283fn exactly_one<T: Ord>(values: BTreeSet<T>) -> Option<T> {
284 if values.len() == 1 {
285 values.into_iter().next()
286 } else {
287 None
288 }
289}
290
291#[cfg(test)]
292mod tests;