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