1use serde::Serialize;
7use std::path::{Path, PathBuf};
8use syster::hir::{Severity, check_file};
9use syster::ide::AnalysisHost;
10use walkdir::WalkDir;
11
12#[derive(Debug, Serialize)]
14pub struct AnalysisResult {
15 pub file_count: usize,
17 pub symbol_count: usize,
19 pub error_count: usize,
21 pub warning_count: usize,
23 pub diagnostics: Vec<DiagnosticInfo>,
25}
26
27#[derive(Debug, Clone, Serialize)]
29pub struct DiagnosticInfo {
30 pub file: String,
32 pub line: u32,
34 pub col: u32,
36 pub end_line: u32,
38 pub end_col: u32,
40 pub message: String,
42 #[serde(serialize_with = "serialize_severity")]
44 pub severity: Severity,
45 pub code: Option<String>,
47}
48
49fn serialize_severity<S>(severity: &Severity, serializer: S) -> Result<S::Ok, S::Error>
51where
52 S: serde::Serializer,
53{
54 let s = match severity {
55 Severity::Error => "error",
56 Severity::Warning => "warning",
57 Severity::Info => "info",
58 Severity::Hint => "hint",
59 };
60 serializer.serialize_str(s)
61}
62
63pub fn run_analysis(
74 input: &Path,
75 verbose: bool,
76 load_stdlib: bool,
77 stdlib_path: Option<&Path>,
78) -> Result<AnalysisResult, String> {
79 let mut host = AnalysisHost::new();
80
81 if load_stdlib {
83 load_stdlib_files(&mut host, stdlib_path, verbose)?;
84 }
85
86 load_input(&mut host, input, verbose)?;
88
89 let _analysis = host.analysis();
91
92 let diagnostics = collect_diagnostics(&host);
94
95 let error_count = diagnostics
97 .iter()
98 .filter(|d| matches!(d.severity, Severity::Error))
99 .count();
100 let warning_count = diagnostics
101 .iter()
102 .filter(|d| matches!(d.severity, Severity::Warning))
103 .count();
104
105 Ok(AnalysisResult {
106 file_count: host.file_count(),
107 symbol_count: host.symbol_index().all_symbols().count(),
108 error_count,
109 warning_count,
110 diagnostics,
111 })
112}
113
114fn load_input(host: &mut AnalysisHost, input: &Path, verbose: bool) -> Result<(), String> {
116 if input.is_file() {
117 load_file(host, input, verbose)
118 } else if input.is_dir() {
119 load_directory(host, input, verbose)
120 } else {
121 Err(format!("Path does not exist: {}", input.display()))
122 }
123}
124
125fn load_file(host: &mut AnalysisHost, path: &Path, verbose: bool) -> Result<(), String> {
127 if verbose {
128 println!(" Loading: {}", path.display());
129 }
130
131 let content = std::fs::read_to_string(path)
132 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
133
134 let path_str = path.to_string_lossy();
135 let parse_errors = host.set_file_content(&path_str, &content);
136
137 for err in parse_errors {
139 eprintln!(
140 "parse error: {}:{}:{}: {}",
141 path.display(),
142 err.position.line,
143 err.position.column,
144 err.message
145 );
146 }
147
148 Ok(())
149}
150
151fn load_directory(host: &mut AnalysisHost, dir: &Path, verbose: bool) -> Result<(), String> {
153 if verbose {
154 println!("Scanning directory: {}", dir.display());
155 }
156
157 for entry in WalkDir::new(dir).follow_links(true) {
158 let entry = entry.map_err(|e| format!("Walk error: {}", e))?;
159 let path = entry.path();
160
161 if is_sysml_file(path) {
162 load_file(host, path, verbose)?;
163 }
164 }
165
166 Ok(())
167}
168
169fn is_sysml_file(path: &Path) -> bool {
171 path.is_file()
172 && matches!(
173 path.extension().and_then(|e| e.to_str()),
174 Some("sysml") | Some("kerml")
175 )
176}
177
178fn load_stdlib_files(
180 host: &mut AnalysisHost,
181 custom_path: Option<&Path>,
182 verbose: bool,
183) -> Result<(), String> {
184 if verbose {
185 println!("Loading standard library...");
186 }
187
188 if let Some(path) = custom_path {
190 if path.exists() {
191 return load_directory(host, path, verbose);
192 } else {
193 return Err(format!("Stdlib path does not exist: {}", path.display()));
194 }
195 }
196
197 let default_paths = [
199 PathBuf::from("sysml.library"),
200 PathBuf::from("../sysml.library"),
201 PathBuf::from("../base/sysml.library"),
202 ];
203
204 for path in &default_paths {
205 if path.exists() {
206 return load_directory(host, path, verbose);
207 }
208 }
209
210 if verbose {
211 println!(" Warning: Standard library not found");
212 }
213
214 Ok(())
215}
216
217fn collect_diagnostics(host: &AnalysisHost) -> Vec<DiagnosticInfo> {
219 let mut all_diagnostics = Vec::new();
220
221 for path in host.files().keys() {
222 if let Some(file_id) = host.get_file_id_for_path(path) {
223 let file_path = path.to_string_lossy().to_string();
224 let diagnostics = check_file(host.symbol_index(), file_id);
225
226 for diag in diagnostics {
227 all_diagnostics.push(DiagnosticInfo {
228 file: file_path.clone(),
229 line: diag.start_line + 1, col: diag.start_col + 1,
231 end_line: diag.end_line + 1,
232 end_col: diag.end_col + 1,
233 message: diag.message.to_string(),
234 severity: diag.severity,
235 code: diag.code.map(|c| c.to_string()),
236 });
237 }
238 }
239 }
240
241 all_diagnostics.sort_by(|a, b| (&a.file, a.line, a.col).cmp(&(&b.file, b.line, b.col)));
243
244 all_diagnostics
245}
246
247#[derive(Debug, Serialize)]
253pub struct ExportSymbol {
254 pub name: String,
255 pub qualified_name: String,
256 pub kind: String,
257 pub file: String,
258 pub start_line: u32,
259 pub start_col: u32,
260 pub end_line: u32,
261 pub end_col: u32,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub doc: Option<String>,
264 #[serde(skip_serializing_if = "Vec::is_empty")]
265 pub supertypes: Vec<String>,
266}
267
268#[derive(Debug, Serialize)]
270pub struct AstExport {
271 pub files: Vec<FileAst>,
272}
273
274#[derive(Debug, Serialize)]
276pub struct FileAst {
277 pub path: String,
278 pub symbols: Vec<ExportSymbol>,
279}
280
281pub fn export_ast(
283 input: &Path,
284 verbose: bool,
285 load_stdlib: bool,
286 stdlib_path: Option<&Path>,
287) -> Result<String, String> {
288 let mut host = AnalysisHost::new();
289
290 if load_stdlib {
291 load_stdlib_files(&mut host, stdlib_path, verbose)?;
292 }
293
294 load_input(&mut host, input, verbose)?;
295 let _analysis = host.analysis();
296
297 let mut files = Vec::new();
298
299 for path in host.files().keys() {
301 let path_str = path.to_string_lossy().to_string();
302
303 if path_str.contains("sysml.library") {
305 continue;
306 }
307
308 if let Some(file_id) = host.get_file_id_for_path(path) {
309 let symbols: Vec<ExportSymbol> = host
310 .symbol_index()
311 .symbols_in_file(file_id)
312 .into_iter()
313 .map(|sym| ExportSymbol {
314 name: sym.name.to_string(),
315 qualified_name: sym.qualified_name.to_string(),
316 kind: format!("{:?}", sym.kind),
317 file: path_str.clone(),
318 start_line: sym.start_line + 1,
319 start_col: sym.start_col + 1,
320 end_line: sym.end_line + 1,
321 end_col: sym.end_col + 1,
322 doc: sym.doc.as_ref().map(|d| d.to_string()),
323 supertypes: sym.supertypes.iter().map(|s| s.to_string()).collect(),
324 })
325 .collect();
326
327 files.push(FileAst {
328 path: path_str,
329 symbols,
330 });
331 }
332 }
333
334 files.sort_by(|a, b| a.path.cmp(&b.path));
336
337 let export = AstExport { files };
338 serde_json::to_string_pretty(&export).map_err(|e| format!("Failed to serialize AST: {}", e))
339}
340
341pub fn export_json(result: &AnalysisResult) -> Result<String, String> {
343 serde_json::to_string_pretty(result).map_err(|e| format!("Failed to serialize result: {}", e))
344}
345
346#[cfg(feature = "interchange")]
367pub fn export_model(
368 input: &Path,
369 format: &str,
370 verbose: bool,
371 load_stdlib: bool,
372 stdlib_path: Option<&Path>,
373 self_contained: bool,
374) -> Result<Vec<u8>, String> {
375 use syster::interchange::{
376 JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
377 };
378
379 let mut host = AnalysisHost::new();
380
381 if load_stdlib {
383 load_stdlib_files(&mut host, stdlib_path, verbose)?;
384 }
385
386 load_input(&mut host, input, verbose)?;
388
389 #[cfg(feature = "interchange")]
391 {
392 use syster::project::WorkspaceLoader;
393 let loader = WorkspaceLoader::new();
394
395 if input.is_file() {
397 let parent_dir = input.parent().unwrap_or(input);
398 if let Err(e) = loader.load_metadata_from_directory(parent_dir, &mut host) {
399 if verbose {
400 eprintln!("Note: Could not load metadata: {}", e);
401 }
402 } else if verbose {
403 println!("Loaded metadata from {}", parent_dir.display());
404 }
405 } else if input.is_dir() {
406 if let Err(e) = loader.load_metadata_from_directory(input, &mut host) {
408 if verbose {
409 eprintln!("Note: Could not load metadata: {}", e);
410 }
411 } else if verbose {
412 println!("Loaded metadata from {}", input.display());
413 }
414 }
415 }
416
417 let analysis = host.analysis();
419
420 let symbols: Vec<_> = if self_contained {
422 analysis.symbol_index().all_symbols().cloned().collect()
424 } else {
425 analysis
427 .symbol_index()
428 .all_symbols()
429 .filter(|sym| {
430 if let Some(path) = analysis.get_file_path(sym.file) {
432 !path.contains("sysml.library")
433 } else {
434 true }
436 })
437 .cloned()
438 .collect()
439 };
440
441 if verbose {
442 println!(
443 "Collecting {} symbols (self_contained={})",
444 symbols.len(),
445 self_contained
446 );
447 }
448
449 let mut model = model_from_symbols(&symbols);
451
452 model = restore_ids_from_symbols(model, analysis.symbol_index());
454 if verbose {
455 println!("Restored element IDs from symbol database");
456 }
457
458 if verbose {
459 println!(
460 "Exported model: {} elements, {} relationships",
461 model.elements.len(),
462 model.relationships.len()
463 );
464 }
465
466 match format.to_lowercase().as_str() {
468 "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
469 "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
470 "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
471 "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
472 _ => Err(format!(
473 "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
474 format
475 )),
476 }
477}
478
479#[cfg(feature = "interchange")]
487pub fn export_from_host(
488 host: &mut AnalysisHost,
489 format: &str,
490 verbose: bool,
491 self_contained: bool,
492) -> Result<Vec<u8>, String> {
493 use syster::interchange::{
494 JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
495 };
496
497 let analysis = host.analysis();
498 let symbols: Vec<_> = if self_contained {
499 analysis.symbol_index().all_symbols().cloned().collect()
500 } else {
501 analysis
502 .symbol_index()
503 .all_symbols()
504 .filter(|sym| {
505 if let Some(path) = analysis.get_file_path(sym.file) {
506 !path.contains("sysml.library")
507 } else {
508 true
509 }
510 })
511 .cloned()
512 .collect()
513 };
514
515 if verbose {
516 println!(
517 "Collecting {} symbols (self_contained={})",
518 symbols.len(),
519 self_contained
520 );
521 }
522
523 let mut model = model_from_symbols(&symbols);
524 model = restore_ids_from_symbols(model, analysis.symbol_index());
525
526 if verbose {
527 println!(
528 "Exported model: {} elements, {} relationships",
529 model.elements.len(),
530 model.relationships.len()
531 );
532 }
533
534 match format.to_lowercase().as_str() {
535 "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
536 "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
537 "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
538 "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
539 _ => Err(format!(
540 "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
541 format
542 )),
543 }
544}
545
546#[cfg(feature = "interchange")]
548#[derive(Debug)]
549pub struct ImportResult {
550 pub element_count: usize,
552 pub relationship_count: usize,
554 pub error_count: usize,
556 pub messages: Vec<String>,
558}
559
560#[cfg(feature = "interchange")]
578pub fn import_model_into_host(
579 host: &mut AnalysisHost,
580 input: &Path,
581 format: Option<&str>,
582 verbose: bool,
583) -> Result<ImportResult, String> {
584 use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
585
586 let bytes =
588 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
589
590 let format_str = format.map(String::from).unwrap_or_else(|| {
592 input
593 .extension()
594 .and_then(|e| e.to_str())
595 .unwrap_or("xmi")
596 .to_string()
597 });
598
599 if verbose {
600 println!(
601 "Importing {} as {} into workspace",
602 input.display(),
603 format_str
604 );
605 }
606
607 let model = match format_str.to_lowercase().as_str() {
609 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
610 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
611 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
612 _ => {
613 if let Some(format_impl) = detect_format(input) {
615 format_impl.read(&bytes).map_err(|e| e.to_string())?
616 } else {
617 return Err(format!(
618 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
619 format_str
620 ));
621 }
622 }
623 };
624
625 let element_count = model.elements.len();
626 let relationship_count = model.relationships.len();
627
628 if verbose {
629 println!(
630 "Parsed {} elements and {} relationships",
631 element_count, relationship_count
632 );
633 }
634
635 let virtual_path = input.to_string_lossy().to_string() + ".sysml";
638 let errors = host.add_model(&model, &virtual_path);
639
640 if verbose {
641 if errors.is_empty() {
642 println!("Loaded model into workspace with preserved element IDs");
643 } else {
644 println!("Loaded model with {} parse warnings", errors.len());
645 }
646 }
647
648 Ok(ImportResult {
649 element_count,
650 relationship_count,
651 error_count: errors.len(),
652 messages: vec![format!("Successfully imported {} elements", element_count)],
653 })
654}
655
656#[cfg(feature = "interchange")]
669pub fn import_model(
670 input: &Path,
671 format: Option<&str>,
672 verbose: bool,
673) -> Result<ImportResult, String> {
674 use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
675
676 let bytes =
678 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
679
680 let format_str = format.map(String::from).unwrap_or_else(|| {
682 input
683 .extension()
684 .and_then(|e| e.to_str())
685 .unwrap_or("xmi")
686 .to_string()
687 });
688
689 if verbose {
690 println!("Importing {} as {}", input.display(), format_str);
691 }
692
693 let model = match format_str.to_lowercase().as_str() {
695 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
696 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
697 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
698 _ => {
699 if let Some(format_impl) = detect_format(input) {
701 format_impl.read(&bytes).map_err(|e| e.to_string())?
702 } else {
703 return Err(format!(
704 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
705 format_str
706 ));
707 }
708 }
709 };
710
711 let mut messages = Vec::new();
713 let mut error_count = 0;
714
715 for rel in &model.relationships {
717 if model.elements.get(&rel.source).is_none() {
718 messages.push(format!(
719 "Warning: Relationship source '{}' not found",
720 rel.source
721 ));
722 error_count += 1;
723 }
724 if model.elements.get(&rel.target).is_none() {
725 messages.push(format!(
726 "Warning: Relationship target '{}' not found",
727 rel.target
728 ));
729 error_count += 1;
730 }
731 }
732
733 if verbose {
734 println!(
735 "Imported: {} elements, {} relationships, {} validation issues",
736 model.elements.len(),
737 model.relationships.len(),
738 error_count
739 );
740 for msg in &messages {
741 println!(" {}", msg);
742 }
743 }
744
745 Ok(ImportResult {
746 element_count: model.elements.len(),
747 relationship_count: model.relationships.len(),
748 error_count,
749 messages,
750 })
751}
752
753#[cfg(feature = "interchange")]
755#[derive(Debug)]
756pub struct DecompileResult {
757 pub sysml_text: String,
759 pub metadata_json: String,
761 pub element_count: usize,
763 pub source_path: String,
765}
766
767#[cfg(feature = "interchange")]
781pub fn decompile_model(
782 input: &Path,
783 format: Option<&str>,
784 verbose: bool,
785) -> Result<DecompileResult, String> {
786 use syster::interchange::{
787 JsonLd, Kpar, ModelFormat, SourceInfo, Xmi, decompile_with_source, detect_format,
788 };
789
790 let bytes =
792 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
793
794 let format_str = format.map(String::from).unwrap_or_else(|| {
796 input
797 .extension()
798 .and_then(|e| e.to_str())
799 .unwrap_or("xmi")
800 .to_string()
801 });
802
803 if verbose {
804 println!("Decompiling {} as {}", input.display(), format_str);
805 }
806
807 let model = match format_str.to_lowercase().as_str() {
809 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
810 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
811 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
812 _ => {
813 if let Some(format_impl) = detect_format(input) {
814 format_impl.read(&bytes).map_err(|e| e.to_string())?
815 } else {
816 return Err(format!(
817 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
818 format_str
819 ));
820 }
821 }
822 };
823
824 let element_count = model.elements.len();
825
826 let source = SourceInfo::from_path(input.to_string_lossy()).with_format(&format_str);
828
829 let result = decompile_with_source(&model, source);
831
832 if verbose {
833 println!(
834 "Decompiled: {} elements -> {} chars of SysML, {} metadata entries",
835 element_count,
836 result.text.len(),
837 result.metadata.elements.len()
838 );
839 }
840
841 let metadata_json = serde_json::to_string_pretty(&result.metadata)
843 .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
844
845 Ok(DecompileResult {
846 sysml_text: result.text,
847 metadata_json,
848 element_count,
849 source_path: input.to_string_lossy().to_string(),
850 })
851}