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 Hint,
30 Info,
32 Warning,
34 Error,
36 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 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 #[serde(default)]
73 pub id: String,
74 pub language: L,
76 pub rewriters: Option<Vec<SerializableRewriter>>,
78 #[serde(default)]
81 pub message: String,
82 pub note: Option<String>,
85 #[serde(default)]
87 pub severity: Severity,
88 pub labels: Option<HashMap<String, LabelConfig>>,
91 pub files: Option<Vec<RuleFileGlob>>,
93 pub ignores: Option<Vec<RuleFileGlob>>,
95 pub url: Option<String>,
97 pub metadata: Option<Metadata>,
99}
100#[derive(Serialize, Deserialize, Clone, JsonSchema)]
101#[serde(untagged)]
102pub enum RuleFileGlob {
103 Glob(String),
105 #[serde(rename_all = "camelCase")]
106 Config {
107 glob: String,
109 #[serde(default)]
111 case_insensitive: bool,
112 },
113}
114
115#[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 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 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}