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