Skip to main content

harn_hostlib/ast/
mod.rs

1//! AST host capability.
2//!
3//! Wraps tree-sitter parsing, symbol extraction, and outline generation.
4//! The implementation is fully wired so AST builtins share one canonical
5//! wire format.
6//!
7//! ## Wire format
8//!
9//! - Row/column coordinates are **0-based** across all three builtins,
10//!   matching tree-sitter's native `Point` representation. `parse_file`,
11//!   `symbols`, and `outline` share one convention.
12//! - `parse_file` emits a flat node list with `parent_id` rather than
13//!   nested children — keeps the wire JSON-serializable without inflating
14//!   it with object copies.
15//! - `symbols` and `outline` carry a `signature` string (e.g.
16//!   `"fn foo(bar: i32)"`) on every entry.
17//!
18//! ## Languages
19//!
20//! [`language::Language`] covers the general-purpose languages
21//! (Harn, TypeScript/TSX, JavaScript/JSX, Python, Go, Rust, Java, C, C++,
22//! C#, Ruby, Kotlin, PHP, Scala, Bash, Swift, Zig, Elixir, Lua, Haskell, R)
23//! plus data/markup/config grammars (JSON, YAML, TOML, CSS, HTML, SQL,
24//! Markdown). The latter support the query-driven edit primitives but
25//! carry no symbol-graph projection — see
26//! [`language::Language::edit_capabilities`] for the per-language matrix.
27//! Adding/dropping languages requires coordinated schema, fixture, and
28//! host-bridge updates.
29//!
30
31use std::sync::Arc;
32
33use crate::code_index::SharedIndex;
34use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
35
36mod apply_node;
37mod batch_apply;
38mod bracket_balance;
39mod capabilities;
40mod changeset;
41mod dry_run;
42mod edit_common;
43mod function_body;
44mod fuzzy;
45mod health;
46mod imports;
47mod insert_at_anchor;
48mod language;
49mod mutation;
50mod outline;
51mod parse;
52mod parse_errors;
53mod search;
54mod structural_diff;
55mod symbols;
56mod symbols_call;
57mod types;
58mod undefined_names;
59mod unified_diff;
60
61pub use health::{Coverage, ParserHealth, ParserOperation, SourceObservation};
62pub use language::{EditCapabilities, Language, TEXT_PATCH_FALLBACK};
63pub use types::{OutlineItem, ParseError, ParsedNode, Symbol, SymbolKind, UndefinedName};
64
65/// Programmatic entry point to the AST builtins. Embedders typically go
66/// through the registered builtins, but tests and tools that want
67/// strongly-typed access can use these helpers directly.
68pub mod api {
69    use std::path::Path;
70
71    use tree_sitter::Tree;
72
73    use crate::error::HostlibError;
74
75    use super::language::Language;
76    use super::outline::build_outline;
77    use super::parse::{parse_source, read_source};
78    use super::symbols::extract;
79    use super::types::{OutlineItem, Symbol};
80
81    /// Parse `path` (with optional language hint) and return its symbols.
82    pub fn symbols(
83        path: &Path,
84        language_hint: Option<&str>,
85    ) -> Result<(Language, Vec<Symbol>), HostlibError> {
86        let language = detect(path, language_hint)?;
87        let source = read_source(&path.to_string_lossy(), 0)?;
88        let tree = parse_source(&source, language)?;
89        Ok((language, extract(&tree, &source, language)))
90    }
91
92    /// Parse `path` and return a hierarchical outline.
93    pub fn outline(
94        path: &Path,
95        language_hint: Option<&str>,
96    ) -> Result<(Language, Vec<OutlineItem>), HostlibError> {
97        let (language, symbols) = symbols(path, language_hint)?;
98        Ok((language, build_outline(symbols)))
99    }
100
101    /// Parse a source `str` for `language` and return its symbols. Useful
102    /// for unit tests where the input lives in-memory rather than on disk.
103    pub fn symbols_from_source(
104        source: &str,
105        language: Language,
106    ) -> Result<Vec<Symbol>, HostlibError> {
107        let tree = parse_source(source, language)?;
108        Ok(extract(&tree, source, language))
109    }
110
111    /// Parse a source `str` for `language` and return the raw tree-sitter
112    /// tree. Used by the typed symbol graph in
113    /// [`crate::code_index::symbol_graph`] to sweep for call sites
114    /// without re-doing the work the AST symbol extractor already did.
115    pub fn parse_tree(source: &str, language: Language) -> Result<Tree, HostlibError> {
116        parse_source(source, language)
117    }
118
119    /// Parse `source` once, then return the tree plus the symbol list
120    /// extracted from it. Lets a caller (e.g. the typed symbol graph)
121    /// avoid paying the parse cost twice when it needs both products.
122    pub fn parse_with_symbols(
123        source: &str,
124        language: Language,
125    ) -> Result<(Tree, Vec<Symbol>), HostlibError> {
126        let tree = parse_source(source, language)?;
127        let symbols = extract(&tree, source, language);
128        Ok((tree, symbols))
129    }
130
131    fn detect(path: &Path, language_hint: Option<&str>) -> Result<Language, HostlibError> {
132        Language::detect(path, language_hint).ok_or_else(|| HostlibError::InvalidParameter {
133            builtin: "ast::api",
134            param: "language",
135            message: format!(
136                "could not infer a tree-sitter grammar for `{}` \
137                 (extension or `language` field unrecognized)",
138                path.display()
139            ),
140        })
141    }
142}
143
144/// AST capability handle. Stateless; tree-sitter parsers are constructed
145/// per-call (cheap relative to grammar lookup) so the capability itself
146/// has nothing to own.
147#[derive(Default)]
148pub struct AstCapability;
149
150/// AST capability registered with access to the shared code-index state.
151///
152/// Most AST builtins are stateless, but `ast.dry_run` can preview
153/// `rename_symbol` plan ops only when it can delegate to the typed
154/// symbol graph owned by `code_index`.
155pub struct AstCapabilityWithCodeIndex {
156    code_index: SharedIndex,
157}
158
159impl AstCapabilityWithCodeIndex {
160    /// Build an AST capability that can delegate dry-run rename previews
161    /// to the supplied code-index state.
162    pub fn new(code_index: SharedIndex) -> Self {
163        Self { code_index }
164    }
165}
166
167impl HostlibCapability for AstCapability {
168    fn module_name(&self) -> &'static str {
169        "ast"
170    }
171
172    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
173        register_ast_builtins(registry, None);
174    }
175}
176
177impl HostlibCapability for AstCapabilityWithCodeIndex {
178    fn module_name(&self) -> &'static str {
179        "ast"
180    }
181
182    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
183        register_ast_builtins(registry, Some(self.code_index.clone()));
184    }
185}
186
187fn register_ast_builtins(registry: &mut BuiltinRegistry, code_index: Option<SharedIndex>) {
188    registry.register_fn("ast", "hostlib_ast_parse_file", "parse_file", parse::run);
189    registry.register_fn("ast", "hostlib_ast_symbols", "symbols", symbols_call::run);
190    registry.register_fn("ast", "hostlib_ast_outline", "outline", outline::run);
191    registry.register_fn(
192        "ast",
193        "hostlib_ast_parse_errors",
194        "parse_errors",
195        parse_errors::run,
196    );
197    registry.register_fn(
198        "ast",
199        "hostlib_ast_undefined_names",
200        "undefined_names",
201        undefined_names::run,
202    );
203    registry.register_fn(
204        "ast",
205        "hostlib_ast_function_body",
206        "function_body",
207        function_body::run_single,
208    );
209    registry.register_fn(
210        "ast",
211        "hostlib_ast_function_bodies",
212        "function_bodies",
213        function_body::run_bulk,
214    );
215    registry.register_fn(
216        "ast",
217        "hostlib_ast_extract_imports",
218        "extract_imports",
219        imports::run,
220    );
221    registry.register_fn(
222        "ast",
223        "hostlib_ast_symbol_extract",
224        "symbol_extract",
225        mutation::run_extract,
226    );
227    registry.register_fn(
228        "ast",
229        "hostlib_ast_symbol_delete",
230        "symbol_delete",
231        mutation::run_delete,
232    );
233    registry.register_fn(
234        "ast",
235        "hostlib_ast_symbol_replace",
236        "symbol_replace",
237        mutation::run_replace,
238    );
239    registry.register_fn(
240        "ast",
241        "hostlib_ast_bracket_balance",
242        "bracket_balance",
243        bracket_balance::run,
244    );
245    // These two write edited source back to disk, so they share the
246    // deterministic-tools gate with `tools::*` file I/O.
247    registry.register_fn(
248        "ast",
249        "hostlib_ast_apply_node",
250        "apply_node",
251        apply_node::run,
252    );
253    registry.register_fn(
254        "ast",
255        "hostlib_ast_insert_at_anchor",
256        "insert_at_anchor",
257        insert_at_anchor::run,
258    );
259    // Multi-file codemod runner. Writes when `dry_run: false`, so it shares
260    // the deterministic-tools write gate with the other mutating builtins.
261    registry.register_fn(
262        "ast",
263        "hostlib_ast_batch_apply",
264        "batch_apply",
265        batch_apply::run,
266    );
267    register_dry_run(registry, code_index.clone());
268    register_changeset_summary(registry, code_index);
269    // Read-only structural search: shares the query machinery with
270    // `apply_node` but never writes, so it carries no deterministic-tools
271    // gate.
272    registry.register_fn("ast", "hostlib_ast_search", "search", search::run);
273    registry.register_fn(
274        "ast",
275        "hostlib_ast_structural_diff",
276        "structural_diff",
277        structural_diff::run,
278    );
279    registry.register_fn(
280        "ast",
281        "hostlib_ast_capabilities",
282        "capabilities",
283        capabilities::run,
284    );
285}
286
287fn register_dry_run(registry: &mut BuiltinRegistry, code_index: Option<SharedIndex>) {
288    match code_index {
289        Some(index) => {
290            let handler: SyncHandler =
291                Arc::new(move |args| dry_run::run_with_code_index(Some(&index), args));
292            registry.register(RegisteredBuiltin {
293                name: "hostlib_ast_dry_run",
294                module: "ast",
295                method: "dry_run",
296                handler,
297            });
298        }
299        None => registry.register_fn("ast", "hostlib_ast_dry_run", "dry_run", dry_run::run),
300    }
301}
302
303fn register_changeset_summary(registry: &mut BuiltinRegistry, code_index: Option<SharedIndex>) {
304    match code_index {
305        Some(index) => {
306            let handler: SyncHandler =
307                Arc::new(move |args| changeset::run_with_code_index(Some(&index), args));
308            registry.register(RegisteredBuiltin {
309                name: "hostlib_ast_changeset_summary",
310                module: "ast",
311                method: "changeset_summary",
312                handler,
313            });
314        }
315        None => registry.register_fn(
316            "ast",
317            "hostlib_ast_changeset_summary",
318            "changeset_summary",
319            changeset::run,
320        ),
321    }
322}