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  pub id: String,
73  /// Specify the language to parse and the file extension to include in matching.
74  pub language: L,
75  /// Rewrite rules for `rewrite` transformation
76  pub rewriters: Option<Vec<SerializableRewriter>>,
77  /// Main message highlighting why this rule fired. It should be single line and concise,
78  /// but specific enough to be understood without additional context.
79  #[serde(default)]
80  pub message: String,
81  /// Additional notes to elaborate the message and provide potential fix to the issue.
82  /// `notes` can contain markdown syntax, but it cannot reference meta-variables.
83  pub note: Option<String>,
84  /// One of: hint, info, warning, or error
85  #[serde(default)]
86  pub severity: Severity,
87  /// Custom label dictionary to configure reporting. Key is the meta-variable name and
88  /// value is the label message and label style.
89  pub labels: Option<HashMap<String, LabelConfig>>,
90  /// Glob patterns to specify that the rule only applies to matching files
91  pub files: Option<Vec<String>>,
92  /// Glob patterns that exclude rules from applying to files
93  pub ignores: Option<Vec<String>>,
94  /// Documentation link to this rule
95  pub url: Option<String>,
96  /// Extra information for the rule
97  pub metadata: Option<Metadata>,
98}
99
100/// A trivial wrapper around a HashMap to work around
101/// the limitation of `serde_yaml::Value` not implementing `JsonSchema`.
102#[derive(Serialize, Deserialize, Clone)]
103pub struct Metadata(HashMap<String, serde_yaml::Value>);
104
105impl JsonSchema for Metadata {
106  fn schema_name() -> Cow<'static, str> {
107    Cow::Borrowed("Metadata")
108  }
109  fn schema_id() -> Cow<'static, str> {
110    Cow::Borrowed("Metadata")
111  }
112  fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
113    json_schema!({
114      "type": "object",
115      "additionalProperties": true,
116      "description": "Additional metadata for the rule, can be used to store extra information."
117    })
118  }
119}
120
121impl<L: Language> SerializableRuleConfig<L> {
122  pub fn get_matcher(&self, globals: &GlobalRules) -> Result<RuleCore, RuleConfigError> {
123    // every RuleConfig has one rewriters, and the rewriter is shared between sub-rules
124    // all RuleConfigs has one common globals
125    // every sub-rule has one util
126    let env = DeserializeEnv::new(self.language.clone()).with_globals(globals);
127    let rule = self.core.get_matcher(env.clone())?;
128    self.register_rewriters(&rule, env)?;
129    self.check_labels(&rule)?;
130    Ok(rule)
131  }
132
133  fn check_labels(&self, rule: &RuleCore) -> Result<(), RuleConfigError> {
134    let Some(labels) = &self.labels else {
135      return Ok(());
136    };
137    // labels var must be vars with node, transform var cannot be used
138    let vars = rule.defined_node_vars();
139    for var in labels.keys() {
140      if !vars.contains(var.as_str()) {
141        return Err(RuleConfigError::LabelVariable(var.clone()));
142      }
143    }
144    Ok(())
145  }
146
147  fn register_rewriters(
148    &self,
149    rule: &RuleCore,
150    env: DeserializeEnv<L>,
151  ) -> Result<(), RuleConfigError> {
152    let Some(ser) = &self.rewriters else {
153      return Ok(());
154    };
155    let reg = &env.registration;
156    let vars = rule.defined_vars();
157    for val in ser {
158      if val.core.fix.is_none() {
159        return Err(RuleConfigError::NoFixInRewriter(val.id.clone()));
160      }
161      let rewriter = val
162        .core
163        .get_matcher_with_hint(env.clone(), CheckHint::Rewriter(&vars))
164        .map_err(|e| RuleConfigError::Rewriter(e, val.id.clone()))?;
165      reg.insert_rewriter(&val.id, rewriter);
166    }
167    check_rewriters_in_transform(rule, reg.get_rewriters())?;
168    Ok(())
169  }
170}
171
172impl<L: Language> Deref for SerializableRuleConfig<L> {
173  type Target = SerializableRuleCore;
174  fn deref(&self) -> &Self::Target {
175    &self.core
176  }
177}
178
179impl<L: Language> DerefMut for SerializableRuleConfig<L> {
180  fn deref_mut(&mut self) -> &mut Self::Target {
181    &mut self.core
182  }
183}
184
185pub struct RuleConfig<L: Language> {
186  inner: SerializableRuleConfig<L>,
187  pub matcher: RuleCore,
188}
189
190impl<L: Language> RuleConfig<L> {
191  pub fn try_from(
192    inner: SerializableRuleConfig<L>,
193    globals: &GlobalRules,
194  ) -> Result<Self, RuleConfigError> {
195    let matcher = inner.get_matcher(globals)?;
196    if matcher.potential_kinds().is_none() {
197      return Err(RuleConfigError::MissingPotentialKinds);
198    }
199    Ok(Self { inner, matcher })
200  }
201
202  pub fn deserialize<'de>(
203    deserializer: Deserializer<'de>,
204    globals: &GlobalRules,
205  ) -> Result<Self, RuleConfigError>
206  where
207    L: Deserialize<'de>,
208  {
209    let inner: SerializableRuleConfig<L> = deserialize(deserializer)?;
210    Self::try_from(inner, globals)
211  }
212
213  pub fn get_message<D>(&self, node: &NodeMatch<D>) -> String
214  where
215    D: Doc,
216  {
217    let env = self.matcher.get_env(self.language.clone());
218    let parsed = Fixer::with_transform(&self.message, &env, &self.transform).expect("should work");
219    let bytes = parsed.generate_replacement(node);
220    <D::Source as Content>::encode_bytes(&bytes).to_string()
221  }
222  pub fn get_fixer(&self) -> Result<Vec<Fixer>, RuleConfigError> {
223    if let Some(fix) = &self.fix {
224      let env = self.matcher.get_env(self.language.clone());
225      let parsed = Fixer::parse(fix, &env, &self.transform).map_err(RuleCoreError::Fixer)?;
226      Ok(parsed)
227    } else {
228      Ok(vec![])
229    }
230  }
231  pub fn get_labels<'t, D: Doc>(&self, node: &NodeMatch<'t, D>) -> Vec<Label<'_, 't, D>> {
232    if let Some(labels_config) = &self.labels {
233      get_labels_from_config(labels_config, node)
234    } else {
235      get_default_labels(node)
236    }
237  }
238}
239impl<L: Language> Deref for RuleConfig<L> {
240  type Target = SerializableRuleConfig<L>;
241  fn deref(&self) -> &Self::Target {
242    &self.inner
243  }
244}
245
246impl<L: Language> DerefMut for RuleConfig<L> {
247  fn deref_mut(&mut self) -> &mut Self::Target {
248    &mut self.inner
249  }
250}
251
252#[cfg(test)]
253mod test {
254  use super::*;
255  use crate::from_str;
256  use crate::rule::SerializableRule;
257  use crate::test::TypeScript;
258  use ast_grep_core::tree_sitter::LanguageExt;
259
260  fn ts_rule_config(rule: SerializableRule) -> SerializableRuleConfig<TypeScript> {
261    let core = SerializableRuleCore {
262      rule,
263      constraints: None,
264      transform: None,
265      utils: None,
266      fix: None,
267    };
268    SerializableRuleConfig {
269      core,
270      id: "".into(),
271      language: TypeScript::Tsx,
272      rewriters: None,
273      message: "".into(),
274      note: None,
275      severity: Severity::Hint,
276      labels: None,
277      files: None,
278      ignores: None,
279      url: None,
280      metadata: None,
281    }
282  }
283
284  #[test]
285  fn test_rule_message() {
286    let globals = GlobalRules::default();
287    let rule = from_str("pattern: class $A {}").expect("cannot parse rule");
288    let mut config = ts_rule_config(rule);
289    config.id = "test".into();
290    config.message = "Found $A".into();
291    let config = RuleConfig::try_from(config, &Default::default()).expect("should work");
292    let grep = TypeScript::Tsx.ast_grep("class TestClass {}");
293    let node_match = grep
294      .root()
295      .find(config.get_matcher(&globals).unwrap())
296      .expect("should find match");
297    assert_eq!(config.get_message(&node_match), "Found TestClass");
298  }
299
300  #[test]
301  fn test_augmented_rule() {
302    let globals = GlobalRules::default();
303    let rule = from_str(
304      "
305pattern: console.log($A)
306inside:
307  stopBy: end
308  pattern: function test() { $$$ }
309",
310    )
311    .expect("should parse");
312    let config = ts_rule_config(rule);
313    let grep = TypeScript::Tsx.ast_grep("console.log(1)");
314    let matcher = config.get_matcher(&globals).unwrap();
315    assert!(grep.root().find(&matcher).is_none());
316    let grep = TypeScript::Tsx.ast_grep("function test() { console.log(1) }");
317    assert!(grep.root().find(&matcher).is_some());
318  }
319
320  #[test]
321  fn test_multiple_augment_rule() {
322    let globals = GlobalRules::default();
323    let rule = from_str(
324      "
325pattern: console.log($A)
326inside:
327  stopBy: end
328  pattern: function test() { $$$ }
329has:
330  stopBy: end
331  pattern: '123'
332",
333    )
334    .expect("should parse");
335    let config = ts_rule_config(rule);
336    let grep = TypeScript::Tsx.ast_grep("function test() { console.log(1) }");
337    let matcher = config.get_matcher(&globals).unwrap();
338    assert!(grep.root().find(&matcher).is_none());
339    let grep = TypeScript::Tsx.ast_grep("function test() { console.log(123) }");
340    assert!(grep.root().find(&matcher).is_some());
341  }
342
343  #[test]
344  fn test_rule_env() {
345    let globals = GlobalRules::default();
346    let rule = from_str(
347      "
348all:
349  - pattern: console.log($A)
350  - inside:
351      stopBy: end
352      pattern: function $B() {$$$}
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 node_match = grep
359      .root()
360      .find(config.get_matcher(&globals).unwrap())
361      .expect("should found");
362    let env = node_match.get_env();
363    let a = env.get_match("A").expect("should exist").text();
364    assert_eq!(a, "1");
365    let b = env.get_match("B").expect("should exist").text();
366    assert_eq!(b, "test");
367  }
368
369  #[test]
370  fn test_transform() {
371    let globals = GlobalRules::default();
372    let rule = from_str("pattern: console.log($A)").expect("should parse");
373    let mut config = ts_rule_config(rule);
374    let transform = from_str(
375      "
376B:
377  substring:
378    source: $A
379    startChar: 1
380    endChar: -1
381",
382    )
383    .expect("should parse");
384    config.transform = Some(transform);
385    let grep = TypeScript::Tsx.ast_grep("function test() { console.log(123) }");
386    let node_match = grep
387      .root()
388      .find(config.get_matcher(&globals).unwrap())
389      .expect("should found");
390    let env = node_match.get_env();
391    let a = env.get_match("A").expect("should exist").text();
392    assert_eq!(a, "123");
393    let b = env.get_transformed("B").expect("should exist");
394    assert_eq!(b, b"2");
395  }
396
397  fn get_matches_config() -> SerializableRuleConfig<TypeScript> {
398    let rule = from_str(
399      "
400matches: test-rule
401",
402    )
403    .unwrap();
404    let utils = from_str(
405      "
406test-rule:
407  pattern: some($A)
408",
409    )
410    .unwrap();
411    let mut ret = ts_rule_config(rule);
412    ret.utils = Some(utils);
413    ret
414  }
415
416  #[test]
417  fn test_utils_rule() {
418    let globals = GlobalRules::default();
419    let config = get_matches_config();
420    let matcher = config.get_matcher(&globals).unwrap();
421    let grep = TypeScript::Tsx.ast_grep("some(123)");
422    assert!(grep.root().find(&matcher).is_some());
423    let grep = TypeScript::Tsx.ast_grep("some()");
424    assert!(grep.root().find(&matcher).is_none());
425  }
426  #[test]
427  fn test_get_fixer() {
428    let globals = GlobalRules::default();
429    let mut config = get_matches_config();
430    config.fix = Some(from_str("string!!").unwrap());
431    let rule = RuleConfig::try_from(config, &globals).unwrap();
432    let fixer = rule.get_fixer().unwrap().remove(0);
433    let grep = TypeScript::Tsx.ast_grep("some(123)");
434    let nm = grep.root().find(&rule.matcher).unwrap();
435    let replacement = fixer.generate_replacement(&nm);
436    assert_eq!(String::from_utf8_lossy(&replacement), "string!!");
437  }
438
439  #[test]
440  fn test_add_rewriters() {
441    let rule: SerializableRuleConfig<TypeScript> = from_str(
442      r"
443id: test
444rule: {pattern: 'a = $A'}
445language: Tsx
446transform:
447  B:
448    rewrite:
449      rewriters: [re]
450      source: $A
451rewriters:
452- id: re
453  rule: {kind: number}
454  fix: yjsnp
455    ",
456    )
457    .expect("should parse");
458    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
459    let grep = TypeScript::Tsx.ast_grep("a = 123");
460    let nm = grep.root().find(&rule.matcher).unwrap();
461    let b = nm.get_env().get_transformed("B").expect("should have");
462    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
463  }
464
465  #[test]
466  fn test_rewriters_access_utils() {
467    let rule: SerializableRuleConfig<TypeScript> = from_str(
468      r"
469id: test
470rule: {pattern: 'a = $A'}
471language: Tsx
472utils:
473  num: { kind: number }
474transform:
475  B:
476    rewrite:
477      rewriters: [re]
478      source: $A
479rewriters:
480- id: re
481  rule: {matches: num, pattern: $NOT}
482  fix: yjsnp
483    ",
484    )
485    .expect("should parse");
486    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
487    let grep = TypeScript::Tsx.ast_grep("a = 456");
488    let nm = grep.root().find(&rule.matcher).unwrap();
489    let b = nm.get_env().get_transformed("B").expect("should have");
490    assert!(nm.get_env().get_match("NOT").is_none());
491    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
492  }
493
494  #[test]
495  fn test_rewriter_utils_should_not_pollute_registration() {
496    let rule: SerializableRuleConfig<TypeScript> = from_str(
497      r"
498id: test
499rule: {matches: num}
500language: Tsx
501transform:
502  B:
503    rewrite:
504      rewriters: [re]
505      source: $B
506rewriters:
507- id: re
508  rule: {matches: num}
509  fix: yjsnp
510  utils:
511    num: { kind: number }
512    ",
513    )
514    .expect("should parse");
515    let ret = RuleConfig::try_from(rule, &Default::default());
516    assert!(matches!(ret, Err(RuleConfigError::Core(_))));
517  }
518
519  #[test]
520  fn test_rewriter_should_have_fix() {
521    let rule: SerializableRuleConfig<TypeScript> = from_str(
522      r"
523id: test
524rule: {kind: number}
525language: Tsx
526rewriters:
527- id: wrong
528  rule: {matches: num}",
529    )
530    .expect("should parse");
531    let ret = RuleConfig::try_from(rule, &Default::default());
532    match ret {
533      Err(RuleConfigError::NoFixInRewriter(name)) => assert_eq!(name, "wrong"),
534      _ => panic!("unexpected error"),
535    }
536  }
537
538  #[test]
539  fn test_utils_in_rewriter_should_work() {
540    let rule: SerializableRuleConfig<TypeScript> = from_str(
541      r"
542id: test
543rule: {pattern: 'a = $A'}
544language: Tsx
545transform:
546  B:
547    rewrite:
548      rewriters: [re]
549      source: $A
550rewriters:
551- id: re
552  rule: {matches: num}
553  fix: yjsnp
554  utils:
555    num: { kind: number }
556    ",
557    )
558    .expect("should parse");
559    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
560    let grep = TypeScript::Tsx.ast_grep("a = 114514");
561    let nm = grep.root().find(&rule.matcher).unwrap();
562    let b = nm.get_env().get_transformed("B").expect("should have");
563    assert_eq!(String::from_utf8_lossy(b), "yjsnp");
564  }
565
566  #[test]
567  fn test_use_rewriter_recursive() {
568    let rule: SerializableRuleConfig<TypeScript> = from_str(
569      r"
570id: test
571rule: {pattern: 'a = $A'}
572language: Tsx
573transform:
574  B: { rewrite: { rewriters: [re], source: $A } }
575rewriters:
576- id: handle-num
577  rule: {regex: '114'}
578  fix: '1919810'
579- id: re
580  rule: {kind: number, pattern: $A}
581  transform:
582    B: { rewrite: { rewriters: [handle-num], source: $A } }
583  fix: $B
584    ",
585    )
586    .expect("should parse");
587    let rule = RuleConfig::try_from(rule, &Default::default()).expect("work");
588    let grep = TypeScript::Tsx.ast_grep("a = 114514");
589    let nm = grep.root().find(&rule.matcher).unwrap();
590    let b = nm.get_env().get_transformed("B").expect("should have");
591    assert_eq!(String::from_utf8_lossy(b), "1919810");
592  }
593
594  fn make_undefined_error(src: &str) -> String {
595    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
596    let err = RuleConfig::try_from(rule, &Default::default());
597    match err {
598      Err(RuleConfigError::UndefinedRewriter(name)) => name,
599      _ => panic!("unexpected parsing result"),
600    }
601  }
602
603  #[test]
604  fn test_undefined_rewriter() {
605    let undefined = make_undefined_error(
606      r"
607id: test
608rule: {pattern: 'a = $A'}
609language: Tsx
610transform:
611  B: { rewrite: { rewriters: [not-defined], source: $A } }
612rewriters:
613- id: re
614  rule: {kind: number, pattern: $A}
615  fix: hah
616    ",
617    );
618    assert_eq!(undefined, "not-defined");
619  }
620  #[test]
621  fn test_wrong_rewriter() {
622    let rule: SerializableRuleConfig<TypeScript> = from_str(
623      r"
624id: test
625rule: {pattern: 'a = $A'}
626language: Tsx
627rewriters:
628- id: wrong
629  rule: {kind: '114'}
630  fix: '1919810'
631    ",
632    )
633    .expect("should parse");
634    let ret = RuleConfig::try_from(rule, &Default::default());
635    match ret {
636      Err(RuleConfigError::Rewriter(_, name)) => assert_eq!(name, "wrong"),
637      _ => panic!("unexpected error"),
638    }
639  }
640
641  #[test]
642  fn test_undefined_rewriter_in_transform() {
643    let undefined = make_undefined_error(
644      r"
645id: test
646rule: {pattern: 'a = $A'}
647language: Tsx
648transform:
649  B: { rewrite: { rewriters: [re], source: $A } }
650rewriters:
651- id: re
652  rule: {kind: number, pattern: $A}
653  transform:
654    C: { rewrite: { rewriters: [nested-undefined], source: $A } }
655  fix: hah
656    ",
657    );
658    assert_eq!(undefined, "nested-undefined");
659  }
660
661  #[test]
662  fn test_rewriter_use_upper_var() {
663    let src = r"
664id: test
665rule: {pattern: '$B = $A'}
666language: Tsx
667transform:
668  D: { rewrite: { rewriters: [re], source: $A } }
669rewriters:
670- id: re
671  rule: {kind: number, pattern: $C}
672  fix: $B.$C
673    ";
674    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
675    let ret = RuleConfig::try_from(rule, &Default::default());
676    assert!(ret.is_ok());
677  }
678
679  #[test]
680  fn test_rewriter_use_undefined_var() {
681    let src = r"
682id: test
683rule: {pattern: '$B = $A'}
684language: Tsx
685transform:
686  B: { rewrite: { rewriters: [re], source: $A } }
687rewriters:
688- id: re
689  rule: {kind: number, pattern: $C}
690  fix: $D.$C
691    ";
692    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
693    let ret = RuleConfig::try_from(rule, &Default::default());
694    assert!(ret.is_err());
695  }
696
697  #[test]
698  fn test_get_message_transform() {
699    let src = r"
700id: test-rule
701language: Tsx
702rule: { kind: string, pattern: $ARG }
703transform:
704  TEST: { replace: { replace: 'a', by: 'b', source: $ARG, } }
705message: $TEST
706    ";
707    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
708    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
709    let grep = TypeScript::Tsx.ast_grep("a = '123'");
710    let nm = grep.root().find(&rule.matcher).unwrap();
711    assert_eq!(rule.get_message(&nm), "'123'");
712  }
713
714  #[test]
715  fn test_get_message_transform_string() {
716    let src = r"
717id: test-rule
718language: Tsx
719rule: { kind: string, pattern: $ARG }
720transform:
721  TEST: replace($ARG, replace=a, by=b)
722message: $TEST
723    ";
724    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
725    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
726    let grep = TypeScript::Tsx.ast_grep("a = '123'");
727    let nm = grep.root().find(&rule.matcher).unwrap();
728    assert_eq!(rule.get_message(&nm), "'123'");
729  }
730
731  #[test]
732  fn test_complex_metadata() {
733    let src = r"
734id: test-rule
735language: Tsx
736rule: { kind: string }
737metadata:
738  test: [1, 2, 3]
739  ";
740    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
741    let rule = RuleConfig::try_from(rule, &Default::default()).expect("should work");
742    let grep = TypeScript::Tsx.ast_grep("a = '123'");
743    let nm = grep.root().find(&rule.matcher);
744    assert!(nm.is_some());
745  }
746
747  #[test]
748  fn test_label() {
749    let src = r"
750id: test-rule
751language: Tsx
752rule: { pattern: Some($A) }
753labels:
754  A: { style: primary, message: 'var label' }
755  ";
756    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
757    let ret = RuleConfig::try_from(rule, &Default::default());
758    assert!(ret.is_ok());
759    let src = r"
760id: test-rule
761language: Tsx
762rule: { pattern: Some($A) }
763labels:
764  B: { style: primary, message: 'var label' }
765  ";
766    let rule: SerializableRuleConfig<TypeScript> = from_str(src).expect("should parse");
767    let ret = RuleConfig::try_from(rule, &Default::default());
768    assert!(matches!(ret, Err(RuleConfigError::LabelVariable(_))));
769  }
770}