use anyhow::Result;
use protobuf::{EnumOrUnknown, Message};
use scip::types::{
symbol_information::Kind, Document, Index, Metadata, Occurrence, PositionEncoding,
SymbolInformation, SymbolRole,
};
use std::collections::HashMap;
use crate::graph::schema::SymbolNode;
use crate::ingest::detect::detect_language;
use super::CodeGraph;
use sqlitegraph::{BackendDirection, GraphBackend, NeighborQuery, SnapshotId};
#[derive(Debug, Clone)]
pub struct ScipExportConfig {
pub project_root: String,
pub project_name: Option<String>,
pub version: Option<String>,
}
impl Default for ScipExportConfig {
fn default() -> Self {
Self {
project_root: ".".to_string(),
project_name: None,
version: None,
}
}
}
fn magellan_symbol_to_scip(fqn: &str, language: &str) -> String {
let parts: Vec<&str> = match language {
"rust" | "cpp" => fqn.split("::").collect(),
"python" | "java" | "javascript" | "typescript" => fqn.split('.').collect(),
_ => vec![fqn],
};
let symbol = parts.last().unwrap_or(&"");
let descriptors: Vec<String> = parts
.iter()
.take(parts.len().saturating_sub(1))
.map(|s| s.to_string())
.collect();
if descriptors.is_empty() {
format!("magellan {}/{}.", language, symbol)
} else {
format!(
"magellan {}/{}/{}.",
language,
descriptors.join("/"),
symbol
)
}
}
fn map_symbol_kind(kind: &str) -> Kind {
match kind {
"Function" => Kind::Function,
"Method" => Kind::Method,
"Struct" => Kind::Class,
"Enum" => Kind::Enum,
"Module" => Kind::Namespace,
"Class" => Kind::Class,
"Interface" => Kind::Interface,
"TypeAlias" => Kind::TypeAlias,
"Union" => Kind::Union,
"Namespace" => Kind::Namespace,
_ => Kind::UnspecifiedKind,
}
}
pub fn export_scip(graph: &CodeGraph, config: &ScipExportConfig) -> Result<Vec<u8>> {
let mut index = Index::new();
let mut metadata = Metadata::new();
metadata.project_root = config.project_root.clone();
use scip::types::TextEncoding;
metadata.text_document_encoding = EnumOrUnknown::new(TextEncoding::UTF8);
index.metadata = protobuf::MessageField::some(metadata);
let mut file_to_symbols: HashMap<String, Vec<(i64, SymbolNode)>> = HashMap::new();
let mut file_to_references: HashMap<String, Vec<super::ReferenceNode>> = HashMap::new();
let mut global_symbol_map: HashMap<String, String> = HashMap::new();
let entity_ids = graph.files.backend.entity_ids()?;
let snapshot = SnapshotId::current();
for entity_id in entity_ids {
let entity = graph.files.backend.get_node(snapshot, entity_id)?;
match entity.kind.as_str() {
"Symbol" => {
if let Ok(symbol_node) = serde_json::from_value::<SymbolNode>(entity.data.clone()) {
let file_path = if let Some(file_id) = graph
.files
.backend
.neighbors(
snapshot,
entity_id,
NeighborQuery {
direction: BackendDirection::Incoming,
edge_type: Some("DEFINES".to_string()),
},
)?
.first()
{
let file_entity = graph.files.backend.get_node(snapshot, *file_id)?;
if let Ok(file_node) =
serde_json::from_value::<super::FileNode>(file_entity.data)
{
file_node.path
} else {
continue;
}
} else {
continue;
};
let language = detect_language(std::path::Path::new(&file_path))
.map(|l| l.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
let fqn = symbol_node.fqn.as_deref().unwrap_or("");
let scip_symbol = if !fqn.is_empty() {
magellan_symbol_to_scip(fqn, &language)
} else {
let name = symbol_node.name.as_deref().unwrap_or("");
format!("magellan {}/{}.", language, name)
};
if !fqn.is_empty() {
global_symbol_map.insert(fqn.to_string(), scip_symbol.clone());
}
if let Some(ref name) = symbol_node.name {
global_symbol_map.insert(name.clone(), scip_symbol.clone());
}
file_to_symbols
.entry(file_path)
.or_default()
.push((entity_id, symbol_node));
}
}
"Reference" => {
if let Ok(ref_node) =
serde_json::from_value::<super::ReferenceNode>(entity.data.clone())
{
file_to_references
.entry(ref_node.file.clone())
.or_default()
.push(ref_node);
}
}
_ => {
}
}
}
for (file_path, symbols) in file_to_symbols {
let language = if let Some(lang) = detect_language(std::path::Path::new(&file_path)) {
lang.as_str().to_string()
} else {
"unknown".to_string()
};
let mut document = Document::new();
document.relative_path = file_path.clone();
document.language = language.clone();
document.position_encoding =
EnumOrUnknown::new(PositionEncoding::UTF8CodeUnitOffsetFromLineStart);
for (_node_id, symbol) in &symbols {
let mut occurrence = Occurrence::new();
occurrence.range = vec![
symbol.start_line as i32,
symbol.start_col as i32,
symbol.end_line as i32,
symbol.end_col as i32,
];
let fqn = symbol.fqn.as_deref().unwrap_or("");
let scip_symbol = if !fqn.is_empty() {
magellan_symbol_to_scip(fqn, &language)
} else {
let name = symbol.name.as_deref().unwrap_or("");
format!("magellan {}/{}.", language, name)
};
occurrence.symbol = scip_symbol.clone();
occurrence.symbol_roles = SymbolRole::Definition as i32;
document.occurrences.push(occurrence);
let mut sym_info = SymbolInformation::new();
sym_info.kind = EnumOrUnknown::new(map_symbol_kind(&symbol.kind));
if let Some(ref name) = symbol.name {
sym_info.display_name = name.clone();
}
sym_info.symbol = scip_symbol;
document.symbols.push(sym_info);
}
if let Some(refs) = file_to_references.get(&file_path) {
for ref_node in refs {
let mut occurrence = Occurrence::new();
occurrence.range = vec![
ref_node.start_line as i32,
ref_node.start_col as i32,
ref_node.end_line as i32,
ref_node.end_col as i32,
];
occurrence.symbol_roles = SymbolRole::ReadAccess as i32;
occurrence.symbol = "magellan unknown/.".to_string();
document.occurrences.push(occurrence);
}
}
index.documents.push(document);
}
let bytes = index.write_to_bytes()?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_magellan_symbol_to_scip_rust() {
let result = magellan_symbol_to_scip("crate::module::function", "rust");
assert_eq!(result, "magellan rust/crate/module/function.");
}
#[test]
fn test_magellan_symbol_to_scip_rust_simple() {
let result = magellan_symbol_to_scip("myfunction", "rust");
assert_eq!(result, "magellan rust/myfunction.");
}
#[test]
fn test_magellan_symbol_to_scip_python() {
let result = magellan_symbol_to_scip("package.module.function", "python");
assert_eq!(result, "magellan python/package/module/function.");
}
#[test]
fn test_magellan_symbol_to_scip_java() {
let result = magellan_symbol_to_scip("com.example.Class.method", "java");
assert_eq!(result, "magellan java/com/example/Class/method.");
}
#[test]
fn test_magellan_symbol_to_scip_cpp() {
let result = magellan_symbol_to_scip("std::vector::push_back", "cpp");
assert_eq!(result, "magellan cpp/std/vector/push_back.");
}
#[test]
fn test_magellan_symbol_to_scip_javascript() {
let result = magellan_symbol_to_scip("module.function", "javascript");
assert_eq!(result, "magellan javascript/module/function.");
}
#[test]
fn test_magellan_symbol_to_scip_typescript() {
let result = magellan_symbol_to_scip("namespace.Class.method", "typescript");
assert_eq!(result, "magellan typescript/namespace/Class/method.");
}
#[test]
fn test_magellan_symbol_to_scip_unknown_language() {
let result = magellan_symbol_to_scip("some_symbol", "unknown");
assert_eq!(result, "magellan unknown/some_symbol.");
}
#[test]
fn test_map_symbol_kind() {
assert_eq!(map_symbol_kind("Function"), Kind::Function);
assert_eq!(map_symbol_kind("Method"), Kind::Method);
assert_eq!(map_symbol_kind("Struct"), Kind::Class);
assert_eq!(map_symbol_kind("Enum"), Kind::Enum);
assert_eq!(map_symbol_kind("Module"), Kind::Namespace);
assert_eq!(map_symbol_kind("Class"), Kind::Class);
assert_eq!(map_symbol_kind("Interface"), Kind::Interface);
assert_eq!(map_symbol_kind("TypeAlias"), Kind::TypeAlias);
assert_eq!(map_symbol_kind("Union"), Kind::Union);
assert_eq!(map_symbol_kind("Namespace"), Kind::Namespace);
assert_eq!(map_symbol_kind("Unknown"), Kind::UnspecifiedKind);
}
#[test]
fn test_scip_export_config_default() {
let config = ScipExportConfig::default();
assert_eq!(config.project_root, ".");
assert!(config.project_name.is_none());
assert!(config.version.is_none());
}
}