thread_ast_engine/lib.rs
1// SPDX-FileCopyrightText: 2022 Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
2// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
3// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
4//
5// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
6//! # thread-ast-engine
7//!
8//! **Core AST engine for Thread: parsing, matching, and transforming code using AST patterns.**
9//!
10//! ## Overview
11//!
12//! `thread-ast-engine` provides powerful tools for working with Abstract Syntax Trees (ASTs).
13//! Forked from [`ast-grep-core`](https://github.com/ast-grep/ast-grep/), it offers language-agnostic
14//! APIs for code analysis and transformation.
15//!
16//! ### What You Can Do
17//!
18//! - **Parse** source code into ASTs using [tree-sitter](https://tree-sitter.github.io/tree-sitter/)
19//! - **Search** for code patterns using flexible meta-variables (like `$VAR`)
20//! - **Transform** code by replacing matched patterns with new code
21//! - **Navigate** AST nodes with intuitive tree traversal methods
22//!
23//! Perfect for building code linters, refactoring tools, and automated code modification systems.
24//!
25//! ## Quick Start
26//!
27//! Add to your `Cargo.toml`:
28//! ```toml
29//! [dependencies]
30//! thread-ast-engine = { version = "0.1.0", features = ["parsing", "matching"] }
31//! ```
32//!
33//! ### Basic Example: Find and Replace Variables
34//!
35//! ```rust,no_run
36//! use thread_ast_engine::Language;
37//! use thread_ast_engine::tree_sitter::LanguageExt;
38//!
39//! // Parse JavaScript/TypeScript code
40//! let mut ast = Language::Tsx.ast_grep("var a = 1; var b = 2;");
41//!
42//! // Replace all 'var' declarations with 'let'
43//! ast.replace("var $NAME = $VALUE", "let $NAME = $VALUE")?;
44//!
45//! // Get the transformed code
46//! println!("{}", ast.generate());
47//! // Output: "let a = 1; let b = 2;"
48//! # Ok::<(), String>(())
49//! ```
50//!
51//! ### Finding Code Patterns
52//!
53//! ```rust,no_run
54//! use thread_ast_engine::matcher::MatcherExt;
55//! # use thread_ast_engine::Language;
56//! # use thread_ast_engine::tree_sitter::LanguageExt;
57//!
58//! let ast = Language::Tsx.ast_grep("function add(a, b) { return a + b; }");
59//! let root = ast.root();
60//!
61//! // Find all function declarations
62//! if let Some(func) = root.find("function $NAME($$$PARAMS) { $$$BODY }") {
63//! println!("Function name: {}", func.get_env().get_match("NAME").unwrap().text());
64//! }
65//!
66//! // Find all return statements
67//! for ret_stmt in root.find_all("return $EXPR") {
68//! println!("Returns: {}", ret_stmt.get_env().get_match("EXPR").unwrap().text());
69//! }
70//! ```
71//!
72//! ### Working with Meta-Variables
73//!
74//! Meta-variables capture parts of the matched code:
75//!
76//! - `$VAR` - Captures a single AST node
77//! - `$$$ITEMS` - Captures multiple consecutive nodes (ellipsis)
78//! - `$_` - Matches any node but doesn't capture it
79//!
80//! ```rust,no_run
81//! # use thread_ast_engine::Language;
82//! # use thread_ast_engine::tree_sitter::LanguageExt;
83//! # use thread_ast_engine::matcher::MatcherExt;
84//! let ast = Language::Tsx.ast_grep("console.log('Hello', 'World', 123)");
85//! let root = ast.root();
86//!
87//! if let Some(call) = root.find("console.log($$$ARGS)") {
88//! let args = call.get_env().get_multiple_matches("ARGS");
89//! println!("Found {} arguments", args.len()); // Output: Found 3 arguments
90//! }
91//! ```
92//!
93//! ## Core Components
94//!
95//! ### [`Node`] - AST Navigation
96//! Navigate and inspect AST nodes with methods like [`Node::children`], [`Node::parent`], and [`Node::find`].
97//!
98//! ### [`Pattern`] - Code Matching
99//! Match code structures using tree-sitter patterns with meta-variables.
100//!
101//! ### [`MetaVarEnv`] - Variable Capture
102//! Store and retrieve captured meta-variables from pattern matches.
103//!
104//! ### [`Replacer`] - Code Transformation
105//! Replace matched code with new content, supporting template-based replacement.
106//!
107//! ### [`Language`] - Language Support
108//! Abstract interface for different programming languages via tree-sitter grammars.
109//!
110//! ## Feature Flags
111//!
112//! - **`parsing`** - Enables tree-sitter parsing (includes tree-sitter dependency)
113//! - **`matching`** - Enables pattern matching and node replacement/transformation engine.
114//!
115//! Use `default-features = false` to opt out of all features and enable only what you need:
116//!
117//! ```toml
118//! [dependencies]
119//! thread-ast-engine = { version = "0.1.0", default-features = false, features = ["matching"] }
120//! ```
121//!
122//! ## Advanced Examples
123//!
124//! ### Custom Pattern Matching
125//!
126//! ```rust,no_run
127//! use thread_ast_engine::ops::Op;
128//! # use thread_ast_engine::Language;
129//! # use thread_ast_engine::tree_sitter::LanguageExt;
130//! # use thread_ast_engine::matcher::MatcherExt;
131//!
132//! // Combine multiple patterns with logical operators
133//! let pattern = Op::either("let $VAR = $VALUE")
134//! .or("const $VAR = $VALUE")
135//! .or("var $VAR = $VALUE");
136//!
137//! let ast = Language::Tsx.ast_grep("const x = 42;");
138//! let root = ast.root();
139//!
140//! if let Some(match_) = root.find(pattern) {
141//! println!("Found variable declaration");
142//! }
143//! ```
144//!
145//! ### Tree Traversal
146//!
147//! ```rust,no_run
148//! # use thread_ast_engine::Language;
149//! # use thread_ast_engine::tree_sitter::LanguageExt;
150//! # use thread_ast_engine::matcher::MatcherExt;
151//! let ast = Language::Tsx.ast_grep("if (condition) { doSomething(); } else { doOther(); }");
152//! let root = ast.root();
153//!
154//! // Traverse all descendants
155//! for node in root.dfs() {
156//! if node.kind() == "identifier" {
157//! println!("Identifier: {}", node.text());
158//! }
159//! }
160//!
161//! // Check relationships between nodes
162//! if let Some(if_stmt) = root.find("if ($COND) { $$$THEN }") {
163//! println!("If statement condition: {}",
164//! if_stmt.get_env().get_match("COND").unwrap().text());
165//! }
166//! ```
167//!
168//! ## License
169//!
170//! Original ast-grep code is licensed under the [MIT license](./LICENSE-MIT),
171//! all changes introduced in this project are licensed under the [AGPL-3.0-or-later](./LICENSE-AGPL-3.0-or-later).
172//!
173//! See [`VENDORED.md`](crates/ast-engine/VENDORED.md) for more information on our fork, changes, and reasons.
174
175pub mod language;
176pub mod source;
177
178// Core AST functionality (always available)
179mod node;
180pub use node::{Node, Position};
181pub use source::Doc;
182// pub use matcher::types::{MatchStrictness, Pattern, PatternBuilder, PatternError, PatternNode};
183
184// Feature-gated modules
185#[cfg(feature = "parsing")]
186pub mod tree_sitter;
187
188// Everything but types feature gated behind "matching" in `matchers`
189mod matchers;
190
191#[cfg(feature = "matching")]
192mod match_tree;
193#[cfg(feature = "matching")]
194pub mod matcher;
195pub mod meta_var;
196#[cfg(feature = "matching")]
197pub mod ops;
198#[doc(hidden)]
199pub mod pinned;
200#[cfg(feature = "matching")]
201pub mod replacer;
202
203// Re-exports
204
205// the bare types with no implementations
206#[cfg(not(feature = "matching"))]
207pub use matchers::{
208 MatchStrictness, Pattern, PatternBuilder, PatternError, PatternNode,
209 matcher::{Matcher, MatcherExt, NodeMatch},
210};
211
212// implemented types
213#[cfg(feature = "matching")]
214pub use matcher::{
215 MatchAll, MatchNone, Matcher, MatcherExt, NodeMatch, Pattern, PatternBuilder, PatternError,
216 PatternNode,
217};
218
219pub use meta_var::MetaVarEnv;
220
221#[cfg(feature = "matching")]
222pub use match_tree::MatchStrictness;
223
224pub use language::Language;
225
226pub use node::Root;
227
228pub type AstGrep<D> = Root<D>;
229
230#[cfg(all(test, feature = "parsing", feature = "matching"))]
231mod test {
232 use super::*;
233 use crate::tree_sitter::LanguageExt;
234 use language::Tsx;
235 use ops::Op;
236
237 pub type Result = std::result::Result<(), String>;
238
239 #[test]
240 fn test_replace() -> Result {
241 let mut ast_grep = Tsx.ast_grep("var a = 1; let b = 2;");
242 ast_grep.replace("var $A = $B", "let $A = $B")?;
243 let source = ast_grep.generate();
244 assert_eq!(source, "let a = 1; let b = 2;"); // note the semicolon
245 Ok(())
246 }
247
248 #[test]
249 fn test_replace_by_rule() -> Result {
250 let rule = Op::either("let a = 123").or("let b = 456");
251 let mut ast_grep = Tsx.ast_grep("let a = 123");
252 let replaced = ast_grep.replace(rule, "console.log('it works!')")?;
253 assert!(replaced);
254 let source = ast_grep.generate();
255 assert_eq!(source, "console.log('it works!')");
256 Ok(())
257 }
258
259 #[test]
260 fn test_replace_unnamed_node() -> Result {
261 // ++ and -- is unnamed node in tree-sitter javascript
262 let mut ast_grep = Tsx.ast_grep("c++");
263 ast_grep.replace("$A++", "$A--")?;
264 let source = ast_grep.generate();
265 assert_eq!(source, "c--");
266 Ok(())
267 }
268
269 #[test]
270 fn test_replace_trivia() -> Result {
271 let mut ast_grep = Tsx.ast_grep("var a = 1 /*haha*/;");
272 ast_grep.replace("var $A = $B", "let $A = $B")?;
273 let source = ast_grep.generate();
274 assert_eq!(source, "let a = 1 /*haha*/;"); // semicolon
275
276 let mut ast_grep = Tsx.ast_grep("var a = 1; /*haha*/");
277 ast_grep.replace("var $A = $B", "let $A = $B")?;
278 let source = ast_grep.generate();
279 assert_eq!(source, "let a = 1; /*haha*/");
280 Ok(())
281 }
282
283 #[test]
284 fn test_replace_trivia_with_skipped() -> Result {
285 let mut ast_grep = Tsx.ast_grep("return foo(1, 2,) /*haha*/;");
286 ast_grep.replace("return foo($A, $B)", "return bar($A, $B)")?;
287 let source = ast_grep.generate();
288 assert_eq!(source, "return bar(1, 2) /*haha*/;"); // semicolon
289 Ok(())
290 }
291}