1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use wasmtime::component::{Component, Linker};
5use wasmtime::{Engine, Store};
6use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
7
8use crate::plugin::{Finding, Location, Severity, SmellCategory};
9use crate::{AnalysisContext, Plugin};
10
11mod bindings {
12 wasmtime::component::bindgen!({
13 path: "wit/plugin.wit",
14 world: "analyzer",
15 });
16}
17
18use bindings::Analyzer;
19pub use bindings::cha::plugin::types as wit;
20
21struct HostState {
22 wasi: WasiCtx,
23 table: ResourceTable,
24}
25
26impl WasiView for HostState {
27 fn ctx(&mut self) -> WasiCtxView<'_> {
28 WasiCtxView {
29 ctx: &mut self.wasi,
30 table: &mut self.table,
31 }
32 }
33}
34
35fn new_host_state() -> HostState {
36 let wasi = WasiCtxBuilder::new().build();
37 HostState {
38 wasi,
39 table: ResourceTable::new(),
40 }
41}
42
43pub struct WasmPlugin {
45 engine: Engine,
46 component: Component,
47 plugin_name: String,
48 plugin_version: String,
49 plugin_description: String,
50 plugin_authors: Vec<String>,
51 plugin_smells: Vec<String>,
52 options: Vec<(String, wit::OptionValue)>,
53}
54
55impl WasmPlugin {
56 pub fn load(path: &Path) -> wasmtime::Result<Self> {
57 let engine = Engine::default();
58 let bytes = std::fs::read(path)?;
59 let component = Component::from_binary(&engine, &bytes)?;
60
61 let mut linker = Linker::<HostState>::new(&engine);
62 wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
63
64 let mut store = Store::new(&engine, new_host_state());
65 let instance = Analyzer::instantiate(&mut store, &component, &linker)?;
66 let name = instance.call_name(&mut store)?;
67 let version = instance.call_version(&mut store)?;
68 let description = instance.call_description(&mut store)?;
69 let authors = instance.call_authors(&mut store)?;
70 let smells = instance.call_smells(&mut store)?;
71
72 Ok(Self {
73 engine,
74 component,
75 plugin_name: name,
76 plugin_version: version,
77 plugin_description: description,
78 plugin_authors: authors,
79 plugin_smells: smells,
80 options: vec![],
81 })
82 }
83
84 pub fn set_options(&mut self, options: Vec<(String, wit::OptionValue)>) {
86 self.options = options;
87 }
88}
89
90impl Plugin for WasmPlugin {
91 fn name(&self) -> &str {
92 &self.plugin_name
93 }
94
95 fn version(&self) -> &str {
96 &self.plugin_version
97 }
98
99 fn description(&self) -> &str {
100 &self.plugin_description
101 }
102
103 fn authors(&self) -> Vec<String> {
104 self.plugin_authors.clone()
105 }
106
107 fn smells(&self) -> Vec<String> {
108 self.plugin_smells.clone()
109 }
110
111 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
112 let result = (|| -> wasmtime::Result<Vec<Finding>> {
113 let mut linker = Linker::<HostState>::new(&self.engine);
114 wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
115
116 let mut store = Store::new(&self.engine, new_host_state());
117 let instance = Analyzer::instantiate(&mut store, &self.component, &linker)?;
118 let input = to_wit_input(ctx, &self.options);
119 let results = instance.call_analyze(&mut store, &input)?;
120 Ok(results.into_iter().map(from_wit_finding).collect())
121 })();
122
123 result.unwrap_or_else(|e| {
124 eprintln!("wasm plugin error: {}", e);
125 vec![]
126 })
127 }
128}
129
130fn to_wit_input(
131 ctx: &AnalysisContext,
132 options: &[(String, wit::OptionValue)],
133) -> wit::AnalysisInput {
134 wit::AnalysisInput {
135 path: ctx.file.path.to_string_lossy().into(),
136 content: ctx.file.content.clone(),
137 language: ctx.model.language.clone(),
138 total_lines: ctx.model.total_lines as u32,
139 functions: convert_functions(&ctx.model.functions),
140 classes: convert_classes(&ctx.model.classes),
141 imports: convert_imports(&ctx.model.imports),
142 options: options.to_vec(),
143 }
144}
145
146fn convert_slice<T, U>(items: &[T], f: impl Fn(&T) -> U) -> Vec<U> {
148 items.iter().map(f).collect()
149}
150
151fn convert_functions(funcs: &[crate::model::FunctionInfo]) -> Vec<wit::FunctionInfo> {
152 convert_slice(funcs, |f| wit::FunctionInfo {
153 name: f.name.clone(),
154 start_line: f.start_line as u32,
155 end_line: f.end_line as u32,
156 name_col: f.name_col as u32,
157 name_end_col: f.name_end_col as u32,
158 line_count: f.line_count as u32,
159 complexity: f.complexity as u32,
160 parameter_count: f.parameter_count as u32,
161 parameter_types: f.parameter_types.clone(),
162 chain_depth: f.chain_depth as u32,
163 switch_arms: f.switch_arms as u32,
164 external_refs: f.external_refs.clone(),
165 is_delegating: f.is_delegating,
166 is_exported: f.is_exported,
167 comment_lines: f.comment_lines as u32,
168 referenced_fields: f.referenced_fields.clone(),
169 null_check_fields: f.null_check_fields.clone(),
170 switch_dispatch_target: f.switch_dispatch_target.clone(),
171 optional_param_count: f.optional_param_count as u32,
172 called_functions: f.called_functions.clone(),
173 cognitive_complexity: f.cognitive_complexity as u32,
174 body_hash: f.body_hash.map(|h| format!("{h:016x}")),
175 })
176}
177
178fn convert_classes(classes: &[crate::model::ClassInfo]) -> Vec<wit::ClassInfo> {
179 convert_slice(classes, |c| wit::ClassInfo {
180 name: c.name.clone(),
181 start_line: c.start_line as u32,
182 end_line: c.end_line as u32,
183 name_col: c.name_col as u32,
184 name_end_col: c.name_end_col as u32,
185 method_count: c.method_count as u32,
186 line_count: c.line_count as u32,
187 delegating_method_count: c.delegating_method_count as u32,
188 field_count: c.field_count as u32,
189 field_names: c.field_names.clone(),
190 field_types: c.field_types.clone(),
191 is_exported: c.is_exported,
192 has_behavior: c.has_behavior,
193 is_interface: c.is_interface,
194 parent_name: c.parent_name.clone(),
195 override_count: c.override_count as u32,
196 self_call_count: c.self_call_count as u32,
197 has_listener_field: c.has_listener_field,
198 has_notify_method: c.has_notify_method,
199 })
200}
201
202fn convert_imports(imports: &[crate::model::ImportInfo]) -> Vec<wit::ImportInfo> {
203 convert_slice(imports, |i| wit::ImportInfo {
204 source: i.source.clone(),
205 line: i.line as u32,
206 col: i.col as u32,
207 })
208}
209
210fn from_wit_finding(f: wit::Finding) -> Finding {
211 Finding {
212 smell_name: f.smell_name,
213 category: convert_category(f.category),
214 severity: convert_severity(f.severity),
215 location: Location {
216 path: PathBuf::from(&f.location.path),
217 start_line: f.location.start_line as usize,
218 start_col: f.location.start_col as usize,
219 end_line: f.location.end_line as usize,
220 end_col: f.location.end_col as usize,
221 name: f.location.name,
222 },
223 message: f.message,
224 suggested_refactorings: f.suggested_refactorings,
225 actual_value: f.actual_value,
226 threshold: f.threshold,
227 }
228}
229
230fn convert_severity(s: wit::Severity) -> Severity {
231 match s {
232 wit::Severity::Hint => Severity::Hint,
233 wit::Severity::Warning => Severity::Warning,
234 wit::Severity::Error => Severity::Error,
235 }
236}
237
238fn convert_category(c: wit::SmellCategory) -> SmellCategory {
239 match c {
240 wit::SmellCategory::Bloaters => SmellCategory::Bloaters,
241 wit::SmellCategory::OoAbusers => SmellCategory::OoAbusers,
242 wit::SmellCategory::ChangePreventers => SmellCategory::ChangePreventers,
243 wit::SmellCategory::Dispensables => SmellCategory::Dispensables,
244 wit::SmellCategory::Couplers => SmellCategory::Couplers,
245 wit::SmellCategory::Security => SmellCategory::Security,
246 }
247}
248
249pub fn load_wasm_plugins(project_dir: &Path) -> Vec<WasmPlugin> {
251 let mut plugins: Vec<WasmPlugin> = Vec::new();
252 let mut seen = HashMap::new();
253
254 let project_plugins = project_dir.join(".cha").join("plugins");
255 let global_plugins = home_dir().join(".cha").join("plugins");
256
257 for dir in [&project_plugins, &global_plugins] {
258 load_plugins_from_dir(dir, &mut seen, &mut plugins);
259 }
260
261 plugins
262}
263
264fn load_plugins_from_dir(
266 dir: &Path,
267 seen: &mut HashMap<String, bool>,
268 plugins: &mut Vec<WasmPlugin>,
269) {
270 let entries = match std::fs::read_dir(dir) {
271 Ok(e) => e,
272 Err(_) => return,
273 };
274 for entry in entries.flatten() {
275 let path = entry.path();
276 if path.extension().is_none_or(|e| e != "wasm") {
277 continue;
278 }
279 let filename = path.file_name().unwrap().to_string_lossy().to_string();
280 if seen.contains_key(&filename) {
281 continue;
282 }
283 match WasmPlugin::load(&path) {
284 Ok(p) => {
285 seen.insert(filename, true);
286 plugins.push(p);
287 }
288 Err(e) => {
289 eprintln!("failed to load wasm plugin {}: {}", path.display(), e);
290 }
291 }
292 }
293}
294
295fn home_dir() -> PathBuf {
296 std::env::var("HOME")
297 .map(PathBuf::from)
298 .unwrap_or_else(|_| PathBuf::from("."))
299}
300
301pub fn toml_to_option_value(v: &toml::Value) -> Option<wit::OptionValue> {
303 match v {
304 toml::Value::String(s) => Some(wit::OptionValue::Str(s.clone())),
305 toml::Value::Integer(i) => Some(wit::OptionValue::Int(*i)),
306 toml::Value::Float(f) => Some(wit::OptionValue::Float(*f)),
307 toml::Value::Boolean(b) => Some(wit::OptionValue::Boolean(*b)),
308 toml::Value::Array(arr) => {
309 let strs: Vec<String> = arr
310 .iter()
311 .filter_map(|v| v.as_str().map(String::from))
312 .collect();
313 Some(wit::OptionValue::ListStr(strs))
314 }
315 _ => None,
316 }
317}