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