Skip to main content

thread_rule_engine/
lib.rs

1// SPDX-FileCopyrightText: 2022 Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
2// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
3// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
4//
5// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
6
7#![feature(portable_simd)]
8
9mod check_var;
10mod combined;
11mod fixer;
12mod label;
13mod maybe;
14mod rule;
15mod rule_collection;
16mod rule_config;
17mod rule_core;
18mod transform;
19
20use serde::Deserialize;
21use serde_yaml::{Deserializer, Error as YamlError, with::singleton_map_recursive::deserialize};
22
23use thread_ast_engine::language::Language;
24
25pub use combined::CombinedScan;
26pub use fixer::Fixer;
27pub use label::{Label, LabelStyle};
28pub use rule::DeserializeEnv;
29pub use rule::referent_rule::GlobalRules;
30pub use rule::{Rule, RuleSerializeError, SerializableRule};
31pub use rule_collection::RuleCollection;
32pub use rule_config::{Metadata, RuleConfig, RuleConfigError, SerializableRuleConfig, Severity};
33pub use rule_core::{RuleCore, RuleCoreError, SerializableRuleCore};
34pub use transform::Transformation;
35
36pub fn from_str<'de, T: Deserialize<'de>>(s: &'de str) -> Result<T, YamlError> {
37    let deserializer = Deserializer::from_str(s);
38    deserialize(deserializer)
39}
40
41pub fn from_yaml_string<'a, L: Language + Deserialize<'a>>(
42    yamls: &'a str,
43    registration: &GlobalRules,
44) -> Result<Vec<RuleConfig<L>>, RuleConfigError> {
45    let mut ret = vec![];
46    for yaml in Deserializer::from_str(yamls) {
47        let config = RuleConfig::deserialize(yaml, registration)?;
48        ret.push(config);
49    }
50    Ok(ret)
51}
52#[cfg(test)]
53mod test {
54    use super::*;
55    use std::path::Path;
56    use thread_ast_engine::Language;
57    use thread_ast_engine::matcher::{Pattern, PatternBuilder, PatternError};
58    use thread_ast_engine::tree_sitter::{LanguageExt, StrDoc, TSLanguage};
59
60    #[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
61    pub enum TypeScript {
62        Tsx,
63    }
64    impl Language for TypeScript {
65        fn kind_to_id(&self, kind: &str) -> u16 {
66            TSLanguage::from(tree_sitter_typescript::LANGUAGE_TSX).id_for_node_kind(kind, true)
67        }
68        fn field_to_id(&self, field: &str) -> Option<u16> {
69            TSLanguage::from(tree_sitter_typescript::LANGUAGE_TSX)
70                .field_id_for_name(field)
71                .map(|f| f.get())
72        }
73        fn from_path<P: AsRef<Path>>(_path: P) -> Option<Self> {
74            Some(TypeScript::Tsx)
75        }
76        fn build_pattern(&self, builder: &PatternBuilder) -> Result<Pattern, PatternError> {
77            builder.build(|src| StrDoc::try_new(src, self.clone()))
78        }
79    }
80    impl LanguageExt for TypeScript {
81        fn get_ts_language(&self) -> TSLanguage {
82            tree_sitter_typescript::LANGUAGE_TSX.into()
83        }
84    }
85
86    fn test_rule_match(yaml: &str, source: &str) {
87        let globals = GlobalRules::default();
88        let config = &from_yaml_string::<TypeScript>(yaml, &globals).expect("rule should parse")[0];
89        let grep = config.language.ast_grep(source);
90        assert!(grep.root().find(&config.matcher).is_some());
91    }
92
93    fn test_rule_unmatch(yaml: &str, source: &str) {
94        let globals = GlobalRules::default();
95        let config = &from_yaml_string::<TypeScript>(yaml, &globals).expect("rule should parse")[0];
96        let grep = config.language.ast_grep(source);
97        assert!(grep.root().find(&config.matcher).is_none());
98    }
99
100    fn make_yaml(rule: &str) -> String {
101        format!(
102            r"
103id: test
104message: test rule
105severity: info
106language: Tsx
107rule:
108{rule}
109"
110        )
111    }
112
113    #[test]
114    fn test_deserialize_rule_config() {
115        let yaml = &make_yaml(
116            "
117  pattern: let a = 123
118",
119        );
120        test_rule_match(yaml, "let a = 123; let b = 33;");
121        test_rule_match(yaml, "class B { func() {let a = 123; }}");
122        test_rule_unmatch(yaml, "const a = 33");
123    }
124
125    #[test]
126    fn test_deserialize_nested() {
127        let yaml = &make_yaml(
128            "
129  all:
130    - pattern: let $A = 123
131    - pattern: let a = $B
132",
133        );
134        test_rule_match(yaml, "let a = 123; let b = 33;");
135        test_rule_match(yaml, "class B { func() {let a = 123; }}");
136        test_rule_unmatch(yaml, "const a = 33");
137        test_rule_unmatch(yaml, "let a = 33");
138    }
139
140    #[test]
141    fn test_deserialize_kind() {
142        let yaml = &make_yaml(
143            "
144    kind: class_body
145",
146        );
147        test_rule_match(yaml, "class B { func() {let a = 123; }}");
148        test_rule_unmatch(yaml, "const B = { func() {let a = 123; }}");
149    }
150
151    #[test]
152    fn test_deserialize_inside() {
153        let yaml = &make_yaml(
154            "
155  all:
156    - inside:
157        kind: class_body
158        stopBy: end
159    - pattern: let a = 123
160",
161        );
162        test_rule_unmatch(yaml, "let a = 123; let b = 33;");
163        test_rule_match(yaml, "class B { func() {let a = 123; }}");
164        test_rule_unmatch(yaml, "let a = 123");
165    }
166
167    #[test]
168    fn test_deserialize_not_inside() {
169        let yaml = &make_yaml(
170            "
171  all:
172    - not:
173        inside:
174          kind: class_body
175          stopBy: end
176    - pattern: let a = 123
177",
178        );
179        test_rule_match(yaml, "let a = 123; let b = 33;");
180        test_rule_unmatch(yaml, "class B { func() {let a = 123; }}");
181        test_rule_unmatch(yaml, "let a = 13");
182    }
183
184    #[test]
185    fn test_deserialize_meta_var() {
186        let yaml = &make_yaml(
187            "
188  all:
189    - inside:
190        any:
191          - pattern: function $A($$$) { $$$ }
192          - pattern: let $A = ($$$) => $$$
193        stopBy: end
194    - pattern: $A($$$)
195",
196        );
197        test_rule_match(yaml, "function recursion() { recursion() }");
198        test_rule_match(yaml, "let recursion = () => { recursion() }");
199        test_rule_unmatch(yaml, "function callOther() { other() }");
200    }
201
202    #[test]
203    fn test_deserialize_constraints() {
204        let yaml = r"
205id: test
206message: test rule
207severity: info
208language: Tsx
209rule:
210  all:
211    - pattern: console.log($A)
212    - inside:
213        pattern: function $B() {$$$}
214        stopBy: end
215constraints:
216  B:
217    regex: test
218";
219        test_rule_match(yaml, "function test() { console.log(1) }");
220        test_rule_match(yaml, "function test() { console.log(2) }");
221        test_rule_unmatch(yaml, "function tt() { console.log(2) }");
222    }
223
224    // https://github.com/ast-grep/ast-grep/issues/813
225    #[test]
226    fn test_util_rule_with_vaargs() {
227        let yaml = r"
228id: sibling
229language: Tsx
230utils:
231  utilpat:
232    pattern: '$A($$$B);'
233rule:
234  matches: utilpat
235  follows:
236    matches: utilpat
237    stopBy: end
238";
239        test_rule_match(yaml, "a();a(123);a();a(123)");
240    }
241}