Skip to main content

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}