Skip to main content

plugin_host/
ast_facts.rs

1//! Wasm-бекенд поверхні `ast_facts` (`wit/ast-facts.wit`, world
2//! `ast-facts-plugin`) — друга реалізація [`harness::registry::AstFactsFn`].
3
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use wasmtime::component::{Component, HasSelf, Linker, ResourceTable};
8use wasmtime::{Engine, Store};
9
10use harness::registry::AstFactsFn;
11
12use crate::engine::new_engine;
13use crate::host_state::HostState;
14use crate::wasi_ctx::read_only_repo_ctx;
15
16mod bindings {
17    wasmtime::component::bindgen!({
18        path: "wit",
19        world: "ast-facts-plugin",
20    });
21}
22
23impl bindings::n7n::fix_deps::host_fs::Host for HostState {
24    fn read_file(&mut self, path: String) -> Result<String, String> {
25        std::fs::read_to_string(self.repo_root.join(path)).map_err(|e| e.to_string())
26    }
27}
28
29/// Wasm-бекенд одного `ast-facts`-компонента — доккоментар
30/// [`crate::detector::WasmDetector`].
31pub struct WasmAstFacts {
32    engine: Engine,
33    component: Component,
34    linker: Linker<HostState>,
35    repo_root: PathBuf,
36}
37
38impl WasmAstFacts {
39    /// Доккоментар [`crate::detector::WasmDetector::from_file`].
40    pub fn from_file(
41        repo_root: impl Into<PathBuf>,
42        wasm_path: impl AsRef<Path>,
43    ) -> wasmtime::Result<Self> {
44        let engine = new_engine()?;
45        let component = Component::from_file(&engine, wasm_path.as_ref())?;
46        let mut linker = Linker::<HostState>::new(&engine);
47        wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
48        bindings::n7n::fix_deps::host_fs::add_to_linker::<_, HasSelf<HostState>>(
49            &mut linker,
50            |s| s,
51        )?;
52        Ok(Self {
53            engine,
54            component,
55            linker,
56            repo_root: repo_root.into(),
57        })
58    }
59
60    fn call(&self, path: &std::path::Path) -> wasmtime::Result<String> {
61        let wasi = read_only_repo_ctx(&self.repo_root)?;
62        let mut store = Store::new(
63            &self.engine,
64            HostState {
65                wasi,
66                table: ResourceTable::new(),
67                repo_root: self.repo_root.clone(),
68            },
69        );
70        let instance =
71            bindings::AstFactsPlugin::instantiate(&mut store, &self.component, &self.linker)?;
72        let facts = instance
73            .n7n_fix_deps_ast_facts()
74            .call_facts(&mut store, &path.to_string_lossy())?;
75        Ok(facts)
76    }
77
78    /// Перетворює на [`AstFactsFn`]. `ast-facts.wit` (`facts: func(path:
79    /// string) -> string`) не має каналу помилки взагалі — той самий брак,
80    /// що вже несе Rust-сигнатура (`Arc<dyn Fn(PathBuf) ->
81    /// BoxFuture<'static, String>>`, без `Option`/`Result`). Інфраструктурний
82    /// збій викликового шляху (компонент не інстанціювався, трап) не має
83    /// куди піти, крім stderr-логу й порожнього рядка — той самий сентинел,
84    /// яким `attempt.rs` уже описує "інструмент недоступний" на рівні поля
85    /// (тут — на рівні одного виклику).
86    #[must_use]
87    pub fn into_ast_facts_fn(self) -> AstFactsFn {
88        let this = Arc::new(self);
89        Arc::new(move |path: PathBuf| {
90            let this = Arc::clone(&this);
91            Box::pin(async move {
92                this.call(&path).unwrap_or_else(|err| {
93                    eprintln!("wasm-компонент ast-facts: {err}");
94                    String::new()
95                })
96            })
97        })
98    }
99}