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