use std::path::Path;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use scip::types::{symbol_information, Index, SymbolRole};
use uuid::Uuid;
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
pub struct ScipRow {
pub symbol: String,
pub role: String,
pub is_definition: bool,
pub display_name: String,
pub kind: String,
pub file: String,
pub start_line: u32,
pub start_col: u32,
}
#[derive(Debug, Default)]
pub struct ScipScan {
pub snapshot_id: Uuid,
pub ts: DateTime<Utc>,
pub repo: String,
pub git_sha: String,
pub rows: Vec<ScipRow>,
}
fn decode_roles(bits: i32) -> (String, bool) {
let mut labels = Vec::new();
let is_def = bits & SymbolRole::Definition as i32 != 0;
if is_def {
labels.push("definition");
}
if bits & SymbolRole::Import as i32 != 0 {
labels.push("import");
}
if bits & SymbolRole::WriteAccess as i32 != 0 {
labels.push("write");
}
if bits & SymbolRole::ReadAccess as i32 != 0 {
labels.push("read");
}
if bits & SymbolRole::Generated as i32 != 0 {
labels.push("generated");
}
if bits & SymbolRole::Test as i32 != 0 {
labels.push("test");
}
if labels.is_empty() {
labels.push("reference");
}
(labels.join("+"), is_def)
}
fn kind_label(kind: symbol_information::Kind) -> &'static str {
use symbol_information::Kind::*;
match kind {
Function => "Function",
Method => "Method",
StaticMethod => "StaticMethod",
Struct => "Struct",
Trait => "Trait",
TraitMethod => "TraitMethod",
Enum => "Enum",
EnumMember => "EnumMember",
Field => "Field",
Module => "Module",
Macro => "Macro",
TypeAlias => "TypeAlias",
Constant => "Constant",
Variable => "Variable",
Parameter => "Parameter",
TypeParameter => "TypeParameter",
AssociatedType => "AssociatedType",
SelfParameter => "SelfParameter",
_ => "Other",
}
}
pub fn ingest_index_file(
index_path: &Path,
repo: &str,
git_sha: &str,
snapshot_id: Uuid,
ts: DateTime<Utc>,
) -> Result<ScipScan> {
let bytes = std::fs::read(index_path)
.with_context(|| format!("reading SCIP index {}", index_path.display()))?;
let index: Index = protobuf_parse(&bytes)?;
Ok(ingest_index(index, repo, git_sha, snapshot_id, ts))
}
fn protobuf_parse(bytes: &[u8]) -> Result<Index> {
use protobuf::Message;
Index::parse_from_bytes(bytes).context("parsing SCIP protobuf Index")
}
pub fn ingest_index(
index: Index,
repo: &str,
git_sha: &str,
snapshot_id: Uuid,
ts: DateTime<Utc>,
) -> ScipScan {
let mut rows = Vec::new();
for doc in &index.documents {
let mut info: std::collections::HashMap<&str, (&'static str, &str)> =
std::collections::HashMap::new();
for si in &doc.symbols {
let kind = si
.kind
.enum_value()
.map(kind_label)
.unwrap_or("Other");
info.insert(si.symbol.as_str(), (kind, si.display_name.as_str()));
}
for occ in &doc.occurrences {
let (line, col) = match occ.range.as_slice() {
[l, c, ..] => (*l, *c),
_ => continue,
};
let (role, is_definition) = decode_roles(occ.symbol_roles);
let (kind, display_name) = info
.get(occ.symbol.as_str())
.map(|(k, d)| ((*k).to_string(), (*d).to_string()))
.unwrap_or_default();
rows.push(ScipRow {
symbol: occ.symbol.clone(),
role,
is_definition,
display_name,
kind,
file: doc.relative_path.clone(),
start_line: (line.max(0) as u32).saturating_add(1),
start_col: (col.max(0) as u32).saturating_add(1),
});
}
}
ScipScan {
snapshot_id,
ts,
repo: repo.to_string(),
git_sha: git_sha.to_string(),
rows,
}
}
impl ScipScan {
pub fn definitions_matching<'a>(&'a self, pattern: &str) -> Vec<&'a ScipRow> {
let p = pattern.to_lowercase();
self.rows
.iter()
.filter(|r| r.is_definition)
.filter(|r| {
r.display_name.to_lowercase().contains(&p)
|| r.symbol.to_lowercase().contains(&p)
})
.collect()
}
pub fn resolve_symbols(&self, pattern: &str) -> Vec<String> {
let mut syms: Vec<String> =
self.definitions_matching(pattern).iter().map(|r| r.symbol.clone()).collect();
syms.sort();
syms.dedup();
syms
}
pub fn usages_of<'a>(&'a self, symbol: &str) -> Vec<&'a ScipRow> {
self.rows
.iter()
.filter(|r| r.symbol == symbol && !r.is_definition)
.collect()
}
pub fn occurrences_of<'a>(&'a self, symbol: &str) -> Vec<&'a ScipRow> {
self.rows.iter().filter(|r| r.symbol == symbol).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use scip::types::{Document, Occurrence, SymbolInformation};
fn sample_index() -> Index {
let mut idx = Index::new();
let mut doc = Document::new();
doc.relative_path = "src/lib.rs".into();
let mut si = SymbolInformation::new();
si.symbol = "rust-analyzer cargo demo 0.1.0 Greet#name().".into();
si.display_name = "name".into();
si.kind = symbol_information::Kind::TraitMethod.into();
doc.symbols.push(si.clone());
let mut def = Occurrence::new();
def.range = vec![10, 4, 10, 8];
def.symbol = si.symbol.clone();
def.symbol_roles = SymbolRole::Definition as i32;
doc.occurrences.push(def);
for line in [20, 30] {
let mut r = Occurrence::new();
r.range = vec![line, 8, line, 12];
r.symbol = si.symbol.clone();
r.symbol_roles = 0; doc.occurrences.push(r);
}
let mut other = Occurrence::new();
other.range = vec![40, 8, 40, 12];
other.symbol = "rust-analyzer cargo demo 0.1.0 Config#name.".into();
other.symbol_roles = SymbolRole::ReadAccess as i32;
doc.occurrences.push(other);
idx.documents.push(doc);
idx
}
#[test]
fn ingest_maps_roles_and_resolves_exact_symbol() {
let scan = ingest_index(
sample_index(),
"demo",
"deadbeef",
Uuid::nil(),
Utc::now(),
);
assert_eq!(scan.rows.len(), 4);
let syms = scan.resolve_symbols("name");
assert_eq!(syms, vec!["rust-analyzer cargo demo 0.1.0 Greet#name().".to_string()]);
let usages = scan.usages_of(&syms[0]);
assert_eq!(usages.len(), 2);
let lines: Vec<u32> = usages.iter().map(|r| r.start_line).collect();
assert_eq!(lines, vec![21, 31]);
let def: Vec<&ScipRow> = scan.rows.iter().filter(|r| r.is_definition).collect();
assert_eq!(def.len(), 1);
assert_eq!(def[0].kind, "TraitMethod");
assert_eq!(def[0].role, "definition");
assert_eq!(def[0].start_line, 11);
}
#[test]
fn protobuf_roundtrip_through_bytes() {
use protobuf::Message;
let idx = sample_index();
let bytes = idx.write_to_bytes().expect("encode");
let back = protobuf_parse(&bytes).expect("decode");
assert_eq!(back.documents.len(), 1);
assert_eq!(back.documents[0].occurrences.len(), 4);
}
}