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 std::sync::Arc;
16use support::{canonical_repository, capabilities};
17
18#[derive(Debug, Clone)]
19pub struct AnalyzerConfig {
20 pub max_file_bytes: u64,
21 pub n8n_max_file_bytes: u64,
24 pub web3_max_file_bytes: u64,
26}
27
28impl Default for AnalyzerConfig {
29 fn default() -> Self {
30 Self {
31 max_file_bytes: crate::language::N8N_DEFAULT_FILE_BYTES,
32 n8n_max_file_bytes: 16 * 1024 * 1024,
33 web3_max_file_bytes: crate::language::WEB3_MAX_BUILD_INFO_BYTES,
34 }
35 }
36}
37
38pub struct Analyzer {
39 config: AnalyzerConfig,
40 languages: Arc<LanguageRegistry>,
41}
42
43#[derive(Debug, Clone)]
44pub struct SourceInput {
45 pub path: String,
46 pub bytes: Vec<u8>,
47 pub content_hash: Option<String>,
48}
49
50impl Default for Analyzer {
51 fn default() -> Self {
52 Self::new(AnalyzerConfig::default())
53 }
54}
55
56impl Analyzer {
57 #[must_use]
58 pub fn new(config: AnalyzerConfig) -> Self {
59 Self {
60 config,
61 languages: Arc::new(LanguageRegistry::default()),
62 }
63 }
64
65 #[must_use]
66 pub fn supports_path(&self, path: &str) -> bool {
67 Path::new(path)
68 .extension()
69 .and_then(|value| value.to_str())
70 .map(str::to_ascii_lowercase)
71 .is_some_and(|extension| self.languages.adapter_for_extension(&extension).is_some())
72 }
73
74 #[must_use]
75 pub const fn max_file_bytes(&self) -> u64 {
76 self.config.max_file_bytes
77 }
78
79 pub fn analyze(&self, repository: impl AsRef<Path>) -> Result<Snapshot> {
86 self.analyze_with_report(repository)
87 .map(|(snapshot, _)| snapshot)
88 }
89
90 pub fn analyze_sources(
100 &self,
101 repository: impl AsRef<Path>,
102 revision: impl Into<String>,
103 sources: impl IntoIterator<Item = SourceInput>,
104 ) -> Result<Snapshot> {
105 let repository = canonical_repository(repository.as_ref())?;
106 let sources = sources
107 .into_iter()
108 .filter(|source| admits_source(&source.path, &source.bytes, &self.config))
109 .collect::<Vec<_>>();
110 let mut parsed = parse_parallel(sources.len(), |index| {
111 let source = &sources[index];
112 parse_source(
113 &source.path,
114 &source.bytes,
115 source.content_hash.as_deref(),
116 self.languages.as_ref(),
117 )
118 })?;
119 mounts::apply(&mut parsed);
120 let (node_hint, edge_hint) = AnalysisState::expected(&parsed);
121 let mut state = AnalysisState::with_capacity(&repository, node_hint, edge_hint)?;
122 for item in parsed {
123 state.integrate(item)?;
124 }
125 state.resolve_references()?;
126 state.into_snapshot(
127 &repository,
128 revision.into(),
129 capabilities(self.languages.as_ref()),
130 )
131 }
132
133 pub fn analyze_json(&self, repository: impl AsRef<Path>, pretty: bool) -> Result<String> {
139 let snapshot = self.analyze(repository)?;
140 if pretty {
141 Ok(blazingly_json::to_string_pretty(&snapshot)?)
142 } else {
143 Ok(blazingly_json::to_string(&snapshot)?)
144 }
145 }
146
147 pub fn analyze_legacy_json(
153 &self,
154 repository: impl AsRef<Path>,
155 pretty: bool,
156 ) -> Result<String> {
157 Ok(self.analyze(repository)?.legacy_json(pretty)?)
158 }
159}
160
161fn admits_source(path: &str, bytes: &[u8], config: &AnalyzerConfig) -> bool {
162 let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
163 if size <= config.max_file_bytes {
164 return true;
165 }
166 if size > config.n8n_max_file_bytes.max(config.web3_max_file_bytes) {
167 return false;
168 }
169 let Ok(text) = std::str::from_utf8(bytes) else {
170 return false;
171 };
172 let extension = Path::new(path)
173 .extension()
174 .and_then(|value| value.to_str())
175 .unwrap_or("");
176 if extension.eq_ignore_ascii_case("json") {
177 crate::language::n8n_looks_promising(text)
178 || crate::language::agent_looks_promising(path, text)
179 || (crate::language::web3_looks_promising(text)
180 && crate::language::web3_admitted_size(text, size)
181 && size <= config.web3_max_file_bytes)
182 } else if extension.eq_ignore_ascii_case("yaml") || extension.eq_ignore_ascii_case("yml") {
183 crate::language::dify_looks_promising(text)
184 } else if extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("mdx") {
185 crate::language::agent_looks_promising(path, text)
186 || crate::language::mermaid_looks_promising(path, text)
187 } else if extension.eq_ignore_ascii_case("mmd") || extension.eq_ignore_ascii_case("mermaid") {
188 crate::language::mermaid_looks_promising(path, text)
189 } else {
190 false
191 }
192}