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