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 #[cfg(debug_assertions)]
65 Docs,
66}
67
68pub fn execute_main() -> Result<()> {
69 match main_with_args(std::env::args()) {
70 Err(error) => exit_with_error(error),
71 ok => ok,
72 }
73}
74
75fn is_command(arg: &str, command: &str) -> bool {
76 let arg = arg.split('=').next().unwrap_or(arg);
77 if arg.starts_with("--") {
78 let arg = arg.trim_start_matches("--");
79 arg == command
80 } else if arg.starts_with('-') {
81 let arg = arg.trim_start_matches('-');
82 arg == &command[..1]
83 } else {
84 false
85 }
86}
87
88fn try_default_run(args: &[String]) -> Result<Option<RunArg>> {
89 let should_use_default_run_command =
91 args.iter().skip(1).any(|p| is_command(p, "pattern")) && args[1].starts_with('-');
92 if should_use_default_run_command {
93 let arg = RunArg::try_parse_from(args)?;
95 Ok(Some(arg))
96 } else {
97 Ok(None)
98 }
99}
100
101fn setup_project_is_possible(args: &[String]) -> Result<Result<ProjectConfig>> {
103 let mut config = None;
104 for i in 0..args.len() {
105 let arg = &args[i];
106 if !is_command(arg, "config") {
107 continue;
108 }
109 if arg.contains('=') {
111 let config_file = arg.split('=').nth(1).unwrap().into();
112 config = Some(config_file);
113 break;
114 }
115 if i + 1 >= args.len() || args[i + 1].starts_with('-') {
117 return Err(anyhow::anyhow!("missing config file after -c"));
118 }
119 let config_file = (&args[i + 1]).into();
120 config = Some(config_file);
121 }
122 ProjectConfig::setup(config)
123}
124
125pub fn main_with_args(args: impl Iterator<Item = String>) -> Result<()> {
127 let args: Vec<_> = args.collect();
128 let project = setup_project_is_possible(&args);
131 if let Some(arg) = try_default_run(&args)? {
132 return run_with_pattern(arg, project?);
133 }
134 let app = App::try_parse_from(args)?;
135 let project = project?; match app.command {
137 Commands::Run(arg) => run_with_pattern(arg, project),
138 Commands::Scan(arg) => run_with_config(arg, project),
139 Commands::Test(arg) => run_test_rule(arg, project),
140 Commands::New(arg) => run_create_new(arg, project),
141 Commands::Lsp(arg) => run_language_server(arg, project),
142 Commands::Completions(arg) => run_shell_completion::<App>(arg),
143 #[cfg(debug_assertions)]
144 Commands::Docs => todo!("todo, generate rule docs based on current config"),
145 }
146}
147
148#[cfg(test)]
149mod test_cli {
150 use super::*;
151
152 fn sg(args: &str) -> Result<App> {
153 let app = App::try_parse_from(
154 std::iter::once("sg".into()).chain(args.split(' ').map(|s| s.to_string())),
155 )?;
156 Ok(app)
157 }
158
159 fn ok(args: &str) -> App {
160 sg(args).expect("should parse")
161 }
162 fn error(args: &str) -> clap::Error {
163 let Err(err) = sg(args) else {
164 panic!("app parsing should fail!")
165 };
166 err
167 .downcast::<clap::Error>()
168 .expect("should have clap::Error")
169 }
170
171 #[test]
172 fn test_wrong_usage() {
173 error("");
174 error("Some($A) -l rs");
175 error("-l rs");
176 }
177
178 #[test]
179 fn test_version_and_help() {
180 let version = error("--version");
181 assert!(version.to_string().starts_with("ast-grep"));
182 let version = error("-V");
183 assert!(version.to_string().starts_with("ast-grep"));
184 let help = error("--help");
185 assert!(help.to_string().contains("Search and Rewrite code"));
186 }
187
188 fn default_run(args: &str) {
189 let args: Vec<_> = std::iter::once("sg".into())
190 .chain(args.split(' ').map(|s| s.to_string()))
191 .collect();
192 assert!(matches!(try_default_run(&args), Ok(Some(_))));
193 }
194 #[test]
195 fn test_no_arg_run() {
196 let ret = main_with_args(["sg".to_owned()].into_iter());
197 let err = ret.unwrap_err();
198 assert!(err.to_string().contains("sg [OPTIONS] <COMMAND>"));
199 }
200 #[test]
201 fn test_default_subcommand() {
202 default_run("-p Some($A) -l rs");
203 default_run("-p Some($A)");
204 default_run("-p Some($A) -l rs -r $A.unwrap()");
205 }
206
207 #[test]
208 fn test_run() {
209 ok("run -p test -i");
210 ok("run -p test --interactive dir");
211 ok("run -p test -r Test dir");
212 ok("run -p test -l rs --debug-query");
213 ok("run -p test -l rs --debug-query not");
214 ok("run -p test -l rs --debug-query=ast");
215 ok("run -p test -l rs --debug-query=cst");
216 ok("run -p test -l rs --color always");
217 ok("run -p test -l rs --heading always");
218 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");
223 ok("run -p test --json dir"); ok("run -p test --strictness ast");
225 ok("run -p test --strictness relaxed");
226 ok("run -p test --selector identifier"); ok("run -p test --selector identifier -l js");
228 ok("run -p test --follow");
229 ok("run -p test --globs '*.js'");
230 ok("run -p test --globs '*.{js, ts}'");
231 ok("run -p test --globs '*.js' --globs '*.ts'");
232 ok("run -p fubuki -j8");
233 ok("run -p test --threads 12");
234 ok("run -p test -l rs -c config.yml"); error("run test");
236 error("run --debug-query test"); error("run -r Test dir");
238 error("run -p test -i --json dir"); error("run -p test -U");
240 error("run -p test --update-all");
241 error("run -p test --strictness not");
242 error("run -p test -l rs --debug-query=not");
243 error("run -p test --selector");
244 error("run -p test --threads");
245 }
246
247 #[test]
248 fn test_scan() {
249 ok("scan");
250 ok("scan dir");
251 ok("scan -r test-rule.yml dir");
252 ok("scan -c test-rule.yml dir");
253 ok("scan -c test-rule.yml");
254 ok("scan --report-style short"); ok("scan dir1 dir2 dir3"); ok("scan -r test.yml --format github");
257 ok("scan --format github");
258 ok("scan --interactive");
259 ok("scan --follow");
260 ok("scan --json --include-metadata");
261 ok("scan -r test.yml -c test.yml --json dir"); ok("scan --globs '*.js'");
263 ok("scan --globs '*.{js, ts}'");
264 ok("scan --globs '*.js' --globs '*.ts'");
265 ok("scan -j 12");
266 ok("scan --threads 12");
267 ok("scan -A 12");
268 ok("scan --after 12");
269 ok("scan --context 1");
270 error("scan -i --json dir"); error("scan --report-style rich --json dir"); error("scan -r test.yml --inline-rules '{}'"); error("scan --format gitlab");
274 error("scan --format github -i");
275 error("scan --format local");
276 error("scan --json=dir"); error("scan --json= not-pretty"); error("scan -j");
279 error("scan --include-metadata"); error("scan --threads");
281 }
282
283 #[test]
284 fn test_test() {
285 ok("test");
286 ok("test -c sgconfig.yml");
287 ok("test --skip-snapshot-tests");
288 ok("test -U");
289 ok("test --update-all");
290 error("test --update-all --skip-snapshot-tests");
291 }
292 #[test]
293 fn test_new() {
294 ok("new");
295 ok("new project");
296 ok("new -c sgconfig.yml rule");
297 ok("new rule -y");
298 ok("new test -y");
299 ok("new util -y");
300 ok("new rule -c sgconfig.yml");
301 error("new --base-dir");
302 }
303
304 #[test]
305 fn test_shell() {
306 ok("completions");
307 ok("completions zsh");
308 ok("completions fish");
309 error("completions not-shell");
310 error("completions --shell fish");
311 }
312}