1use std::collections::{BTreeSet, HashMap};
2use std::path::Path;
3
4use protobuf::Message;
5use scip::types::Index;
6use sinter_core::{Edge, Node, NodeId, Reference, Relation, Span, SymbolKind};
7
8use crate::resolver::Binding;
9
10pub struct ScipResolution {
14 pub bindings: Vec<Binding>,
16 pub external: Vec<Binding>,
20 pub external_nodes: Vec<Node>,
23 pub external_skipped: usize,
27 pub unanchored: Vec<Edge>,
32}
33
34#[derive(Debug, thiserror::Error)]
35pub enum ScipError {
36 #[error("cannot read SCIP index {path}: {source}")]
37 Io {
38 path: String,
39 source: std::io::Error,
40 },
41 #[error("cannot parse SCIP index {path}: {source}")]
42 Parse {
43 path: String,
44 source: protobuf::Error,
45 },
46}
47
48pub fn load_index(path: &Path) -> Result<Index, ScipError> {
49 let bytes = std::fs::read(path).map_err(|source| ScipError::Io {
50 path: path.display().to_string(),
51 source,
52 })?;
53 Index::parse_from_bytes(&bytes).map_err(|source| ScipError::Parse {
54 path: path.display().to_string(),
55 source,
56 })
57}
58
59pub fn prefix_index_paths(path: &Path, prefix: &str) -> Result<(), ScipError> {
64 if prefix.is_empty() {
65 return Ok(());
66 }
67 let mut index = load_index(path)?;
68 let prefix = prefix.trim_end_matches('/');
69 for document in &mut index.documents {
70 let relative = document.relative_path.replace('\\', "/");
71 if !Path::new(&relative).is_absolute()
72 && relative != prefix
73 && !relative.starts_with(&format!("{prefix}/"))
74 {
75 document.relative_path = format!("{prefix}/{relative}");
76 } else {
77 document.relative_path = relative;
78 }
79 }
80 let bytes = index.write_to_bytes().map_err(|source| ScipError::Parse {
81 path: path.display().to_string(),
82 source,
83 })?;
84 std::fs::write(path, bytes).map_err(|source| ScipError::Io {
85 path: path.display().to_string(),
86 source,
87 })
88}
89
90pub fn merge_index_files(paths: &[&Path], out: &Path) -> Result<(), ScipError> {
94 let (first, rest) = paths.split_first().expect("at least one index");
95 let mut merged = load_index(first)?;
96 for path in rest {
97 let mut index = load_index(path)?;
98 merged.documents.append(&mut index.documents);
99 merged.external_symbols.append(&mut index.external_symbols);
100 }
101 let bytes = merged.write_to_bytes().map_err(|source| ScipError::Parse {
102 path: out.display().to_string(),
103 source,
104 })?;
105 std::fs::write(out, bytes).map_err(|source| ScipError::Io {
106 path: out.display().to_string(),
107 source,
108 })
109}
110
111pub fn resolve_with_index(
120 index: &Index,
121 nodes: &[Node],
122 references: &[Reference],
123 scope: &BTreeSet<String>,
124 mut read_source: impl FnMut(&str) -> Option<String>,
125) -> ScipResolution {
126 let mut line_starts: HashMap<String, Vec<u64>> = HashMap::new();
127 for document in &index.documents {
128 if let Some(source) = read_source(&document.relative_path) {
129 let mut starts = vec![0u64];
130 for (i, b) in source.bytes().enumerate() {
131 if b == b'\n' {
132 starts.push(i as u64 + 1);
133 }
134 }
135 line_starts.insert(document.relative_path.clone(), starts);
136 }
137 }
138 let to_byte = |file: &str, line: i32, col: i32| -> Option<u64> {
139 let starts = line_starts.get(file)?;
140 Some(starts.get(line as usize)? + col as u64)
141 };
142
143 let mut nodes_by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
145 for node in nodes {
146 nodes_by_file
147 .entry(node.file.as_str())
148 .or_default()
149 .push(node);
150 }
151 let mut def_of_symbol: HashMap<&str, &Node> = HashMap::new();
158 let mut ambiguous: std::collections::HashSet<&str> = std::collections::HashSet::new();
159 let mut same_file_def: HashMap<(&str, &str), &Node> = HashMap::new();
160 for document in &index.documents {
161 for occ in &document.occurrences {
162 if occ.symbol_roles & scip::types::SymbolRole::Definition as i32 == 0 {
163 continue;
164 }
165 if occ.symbol.starts_with("local ") {
168 continue;
169 }
170 let Some(pos) = occ
171 .range
172 .first()
173 .zip(occ.range.get(1))
174 .and_then(|(l, c)| to_byte(&document.relative_path, *l, *c))
175 else {
176 continue;
177 };
178 let target = nodes_by_file
179 .get(document.relative_path.as_str())
180 .into_iter()
181 .flatten()
182 .filter(|n| span_contains(n.span, pos))
183 .min_by_key(|n| n.span.end - n.span.start);
184 if let Some(node) = target {
185 same_file_def.insert((&occ.symbol, &document.relative_path), node);
186 match def_of_symbol.entry(&occ.symbol) {
187 std::collections::hash_map::Entry::Vacant(e) => {
188 e.insert(node);
189 }
190 std::collections::hash_map::Entry::Occupied(e) => {
191 if e.get().file != node.file {
192 ambiguous.insert(&occ.symbol);
193 }
194 }
195 }
196 }
197 }
198 }
199
200 let mut refs_by_file: HashMap<&str, Vec<(usize, &Reference)>> = HashMap::new();
202 for (i, r) in references.iter().enumerate() {
203 refs_by_file
204 .entry(r.file.as_str())
205 .or_default()
206 .push((i, r));
207 }
208 let mut best: HashMap<usize, (u64, Edge)> = HashMap::new();
214 let mut best_ext: HashMap<usize, (u64, Edge)> = HashMap::new();
215 let mut dep_cache: HashMap<String, Option<Node>> = HashMap::new();
217 let mut skipped: std::collections::HashSet<String> = std::collections::HashSet::new();
218 let mut unanchored: Vec<Edge> = Vec::new();
219 for document in &index.documents {
220 let file = document.relative_path.as_str();
221 let in_scope = scope.contains(file);
222 let file_refs = refs_by_file.get(file).map_or(&[][..], Vec::as_slice);
223 if file_refs.is_empty() && !in_scope {
224 continue;
225 }
226 for occ in &document.occurrences {
227 if occ.symbol_roles & scip::types::SymbolRole::Definition as i32 != 0
228 || occ.symbol.starts_with("local ")
229 {
230 continue;
231 }
232 let target = if ambiguous.contains(occ.symbol.as_str()) {
233 same_file_def.get(&(occ.symbol.as_str(), document.relative_path.as_str()))
234 } else {
235 def_of_symbol.get(occ.symbol.as_str())
236 };
237 let Some(pos) = occ
238 .range
239 .first()
240 .zip(occ.range.get(1))
241 .and_then(|(l, c)| to_byte(&document.relative_path, *l, *c))
242 else {
243 continue;
244 };
245 let mut anchored = false;
246 for (i, r) in file_refs {
247 if !span_contains(r.span, pos) {
248 continue;
249 }
250 anchored = true;
251 let distance = pos - r.span.start;
252 let make_edge = |dst: NodeId| Edge {
253 src: r
254 .enclosing
255 .clone()
256 .unwrap_or_else(|| NodeId::new(r.file.clone())),
257 dst,
258 relation: r.relation,
259 evidence: sinter_core::Evidence::Scip,
260 confidence: sinter_core::Evidence::Scip.confidence(),
261 site: Some(r.span),
262 };
263 match target {
264 Some(target) => {
265 if best.get(i).is_none_or(|(d, _)| distance > *d) {
266 best.insert(*i, (distance, make_edge(target.id.clone())));
267 }
268 }
269 None => {
273 let node = dep_cache
274 .entry(occ.symbol.clone())
275 .or_insert_with(|| dep_node(&occ.symbol));
276 match node {
277 Some(node) => {
278 if best_ext.get(i).is_none_or(|(d, _)| distance > *d) {
279 best_ext.insert(*i, (distance, make_edge(node.id.clone())));
280 }
281 }
282 None => {
283 skipped.insert(occ.symbol.clone());
284 }
285 }
286 }
287 }
288 }
289 if anchored || !in_scope {
290 continue;
291 }
292 let (Some(target), Some(src)) = (
295 target,
296 nodes_by_file
297 .get(file)
298 .into_iter()
299 .flatten()
300 .filter(|n| span_contains(n.span, pos))
301 .min_by_key(|n| n.span.end - n.span.start),
302 ) else {
303 continue;
304 };
305 let end = match occ.range.as_slice() {
306 [_, _, c] => to_byte(file, occ.range[0], *c),
307 [_, _, l, c] => to_byte(file, *l, *c),
308 _ => None,
309 }
310 .unwrap_or(pos);
311 unanchored.push(Edge {
312 src: src.id.clone(),
313 dst: target.id.clone(),
314 relation: match target.kind {
315 SymbolKind::Function | SymbolKind::Method => Relation::Calls,
316 _ => Relation::Uses,
317 },
318 evidence: sinter_core::Evidence::Scip,
319 confidence: sinter_core::Evidence::Scip.confidence(),
320 site: Some(Span { start: pos, end }),
321 });
322 }
323 }
324 let mut external_nodes: HashMap<String, Node> = HashMap::new();
325 for node in dep_cache.into_values().flatten() {
326 external_nodes.insert(node.id.as_str().to_string(), node);
327 }
328 ScipResolution {
329 bindings: best
330 .into_iter()
331 .map(|(reference, (_, edge))| Binding { edge, reference })
332 .collect(),
333 external: best_ext
334 .into_iter()
335 .map(|(reference, (_, edge))| Binding { edge, reference })
336 .collect(),
337 external_nodes: external_nodes.into_values().collect(),
338 external_skipped: skipped.len(),
339 unanchored,
340 }
341}
342
343fn dep_node(symbol: &str) -> Option<Node> {
358 let mut parts = symbol.splitn(5, ' ');
359 let _scheme = parts.next()?;
360 let manager = parts.next()?;
361 let package = parts.next()?;
362 let version = parts.next()?;
363 let descriptors = parts.next()?;
364 if manager == "."
365 || package == "."
366 || package.is_empty()
367 || version == "."
368 || version.is_empty()
369 {
370 return None;
371 }
372 let (path, kind) = parse_descriptors(descriptors)?;
373 let file = format!("dep:{package}@{version}");
374 let head = package.replace('-', "_");
375 let name = path.last().cloned().unwrap_or_else(|| head.clone());
376 let qualified = std::iter::once(head)
377 .chain(path)
378 .collect::<Vec<_>>()
379 .join("::");
380 Some(Node {
381 id: NodeId::new(format!("{file}#{qualified}@0")),
382 kind,
383 name,
384 file,
385 span: Span { start: 0, end: 0 },
386 signature: String::new(),
387 doc: None,
388 })
389}
390
391fn parse_descriptors(desc: &str) -> Option<(Vec<String>, SymbolKind)> {
396 let mut segs: Vec<(String, char)> = Vec::new();
399 let mut cur = String::new();
400 let mut chars = desc.chars();
401 while let Some(c) = chars.next() {
402 match c {
403 '`' => loop {
404 match chars.next()? {
405 '`' => break,
406 escaped => cur.push(escaped),
407 }
408 },
409 '/' | '#' | '.' | '!' | ':' => {
410 if cur.is_empty() {
411 return None;
412 }
413 segs.push((std::mem::take(&mut cur), c));
414 }
415 '(' => {
416 if cur.is_empty() {
417 return None;
419 }
420 loop {
421 if chars.next()? == ')' {
422 break;
423 }
424 }
425 if chars.next() != Some('.') {
426 return None;
427 }
428 segs.push((std::mem::take(&mut cur), 'm'));
429 }
430 '[' => return None,
431 _ => cur.push(c),
432 }
433 }
434 if !cur.is_empty() || segs.is_empty() {
435 return None;
436 }
437 let kind = match segs.last()?.1 {
438 'm' if segs.len() >= 2 && segs[segs.len() - 2].1 == '#' => SymbolKind::Method,
439 'm' => SymbolKind::Function,
440 '#' => SymbolKind::Struct,
441 '/' => SymbolKind::Module,
442 '!' => SymbolKind::Macro,
443 '.' => SymbolKind::Constant,
444 _ => return None,
446 };
447 Some((segs.into_iter().map(|(s, _)| s).collect(), kind))
448}
449
450fn span_contains(span: Span, pos: u64) -> bool {
451 span.start <= pos && pos < span.end
452}