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