1mod completions;
2mod config;
3mod lang;
4mod lsp;
5mod new;
6mod print;
7mod run;
8mod scan;
9mod utils;
10mod verify;
11
12use anyhow::Result;
13use clap::{Parser, Subcommand};
14use std::path::PathBuf;
15
16use completions::{run_shell_completion, CompletionsArg};
17use config::ProjectConfig;
18use lsp::{run_language_server, LspArg};
19use new::{run_create_new, NewArg};
20use run::{run_with_pattern, RunArg};
21use scan::{run_with_config, ScanArg};
22use utils::exit_with_error;
23use verify::{run_test_rule, TestArg};
24
25const LOGO: &str = r#"
26Search and Rewrite code at large scale using AST pattern.
27 __
28 ____ ______/ /_ ____ _________ ____
29 / __ `/ ___/ __/_____/ __ `/ ___/ _ \/ __ \
30 / /_/ (__ ) /_/_____/ /_/ / / / __/ /_/ /
31 \__,_/____/\__/ \__, /_/ \___/ .___/
32 /____/ /_/
33"#;
34#[derive(Parser)]
35#[clap(author, version, about, long_about = LOGO)]
36struct App {
42 #[clap(subcommand)]
43 command: Commands,
44 #[clap(short, long, global = true, value_name = "CONFIG_FILE")]
46 config: Option<PathBuf>,
47}
48
49#[derive(Subcommand)]
50enum Commands {
51 Run(RunArg),
53 Scan(ScanArg),
55 Test(TestArg),
57 New(NewArg),
59 Lsp(LspArg),
61 Completions(CompletionsArg),
63 Docs,
65}
66
67pub fn execute_main() -> Result<()> {
68 match main_with_args(std::env::args()) {
69 Err(error) => exit_with_error(error),
70 ok => ok,
71 }
72}
73
74fn is_command(arg: &str, command: &str) -> bool {
75 let arg = arg.split('=').next().unwrap_or(arg);
76 if arg.starts_with("--") {
77 let arg = arg.trim_start_matches("--");
78 arg == command
79 } else if arg.starts_with('-') {
80 let arg = arg.trim_start_matches('-');
81 arg == &command[..1]
82 } else {
83 false
84 }
85}
86
87fn try_default_run(args: &[String]) -> Result<Option<RunArg>> {
88 let should_use_default_run_command =
90 args.iter().skip(1).any(|p| is_command(p, "pattern")) && args[1].starts_with('-');
91 if should_use_default_run_command {
92 let arg = RunArg::try_parse_from(args)?;
94 Ok(Some(arg))
95 } else {
96 Ok(None)
97 }
98}
99
100fn setup_project_is_possible(args: &[String]) -> Result<Result<ProjectConfig>> {
102 let mut config = None;
103 for i in 0..args.len() {
104 let arg = &args[i];
105 if !is_command(arg, "config") {
106 continue;
107 }
108 if arg.contains('=') {
110 let config_file = arg.split('=').nth(1).unwrap().into();
111 config = Some(config_file);
112 break;
113 }
114 if i + 1 >= args.len() || args[i + 1].starts_with('-') {
116 return Err(anyhow::anyhow!("missing config file after -c"));
117 }
118 let config_file = (&args[i + 1]).into();
119 config = Some(config_file);
120 }
121 ProjectConfig::setup(config)
122}
123
124pub fn main_with_args(args: impl Iterator<Item = String>) -> Result<()> {
126 let args: Vec<_> = args.collect();
127 let project = setup_project_is_possible(&args)?;
128 if let Some(arg) = try_default_run(&args)? {
130 return run_with_pattern(arg, project);
131 }
132
133 let app = App::try_parse_from(args)?;
134 match app.command {
135 Commands::Run(arg) => run_with_pattern(arg, project),
136 Commands::Scan(arg) => run_with_config(arg, project),
137 Commands::Test(arg) => run_test_rule(arg, project),
138 Commands::New(arg) => run_create_new(arg, project),
139 Commands::Lsp(arg) => run_language_server(arg, project),
140 Commands::Completions(arg) => run_shell_completion::<App>(arg),
141 Commands::Docs => todo!("todo, generate rule docs based on current config"),
142 }
143}
144
145#[cfg(test)]
146mod test_cli {
147 use super::*;
148
149 fn sg(args: &str) -> Result<App> {
150 let app = App::try_parse_from(
151 std::iter::once("sg".into()).chain(args.split(' ').map(|s| s.to_string())),
152 )?;
153 Ok(app)
154 }
155
156 fn ok(args: &str) -> App {
157 sg(args).expect("should parse")
158 }
159 fn error(args: &str) -> clap::Error {
160 let Err(err) = sg(args) else {
161 panic!("app parsing should fail!")
162 };
163 err
164 .downcast::<clap::Error>()
165 .expect("should have clap::Error")
166 }
167
168 #[test]
169 fn test_wrong_usage() {
170 error("");
171 error("Some($A) -l rs");
172 error("-l rs");
173 }
174
175 #[test]
176 fn test_version_and_help() {
177 let version = error("--version");
178 assert!(version.to_string().starts_with("ast-grep"));
179 let version = error("-V");
180 assert!(version.to_string().starts_with("ast-grep"));
181 let help = error("--help");
182 assert!(help.to_string().contains("Search and Rewrite code"));
183 }
184
185 fn default_run(args: &str) {
186 let args: Vec<_> = std::iter::once("sg".into())
187 .chain(args.split(' ').map(|s| s.to_string()))
188 .collect();
189 assert!(matches!(try_default_run(&args), Ok(Some(_))));
190 }
191 #[test]
192 fn test_no_arg_run() {
193 let ret = main_with_args(["sg".to_owned()].into_iter());
194 let err = ret.unwrap_err();
195 assert!(err.to_string().contains("sg [OPTIONS] <COMMAND>"));
196 }
197 #[test]
198 fn test_default_subcommand() {
199 default_run("-p Some($A) -l rs");
200 default_run("-p Some($A)");
201 default_run("-p Some($A) -l rs -r $A.unwrap()");
202 }
203
204 #[test]
205 fn test_run() {
206 ok("run -p test -i");
207 ok("run -p test --interactive dir");
208 ok("run -p test -r Test dir");
209 ok("run -p test -l rs --debug-query");
210 ok("run -p test -l rs --debug-query not");
211 ok("run -p test -l rs --debug-query=ast");
212 ok("run -p test -l rs --debug-query=cst");
213 ok("run -p test -l rs --color always");
214 ok("run -p test -l rs --heading always");
215 ok("run -p test dir1 dir2 dir3"); ok("run -p testm -r restm -U"); ok("run -p testm -r restm --update-all"); ok("run -p test --json compact"); ok("run -p test --json=pretty dir");
220 ok("run -p test --json dir"); ok("run -p test --strictness ast");
222 ok("run -p test --strictness relaxed");
223 ok("run -p test --selector identifier"); ok("run -p test --selector identifier -l js");
225 ok("run -p test --follow");
226 ok("run -p test --globs '*.js'");
227 ok("run -p test --globs '*.{js, ts}'");
228 ok("run -p test --globs '*.js' --globs '*.ts'");
229 ok("run -p fubuki -j8");
230 ok("run -p test --threads 12");
231 ok("run -p test -l rs -c config.yml"); error("run test");
233 error("run --debug-query test"); error("run -r Test dir");
235 error("run -p test -i --json dir"); error("run -p test -U");
237 error("run -p test --update-all");
238 error("run -p test --strictness not");
239 error("run -p test -l rs --debug-query=not");
240 error("run -p test --selector");
241 error("run -p test --threads");
242 }
243
244 #[test]
245 fn test_scan() {
246 ok("scan");
247 ok("scan dir");
248 ok("scan -r test-rule.yml dir");
249 ok("scan -c test-rule.yml dir");
250 ok("scan -c test-rule.yml");
251 ok("scan --report-style short"); ok("scan dir1 dir2 dir3"); ok("scan -r test.yml --format github");
254 ok("scan --format github");
255 ok("scan --interactive");
256 ok("scan --follow");
257 ok("scan -r test.yml -c test.yml --json dir"); ok("scan --globs '*.js'");
259 ok("scan --globs '*.{js, ts}'");
260 ok("scan --globs '*.js' --globs '*.ts'");
261 ok("scan -j 12");
262 ok("scan --threads 12");
263 ok("scan -A 12");
264 ok("scan --after 12");
265 ok("scan --context 1");
266 error("scan -i --json dir"); error("scan --report-style rich --json dir"); error("scan -r test.yml --inline-rules '{}'"); error("scan --format gitlab");
270 error("scan --format github -i");
271 error("scan --format local");
272 error("scan --json=dir"); error("scan --json= not-pretty"); error("scan -j");
275 error("scan --threads");
276 }
277
278 #[test]
279 fn test_test() {
280 ok("test");
281 ok("test -c sgconfig.yml");
282 ok("test --skip-snapshot-tests");
283 ok("test -U");
284 ok("test --update-all");
285 error("test --update-all --skip-snapshot-tests");
286 }
287 #[test]
288 fn test_new() {
289 ok("new");
290 ok("new project");
291 ok("new -c sgconfig.yml rule");
292 ok("new rule -y");
293 ok("new test -y");
294 ok("new util -y");
295 ok("new rule -c sgconfig.yml");
296 error("new --base-dir");
297 }
298
299 #[test]
300 fn test_shell() {
301 ok("completions");
302 ok("completions zsh");
303 ok("completions fish");
304 error("completions not-shell");
305 error("completions --shell fish");
306 }
307}