Skip to main content

amql_engine/resolver/
mod.rs

1//! Code Resolver adapter interface.
2//!
3//! Resolvers transform language-specific ASTs into universal CodeElements.
4//! They delegate parsing to existing tools and only handle the mapping.
5
6mod go;
7mod rust;
8mod typescript;
9
10pub use go::GoResolver;
11pub use rust::RustResolver;
12pub use typescript::TypeScriptResolver;
13
14use crate::error::AqlError;
15use crate::types::{AttrName, CodeElementName, RelativePath, TagName};
16use rustc_hash::FxHashMap;
17use serde::Serialize;
18use std::path::Path;
19
20/// A node in the universal code element tree.
21#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
22#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
23#[cfg_attr(feature = "ts", ts(export))]
24#[cfg_attr(feature = "flow", flow(export))]
25#[derive(Debug, Clone, Serialize)]
26pub struct CodeElement {
27    /// Element kind (e.g. "function", "struct", "impl").
28    pub tag: TagName,
29    /// Name of this code element (e.g. function name, struct name).
30    pub name: CodeElementName,
31    /// Key-value attributes extracted from the source (visibility, async, etc.).
32    #[cfg_attr(
33        feature = "ts",
34        ts(as = "std::collections::HashMap<AttrName, serde_json::Value>")
35    )]
36    #[cfg_attr(
37        feature = "flow",
38        flow(as = "std::collections::HashMap<AttrName, serde_json::Value>")
39    )]
40    pub attrs: FxHashMap<AttrName, serde_json::Value>,
41    /// Nested child elements (e.g. methods inside an impl block).
42    pub children: Vec<CodeElement>,
43    /// Source file location of this element.
44    pub source: SourceLocation,
45}
46
47/// Location of a code element in a source file.
48#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
49#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
50#[cfg_attr(feature = "ts", ts(export))]
51#[cfg_attr(feature = "flow", flow(export))]
52#[derive(Debug, Clone, Serialize)]
53pub struct SourceLocation {
54    /// Relative path to the source file.
55    pub file: RelativePath,
56    /// 1-based start line number.
57    pub line: usize,
58    /// 0-based start column offset.
59    pub column: usize,
60    /// 1-based end line number, if known.
61    pub end_line: Option<usize>,
62    /// 0-based end column offset, if known.
63    pub end_column: Option<usize>,
64    /// Byte offset of the start of this element in the source file.
65    pub start_byte: usize,
66    /// Byte offset of the end of this element in the source file.
67    pub end_byte: usize,
68}
69
70/// A Code Resolver resolves source files into universal CodeElement trees.
71pub trait CodeResolver: Send + Sync {
72    /// Parse a source file and return its CodeElement tree.
73    fn resolve(&self, file_path: &Path) -> Result<CodeElement, AqlError>;
74
75    /// File extensions this resolver can handle (including the dot, e.g. ".rs").
76    fn extensions(&self) -> &[&str];
77
78    /// Code element tags this resolver produces (e.g. "function", "class").
79    fn code_tags(&self) -> &[&str];
80}
81
82/// Registry of Code Resolvers by file extension.
83/// Uses an FxHashMap for O(1) extension lookup instead of linear scan.
84pub struct ResolverRegistry {
85    resolvers: Vec<Box<dyn CodeResolver>>,
86    by_ext: FxHashMap<String, usize>,
87}
88
89impl ResolverRegistry {
90    /// Create an empty resolver registry.
91    pub fn new() -> Self {
92        Self {
93            resolvers: Vec::new(),
94            by_ext: FxHashMap::default(),
95        }
96    }
97
98    /// Create a registry with all built-in resolvers registered.
99    pub fn with_defaults() -> Self {
100        let mut reg = Self::new();
101        reg.register(Box::new(RustResolver));
102        reg.register(Box::new(TypeScriptResolver));
103        reg.register(Box::new(GoResolver));
104        reg
105    }
106
107    /// Register a resolver, indexing it by each extension it supports.
108    pub fn register(&mut self, resolver: Box<dyn CodeResolver>) {
109        let idx = self.resolvers.len();
110        for ext in resolver.extensions() {
111            self.by_ext.insert(ext.to_string(), idx);
112        }
113        self.resolvers.push(resolver);
114    }
115
116    /// Look up the resolver for a file based on its extension.
117    pub fn get_for_file(&self, file_path: &Path) -> Option<&dyn CodeResolver> {
118        let ext = file_path
119            .extension()
120            .map(|e| format!(".{}", e.to_string_lossy()))
121            .unwrap_or_default();
122
123        self.by_ext
124            .get(&ext)
125            .and_then(|&idx| self.resolvers.get(idx))
126            .map(|r| r.as_ref())
127    }
128
129    /// Return true if a resolver exists for this file's extension.
130    pub fn has_resolver_for(&self, file_path: &Path) -> bool {
131        self.get_for_file(file_path).is_some()
132    }
133
134    /// Return all unique code element tags across all registered resolvers.
135    pub fn all_code_tags(&self) -> Vec<&str> {
136        let mut tags: Vec<&str> = self
137            .resolvers
138            .iter()
139            .flat_map(|r| r.code_tags().iter().copied())
140            .collect();
141        tags.sort_unstable();
142        tags.dedup();
143        tags
144    }
145}
146
147impl Default for ResolverRegistry {
148    fn default() -> Self {
149        Self::new()
150    }
151}