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