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 dry_run;
41mod edit_common;
42mod function_body;
43mod fuzzy;
44mod health;
45mod imports;
46mod insert_at_anchor;
47mod language;
48mod mutation;
49mod outline;
50mod parse;
51mod parse_errors;
52mod search;
53mod structural_diff;
54mod symbols;
55mod symbols_call;
56mod types;
57mod undefined_names;
58mod unified_diff;
59
60pub use health::{Coverage, ParserHealth, ParserOperation, SourceObservation};
61pub use language::{EditCapabilities, Language, TEXT_PATCH_FALLBACK};
62pub use types::{OutlineItem, ParseError, ParsedNode, Symbol, SymbolKind, UndefinedName};
63
64/// Programmatic entry point to the AST builtins. Embedders typically go
65/// through the registered builtins, but tests and tools that want
66/// strongly-typed access can use these helpers directly.
67pub mod api {
68    use std::path::Path;
69
70    use tree_sitter::Tree;
71
72    use crate::error::HostlibError;
73
74    use super::language::Language;
75    use super::outline::build_outline;
76    use super::parse::{parse_source, read_source};
77    use super::symbols::extract;
78    use super::types::{OutlineItem, Symbol};
79
80    /// Parse `path` (with optional language hint) and return its symbols.
81    pub fn symbols(
82        path: &Path,
83        language_hint: Option<&str>,
84    ) -> Result<(Language, Vec<Symbol>), HostlibError> {
85        let language = detect(path, language_hint)?;
86        let source = read_source(&path.to_string_lossy(), 0)?;
87        let tree = parse_source(&source, language)?;
88        Ok((language, extract(&tree, &source, language)))
89    }
90
91    /// Parse `path` and return a hierarchical outline.
92    pub fn outline(
93        path: &Path,
94        language_hint: Option<&str>,
95    ) -> Result<(Language, Vec<OutlineItem>), HostlibError> {
96        let (language, symbols) = symbols(path, language_hint)?;
97        Ok((language, build_outline(symbols)))
98    }
99
100    /// Parse a source `str` for `language` and return its symbols. Useful
101    /// for unit tests where the input lives in-memory rather than on disk.
102    pub fn symbols_from_source(
103        source: &str,
104        language: Language,
105    ) -> Result<Vec<Symbol>, HostlibError> {
106        let tree = parse_source(source, language)?;
107        Ok(extract(&tree, source, language))
108    }
109
110    /// Parse a source `str` for `language` and return the raw tree-sitter
111    /// tree. Used by the typed symbol graph in
112    /// [`crate::code_index::symbol_graph`] to sweep for call sites
113    /// without re-doing the work the AST symbol extractor already did.
114    pub fn parse_tree(source: &str, language: Language) -> Result<Tree, HostlibError> {
115        parse_source(source, language)
116    }
117
118    /// Parse `source` once, then return the tree plus the symbol list
119    /// extracted from it. Lets a caller (e.g. the typed symbol graph)
120    /// avoid paying the parse cost twice when it needs both products.
121    pub fn parse_with_symbols(
122        source: &str,
123        language: Language,
124    ) -> Result<(Tree, Vec<Symbol>), HostlibError> {
125        let tree = parse_source(source, language)?;
126        let symbols = extract(&tree, source, language);
127        Ok((tree, symbols))
128    }
129
130    fn detect(path: &Path, language_hint: Option<&str>) -> Result<Language, HostlibError> {
131        Language::detect(path, language_hint).ok_or_else(|| HostlibError::InvalidParameter {
132            builtin: "ast::api",
133            param: "language",
134            message: format!(
135                "could not infer a tree-sitter grammar for `{}` \
136                 (extension or `language` field unrecognized)",
137                path.display()
138            ),
139        })
140    }
141}
142
143/// AST capability handle. Stateless; tree-sitter parsers are constructed
144/// per-call (cheap relative to grammar lookup) so the capability itself
145/// has nothing to own.
146#[derive(Default)]
147pub struct AstCapability;
148
149/// AST capability registered with access to the shared code-index state.
150///
151/// Most AST builtins are stateless, but `ast.dry_run` can preview
152/// `rename_symbol` plan ops only when it can delegate to the typed
153/// symbol graph owned by `code_index`.
154pub struct AstCapabilityWithCodeIndex {
155    code_index: SharedIndex,
156}
157
158impl AstCapabilityWithCodeIndex {
159    /// Build an AST capability that can delegate dry-run rename previews
160    /// to the supplied code-index state.
161    pub fn new(code_index: SharedIndex) -> Self {
162        Self { code_index }
163    }
164}
165
166impl HostlibCapability for AstCapability {
167    fn module_name(&self) -> &'static str {
168        "ast"
169    }
170
171    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
172        register_ast_builtins(registry, None);
173    }
174}
175
176impl HostlibCapability for AstCapabilityWithCodeIndex {
177    fn module_name(&self) -> &'static str {
178        "ast"
179    }
180
181    fn register_builtins(&self, registry: &mut BuiltinRegistry) {
182        register_ast_builtins(registry, Some(self.code_index.clone()));
183    }
184}
185
186fn register_ast_builtins(registry: &mut BuiltinRegistry, code_index: Option<SharedIndex>) {
187    registry.register_fn("ast", "hostlib_ast_parse_file", "parse_file", parse::run);
188    registry.register_fn("ast", "hostlib_ast_symbols", "symbols", symbols_call::run);
189    registry.register_fn("ast", "hostlib_ast_outline", "outline", outline::run);
190    registry.register_fn(
191        "ast",
192        "hostlib_ast_parse_errors",
193        "parse_errors",
194        parse_errors::run,
195    );
196    registry.register_fn(
197        "ast",
198        "hostlib_ast_undefined_names",
199        "undefined_names",
200        undefined_names::run,
201    );
202    registry.register_fn(
203        "ast",
204        "hostlib_ast_function_body",
205        "function_body",
206        function_body::run_single,
207    );
208    registry.register_fn(
209        "ast",
210        "hostlib_ast_function_bodies",
211        "function_bodies",
212        function_body::run_bulk,
213    );
214    registry.register_fn(
215        "ast",
216        "hostlib_ast_extract_imports",
217        "extract_imports",
218        imports::run,
219    );
220    registry.register_fn(
221        "ast",
222        "hostlib_ast_symbol_extract",
223        "symbol_extract",
224        mutation::run_extract,
225    );
226    registry.register_fn(
227        "ast",
228        "hostlib_ast_symbol_delete",
229        "symbol_delete",
230        mutation::run_delete,
231    );
232    registry.register_fn(
233        "ast",
234        "hostlib_ast_symbol_replace",
235        "symbol_replace",
236        mutation::run_replace,
237    );
238    registry.register_fn(
239        "ast",
240        "hostlib_ast_bracket_balance",
241        "bracket_balance",
242        bracket_balance::run,
243    );
244    // These two write edited source back to disk, so they share the
245    // deterministic-tools gate with `tools::*` file I/O.
246    registry.register_gated_fn(
247        "ast",
248        "hostlib_ast_apply_node",
249        "apply_node",
250        apply_node::run,
251    );
252    registry.register_gated_fn(
253        "ast",
254        "hostlib_ast_insert_at_anchor",
255        "insert_at_anchor",
256        insert_at_anchor::run,
257    );
258    // Multi-file codemod runner. Writes when `dry_run: false`, so it shares
259    // the deterministic-tools write gate with the other mutating builtins.
260    registry.register_gated_fn(
261        "ast",
262        "hostlib_ast_batch_apply",
263        "batch_apply",
264        batch_apply::run,
265    );
266    register_dry_run(registry, code_index);
267    // Read-only structural search: shares the query machinery with
268    // `apply_node` but never writes, so it carries no deterministic-tools
269    // gate.
270    registry.register_fn("ast", "hostlib_ast_search", "search", search::run);
271    registry.register_fn(
272        "ast",
273        "hostlib_ast_structural_diff",
274        "structural_diff",
275        structural_diff::run,
276    );
277    registry.register_fn(
278        "ast",
279        "hostlib_ast_capabilities",
280        "capabilities",
281        capabilities::run,
282    );
283}
284
285fn register_dry_run(registry: &mut BuiltinRegistry, code_index: Option<SharedIndex>) {
286    match code_index {
287        Some(index) => {
288            let handler: SyncHandler =
289                Arc::new(move |args| dry_run::run_with_code_index(Some(&index), args));
290            registry.register(RegisteredBuiltin {
291                name: "hostlib_ast_dry_run",
292                module: "ast",
293                method: "dry_run",
294                handler,
295            });
296        }
297        None => registry.register_fn("ast", "hostlib_ast_dry_run", "dry_run", dry_run::run),
298    }
299}