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