Skip to main content

weavatrix_rust/language/
mod.rs

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