Skip to main content

thread_rule_engine/transform/
mod.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
7mod parse;
8mod rewrite;
9mod string_case;
10mod trans;
11
12use crate::{DeserializeEnv, RuleCore};
13
14use thread_ast_engine::Doc;
15use thread_ast_engine::Language;
16use thread_ast_engine::meta_var::MetaVarEnv;
17use thread_ast_engine::meta_var::MetaVariable;
18
19use parse::ParseTransError;
20use schemars::JsonSchema;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use thread_utilities::RapidMap;
24
25pub use trans::Trans;
26
27#[derive(Serialize, Deserialize, Clone, JsonSchema, Debug)]
28#[serde(untagged)]
29pub enum Transformation {
30    Simplified(String),
31    Object(Trans<String>),
32}
33
34impl Transformation {
35    pub fn parse<L: Language>(&self, lang: &L) -> Result<Trans<MetaVariable>, TransformError> {
36        match self {
37            Transformation::Simplified(s) => {
38                let t: Trans<String> = s.parse()?;
39                t.parse(lang)
40            }
41            Transformation::Object(t) => t.parse(lang),
42        }
43    }
44}
45
46#[derive(Error, Debug)]
47pub enum TransformError {
48    #[error("Cannot parse transform string.")]
49    Parse(#[from] ParseTransError),
50    #[error("`{0}` has a cyclic dependency.")]
51    Cyclic(String),
52    #[error("Transform var `{0}` has already defined.")]
53    AlreadyDefined(String),
54    #[error("source `{0}` should be $-prefixed.")]
55    MalformedVar(String),
56}
57
58#[derive(Clone, Debug)]
59pub struct Transform {
60    transforms: Vec<(String, Trans<MetaVariable>)>,
61}
62
63impl Transform {
64    pub fn deserialize<L: Language>(
65        map: &RapidMap<String, Transformation>,
66        env: &DeserializeEnv<L>,
67    ) -> Result<Self, TransformError> {
68        let map: Result<_, _> = map
69            .iter()
70            .map(|(key, val)| val.parse(&env.lang).map(|t| (key.to_string(), t)))
71            .collect();
72        let map = map?;
73        let order = env
74            .get_transform_order(&map)
75            .map_err(TransformError::Cyclic)?;
76        let transforms = order
77            .iter()
78            .map(|&key| (key.to_string(), map[key].clone()))
79            .collect();
80        Ok(Self { transforms })
81    }
82
83    pub fn apply_transform<'c, D: Doc>(
84        &self,
85        env: &mut MetaVarEnv<'c, D>,
86        rewriters: &RapidMap<String, RuleCore>,
87        enclosing_env: &MetaVarEnv<'c, D>,
88    ) {
89        let mut ctx = Ctx {
90            env,
91            rewriters,
92            enclosing_env,
93        };
94        for (key, tr) in &self.transforms {
95            tr.insert(key, &mut ctx);
96        }
97    }
98
99    pub(crate) fn keys(&self) -> impl Iterator<Item = &String> {
100        self.transforms.iter().map(|t| &t.0)
101    }
102
103    pub(crate) fn values(&self) -> impl Iterator<Item = &Trans<MetaVariable>> {
104        self.transforms.iter().map(|t| &t.1)
105    }
106}
107
108// two lifetime to represent env root lifetime and lang/trans lifetime
109struct Ctx<'b, 'c, D: Doc> {
110    rewriters: &'b RapidMap<String, RuleCore>,
111    env: &'b mut MetaVarEnv<'c, D>,
112    enclosing_env: &'b MetaVarEnv<'c, D>,
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118    use crate::from_str;
119    use crate::test::TypeScript;
120    use thread_ast_engine::tree_sitter::LanguageExt;
121
122    #[test]
123    fn test_transform_str() {}
124
125    #[test]
126    fn test_single_cyclic_transform() {
127        let mut trans = RapidMap::default();
128        let trans_a = from_str("substring: {source: $A}").unwrap();
129        trans.insert("A".into(), trans_a);
130        let env = DeserializeEnv::new(TypeScript::Tsx);
131        match Transform::deserialize(&trans, &env) {
132            Err(TransformError::Cyclic(a)) => assert_eq!(a, "A"),
133            _ => panic!("unexpected error"),
134        }
135    }
136
137    #[test]
138    fn test_cyclic_transform() {
139        let mut trans = RapidMap::default();
140        let trans_a = from_str("substring: {source: $B}").unwrap();
141        trans.insert("A".into(), trans_a);
142        let trans_b = from_str("substring: {source: $A}").unwrap();
143        trans.insert("B".into(), trans_b);
144        let env = DeserializeEnv::new(TypeScript::Tsx);
145        let ret = Transform::deserialize(&trans, &env);
146        assert!(matches!(ret, Err(TransformError::Cyclic(_))));
147    }
148
149    #[test]
150    fn test_transform_use_matched() {
151        let mut trans = RapidMap::default();
152        let trans_a = from_str("substring: {source: $C}").unwrap();
153        trans.insert("A".into(), trans_a);
154        let trans_b = from_str("substring: {source: $A}").unwrap();
155        trans.insert("B".into(), trans_b);
156        let env = DeserializeEnv::new(TypeScript::Tsx);
157        let ret = Transform::deserialize(&trans, &env);
158        assert!(ret.is_ok());
159    }
160
161    #[test]
162    fn test_transform_indentation() {
163        let src = "
164if (true) {
165  let a = {
166    b: 123
167  }
168}
169";
170        let expected = "{
171  b: 123
172}";
173        let mut trans = RapidMap::default();
174        let tr = from_str("{ substring: { source: $A } }").expect("should work");
175        trans.insert("TR".into(), tr);
176        let grep = TypeScript::Tsx.ast_grep(src);
177        let root = grep.root();
178        let mut nm = root.find("let a = $A").expect("should find");
179        let env = DeserializeEnv::new(TypeScript::Tsx);
180        let trans = Transform::deserialize(&trans, &env).expect("should deserialize");
181        trans.apply_transform(nm.get_env_mut(), &Default::default(), &Default::default());
182        let actual = nm.get_env().get_transformed("TR").expect("should have TR");
183        let actual = std::str::from_utf8(actual).expect("should work");
184        assert_eq!(actual, expected);
185    }
186}