ast_grep/
lib.rs

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)]
36/**
37 * TODO: add some description for ast-grep: sg
38 * Example:
39 * sg -p "$PATTERN.to($MATCH)" -l ts --rewrite "use($MATCH)"
40 */
41struct App {
42  #[clap(subcommand)]
43  command: Commands,
44  /// Path to ast-grep root config, default is sgconfig.yml.
45  #[clap(short, long, global = true, value_name = "CONFIG_FILE")]
46  config: Option<PathBuf>,
47}
48
49#[derive(Subcommand)]
50enum Commands {
51  /// Run one time search or rewrite in command line. (default command)
52  Run(RunArg),
53  /// Scan and rewrite code by configuration.
54  Scan(ScanArg),
55  /// Test ast-grep rules.
56  Test(TestArg),
57  /// Create new ast-grep project or items like rules/tests.
58  New(NewArg),
59  /// Start language server.
60  Lsp(LspArg),
61  /// Generate shell completion script.
62  Completions(CompletionsArg),
63  /// Generate rule docs for current configuration. (Not Implemented Yet)
64  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  // use `run` if there is at lease one pattern arg with no user provided command
89  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    // handle no subcommand
93    let arg = RunArg::try_parse_from(args)?;
94    Ok(Some(arg))
95  } else {
96    Ok(None)
97  }
98}
99
100/// finding project and setup custom language configuration
101fn 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    // handle --config=config.yml, see ast-grep/ast-grep#1617
109    if arg.contains('=') {
110      let config_file = arg.split('=').nth(1).unwrap().into();
111      config = Some(config_file);
112      break;
113    }
114    // handle -c config.yml, arg value should be next
115    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
124// this wrapper function is for testing
125pub 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  // register_custom_language_if_is_run(&args)?;
129  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"); // multiple paths
216    ok("run -p testm -r restm -U"); // update all
217    ok("run -p testm -r restm --update-all"); // update all
218    ok("run -p test --json compact"); // argument after --json should not be parsed as JsonStyle
219    ok("run -p test --json=pretty dir");
220    ok("run -p test --json dir"); // arg after --json should not be parsed as JsonStyle
221    ok("run -p test --strictness ast");
222    ok("run -p test --strictness relaxed");
223    ok("run -p test --selector identifier"); // pattern + selector
224    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"); // global config arg
232    error("run test");
233    error("run --debug-query test"); // missing lang
234    error("run -r Test dir");
235    error("run -p test -i --json dir"); // conflict
236    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"); // conflict
252    ok("scan dir1 dir2 dir3"); // multiple paths
253    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"); // allow registering custom lang
258    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"); // conflict
267    error("scan --report-style rich --json dir"); // conflict
268    error("scan -r test.yml --inline-rules '{}'"); // conflict
269    error("scan --format gitlab");
270    error("scan --format github -i");
271    error("scan --format local");
272    error("scan --json=dir"); // wrong json flag
273    error("scan --json= not-pretty"); // wrong json flag
274    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}