Skip to main content

ast_grep_config/
rule_core.rs

1use crate::check_var::{check_rule_with_hint, CheckHint};
2use crate::fixer::{Fixer, FixerError, SerializableFixer};
3use crate::rule::referent_rule::RuleRegistration;
4use crate::rule::Rule;
5use crate::rule::{RuleSerializeError, SerializableRule};
6use crate::transform::{Transform, TransformError, Transformation};
7use crate::DeserializeEnv;
8
9use ast_grep_core::language::Language;
10use ast_grep_core::meta_var::MetaVarEnv;
11use ast_grep_core::{Doc, Matcher, Node};
12use serde::{Deserialize, Serialize};
13use serde_yaml::Error as YamlError;
14
15use bit_set::BitSet;
16use schemars::JsonSchema;
17use thiserror::Error;
18
19use std::borrow::Cow;
20use std::collections::{HashMap, HashSet};
21use std::ops::Deref;
22
23#[derive(Debug, Error)]
24pub enum RuleCoreError {
25  #[error("Fail to parse yaml as RuleConfig")]
26  Yaml(#[from] YamlError),
27  #[error("`utils` is not configured correctly.")]
28  Utils(#[source] RuleSerializeError),
29  #[error("`rule` is not configured correctly.")]
30  Rule(#[from] RuleSerializeError),
31  #[error("`constraints` is not configured correctly.")]
32  Constraints(#[source] RuleSerializeError),
33  #[error("`transform` is not configured correctly.")]
34  Transform(#[from] TransformError),
35  #[error("`fix` pattern is invalid.")]
36  Fixer(#[from] FixerError),
37  #[error("Undefined meta var `{0}` used in `{1}`.")]
38  UndefinedMetaVar(String, &'static str),
39}
40
41type RResult<T> = std::result::Result<T, RuleCoreError>;
42
43/// Used for global rules, rewriters, and pyo3/napi
44#[derive(Serialize, Deserialize, Clone, JsonSchema)]
45pub struct SerializableRuleCore {
46  /// A rule object to find matching AST nodes
47  pub rule: SerializableRule,
48  /// Additional meta variables pattern to filter matching
49  pub constraints: Option<HashMap<String, SerializableRule>>,
50  /// Utility rules that can be used in `matches`
51  pub utils: Option<HashMap<String, SerializableRule>>,
52  /// A dictionary for metavariable manipulation. Dict key is the new variable name.
53  /// Dict value is a [transformation] that specifies how meta var is processed.
54  /// See [transformation doc](https://ast-grep.github.io/reference/yaml/transformation.html).
55  pub transform: Option<HashMap<String, Transformation>>,
56  /// A pattern string or a FixConfig object to auto fix the issue.
57  /// It can reference metavariables appeared in rule.
58  /// See details in fix [object reference](https://ast-grep.github.io/reference/yaml/fix.html#fixconfig).
59  pub fix: Option<SerializableFixer>,
60}
61
62impl SerializableRuleCore {
63  /// This function assumes env's local is empty.
64  fn get_deserialize_env<L: Language>(&self, env: DeserializeEnv<L>) -> RResult<DeserializeEnv<L>> {
65    if let Some(utils) = &self.utils {
66      let env = env.with_utils(utils).map_err(RuleCoreError::Utils)?;
67      Ok(env)
68    } else {
69      Ok(env)
70    }
71  }
72
73  fn get_constraints<L: Language>(
74    &self,
75    env: &DeserializeEnv<L>,
76  ) -> RResult<HashMap<String, Rule>> {
77    let mut constraints = HashMap::new();
78    let Some(serde_cons) = &self.constraints else {
79      return Ok(constraints);
80    };
81    for (key, ser) in serde_cons {
82      let constraint = env
83        .deserialize_rule(ser.clone())
84        .map_err(RuleCoreError::Constraints)?;
85      constraints.insert(key.to_string(), constraint);
86    }
87    Ok(constraints)
88  }
89
90  fn get_fixer<L: Language>(&self, env: &DeserializeEnv<L>) -> RResult<Vec<Fixer>> {
91    if let Some(fix) = &self.fix {
92      let parsed = Fixer::parse(fix, env, &self.transform)?;
93      Ok(parsed)
94    } else {
95      Ok(vec![])
96    }
97  }
98
99  fn get_matcher_from_env<L: Language>(&self, env: &DeserializeEnv<L>) -> RResult<RuleCore> {
100    let rule = env.deserialize_rule(self.rule.clone())?;
101    let constraints = self.get_constraints(env)?;
102    let transform = self
103      .transform
104      .as_ref()
105      .map(|t| Transform::deserialize(t, env))
106      .transpose()?;
107    let fixer = self.get_fixer(env)?;
108    Ok(
109      RuleCore::new(rule)
110        .with_matchers(constraints)
111        .with_registration(env.registration.clone())
112        .with_transform(transform)
113        .with_fixer(fixer),
114    )
115  }
116
117  pub fn get_matcher<L: Language>(&self, env: DeserializeEnv<L>) -> RResult<RuleCore> {
118    self.get_matcher_with_hint(env, CheckHint::Normal)
119  }
120
121  pub(crate) fn get_matcher_with_hint<L: Language>(
122    &self,
123    env: DeserializeEnv<L>,
124    hint: CheckHint,
125  ) -> RResult<RuleCore> {
126    let env = self.get_deserialize_env(env)?;
127    let ret = self.get_matcher_from_env(&env)?;
128    check_rule_with_hint(
129      &ret.rule,
130      &ret.registration,
131      &ret.constraints,
132      &ret.transform,
133      &ret.fixer,
134      hint,
135    )?;
136    Ok(ret)
137  }
138}
139
140pub struct RuleCore {
141  rule: Rule,
142  constraints: HashMap<String, Rule>,
143  kinds: Option<BitSet>,
144  pub(crate) transform: Option<Transform>,
145  pub fixer: Vec<Fixer>,
146  // this is required to hold util rule reference
147  registration: RuleRegistration,
148}
149
150impl RuleCore {
151  #[inline]
152  pub fn new(rule: Rule) -> Self {
153    let kinds = rule.potential_kinds();
154    Self {
155      rule,
156      kinds,
157      ..Default::default()
158    }
159  }
160
161  #[inline]
162  pub fn with_matchers(self, constraints: HashMap<String, Rule>) -> Self {
163    Self {
164      constraints,
165      ..self
166    }
167  }
168
169  #[inline]
170  pub fn with_registration(self, registration: RuleRegistration) -> Self {
171    Self {
172      registration,
173      ..self
174    }
175  }
176
177  #[inline]
178  pub fn with_transform(self, transform: Option<Transform>) -> Self {
179    Self { transform, ..self }
180  }
181
182  #[inline]
183  pub fn with_fixer(self, fixer: Vec<Fixer>) -> Self {
184    Self { fixer, ..self }
185  }
186
187  pub fn get_env<L: Language>(&self, lang: L) -> DeserializeEnv<L> {
188    DeserializeEnv::from_registration(lang, self.registration.clone())
189  }
190
191  /// Get the meta variables that have real ast node matches
192  /// that is, meta vars defined in the rules and constraints
193  pub(crate) fn defined_node_vars(&self) -> HashSet<&str> {
194    let mut ret = self.rule.defined_vars();
195    for v in self.registration.get_local_util_vars() {
196      ret.insert(v);
197    }
198    for constraint in self.constraints.values() {
199      for var in constraint.defined_vars() {
200        ret.insert(var);
201      }
202    }
203    ret
204  }
205
206  pub fn defined_vars(&self) -> HashSet<&str> {
207    let mut ret = self.defined_node_vars();
208    if let Some(trans) = &self.transform {
209      for key in trans.keys() {
210        ret.insert(key);
211      }
212    }
213    ret
214  }
215
216  pub(crate) fn do_match<'tree, D: Doc>(
217    &self,
218    node: Node<'tree, D>,
219    env: &mut Cow<MetaVarEnv<'tree, D>>,
220    enclosing_env: Option<&MetaVarEnv<'tree, D>>,
221  ) -> Option<Node<'tree, D>> {
222    if let Some(kinds) = &self.kinds {
223      if !kinds.contains(node.kind_id().into()) {
224        return None;
225      }
226    }
227    let ret = self.rule.match_node_with_env(node, env)?;
228    if !env.to_mut().match_constraints(&self.constraints) {
229      return None;
230    }
231    if let Some(trans) = &self.transform {
232      let rewriters = self.registration.get_rewriters();
233      let env = env.to_mut();
234      if let Some(enclosing) = enclosing_env {
235        trans.apply_transform(env, rewriters, enclosing);
236      } else {
237        let enclosing = env.clone();
238        trans.apply_transform(env, rewriters, &enclosing);
239      };
240    }
241    Some(ret)
242  }
243}
244impl Deref for RuleCore {
245  type Target = Rule;
246  fn deref(&self) -> &Self::Target {
247    &self.rule
248  }
249}
250
251impl Default for RuleCore {
252  #[inline]
253  fn default() -> Self {
254    Self {
255      rule: Rule::default(),
256      constraints: HashMap::default(),
257      kinds: None,
258      transform: None,
259      fixer: vec![],
260      registration: RuleRegistration::default(),
261    }
262  }
263}
264
265impl Matcher for RuleCore {
266  fn match_node_with_env<'tree, D: Doc>(
267    &self,
268    node: Node<'tree, D>,
269    env: &mut Cow<MetaVarEnv<'tree, D>>,
270  ) -> Option<Node<'tree, D>> {
271    self.do_match(node, env, None)
272  }
273
274  fn potential_kinds(&self) -> Option<BitSet> {
275    self.kinds.clone()
276  }
277}
278
279#[cfg(test)]
280mod test {
281  use super::*;
282  use crate::from_str;
283  use crate::rule::referent_rule::{ReferentRule, ReferentRuleError};
284  use crate::test::TypeScript;
285  use crate::SerializableGlobalRule;
286  use ast_grep_core::matcher::{Pattern, RegexMatcher};
287  use ast_grep_core::tree_sitter::LanguageExt;
288
289  fn get_matcher(src: &str) -> RResult<RuleCore> {
290    let env = DeserializeEnv::new(TypeScript::Tsx);
291    let rule: SerializableRuleCore = from_str(src).expect("should word");
292    rule.get_matcher(env)
293  }
294
295  fn get_matcher_with_globals(rule_src: &str, globals_src: &str) -> RResult<RuleCore> {
296    let globals: Vec<SerializableGlobalRule<TypeScript>> =
297      from_str(globals_src).expect("should parse globals");
298    let globals = DeserializeEnv::parse_global_utils(globals).expect("should parse global rules");
299    let env = DeserializeEnv::new(TypeScript::Tsx).with_globals(&globals);
300    let rule: SerializableRuleCore = from_str(rule_src).expect("should parse rule");
301    rule.get_matcher(env)
302  }
303
304  #[test]
305  fn test_rule_error() {
306    let ret = get_matcher(r"rule: {kind: bbb}");
307    assert!(matches!(ret, Err(RuleCoreError::Rule(_))));
308  }
309
310  #[test]
311  fn test_utils_error() {
312    let ret = get_matcher(
313      r"
314rule: { kind: number }
315utils: { testa: {kind: bbb} }
316  ",
317    );
318    assert!(matches!(ret, Err(RuleCoreError::Utils(_))));
319  }
320
321  #[test]
322  fn test_undefined_utils_error() {
323    let ret = get_matcher(r"rule: { kind: number, matches: undefined-util }");
324    match ret {
325      Err(RuleCoreError::Rule(RuleSerializeError::MatchesReference(
326        ReferentRuleError::UndefinedUtil(name),
327      ))) => {
328        assert_eq!(name, "undefined-util");
329      }
330      _ => panic!("wrong error"),
331    }
332  }
333
334  #[test]
335  fn test_cyclic_transform_error() {
336    let ret = get_matcher(
337      r"
338rule: { kind: number }
339transform:
340  A: {substring: {source: $B}}
341  B: {substring: {source: $A}}",
342    );
343    assert!(matches!(
344      ret,
345      Err(RuleCoreError::Transform(TransformError::Cyclic(_)))
346    ));
347  }
348
349  #[test]
350  fn test_rule_reg_with_utils() {
351    let env = DeserializeEnv::new(TypeScript::Tsx);
352    let ser_rule: SerializableRuleCore =
353      from_str("{rule: {matches: test}, utils: {test: {kind: number}} }").expect("should deser");
354    let rule = ReferentRule::try_new("test".into(), &env.registration).expect("should work");
355    let not = ReferentRule::try_new("test2".into(), &env.registration).expect("should work");
356    let matcher = ser_rule.get_matcher(env).expect("should parse");
357    let grep = TypeScript::Tsx.ast_grep("a = 123");
358    assert!(grep.root().find(&matcher).is_some());
359    assert!(grep.root().find(&rule).is_some());
360    assert!(grep.root().find(&not).is_none());
361    let grep = TypeScript::Tsx.ast_grep("a = '123'");
362    assert!(grep.root().find(&matcher).is_none());
363    assert!(grep.root().find(&rule).is_none());
364    assert!(grep.root().find(&not).is_none());
365  }
366
367  #[test]
368  fn test_parameterized_global_rule_requires_all_args() {
369    let ret = get_matcher_with_globals(
370      r"
371rule:
372  matches:
373    wrap: {}
374",
375      r"
376- id: wrap
377  arguments: [BODY]
378  language: Tsx
379  rule:
380    matches: BODY
381",
382    );
383    assert!(matches!(
384      ret,
385      Err(RuleCoreError::Rule(RuleSerializeError::InvalidUtils(
386        crate::rule::ParameterizedUtilError::MissingUtilityArgument { callee, arg }
387      ))) if callee == "wrap" && arg == "BODY"
388    ));
389  }
390
391  #[test]
392  fn test_parameterized_global_rule_rejects_unknown_args() {
393    let ret = get_matcher_with_globals(
394      r"
395rule:
396  matches:
397    wrap:
398      OTHER:
399        kind: number
400      BODY:
401        kind: number
402",
403      r"
404- id: wrap
405  arguments: [BODY]
406  language: Tsx
407  rule:
408    matches: BODY
409",
410    );
411    assert!(matches!(
412      ret,
413      Err(RuleCoreError::Rule(RuleSerializeError::InvalidUtils(
414        crate::rule::ParameterizedUtilError::UnknownUtilityArgument { callee, arg }
415      ))) if callee == "wrap" && arg == "OTHER"
416    ));
417  }
418
419  #[test]
420  fn test_bare_parameterized_global_rule_without_args_is_rejected() {
421    let ret = get_matcher_with_globals(
422      r"
423rule:
424  matches: wrap
425",
426      r"
427- id: wrap
428  arguments: [BODY]
429  language: Tsx
430  rule:
431    matches: BODY
432",
433    );
434    assert!(matches!(
435      ret,
436      Err(RuleCoreError::Rule(RuleSerializeError::InvalidUtils(
437        crate::rule::ParameterizedUtilError::MissingUtilityArguments(name)
438      ))) if name == "wrap"
439    ));
440  }
441
442  #[test]
443  fn test_parameterized_global_call_cycle_in_argument_rule() {
444    let ret = get_matcher_with_globals(
445      r"
446rule:
447  matches:
448    RECUR:
449      x:
450        matches:
451          RECUR:
452            x:
453              kind: number
454",
455      r"
456- id: RECUR
457  arguments: [x]
458  language: Tsx
459  rule:
460    matches: x
461",
462    );
463    assert!(matches!(
464      ret,
465      Err(RuleCoreError::Rule(RuleSerializeError::MatchesReference(
466        ReferentRuleError::CyclicRule(rule)
467      ))) if rule == "RECUR"
468    ));
469  }
470
471  #[test]
472  fn test_parameterized_global_call_cycle_in_argument_rule_with_param_reference() {
473    let ret = get_matcher_with_globals(
474      r"
475rule:
476  matches:
477    RECUR:
478      x:
479        matches:
480          RECUR:
481            x:
482              matches: x
483",
484      r"
485- id: RECUR
486  arguments: [x]
487  language: Tsx
488  rule:
489    matches: x
490",
491    );
492    assert!(matches!(
493      ret,
494      Err(RuleCoreError::Rule(RuleSerializeError::MatchesReference(
495        ReferentRuleError::CyclicRule(rule)
496      ))) if rule == "RECUR"
497    ));
498  }
499
500  #[test]
501  fn test_local_utils_in_parameterized_global_rule_can_match_param_rule() {
502    let matcher = get_matcher_with_globals(
503      r"
504rule:
505  matches:
506    wrap:
507      BODY:
508        pattern: Some($INNER)
509",
510      r"
511- id: wrap
512  arguments: [BODY]
513  language: Tsx
514  rule:
515    matches: helper
516  utils:
517    helper:
518      matches: BODY
519",
520    )
521    .expect("should parse");
522    let grep = TypeScript::Tsx.ast_grep("Some(123)");
523    assert!(grep.root().find(&matcher).is_some());
524    let grep = TypeScript::Tsx.ast_grep("None");
525    assert!(grep.root().find(&matcher).is_none());
526  }
527
528  #[test]
529  fn test_nested_parameterized_global_rule_can_use_outer_param_in_nested_call() {
530    let matcher = get_matcher_with_globals(
531      r"
532rule:
533  matches:
534    outer:
535      X:
536        pattern: Some($INNER)
537",
538      r"
539- id: outer
540  arguments: [X]
541  language: Tsx
542  rule:
543    matches:
544      inner:
545        Y:
546          matches: X
547- id: inner
548  arguments: [Y]
549  language: Tsx
550  rule:
551    matches: Y
552",
553    )
554    .expect("should parse");
555    let grep = TypeScript::Tsx.ast_grep("Some(123)");
556    assert!(grep.root().find(&matcher).is_some());
557    let grep = TypeScript::Tsx.ast_grep("None");
558    assert!(grep.root().find(&matcher).is_none());
559  }
560
561  #[test]
562  fn test_rule_with_constraints() {
563    let mut constraints = HashMap::new();
564    constraints.insert(
565      "A".to_string(),
566      Rule::Regex(RegexMatcher::try_new("a").unwrap()),
567    );
568    let rule =
569      RuleCore::new(Rule::Pattern(Pattern::new("$A", TypeScript::Tsx))).with_matchers(constraints);
570    let grep = TypeScript::Tsx.ast_grep("a");
571    assert!(grep.root().find(&rule).is_some());
572    let grep = TypeScript::Tsx.ast_grep("bbb");
573    assert!(grep.root().find(&rule).is_none());
574  }
575
576  #[test]
577  fn test_constraints_inheriting_env() {
578    let env = DeserializeEnv::new(TypeScript::Tsx);
579    let ser_rule: SerializableRuleCore =
580      from_str("{rule: {pattern: $A = $B}, constraints: {A: {pattern: $B}} }")
581        .expect("should deser");
582    let matcher = ser_rule.get_matcher(env).expect("should parse");
583    let grep = TypeScript::Tsx.ast_grep("a = a");
584    assert!(grep.root().find(&matcher).is_some());
585    let grep = TypeScript::Tsx.ast_grep("a = b");
586    assert!(grep.root().find(&matcher).is_none());
587  }
588
589  #[test]
590  fn test_constraints_writing_to_env() {
591    let env = DeserializeEnv::new(TypeScript::Tsx);
592    let ser_rule: SerializableRuleCore =
593      from_str("{rule: {pattern: $A = $B}, constraints: {B: {pattern: $C + $D}} }")
594        .expect("should deser");
595    let matcher = ser_rule.get_matcher(env).expect("should parse");
596    let grep = TypeScript::Tsx.ast_grep("a = a");
597    assert!(grep.root().find(&matcher).is_none());
598    let grep = TypeScript::Tsx.ast_grep("a = 1 + 2");
599    let nm = grep.root().find(&matcher).expect("should match");
600    let env = nm.get_env();
601    let matched = env.get_match("C").expect("should match C").text();
602    assert_eq!(matched, "1");
603    let matched = env.get_match("D").expect("should match D").text();
604    assert_eq!(matched, "2");
605  }
606
607  fn get_rewriters() -> (&'static str, RuleCore) {
608    // NOTE: initialize a DeserializeEnv here is not 100% correct
609    // it does not inherit global rules or local rules
610    let env = DeserializeEnv::new(TypeScript::Tsx);
611    let rewriter: SerializableRuleCore =
612      from_str("{rule: {kind: number, pattern: $REWRITE}, fix: yjsnp}").expect("should parse");
613    let rewriter = rewriter.get_matcher(env).expect("should work");
614    ("re", rewriter)
615  }
616
617  #[test]
618  fn test_rewriter_writing_to_env() {
619    let (id, rewriter) = get_rewriters();
620    let env = DeserializeEnv::new(TypeScript::Tsx);
621    env.registration.insert_rewriter(id, rewriter);
622    let ser_rule: SerializableRuleCore = from_str(
623      r"
624rule: {pattern: $A = $B}
625transform:
626  C:
627    rewrite:
628      source: $B
629      rewriters: [re]",
630    )
631    .expect("should deser");
632    let matcher = ser_rule.get_matcher(env).expect("should parse");
633    let grep = TypeScript::Tsx.ast_grep("a = 1 + 2");
634    let nm = grep.root().find(&matcher).expect("should match");
635    let env = nm.get_env();
636    let matched = env.get_match("B").expect("should match").text();
637    assert_eq!(matched, "1 + 2");
638    let matched = env.get_match("A").expect("should match").text();
639    assert_eq!(matched, "a");
640    let transformed = env.get_transformed("C").expect("should transform");
641    assert_eq!(String::from_utf8_lossy(transformed), "yjsnp + yjsnp");
642    assert!(env.get_match("REWRITE").is_none());
643
644    let grep = TypeScript::Tsx.ast_grep("a = a");
645    let nm = grep.root().find(&matcher).expect("should match");
646    let env = nm.get_env();
647    let matched = env.get_match("B").expect("should match").text();
648    assert_eq!(matched, "a");
649    let transformed = env.get_transformed("C").expect("should transform");
650    assert_eq!(String::from_utf8_lossy(transformed), "a");
651  }
652}