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