use std::path::Path;
use std::sync::Arc;
use crate::base::FileId;
use crate::syntax::SyntaxFile;
use crate::syntax::sysml::ast::types::SysMLFile;
use super::input::SourceRoot;
use super::symbols::{HirSymbol, extract_symbols_unified};
#[salsa::input]
pub struct FileText {
pub file: FileId,
#[return_ref]
pub text: String,
}
#[salsa::input]
pub struct SourceRootInput {
#[return_ref]
pub root: SourceRoot,
}
#[salsa::db]
#[derive(Default, Clone)]
pub struct RootDatabase {
storage: salsa::Storage<Self>,
}
#[salsa::db]
impl salsa::Database for RootDatabase {
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {
}
}
impl RootDatabase {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ParseResult {
pub success: bool,
pub errors: Vec<String>,
pub ast: Option<Arc<SysMLFile>>,
}
impl Eq for ParseResult {}
impl ParseResult {
pub fn ok(ast: SysMLFile) -> Self {
Self {
success: true,
errors: Vec::new(),
ast: Some(Arc::new(ast)),
}
}
pub fn ok_with_errors(ast: SysMLFile, errors: Vec<String>) -> Self {
Self {
success: true,
errors,
ast: Some(Arc::new(ast)),
}
}
pub fn err(errors: Vec<String>) -> Self {
Self {
success: false,
errors,
ast: None,
}
}
pub fn is_ok(&self) -> bool {
self.success
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn get_ast(&self) -> Option<&SysMLFile> {
self.ast.as_deref()
}
}
#[salsa::tracked]
pub fn parse_file(db: &dyn salsa::Database, file_text: FileText) -> ParseResult {
let text = file_text.text(db);
let result = crate::syntax::sysml::parser::parse_with_result(text, Path::new("file.sysml"));
if let Some(ast) = result.content {
let errors: Vec<String> = result.errors.iter().map(|e| format!("{:?}", e)).collect();
if errors.is_empty() {
ParseResult::ok(ast)
} else {
ParseResult::ok_with_errors(ast, errors)
}
} else {
ParseResult::err(result.errors.iter().map(|e| format!("{:?}", e)).collect())
}
}
pub fn file_symbols(file: FileId, ast: &SysMLFile) -> Vec<HirSymbol> {
extract_symbols_unified(file, &SyntaxFile::SysML(ast.clone()))
}
#[salsa::tracked]
pub fn file_symbols_from_text(db: &dyn salsa::Database, file_text: FileText) -> Vec<HirSymbol> {
let file = file_text.file(db);
let result = parse_file(db, file_text);
match result.ast {
Some(ast) => extract_symbols_unified(file, &SyntaxFile::SysML((*ast).clone())),
None => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::symbols::SymbolKind;
#[test]
fn test_database_creation() {
let _db = RootDatabase::new();
}
#[test]
fn test_parse_result() {
let ast = SysMLFile {
namespace: None,
namespaces: Vec::new(),
elements: Vec::new(),
};
let ok = ParseResult::ok(ast.clone());
assert!(ok.is_ok());
assert!(!ok.has_errors());
assert!(ok.get_ast().is_some());
let err = ParseResult::err(vec!["error".to_string()]);
assert!(!err.is_ok());
assert!(err.has_errors());
assert!(err.get_ast().is_none());
}
#[test]
fn test_file_symbols_empty() {
let ast = SysMLFile {
namespace: None,
namespaces: Vec::new(),
elements: Vec::new(),
};
let file = FileId::new(0);
let symbols = file_symbols(file, &ast);
assert!(symbols.is_empty());
}
#[test]
fn test_file_symbols_from_real_sysml() {
let sysml = r#"
package Vehicle {
part def Car {
attribute mass : Real;
part engine : Engine;
}
part def Engine {
attribute power : Real;
}
}
"#;
let result = crate::syntax::sysml::parser::parse_with_result(
sysml,
std::path::Path::new("test.sysml"),
);
assert!(result.content.is_some(), "Parse failed");
let ast = result.content.unwrap();
let file = FileId::new(1);
let symbols = file_symbols(file, &ast);
assert!(
symbols.len() >= 4,
"Expected at least 4 symbols, got {}",
symbols.len()
);
let vehicle = symbols.iter().find(|s| s.name.as_ref() == "Vehicle");
assert!(vehicle.is_some(), "Vehicle package not found");
assert_eq!(vehicle.unwrap().kind, SymbolKind::Package);
let car = symbols.iter().find(|s| s.name.as_ref() == "Car");
assert!(car.is_some(), "Car part def not found");
assert_eq!(car.unwrap().kind, SymbolKind::PartDef);
assert_eq!(car.unwrap().qualified_name.as_ref(), "Vehicle::Car");
let engine_def = symbols
.iter()
.find(|s| s.name.as_ref() == "Engine" && s.kind == SymbolKind::PartDef);
assert!(engine_def.is_some(), "Engine part def not found");
}
#[test]
fn test_file_symbols_nested_usages() {
let sysml = r#"
part def Outer {
part inner : Inner;
}
part def Inner;
"#;
let result = crate::syntax::sysml::parser::parse_with_result(
sysml,
std::path::Path::new("test.sysml"),
);
let ast = result.content.unwrap();
let symbols = file_symbols(FileId::new(0), &ast);
let inner_usage = symbols.iter().find(|s| s.name.as_ref() == "inner");
assert!(inner_usage.is_some(), "inner usage not found");
assert_eq!(inner_usage.unwrap().kind, SymbolKind::PartUsage);
assert_eq!(inner_usage.unwrap().qualified_name.as_ref(), "Outer::inner");
}
#[test]
fn test_salsa_tracked_parse_query() {
let db = RootDatabase::new();
let sysml = "part def Car;";
let file_text = FileText::new(&db, FileId::new(0), sysml.to_string());
let result = parse_file(&db, file_text);
assert!(
result.is_ok(),
"Parse failed with errors: {:?}",
result.errors
);
assert!(result.get_ast().is_some());
}
#[test]
fn test_salsa_tracked_symbols_query() {
let db = RootDatabase::new();
let sysml = r#"
package Test {
part def Widget;
}
"#;
let file_text = FileText::new(&db, FileId::new(0), sysml.to_string());
let symbols = file_symbols_from_text(&db, file_text);
assert!(!symbols.is_empty());
let widget = symbols.iter().find(|s| s.name.as_ref() == "Widget");
assert!(widget.is_some(), "Widget not found in symbols");
assert_eq!(widget.unwrap().kind, SymbolKind::PartDef);
}
#[test]
fn test_salsa_memoization() {
let db = RootDatabase::new();
let sysml = "part def MemoTest;";
let file_text = FileText::new(&db, FileId::new(0), sysml.to_string());
let symbols1 = file_symbols_from_text(&db, file_text);
let symbols2 = file_symbols_from_text(&db, file_text);
assert_eq!(symbols1, symbols2);
}
}