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