weavatrix_rust/analyzer/
mod.rs1mod imports;
4mod mounts;
5mod pipeline;
6mod references;
7mod state;
8mod support;
9
10use crate::language::LanguageRegistry;
11use crate::model::{Result, Snapshot};
12use pipeline::parse_parallel;
13use state::{AnalysisState, parse_source};
14use std::path::Path;
15use support::{canonical_repository, capabilities};
16
17#[derive(Debug, Clone)]
18pub struct AnalyzerConfig {
19 pub max_file_bytes: u64,
20 pub n8n_max_file_bytes: u64,
23}
24
25impl Default for AnalyzerConfig {
26 fn default() -> Self {
27 Self {
28 max_file_bytes: crate::language::N8N_DEFAULT_FILE_BYTES,
29 n8n_max_file_bytes: 16 * 1024 * 1024,
30 }
31 }
32}
33
34pub struct Analyzer {
35 config: AnalyzerConfig,
36 languages: LanguageRegistry,
37}
38
39#[derive(Debug, Clone)]
40pub struct SourceInput {
41 pub path: String,
42 pub bytes: Vec<u8>,
43 pub content_hash: Option<String>,
44}
45
46impl Default for Analyzer {
47 fn default() -> Self {
48 Self::new(AnalyzerConfig::default())
49 }
50}
51
52impl Analyzer {
53 #[must_use]
54 pub fn new(config: AnalyzerConfig) -> Self {
55 Self {
56 config,
57 languages: LanguageRegistry::default(),
58 }
59 }
60
61 #[must_use]
62 pub fn supports_path(&self, path: &str) -> bool {
63 Path::new(path)
64 .extension()
65 .and_then(|value| value.to_str())
66 .map(str::to_ascii_lowercase)
67 .is_some_and(|extension| self.languages.adapter_for_extension(&extension).is_some())
68 }
69
70 #[must_use]
71 pub const fn max_file_bytes(&self) -> u64 {
72 self.config.max_file_bytes
73 }
74
75 pub fn analyze(&self, repository: impl AsRef<Path>) -> Result<Snapshot> {
82 self.analyze_with_report(repository)
83 .map(|(snapshot, _)| snapshot)
84 }
85
86 pub fn analyze_sources(
96 &self,
97 repository: impl AsRef<Path>,
98 revision: impl Into<String>,
99 sources: impl IntoIterator<Item = SourceInput>,
100 ) -> Result<Snapshot> {
101 let repository = canonical_repository(repository.as_ref())?;
102 let sources = sources
103 .into_iter()
104 .filter(|source| admits_source(&source.path, &source.bytes, &self.config))
105 .collect::<Vec<_>>();
106 let mut parsed = parse_parallel(sources.len(), |index| {
107 let source = &sources[index];
108 parse_source(
109 &source.path,
110 &source.bytes,
111 source.content_hash.as_deref(),
112 &self.languages,
113 )
114 })?;
115 mounts::apply(&mut parsed);
116 let (node_hint, edge_hint) = AnalysisState::expected(&parsed);
117 let mut state = AnalysisState::with_capacity(&repository, node_hint, edge_hint)?;
118 for item in parsed {
119 state.integrate(item)?;
120 }
121 state.resolve_references()?;
122 state.into_snapshot(&repository, revision.into(), capabilities(&self.languages))
123 }
124
125 pub fn analyze_json(&self, repository: impl AsRef<Path>, pretty: bool) -> Result<String> {
131 let snapshot = self.analyze(repository)?;
132 if pretty {
133 Ok(blazingly_json::to_string_pretty(&snapshot)?)
134 } else {
135 Ok(blazingly_json::to_string(&snapshot)?)
136 }
137 }
138
139 pub fn analyze_legacy_json(
145 &self,
146 repository: impl AsRef<Path>,
147 pretty: bool,
148 ) -> Result<String> {
149 Ok(self.analyze(repository)?.legacy_json(pretty)?)
150 }
151}
152
153fn admits_source(path: &str, bytes: &[u8], config: &AnalyzerConfig) -> bool {
154 let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
155 if size <= config.max_file_bytes {
156 return true;
157 }
158 if size > config.n8n_max_file_bytes {
159 return false;
160 }
161 let Ok(text) = std::str::from_utf8(bytes) else {
162 return false;
163 };
164 let extension = Path::new(path)
165 .extension()
166 .and_then(|value| value.to_str())
167 .unwrap_or("");
168 if extension.eq_ignore_ascii_case("json") {
169 crate::language::n8n_looks_promising(text)
170 || crate::language::agent_looks_promising(path, text)
171 } else if extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml") {
172 crate::language::dify_looks_promising(text)
173 } else if extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("mdx") {
174 crate::language::agent_looks_promising(path, text)
175 || crate::language::mermaid_looks_promising(path, text)
176 } else if extension.eq_ignore_ascii_case("mmd") || extension.eq_ignore_ascii_case("mermaid") {
177 crate::language::mermaid_looks_promising(path, text)
178 } else {
179 false
180 }
181}