Skip to main content

ast_grep_config/
rule_config.rs

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