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 type this symbol was declared inside, when it was.
89    ///
90    /// A class and its methods are joined by their own edge rather than by
91    /// containment alone, because "what does this type do" is a different
92    /// question from "what is in this file".
93    pub owner: Option<String>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct SymbolLocator {
98    pub name: String,
99    pub kind: NodeKind,
100    pub span: SourceSpan,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ReferenceFact {
105    pub name: String,
106    pub kind: EdgeKind,
107    /// Receiver written before the referenced name, when the source used a
108    /// qualified/member form such as `JSON.parse` or `entry.isFile`.
109    pub receiver: Option<String>,
110    /// Whether the source qualified the reference with a member/path
111    /// operator. This remains true for expression receivers such as
112    /// `statSync(path).isFile`, where there is no single receiver name.
113    pub qualified: bool,
114    pub span: SourceSpan,
115    pub owner: Option<SymbolLocator>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct ImportBindingFact {
120    /// The name exported by the imported module.
121    pub imported: String,
122    /// The name made available in the importing file.
123    pub local: String,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct ImportFact {
128    pub target: String,
129    pub span: SourceSpan,
130    /// A type-position import (`import type { X } from ...`). It disappears at
131    /// compile time, so it couples declarations without coupling runtime
132    /// behaviour, and architecture rules distinguish the two.
133    pub type_only: bool,
134    /// Exact exported-to-local bindings, when the parser can prove them.
135    pub bindings: Vec<ImportBindingFact>,
136}
137
138impl ImportFact {
139    #[must_use]
140    pub fn new(target: String, span: SourceSpan) -> Self {
141        Self {
142            target,
143            span,
144            type_only: false,
145            bindings: Vec::new(),
146        }
147    }
148
149    #[must_use]
150    pub fn type_only(target: String, span: SourceSpan) -> Self {
151        Self {
152            target,
153            span,
154            type_only: true,
155            bindings: Vec::new(),
156        }
157    }
158
159    #[must_use]
160    pub fn with_bindings(mut self, bindings: Vec<ImportBindingFact>) -> Self {
161        self.bindings = bindings;
162        self
163    }
164}
165
166/// One `use(prefix, target)`-style router mount observed in a source file.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct MountFact {
169    /// Path prefix the target is mounted under; empty for bare `use(x)`.
170    pub prefix: String,
171    /// Module specifier of the mounted router.
172    pub target: String,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct DomainFact {
177    pub name: String,
178    pub kind: NodeKind,
179    pub relation: EdgeKind,
180    pub span: SourceSpan,
181    pub owner: Option<SymbolLocator>,
182}
183
184#[derive(Debug, Default)]
185pub struct FileFacts {
186    pub symbols: Vec<SymbolFact>,
187    pub references: Vec<ReferenceFact>,
188    pub imports: Vec<ImportFact>,
189    pub domains: Vec<DomainFact>,
190    pub diagnostics: Vec<Diagnostic>,
191    pub mounts: Vec<MountFact>,
192    /// `export ... from 'x'` specifiers: this file forwards another module's
193    /// surface, so importers of this file reach that module transitively.
194    pub reexports: Vec<ImportFact>,
195}
196
197pub trait LanguageAdapter: Send + Sync {
198    fn language(&self) -> Language;
199    fn extensions(&self) -> &'static [&'static str];
200    fn extractor(&self) -> &'static str;
201    /// Parses one source file into language-neutral facts.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error when the adapter itself cannot initialize or maintain
206    /// its parser contract. Recoverable source syntax errors are diagnostics.
207    fn parse(&self, source: SourceFile<'_>) -> Result<FileFacts>;
208}
209
210pub struct LanguageRegistry {
211    adapters: Vec<Box<dyn LanguageAdapter>>,
212}
213
214impl Default for LanguageRegistry {
215    fn default() -> Self {
216        #[cfg(feature = "lang-rust")]
217        let mut adapters: Vec<Box<dyn LanguageAdapter>> = vec![Box::new(RustAdapter)];
218        #[cfg(not(feature = "lang-rust"))]
219        let mut adapters: Vec<Box<dyn LanguageAdapter>> = Vec::new();
220        adapters.extend([
221            Box::new(graphql::GraphqlAdapter) as Box<dyn LanguageAdapter>,
222            Box::new(protobuf::ProtobufAdapter) as Box<dyn LanguageAdapter>,
223            Box::new(json::JsonAdapter) as Box<dyn LanguageAdapter>,
224            Box::new(yaml::YamlAdapter) as Box<dyn LanguageAdapter>,
225        ]);
226        // `adapter_for_extension` takes the first adapter claiming an
227        // extension, and the tokenizer answers correctly where reading lines
228        // only usually does: a comment is a comment wherever it appears, a
229        // brace inside a string is text, a declaration may span three lines,
230        // and a span covers the name rather than the whole line.
231        adapters.extend(
232            tokenized::TokenizedAdapter::defaults()
233                // `weavatrix-parse` is the Rust implementation in the
234                // dependency-light build; the full build keeps exactly one
235                // `.rs` adapter and lets the richer syn path win.
236                .filter(|adapter| {
237                    !cfg!(feature = "lang-rust") || !adapter.extensions().contains(&"rs")
238                })
239                .map(|adapter| Box::new(adapter) as Box<dyn LanguageAdapter>),
240        );
241        Self { adapters }
242    }
243}
244
245impl LanguageRegistry {
246    pub fn extensions(&self) -> impl Iterator<Item = &'static str> + '_ {
247        self.adapters
248            .iter()
249            .flat_map(|adapter| adapter.extensions().iter().copied())
250    }
251
252    #[must_use]
253    pub fn adapter_for_extension(&self, extension: &str) -> Option<&dyn LanguageAdapter> {
254        self.adapters
255            .iter()
256            .find(|adapter| adapter.extensions().contains(&extension))
257            .map(AsRef::as_ref)
258    }
259
260    pub fn languages(&self) -> impl Iterator<Item = Language> + '_ {
261        self.adapters
262            .iter()
263            .map(|adapter| adapter.language())
264            .collect::<std::collections::BTreeSet<_>>()
265            .into_iter()
266    }
267}