Skip to main content

ast_grep_config/
rule_config.rs

1use crate::GlobalRules;
2
3use crate::check_var::{check_fix, check_rewriters_in_transform};
4use crate::fixer::{Fixer, FixerError, SerializableFixer};
5use crate::label::{Label, LabelConfig, get_default_labels, get_labels_from_config};
6use crate::rewriter::RewriterError;
7pub use crate::rewriter::SerializableRewriter;
8use crate::rule::DeserializeEnv;
9use crate::rule_core::{RuleCore, RuleCoreError, SerializableRuleCore};
10
11use ast_grep_core::language::Language;
12use ast_grep_core::replacer::Replacer;
13use ast_grep_core::source::Content;
14use ast_grep_core::{Doc, Matcher, NodeMatch};
15
16use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
17use serde::{Deserialize, Serialize};
18use serde_yaml::Error as YamlError;
19use serde_yaml::{Deserializer, with::singleton_map_recursive::deserialize};
20use thiserror::Error;
21
22use std::borrow::Cow;
23use std::collections::HashMap;
24use std::ops::{Deref, DerefMut};
25
26#[derive(Serialize, Deserialize, Clone, Default, JsonSchema, Debug)]
27#[serde(rename_all = "camelCase")]
28pub enum Severity {
29  #[default]
30  /// A kind reminder for code with potential improvement.
31  Hint,
32  /// A suggestion that code can be improved or optimized.
33  Info,
34  /// A warning that code might produce bugs or does not follow best practice.
35  Warning,
36  /// An error that code produces bugs or has logic errors.
37  Error,
38  /// Turns off the rule.
39  Off,
40}
41
42#[derive(Debug, Error)]
43pub enum RuleConfigError {
44  #[error("Fail to parse yaml as RuleConfig")]
45  Yaml(#[from] YamlError),
46  #[error("Fail to parse yaml as Rule.")]
47  Core(#[from] RuleCoreError),
48  #[error("`fix` pattern is invalid.")]
49  Fixer(#[from] FixerError),
50  #[error(transparent)]
51  Rewriter(#[from] RewriterError),
52  #[error("Undefined rewriter `{0}` used in transform.")]
53  UndefinedRewriter(String),
54  #[error("Label meta-variable `{0}` must be defined in `rule` or `constraints`.")]
55  LabelVariable(String),
56  #[error("Rule must specify a set of AST kinds to match. Try adding `kind` rule.")]
57  MissingPotentialKinds,
58}
59
60#[derive(Serialize, Deserialize, Clone, JsonSchema)]
61#[schemars(title = "ast-grep rule")]
62pub struct SerializableRuleConfig<L: Language> {
63  #[serde(flatten)]
64  pub core: SerializableRuleCore,
65  /// A pattern string or a FixConfig object to auto fix the issue.
66  /// It can reference metavariables appeared in rule.
67  /// See details in fix [object reference](https://ast-grep.github.io/reference/yaml/fix.html#fixconfig).
68  pub fix: Option<SerializableFixer>,
69  /// Rewrite rules for `rewrite` transformation
70  pub rewriters: Option<Vec<SerializableRewriter>>,
71  /// Unique, descriptive identifier, e.g., no-unused-variable
72  #[serde(default)]
73  pub id: String,
74  /// Specify the language to parse and the file extension to include in matching.
75  pub language: L,
76  /// Main message highlighting why this rule fired. It should be single line and concise,
77  /// but specific enough to be understood without additional context.
78  #[serde(default)]
79  pub message: String,
80  /// Additional notes to elaborate the message and provide potential fix to the issue.
81  /// `notes` can contain markdown syntax, but it cannot reference meta-variables.
82  pub note: Option<String>,
83  /// One of: hint, info, warning, or error
84  #[serde(default)]
85  pub severity: Severity,
86  /// Custom label dictionary to configure reporting. Key is the meta-variable name and
87  /// value is the label message and label style.
88  pub labels: Option<HashMap<String, LabelConfig>>,
89  /// Glob patterns to specify that the rule only applies to matching files
90  pub files: Option<Vec<RuleFileGlob>>,
91  /// Glob patterns that exclude rules from applying to files
92  pub ignores: Option<Vec<RuleFileGlob>>,
93  /// Documentation link to this rule
94  pub url: Option<String>,
95  /// Extra information for the rule
96  pub metadata: Option<Metadata>,
97}
98#[derive(Serialize, Deserialize, Clone, JsonSchema)]
99#[serde(untagged)]
100pub enum RuleFileGlob {
101  /// A glob pattern string
102  Glob(String),
103  #[serde(rename_all = "camelCase")]
104  Config {
105    /// A glob pattern string
106    glob: String,
107    /// Whether the glob matching is case insensitive
108    #[serde(default)]
109    case_insensitive: bool,
110  },
111}
112
113/// A trivial wrapper around a HashMap to work around
114/// the limitation of `serde_yaml::Value` not implementing `JsonSchema`.
115#[derive(Serialize, Deserialize, Clone)]
116pub struct Metadata(HashMap<String, serde_yaml::Value>);
117
118impl JsonSchema for Metadata {
119  fn schema_name() -> Cow<'static, str> {
120    Cow::Borrowed("Metadata")
121  }
122  fn schema_id() -> Cow<'static, str> {
123    Cow::Borrowed("Metadata")
124  }
125  fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
126    json_schema!({
127      "type": "object",
128      "additionalProperties": true,
129      "description": "Additional metadata for the rule, can be used to store extra information."
130    })
131  }
132}
133
134impl<L: Language> SerializableRuleConfig<L> {
135  pub fn get_matcher(&self, globals: &GlobalRules) -> Result<RuleCore, RuleConfigError> {
136    // every RuleConfig has one rewriters, and the rewriter is shared between sub-rules
137    // all RuleConfigs has one common globals
138    // every sub-rule has one util
139    let env = DeserializeEnv::new(self.language.clone()).with_globals(globals);
140    let rule = self.core.get_matcher(env.clone())?;
141    self.register_rewriters(&rule, env)?;
142    self.check_labels(&rule)?;
143    Ok(rule)
144  }
145
146  fn check_labels(&self, rule: &RuleCore) -> Result<(), RuleConfigError> {
147    let Some(labels) = &self.labels else {
148      return Ok(());
149    };
150    // labels var must be vars with node, transform var cannot be used
151    let vars = rule.defined_node_vars();
152    for var in labels.keys() {
153      if !vars.contains(var.as_str()) {
154        return Err(RuleConfigError::LabelVariable(var.clone()));
155      }
156    }
157    Ok(())
158  }
159
160  fn register_rewriters(
161    &self,
162    rule: &RuleCore,
163    env: DeserializeEnv<L>,
164  ) -> Result<(), RuleConfigError> {
165    let Some(ser) = &self.rewriters else {
166      return Ok(());
167    };
168    let reg = &env.registration;
169    let vars = rule.defined_vars();
170    for val in ser {
171      let rewriter = val.try_parse_rewriter(&vars, &env)?;
172      reg.insert_rewriter(&val.id, rewriter);
173    }
174    check_rewriters_in_transform(rule, reg.get_rewriters())?;
175    Ok(())
176  }
177}
178
179impl<L: Language> Deref for SerializableRuleConfig<L> {
180  type Target = SerializableRuleCore;
181  fn deref(&self) -> &Self::Target {
182    &self.core
183  }
184}
185
186impl<L: Language> DerefMut for SerializableRuleConfig<L> {
187  fn deref_mut(&mut self) -> &mut Self::Target {
188    &mut self.core
189  }
190}
191
192pub struct RuleConfig<L: Language> {
193  inner: SerializableRuleConfig<L>,
194  pub matcher: RuleCore,
195  pub fixer: Vec<Fixer>,
196}
197
198impl<L: Language> RuleConfig<L> {
199  pub fn try_from(
200    inner: SerializableRuleConfig<L>,
201    globals: &GlobalRules,
202  ) -> Result<Self, RuleConfigError> {
203    let matcher = inner.get_matcher(globals)?;
204    if matcher.potential_kinds().is_none() {
205      return Err(RuleConfigError::MissingPotentialKinds);
206    }
207    let fixer = if let Some(fix) = &inner.fix {
208      let env = matcher.get_env(inner.language.clone());
209      Fixer::parse(fix, &env, &inner.transform)?
210    } else {
211      vec![]
212    };
213    check_fix(&matcher, &fixer)?;
214    Ok(Self {
215      inner,
216      matcher,
217      fixer,
218    })
219  }
220
221  pub fn deserialize<'de>(
222    deserializer: Deserializer<'de>,
223    globals: &GlobalRules,
224  ) -> Result<Self, RuleConfigError>
225  where
226    L: Deserialize<'de>,
227  {
228    let inner: SerializableRuleConfig<L> = deserialize(deserializer)?;
229    Self::try_from(inner, globals)
230  }
231
232  pub fn get_message<D>(&self, node: &NodeMatch<D>) -> String
233  where
234    D: Doc,
235  {
236    let env = self.matcher.get_env(self.language.clone());
237    let parsed = Fixer::with_transform(&self.message, &env, &self.transform).expect("should work");
238    let bytes = parsed.generate_replacement(node);
239    <D::Source as Content>::encode_bytes(&bytes).to_string()
240  }
241  pub fn get_fixer(&self) -> Result<Vec<Fixer>, RuleConfigError> {
242    if let Some(fix) = &self.fix {
243      let env = self.matcher.get_env(self.language.clone());
244      let parsed = Fixer::parse(fix, &env, &self.transform)?;
245      Ok(parsed)
246    } else {
247      Ok(vec![])
248    }
249  }
250  pub fn get_labels<'t, D: Doc>(&self, node: &NodeMatch<'t, D>) -> Vec<Label<'_, 't, D>> {
251    if let Some(labels_config) = &self.labels {
252      get_labels_from_config(labels_config, node)
253    } else {
254      get_default_labels(node)
255    }
256  }
257}
258impl<L: Language> Deref for RuleConfig<L> {
259  type Target = SerializableRuleConfig<L>;
260  fn deref(&self) -> &Self::Target {
261    &self.inner
262  }
263}
264
265impl<L: Language> DerefMut for RuleConfig<L> {
266  fn deref_mut(&mut self) -> &mut Self::Target {
267    &mut self.inner
268  }
269}
270
271#[cfg(test)]
272mod test {
273  use super::*;
274  use crate::from_str;
275  use crate::rewriter::RewriterErrorReason;
276  use crate::test::TypeScript;
277  use crate::{SerializableGlobalRule, SerializableRule};
278  use ast_grep_core::tree_sitter::LanguageExt;
279
280  fn ts_rule_config(rule: SerializableRule) -> SerializableRuleConfig<TypeScript> {
281    let core = SerializableRuleCore {
282      rule,
283      constraints: None,
284      transform: None,
285      utils: None,
286    };
287    SerializableRuleConfig {
288      core,
289      id: "".into(),
290      language: TypeScript::Tsx,
291      rewriters: None,
292      fix: None,
293      message: "".into(),
294      note: None,
295      severity: Severity::Hint,
296      labels: None,
297      files: None,
298      ignores: None,
299      url: None,
300      metadata: None,
301    }
302  }
303
304  fn ts_global_rules(yaml: &str) -> GlobalRules {
305    let globals: Vec<SerializableGlobalRule<TypeScript>> =
306      from_str(yaml).expect("should parse globals");
307    DeserializeEnv::parse_global_utils(globals).expect("should parse global rules")
308  }
309
310  #[test]
311  fn test_rule_message() {
312    let rule = from_str("pattern: class $A {}").expect("cannot parse rule");
313    let mut config = ts_rule_config(rule);
314    config.id = "test".into();
315    config.message = "Found $A".into();
316    let config = RuleConfig::try_from(config, &Default::default()).expect("should work");
317    let grep = TypeScript::Tsx.ast_grep("class TestClass {}");
318    let node_match = grep
319      .root()
320      .find(&config.matcher)
321      .expect("should find match");
322    assert_eq!(config.get_message(&node_match), "Found TestClass");
323  }
324
325  #[test]
326  fn test_parameterized_global_rule_exports_argument_env_only() {
327    let globals = ts_global_rules(
328      r"
329- id: global-rule
330  arguments: [export-var]
331  language: Tsx
332  rule:
333    pattern: Some($A)
334    matches: export-var
335",
336    );
337    let rule = from_str(
338      r"
339matches:
340  global-rule:
341    export-var:
342      pattern: $EXP
343",
344    )
345    .expect("should parse");
346    let config = ts_rule_config(rule);
347    let grep = TypeScript::Tsx.ast_grep("let value = Some(123)");
348    let node_match = grep
349      .root()
350      .find(config.get_matcher(&globals).unwrap())
351      .expect("should found");
352    let env = node_match.get_env();
353    let exp = env.get_match("EXP").expect("should export argument").text();
354    assert_eq!(exp, "Some(123)");
355    assert!(env.get_match("A").is_none());
356  }
357
358  #[test]
359  fn test_parameterized_global_rule_does_not_interfere_with_same_name_local_var() {
360    let globals = ts_global_rules(
361      r"
362- id: global-rule
363  arguments: [export-var]
364  language: Tsx
365  rule:
366    pattern: Some($A)
367    matches: export-var
368",
369    );
370    let rule = from_str(
371      r"
372all:
373  - pattern: $A
374  - matches:
375      global-rule:
376        export-var:
377          pattern: $EXP
378",
379    )
380    .expect("should parse");
381    let config = ts_rule_config(rule);
382    let grep = TypeScript::Tsx.ast_grep("Some(123)");
383    let node_match = grep
384      .root()
385      .find(config.get_matcher(&globals).unwrap())
386      .expect("should found");
387    let env = node_match.get_env();
388    let a = env.get_match("A").expect("should keep local A").text();
389    assert_eq!(a, "Some(123)");
390    let exp = env.get_match("EXP").expect("should export argument").text();
391    assert_eq!(exp, "Some(123)");
392  }
393
394  #[test]
395  fn test_parameterized_global_rule_metavar_does_not_affect_yaml_rule_matching() {
396    let globals = ts_global_rules(
397      r"
398- id: global-rule
399  arguments: [export-var]
400  language: Tsx
401  rule:
402    pattern: Some($A)
403    matches: export-var
404",
405    );
406    let rule = from_str(
407      r"
408all:
409  - pattern: wrapper($A)
410  - has:
411      stopBy: end
412      matches:
413        global-rule:
414          export-var:
415            pattern: $EXP
416",
417    )
418    .expect("should parse");
419    let config = ts_rule_config(rule);
420    let grep = TypeScript::Tsx.ast_grep("wrapper(Some(123))");
421    let node_match = grep
422      .root()
423      .find(config.get_matcher(&globals).unwrap())
424      .expect("should found");
425    let env = node_match.get_env();
426    let a = env.get_match("A").expect("should keep yaml A").text();
427    assert_eq!(a, "Some(123)");
428    let exp = env.get_match("EXP").expect("should export argument").text();
429    assert_eq!(exp, "Some(123)");
430  }
431
432  #[test]
433  fn test_parameterized_global_rule_argument_vars_do_not_conflict_with_internal_vars() {
434    let globals = ts_global_rules(
435      r"
436- id: global-rule
437  arguments: [export-var]
438  language: Tsx
439  rule:
440    pattern: Some($A)
441    matches: export-var
442",
443    );
444    let rule = from_str(
445      r"
446matches:
447  global-rule:
448    export-var:
449      pattern: $A
450",
451    )
452    .expect("should parse");
453    let config = ts_rule_config(rule);
454    let grep = TypeScript::Tsx.ast_grep("Some(123)");
455    let node_match = grep
456      .root()
457      .find(config.get_matcher(&globals).unwrap())
458      .expect("should found");
459    let env = node_match.get_env();
460    let a = env.get_match("A").expect("should export caller A").text();
461    assert_eq!(a, "Some(123)");
462  }
463
464  fn get_matches_config() -> SerializableRuleConfig<TypeScript> {
465    let rule = from_str(
466      "
467matches: test-rule
468",
469    )
470    .unwrap();
471    let utils = from_str(
472      "
473test-rule:
474  pattern: some($A)
475",
476    )
477    .unwrap();
478    let mut ret = ts_rule_config(rule);
479    ret.utils = Some(utils);
480    ret
481  }
482
483  #[test]
484  fn test_get_fixer() {
485    let globals = GlobalRules::default();
486    let mut config = get_matches_config();
487    config.fix = Some(from_str("string!!").unwrap());
488    let rule = RuleConfig::try_from(config, &globals).unwrap();
489    let fixer = rule.get_fixer().unwrap().remove(0);
490    let grep = TypeScript::Tsx.ast_grep("some(123)");
491    let nm = grep.root().find(&rule.matcher).unwrap();
492    let replacement = fixer.generate_replacement(&nm);
493    assert_eq!(String::from_utf8_lossy(&replacement), "string!!");
494  }
495
496  #[test]
497  fn test_undefined_vars_in_fix() {
498    let src = r"
499id: test
500language: Tsx
501rule: {pattern: console.log($A)}
502constraints: {A: {pattern: $C}}
503transform:
504  B:
505    replace: {source: $C, replace: a, by: b }
506fix: $D
507    ";
508    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
509    let ret = RuleConfig::try_from(rule, &Default::default());
510    match ret {
511      Err(RuleConfigError::Core(RuleCoreError::UndefinedMetaVar(name, section))) => {
512        assert_eq!(name, "D");
513        assert_eq!(section, "fix");
514      }
515      _ => panic!("unexpected result"),
516    }
517  }
518
519  #[test]
520  fn test_add_rewriters() {
521    let rule: SerializableRuleConfig<TypeScript> = from_str(
522      r"
523id: test
524rule: {pattern: 'a = $A'}
525language: Tsx
526transform:
527  B:
528    rewrite:
529      rewriters: [re]
530      source: $A
531rewriters:
532- id: re
533  rule: {kind: number}
534  fix: yjsnp
535    ",
536    )
537    .expect("should parse");
538    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
539    let grep = TypeScript::Tsx.ast_grep("a = 123");
540    let nm = grep.root().find(&rule.matcher).unwrap();
541    let b = nm.get_env().get_transformed("B").expect("should have");
542    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
543  }
544
545  #[test]
546  fn test_rewriters_access_utils() {
547    let rule: SerializableRuleConfig<TypeScript> = from_str(
548      r"
549id: test
550rule: {pattern: 'a = $A'}
551language: Tsx
552utils:
553  num: { kind: number }
554transform:
555  B:
556    rewrite:
557      rewriters: [re]
558      source: $A
559rewriters:
560- id: re
561  rule: {matches: num, pattern: $NOT}
562  fix: yjsnp
563    ",
564    )
565    .expect("should parse");
566    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
567    let grep = TypeScript::Tsx.ast_grep("a = 456");
568    let nm = grep.root().find(&rule.matcher).unwrap();
569    let b = nm.get_env().get_transformed("B").expect("should have");
570    assert!(nm.get_env().get_match("NOT").is_none());
571    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
572  }
573
574  #[test]
575  fn test_rewriter_utils_should_not_pollute_registration() {
576    let rule: SerializableRuleConfig<TypeScript> = from_str(
577      r"
578id: test
579rule: {matches: num}
580language: Tsx
581transform:
582  B:
583    rewrite:
584      rewriters: [re]
585      source: $B
586rewriters:
587- id: re
588  rule: {matches: num}
589  fix: yjsnp
590  utils:
591    num: { kind: number }
592    ",
593    )
594    .expect("should parse");
595    let ret = RuleConfig::try_from(rule, &Default::default());
596    assert!(matches!(ret, Err(RuleConfigError::Core(_))));
597  }
598
599  #[test]
600  fn test_rewriter_should_have_fix() {
601    let ret: Result<SerializableRuleConfig<TypeScript>, _> = from_str(
602      r"
603id: test
604rule: {kind: number}
605language: Tsx
606rewriters:
607- id: wrong
608  rule: {matches: num}",
609    );
610    let is_missing_err = matches!(ret, Err(e) if e.to_string().contains("missing field"));
611    assert!(is_missing_err);
612  }
613
614  #[test]
615  fn test_utils_in_rewriter_should_work() {
616    let rule: SerializableRuleConfig<TypeScript> = from_str(
617      r"
618id: test
619rule: {pattern: 'a = $A'}
620language: Tsx
621transform:
622  B:
623    rewrite:
624      rewriters: [re]
625      source: $A
626rewriters:
627- id: re
628  rule: {matches: num}
629  fix: yjsnp
630  utils:
631    num: { kind: number }
632    ",
633    )
634    .expect("should parse");
635    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
636    let grep = TypeScript::Tsx.ast_grep("a = 114514");
637    let nm = grep.root().find(&rule.matcher).unwrap();
638    let b = nm.get_env().get_transformed("B").expect("should have");
639    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
640  }
641
642  #[test]
643  fn test_use_rewriter_recursive() {
644    let rule: SerializableRuleConfig<TypeScript> = from_str(
645      r"
646id: test
647rule: {pattern: 'a = $A'}
648language: Tsx
649transform:
650  B: { rewrite: { rewriters: [re], source: $A } }
651rewriters:
652- id: handle-num
653  rule: {regex: '114'}
654  fix: '1919810'
655- id: re
656  rule: {kind: number, pattern: $A}
657  transform:
658    B: { rewrite: { rewriters: [handle-num], source: $A } }
659  fix: $B
660    ",
661    )
662    .expect("should parse");
663    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
664    let grep = TypeScript::Tsx.ast_grep("a = 114514");
665    let nm = grep.root().find(&rule.matcher).unwrap();
666    let b = nm.get_env().get_transformed("B").expect("should have");
667    assert_eq!(String::from_utf8_lossy(b), "1919810");
668  }
669
670  fn make_undefined_error(src: &str) -> String {
671    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
672    let err = RuleConfig::try_from(rule, &Default::default());
673    match err {
674      Err(RuleConfigError::UndefinedRewriter(name)) => name,
675      _ => panic!("unexpected parsing result"),
676    }
677  }
678
679  #[test]
680  fn test_undefined_rewriter() {
681    let undefined = make_undefined_error(
682      r"
683id: test
684rule: {pattern: 'a = $A'}
685language: Tsx
686transform:
687  B: { rewrite: { rewriters: [not-defined], source: $A } }
688rewriters:
689- id: re
690  rule: {kind: number, pattern: $A}
691  fix: hah
692    ",
693    );
694    assert_eq!(undefined, "not-defined");
695  }
696  #[test]
697  fn test_wrong_rewriter() {
698    let rule: SerializableRuleConfig<TypeScript> = from_str(
699      r"
700id: test
701rule: {pattern: 'a = $A'}
702language: Tsx
703rewriters:
704- id: wrong
705  rule: {kind: '114'}
706  fix: '1919810'
707    ",
708    )
709    .expect("should parse");
710    let ret = RuleConfig::try_from(rule, &Default::default());
711    match ret {
712      Err(RuleConfigError::Rewriter(e)) => assert_eq!(e.id, "wrong"),
713      _ => panic!("unexpected error"),
714    }
715  }
716
717  #[test]
718  fn test_undefined_rewriter_in_transform() {
719    let undefined = make_undefined_error(
720      r"
721id: test
722rule: {pattern: 'a = $A'}
723language: Tsx
724transform:
725  B: { rewrite: { rewriters: [re], source: $A } }
726rewriters:
727- id: re
728  rule: {kind: number, pattern: $A}
729  transform:
730    C: { rewrite: { rewriters: [nested-undefined], source: $A } }
731  fix: hah
732    ",
733    );
734    assert_eq!(undefined, "nested-undefined");
735  }
736
737  #[test]
738  fn test_rewriter_use_upper_var() {
739    let src = r"
740id: test
741rule: {pattern: '$B = $A'}
742language: Tsx
743transform:
744  D: { rewrite: { rewriters: [re], source: $A } }
745rewriters:
746- id: re
747  rule: {kind: number, pattern: $C}
748  fix: $B.$C
749    ";
750    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
751    let ret = RuleConfig::try_from(rule, &Default::default());
752    assert!(ret.is_ok());
753  }
754
755  #[test]
756  fn test_rewriter_fix_rejects_undefined_var() {
757    let src = r"
758id: test
759rule: {pattern: '$B = $A'}
760language: Tsx
761transform:
762  D: { rewrite: { rewriters: [re], source: $A } }
763rewriters:
764- id: re
765  rule: {kind: number, pattern: $C}
766  fix: $MISSING.$C
767    ";
768    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
769    let ret = RuleConfig::try_from(rule, &Default::default());
770    match ret {
771      Err(RuleConfigError::Rewriter(RewriterError {
772        id,
773        reason: RewriterErrorReason::Core(RuleCoreError::UndefinedMetaVar(name, section)),
774      })) => {
775        assert_eq!(id, "re");
776        assert_eq!(name, "MISSING");
777        assert_eq!(section, "fix");
778      }
779      _ => panic!("unexpected result"),
780    }
781  }
782
783  #[test]
784  fn test_rewriter_rejects_empty_fix_list() {
785    let src = r"
786id: test
787rule: {pattern: 'a = $A'}
788language: Tsx
789transform:
790  B: { rewrite: { rewriters: [re], source: $A } }
791rewriters:
792- id: re
793  rule: {kind: number, pattern: $C}
794  fix: []
795    ";
796    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
797    let ret = RuleConfig::try_from(rule, &Default::default());
798    assert!(matches!(
799      ret,
800      Err(RuleConfigError::Rewriter(RewriterError {
801        id,
802        reason: RewriterErrorReason::NoFixInRewriter,
803      })) if id == "re"
804    ));
805  }
806
807  #[test]
808  fn test_rewriter_use_undefined_var() {
809    let src = r"
810id: test
811rule: {pattern: '$B = $A'}
812language: Tsx
813transform:
814  B: { rewrite: { rewriters: [re], source: $A } }
815rewriters:
816- id: re
817  rule: {kind: number, pattern: $C}
818  fix: $D.$C
819    ";
820    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
821    let ret = RuleConfig::try_from(rule, &Default::default());
822    assert!(ret.is_err());
823  }
824
825  #[test]
826  fn test_get_message_transform() {
827    let src = r"
828id: test-rule
829language: Tsx
830rule: { kind: string, pattern: $ARG }
831transform:
832  TEST: { replace: { replace: 'a', by: 'b', source: $ARG, } }
833message: $TEST
834    ";
835    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
836    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
837    let grep = TypeScript::Tsx.ast_grep("a = '123'");
838    let nm = grep.root().find(&rule.matcher).unwrap();
839    assert_eq!(rule.get_message(&nm), "'123'");
840  }
841
842  #[test]
843  fn test_get_message_transform_string() {
844    let src = r"
845id: test-rule
846language: Tsx
847rule: { kind: string, pattern: $ARG }
848transform:
849  TEST: replace($ARG, replace=a, by=b)
850message: $TEST
851    ";
852    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
853    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
854    let grep = TypeScript::Tsx.ast_grep("a = '123'");
855    let nm = grep.root().find(&rule.matcher).unwrap();
856    assert_eq!(rule.get_message(&nm), "'123'");
857  }
858
859  #[test]
860  fn test_complex_metadata() {
861    let src = r"
862id: test-rule
863language: Tsx
864rule: { kind: string }
865metadata:
866  test: [1, 2, 3]
867  ";
868    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
869    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
870    let grep = TypeScript::Tsx.ast_grep("a = '123'");
871    let nm = grep.root().find(&rule.matcher);
872    assert!(nm.is_some());
873  }
874
875  #[test]
876  fn test_label() {
877    let src = r"
878id: test-rule
879language: Tsx
880rule: { pattern: Some($A) }
881labels:
882  A: { style: primary, message: 'var label' }
883  ";
884    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
885    let ret = RuleConfig::try_from(rule, &Default::default());
886    assert!(ret.is_ok());
887    let src = r"
888id: test-rule
889language: Tsx
890rule: { pattern: Some($A) }
891labels:
892  B: { style: primary, message: 'var label' }
893  ";
894    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
895    let ret = RuleConfig::try_from(rule, &Default::default());
896    assert!(matches!(ret, Err(RuleConfigError::LabelVariable(_))));
897  }
898
899  #[test]
900  fn test_file_glob_simple_string() {
901    let src = r"
902id: test-rule
903language: Tsx
904rule: { kind: string }
905files:
906  - '*.ts'
907  - 'src/**/*.tsx'
908  ";
909    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
910    assert!(rule.files.is_some());
911    let files = rule.files.as_ref().unwrap();
912    assert_eq!(files.len(), 2);
913    assert!(matches!(files[0], RuleFileGlob::Glob(_)));
914    assert!(matches!(files[1], RuleFileGlob::Glob(_)));
915  }
916
917  #[test]
918  fn test_file_glob_case_insensitive() {
919    let src = r"
920id: test-rule
921language: Tsx
922rule: { kind: string }
923files:
924  - glob: '*.ts'
925    caseInsensitive: true
926  - glob: 'README.md'
927    caseInsensitive: true
928  ";
929    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
930    assert!(rule.files.is_some());
931    let files = rule.files.as_ref().unwrap();
932    assert_eq!(files.len(), 2);
933    match &files[0] {
934      RuleFileGlob::Config {
935        glob,
936        case_insensitive,
937      } => {
938        assert_eq!(glob, "*.ts");
939        assert!(case_insensitive);
940      }
941      _ => panic!("Expected Config variant"),
942    }
943    match &files[1] {
944      RuleFileGlob::Config {
945        glob,
946        case_insensitive,
947      } => {
948        assert_eq!(glob, "README.md");
949        assert!(case_insensitive);
950      }
951      _ => panic!("Expected Config variant"),
952    }
953  }
954
955  #[test]
956  fn test_file_glob_case_sensitive() {
957    let src = r"
958id: test-rule
959language: Tsx
960rule: { kind: string }
961files:
962  - glob: '*.ts'
963    caseInsensitive: false
964  ";
965    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
966    assert!(rule.files.is_some());
967    let files = rule.files.as_ref().unwrap();
968    assert_eq!(files.len(), 1);
969    match &files[0] {
970      RuleFileGlob::Config {
971        glob,
972        case_insensitive,
973      } => {
974        assert_eq!(glob, "*.ts");
975        assert!(!case_insensitive);
976      }
977      _ => panic!("Expected Config variant"),
978    }
979  }
980
981  #[test]
982  fn test_file_glob_mixed_formats() {
983    let src = r"
984id: test-rule
985language: Tsx
986rule: { kind: string }
987files:
988  - '*.ts'
989  - glob: 'README.md'
990    caseInsensitive: true
991  - 'src/**/*.tsx'
992ignores:
993  - 'test/**'
994  - glob: 'BUILD'
995    caseInsensitive: true
996  ";
997    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
998
999    assert!(rule.files.is_some());
1000    let files = rule.files.as_ref().unwrap();
1001    assert_eq!(files.len(), 3);
1002    assert!(matches!(files[0], RuleFileGlob::Glob(_)));
1003    assert!(matches!(files[1], RuleFileGlob::Config { .. }));
1004    assert!(matches!(files[2], RuleFileGlob::Glob(_)));
1005
1006    assert!(rule.ignores.is_some());
1007    let ignores = rule.ignores.as_ref().unwrap();
1008    assert_eq!(ignores.len(), 2);
1009    assert!(matches!(ignores[0], RuleFileGlob::Glob(_)));
1010    assert!(matches!(ignores[1], RuleFileGlob::Config { .. }));
1011  }
1012}