1use std::error::Error;
2use std::path::Path;
3
4use crate::model::{LanguageId, SemanticCallGraph};
5
6pub mod ocaml;
7pub mod rust;
8
9pub type FrontendResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
10
11pub struct FileContext<'a> {
12 pub path: &'a Path,
13 pub module: &'a [String],
14}
15
16pub struct ProjectContext<'a> {
17 pub root: &'a Path,
18 pub driver_executable: Option<&'a Path>,
20 pub entries: &'a [String],
21 pub cache: Option<ProjectCache<'a>>,
22}
23
24#[derive(Clone, Copy)]
25pub struct ProjectCache<'a> {
26 pub project_root: &'a Path,
27 pub endpoint: &'a str,
28 pub verbose: bool,
29}
30
31pub trait LanguageBackend: Send + Sync {
36 fn language(&self) -> LanguageId;
37 fn extensions(&self) -> &'static [&'static str];
38 fn analyze_file(
39 &self,
40 context: &FileContext<'_>,
41 source: &str,
42 ) -> FrontendResult<SemanticCallGraph>;
43 fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<SemanticCallGraph>;
44}
45
46pub fn backend_for_extension(path: &Path) -> Option<&'static dyn LanguageBackend> {
47 let extension = path.extension()?.to_str()?;
48 builtin_backends().into_iter().find(|backend| {
49 backend
50 .extensions()
51 .iter()
52 .any(|candidate| candidate.eq_ignore_ascii_case(extension))
53 })
54}
55
56pub fn backend_for_language(language: &str) -> Option<&'static dyn LanguageBackend> {
57 builtin_backends()
58 .into_iter()
59 .find(|backend| backend.language().0.eq_ignore_ascii_case(language))
60}
61
62pub fn builtin_backends() -> [&'static dyn LanguageBackend; 2] {
63 [&rust::RUST_BACKEND, &ocaml::OCAML_BACKEND]
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn registry_routes_extensions_without_engine_language_branches() {
72 assert_eq!(
73 backend_for_extension(Path::new("service.RS"))
74 .unwrap()
75 .language()
76 .0,
77 "rust"
78 );
79 assert_eq!(
80 backend_for_extension(Path::new("service.mli"))
81 .unwrap()
82 .language()
83 .0,
84 "ocaml"
85 );
86 }
87}