mirror of
https://github.com/ast-grep/ast-grep.git
synced 2026-05-06 06:06:46 -04:00
[feat] add sgconfig yml
This commit is contained in:
+40
-11
@@ -4,7 +4,7 @@ mod interaction;
|
||||
use ansi_term::Color::{Cyan, Green, Red};
|
||||
use ansi_term::Style;
|
||||
use ast_grep_core::language::Language;
|
||||
use ast_grep_core::{Node, Pattern};
|
||||
use ast_grep_core::{Node, Pattern, Matcher};
|
||||
use clap::Parser;
|
||||
use guess_language::{SupportLang, file_types};
|
||||
use ignore::{WalkBuilder, WalkParallel, WalkState};
|
||||
@@ -39,6 +39,10 @@ struct Args {
|
||||
#[clap(short, long)]
|
||||
lang: Option<SupportLang>,
|
||||
|
||||
/// Path to ast-grep config, either YAML or folder of YAMLs
|
||||
#[clap(short, long, conflicts_with("pattern"))]
|
||||
config: Option<String>,
|
||||
|
||||
/// Include hidden files in search
|
||||
#[clap(short, long, parse(from_flag))]
|
||||
hidden: bool,
|
||||
@@ -58,16 +62,15 @@ struct Args {
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
if args.pattern.is_some() {
|
||||
run_with_pattern(args);
|
||||
run_with_pattern(args)
|
||||
} else {
|
||||
run_with_config(args);
|
||||
run_with_config(args)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Every run will include Search or Replace
|
||||
// Search or Replace by arguments `pattern` and `rewrite` passed from CLI
|
||||
fn run_with_pattern(args: Args) {
|
||||
fn run_with_pattern(args: Args) -> Result<()> {
|
||||
let pattern = args.pattern.unwrap();
|
||||
let threads = num_cpus::get().min(12);
|
||||
let lang = args.lang.unwrap();
|
||||
@@ -130,16 +133,42 @@ fn run_with_pattern(args: Args) {
|
||||
while let Ok((grep, path)) = rx.recv() {
|
||||
interaction::clear();
|
||||
let matches = grep.root().find_all(pattern.clone());
|
||||
print_matches(matches, &path, &pattern, &rewrite);
|
||||
print_matches(matches, &path, pattern.clone(), &rewrite);
|
||||
interaction::prompt("Confirm", "yn", Some('y'))
|
||||
.expect("Error happened during prompt");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_with_config(_arg: Args) {
|
||||
use ast_grep_config::from_yaml_string;
|
||||
println!("TODO! add the code path after config");
|
||||
fn run_with_config(args: Args) -> Result<()> {
|
||||
use ast_grep_config::{from_yaml_string};
|
||||
let config_file = args.config.unwrap_or_else(find_default_config);
|
||||
let yaml = read_to_string(config_file)?;
|
||||
let config = from_yaml_string(&yaml).unwrap();
|
||||
let threads = num_cpus::get().min(12);
|
||||
let walker = WalkBuilder::new(&args.path)
|
||||
.hidden(args.hidden)
|
||||
.threads(threads)
|
||||
.build_parallel();
|
||||
let lang = config.language;
|
||||
run_walker(walker, |path| {
|
||||
let file_content = match read_to_string(&path) {
|
||||
Ok(content) => content,
|
||||
_ => return,
|
||||
};
|
||||
let grep = lang.new(file_content);
|
||||
let mut matches = grep.root().find_all(config.clone()).peekable();
|
||||
if matches.peek().is_none() {
|
||||
return;
|
||||
}
|
||||
print_matches(matches, path, config.clone(), &None);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_default_config() -> String {
|
||||
"sgconfig.yml".to_string()
|
||||
}
|
||||
|
||||
fn run_walker(walker: WalkParallel, f: impl Fn(&Path) -> () + Sync) {
|
||||
@@ -180,13 +209,13 @@ fn match_one_file(
|
||||
if matches.peek().is_none() {
|
||||
return;
|
||||
}
|
||||
print_matches(matches, path, pattern, rewrite);
|
||||
print_matches(matches, path, pattern.clone(), rewrite);
|
||||
}
|
||||
|
||||
fn print_matches<'a>(
|
||||
matches: impl Iterator<Item = Node<'a, SupportLang>>,
|
||||
path: &Path,
|
||||
pattern: &Pattern<SupportLang>,
|
||||
pattern: impl Matcher<SupportLang> + Clone,
|
||||
rewrite: &Option<Pattern<SupportLang>>,
|
||||
) {
|
||||
let lock = std::io::stdout().lock(); // lock stdout to avoid interleaving output
|
||||
|
||||
+56
-30
@@ -2,13 +2,13 @@ pub mod support_language;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use ast_grep_core::{Rule, Matcher, PositiveMatcher};
|
||||
use ast_grep_core::{Rule, Matcher, PositiveMatcher, meta_var::MetaVarEnv};
|
||||
use ast_grep_core as core;
|
||||
|
||||
pub use support_language::SupportLang;
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
@@ -16,7 +16,7 @@ pub enum Severity {
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SerializableRule {
|
||||
All(Vec<SerializableRule>),
|
||||
@@ -28,28 +28,53 @@ pub enum SerializableRule {
|
||||
Kind(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AstGrepRuleConfig {
|
||||
/// Unique, descriptive identifier, e.g., no-unused-variable
|
||||
id: String,
|
||||
/// Message highlighting why this rule fired and how to remediate the issue
|
||||
message: String,
|
||||
/// One of: Info, Warning, or Error
|
||||
severity: Severity,
|
||||
/// Specify the language to parse and the file extension to includ in matching.
|
||||
language: SupportLang,
|
||||
/// Pattern rules to find matching AST nodes
|
||||
rule: SerializableRule,
|
||||
/// Addtionaly meta variables pattern to filter matching
|
||||
#[serde(default)]
|
||||
meta_variables: HashMap<String, String>,
|
||||
impl Matcher<SupportLang> for AstGrepRuleConfig {
|
||||
fn match_node_with_env<'tree>(&self, node: core::Node<'tree, SupportLang>, env: &mut MetaVarEnv<'tree, SupportLang>) -> Option<core::Node<'tree, SupportLang>> {
|
||||
use SerializableRule::*;
|
||||
let lang = self.language;
|
||||
match &self.rule {
|
||||
All(rules) => {
|
||||
core::All::new(rules.iter().map(|r| convert_serializable_rule(r, lang))).match_node_with_env(node, env)
|
||||
}
|
||||
Any(rules) => {
|
||||
core::Either::new(rules.into_iter().map(|r| convert_serializable_rule_to_positive(r, lang))).match_node_with_env(node, env)
|
||||
}
|
||||
Not(rule) => {
|
||||
core::Rule::not(convert_serializable_rule(rule, lang)).match_node_with_env(node, env)
|
||||
}
|
||||
Inside(rule) => {
|
||||
core::rule::Inside::new(convert_serializable_rule(rule, lang)).match_node_with_env(node, env)
|
||||
}
|
||||
Has(rule) => {
|
||||
core::rule::Inside::new(convert_serializable_rule(rule, lang)).match_node_with_env(node, env)
|
||||
}
|
||||
Pattern(pattern) => core::Pattern::new(&pattern, lang).match_node_with_env(node, env),
|
||||
Kind(kind_name) => core::KindMatcher::new(&kind_name, lang).match_node_with_env(node, env),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Parsed = Rule<SupportLang, Box<dyn PositiveMatcher<SupportLang>>>;
|
||||
pub fn from_yaml_string(yaml: &str) -> Result<Parsed, serde_yaml::Error> {
|
||||
let ast_grep_rule: AstGrepRuleConfig = serde_yaml::from_str(yaml)?;
|
||||
let matcher = convert_serializable_rule_to_positive(ast_grep_rule.rule, ast_grep_rule.language);
|
||||
Ok(Rule::new(matcher))
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AstGrepRuleConfig {
|
||||
/// Unique, descriptive identifier, e.g., no-unused-variable
|
||||
pub id: String,
|
||||
/// Message highlighting why this rule fired and how to remediate the issue
|
||||
pub message: String,
|
||||
/// One of: Info, Warning, or Error
|
||||
pub severity: Severity,
|
||||
/// Specify the language to parse and the file extension to includ in matching.
|
||||
pub language: SupportLang,
|
||||
/// Pattern rules to find matching AST nodes
|
||||
pub rule: SerializableRule,
|
||||
/// Addtionaly meta variables pattern to filter matching
|
||||
#[serde(default)]
|
||||
pub meta_variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
|
||||
type Parsed = Rule<SupportLang, SerializableRule>;
|
||||
pub fn from_yaml_string(yaml: &str) -> Result<AstGrepRuleConfig, serde_yaml::Error> {
|
||||
serde_yaml::from_str(yaml)
|
||||
}
|
||||
|
||||
enum SerializeError {
|
||||
@@ -57,7 +82,7 @@ enum SerializeError {
|
||||
MissPositiveMatcher,
|
||||
}
|
||||
|
||||
fn convert_serializable_rule_to_positive(rule: SerializableRule, lang: SupportLang) -> Box<dyn PositiveMatcher<SupportLang>> {
|
||||
fn convert_serializable_rule_to_positive(rule: &SerializableRule, lang: SupportLang) -> Box<dyn PositiveMatcher<SupportLang>> {
|
||||
use SerializableRule::*;
|
||||
match rule {
|
||||
All(rules) => {
|
||||
@@ -72,7 +97,7 @@ fn convert_serializable_rule_to_positive(rule: SerializableRule, lang: SupportLa
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_serializable_rule(rule: SerializableRule, lang: SupportLang) -> Box<dyn Matcher<SupportLang>> {
|
||||
fn convert_serializable_rule(rule: &SerializableRule, lang: SupportLang) -> Box<dyn Matcher<SupportLang>> {
|
||||
use SerializableRule::*;
|
||||
match rule {
|
||||
All(rules) => {
|
||||
@@ -82,13 +107,13 @@ fn convert_serializable_rule(rule: SerializableRule, lang: SupportLang) -> Box<d
|
||||
Box::new(core::Either::new(rules.into_iter().map(|r| convert_serializable_rule_to_positive(r, lang))))
|
||||
}
|
||||
Not(rule) => {
|
||||
Box::new(core::Rule::not(convert_serializable_rule(*rule, lang)))
|
||||
Box::new(core::Rule::not(convert_serializable_rule(rule, lang)))
|
||||
}
|
||||
Inside(rule) => {
|
||||
Box::new(core::rule::Inside::new(convert_serializable_rule(*rule, lang)))
|
||||
Box::new(core::rule::Inside::new(convert_serializable_rule(rule, lang)))
|
||||
}
|
||||
Has(rule) => {
|
||||
Box::new(core::rule::Inside::new(convert_serializable_rule(*rule, lang)))
|
||||
Box::new(core::rule::Inside::new(convert_serializable_rule(rule, lang)))
|
||||
}
|
||||
Pattern(pattern) => Box::new(core::Pattern::new(&pattern, lang)),
|
||||
Kind(kind_name) => Box::new(core::KindMatcher::new(&kind_name, lang)),
|
||||
@@ -99,16 +124,17 @@ fn convert_serializable_rule(rule: SerializableRule, lang: SupportLang) -> Box<d
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use ast_grep_core::language::Language;
|
||||
|
||||
fn test_rule_match(yaml: &str, source: &str) {
|
||||
let config = from_yaml_string(yaml).expect("rule should parse");
|
||||
let grep = core::AstGrep::new(source, SupportLang::TypeScript);
|
||||
let grep = config.language.new(source);
|
||||
assert!(grep.root().find(config).is_some());
|
||||
}
|
||||
|
||||
fn test_rule_unmatch(yaml: &str, source: &str) {
|
||||
let config = from_yaml_string(yaml).expect("rule should parse");
|
||||
let grep = core::AstGrep::new(source, SupportLang::TypeScript);
|
||||
let grep = config.language.new(source);
|
||||
assert!(grep.root().find(config).is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod language;
|
||||
mod match_tree;
|
||||
mod matcher;
|
||||
mod meta_var;
|
||||
pub mod meta_var;
|
||||
mod node;
|
||||
mod pattern;
|
||||
mod replacer;
|
||||
|
||||
@@ -210,6 +210,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Rule<L: Language, M: Matcher<L>> {
|
||||
inner: M,
|
||||
meta_vars: MetaVarMatchers<L>,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
let a = 123
|
||||
@@ -0,0 +1,6 @@
|
||||
id: test
|
||||
message: test rule
|
||||
severity: info
|
||||
language: TypeScript
|
||||
rule:
|
||||
pattern: $A && $A()
|
||||
Reference in New Issue
Block a user