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