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