Skip to main content

thread_rule_engine/
rule_config.rs

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