Skip to main content

weavatrix_rust/language/
mod.rs

1use crate::model::{Diagnostic, Result};
2use std::fmt::{Display, Formatter};
3use weavatrix_graph::{EdgeKind, NodeKind, SourceSpan};
4
5mod agent;
6mod contract;
7mod dify;
8mod graphql;
9mod json;
10mod mermaid;
11mod n8n;
12mod protobuf;
13#[cfg(feature = "lang-rust")]
14mod rust;
15pub mod tokenized;
16pub(crate) mod web3;
17mod yaml;
18mod yaml_doc;
19
20pub(crate) use agent::looks_promising as agent_looks_promising;
21pub(crate) use contract::file_facts_have_transport_evidence;
22#[cfg(test)]
23pub(crate) use contract::may_contain_transport_marker;
24pub(crate) use dify::DEFAULT_FILE_BYTES as DIFY_DEFAULT_FILE_BYTES;
25pub(crate) use dify::looks_promising as dify_looks_promising;
26pub(crate) use dify::secret_label as dify_secret_label;
27pub(crate) use mermaid::looks_promising as mermaid_looks_promising;
28pub(crate) use n8n::{
29    DEFAULT_FILE_BYTES as N8N_DEFAULT_FILE_BYTES, looks_promising as n8n_looks_promising,
30};
31pub(crate) use web3::{
32    MAX_BUILD_INFO_BYTES as WEB3_MAX_BUILD_INFO_BYTES, admitted_size as web3_admitted_size,
33    looks_promising as web3_looks_promising,
34};
35
36#[cfg(feature = "lang-rust")]
37pub use rust::RustAdapter;
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[non_exhaustive]
41pub enum Language {
42    Rust,
43    Go,
44    C,
45    Cpp,
46    Bash,
47    Sql,
48    Kubernetes,
49    JavaScript,
50    TypeScript,
51    Graphql,
52    Protobuf,
53    Json,
54    Python,
55    Java,
56    CSharp,
57    Swift,
58    Custom(String),
59}
60
61impl Language {
62    #[must_use]
63    pub fn as_str(&self) -> &str {
64        match self {
65            Self::Rust => "rust",
66            Self::Go => "go",
67            Self::C => "c",
68            Self::Cpp => "cpp",
69            Self::Bash => "bash",
70            Self::Sql => "sql",
71            Self::Kubernetes => "kubernetes",
72            Self::JavaScript => "javascript",
73            Self::TypeScript => "typescript",
74            Self::Graphql => "graphql",
75            Self::Protobuf => "protobuf",
76            Self::Json => "json",
77            Self::Python => "python",
78            Self::Java => "java",
79            Self::CSharp => "csharp",
80            Self::Swift => "swift",
81            Self::Custom(value) => value,
82        }
83    }
84}
85
86impl Display for Language {
87    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
88        formatter.write_str(self.as_str())
89    }
90}
91
92#[derive(Debug)]
93pub struct SourceFile<'a> {
94    pub path: &'a str,
95    pub text: &'a str,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct SymbolFact {
100    pub name: String,
101    pub kind: NodeKind,
102    pub span: SourceSpan,
103    /// This declaration is compiled only for tests, either because it carries
104    /// a test attribute itself or because it is nested below `#[cfg(test)]`.
105    pub test_only: bool,
106    /// The language adapter proved this declaration is externally exported.
107    pub exported: bool,
108    /// Stable fingerprint of the declaration span, populated by the analyzer.
109    pub source_fingerprint: Option<String>,
110    /// Full parser-owned declaration extent used only to fingerprint content.
111    pub source_extent: Option<SourceSpan>,
112    /// The type this symbol was declared inside, when it was.
113    ///
114    /// A class and its methods are joined by their own edge rather than by
115    /// containment alone, because "what does this type do" is a different
116    /// question from "what is in this file".
117    pub owner: Option<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct SymbolLocator {
122    pub name: String,
123    pub kind: NodeKind,
124    pub span: SourceSpan,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct ReferenceFact {
129    pub name: String,
130    pub kind: EdgeKind,
131    /// Receiver written before the referenced name, when the source used a
132    /// qualified/member form such as `JSON.parse` or `entry.isFile`.
133    pub receiver: Option<String>,
134    /// Whether the source qualified the reference with a member/path
135    /// operator. This remains true for expression receivers such as
136    /// `statSync(path).isFile`, where there is no single receiver name.
137    pub qualified: bool,
138    pub span: SourceSpan,
139    pub owner: Option<SymbolLocator>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct ImportBindingFact {
144    /// The name exported by the imported module.
145    pub imported: String,
146    /// The name made available in the importing file.
147    pub local: String,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct ImportFact {
152    pub target: String,
153    pub span: SourceSpan,
154    /// A type-position import (`import type { X } from ...`). It disappears at
155    /// compile time, so it couples declarations without coupling runtime
156    /// behaviour, and architecture rules distinguish the two.
157    pub type_only: bool,
158    /// Exact exported-to-local bindings, when the parser can prove them.
159    pub bindings: Vec<ImportBindingFact>,
160}
161
162impl ImportFact {
163    #[must_use]
164    pub fn new(target: String, span: SourceSpan) -> Self {
165        Self {
166            target,
167            span,
168            type_only: false,
169            bindings: Vec::new(),
170        }
171    }
172
173    #[must_use]
174    pub fn type_only(target: String, span: SourceSpan) -> Self {
175        Self {
176            target,
177            span,
178            type_only: true,
179            bindings: Vec::new(),
180        }
181    }
182
183    #[must_use]
184    pub fn with_bindings(mut self, bindings: Vec<ImportBindingFact>) -> Self {
185        self.bindings = bindings;
186        self
187    }
188}
189
190/// One `use(prefix, target)`-style router mount observed in a source file.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct MountFact {
193    /// Path prefix the target is mounted under; empty for bare `use(x)`.
194    pub prefix: String,
195    /// Module specifier of the mounted router.
196    pub target: String,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct DomainFact {
201    pub name: String,
202    pub kind: NodeKind,
203    pub relation: EdgeKind,
204    pub span: SourceSpan,
205    pub owner: Option<SymbolLocator>,
206}
207
208/// A domain relation whose endpoints are already known. Indexing must not
209/// resolve these again by display name.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct BoundEdgeFact {
212    pub from: SymbolLocator,
213    pub to: SymbolLocator,
214    pub kind: EdgeKind,
215    pub span: SourceSpan,
216    pub detail: String,
217}
218
219#[derive(Debug, Default)]
220pub struct FileFacts {
221    pub symbols: Vec<SymbolFact>,
222    pub references: Vec<ReferenceFact>,
223    pub imports: Vec<ImportFact>,
224    pub domains: Vec<DomainFact>,
225    pub bound_edges: Vec<BoundEdgeFact>,
226    pub diagnostics: Vec<Diagnostic>,
227    pub mounts: Vec<MountFact>,
228    /// `export ... from 'x'` specifiers: this file forwards another module's
229    /// surface, so importers of this file reach that module transitively.
230    pub reexports: Vec<ImportFact>,
231}
232
233pub trait LanguageAdapter: Send + Sync {
234    fn language(&self) -> Language;
235    fn extensions(&self) -> &'static [&'static str];
236    fn extractor(&self) -> &'static str;
237    /// Parses one source file into language-neutral facts.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error when the adapter itself cannot initialize or maintain
242    /// its parser contract. Recoverable source syntax errors are diagnostics.
243    fn parse(&self, source: SourceFile<'_>) -> Result<FileFacts>;
244}
245
246pub struct LanguageRegistry {
247    adapters: Vec<Box<dyn LanguageAdapter>>,
248}
249
250impl Default for LanguageRegistry {
251    fn default() -> Self {
252        #[cfg(feature = "lang-rust")]
253        let mut adapters: Vec<Box<dyn LanguageAdapter>> = vec![Box::new(RustAdapter)];
254        #[cfg(not(feature = "lang-rust"))]
255        let mut adapters: Vec<Box<dyn LanguageAdapter>> = Vec::new();
256        adapters.extend([
257            Box::new(graphql::GraphqlAdapter) as Box<dyn LanguageAdapter>,
258            Box::new(protobuf::ProtobufAdapter) as Box<dyn LanguageAdapter>,
259            Box::new(json::JsonAdapter) as Box<dyn LanguageAdapter>,
260            Box::new(yaml::YamlAdapter) as Box<dyn LanguageAdapter>,
261            Box::new(mermaid::MermaidAdapter) as Box<dyn LanguageAdapter>,
262        ]);
263        // First adapter claiming an extension wins; the tokenizer owns comments and strings.
264        adapters.extend(
265            tokenized::TokenizedAdapter::defaults()
266                // Full builds keep the syn `.rs` adapter; the lossless path stays for the rest.
267                .filter(|adapter| {
268                    !cfg!(feature = "lang-rust") || !adapter.extensions().contains(&"rs")
269                })
270                .map(|adapter| Box::new(adapter) as Box<dyn LanguageAdapter>),
271        );
272        Self { adapters }
273    }
274}
275
276impl LanguageRegistry {
277    pub fn extensions(&self) -> impl Iterator<Item = &'static str> + '_ {
278        self.adapters
279            .iter()
280            .flat_map(|adapter| adapter.extensions().iter().copied())
281    }
282
283    #[must_use]
284    pub fn adapter_for_extension(&self, extension: &str) -> Option<&dyn LanguageAdapter> {
285        self.adapters
286            .iter()
287            .find(|adapter| adapter.extensions().contains(&extension))
288            .map(AsRef::as_ref)
289    }
290
291    pub fn languages(&self) -> impl Iterator<Item = Language> + '_ {
292        self.adapters
293            .iter()
294            .map(|adapter| adapter.language())
295            .collect::<std::collections::BTreeSet<_>>()
296            .into_iter()
297    }
298}