darklua_core/rules/rename_variables/
mod.rs

1mod function_names;
2mod globals;
3mod rename_processor;
4
5use rename_processor::RenameProcessor;
6
7use crate::nodes::Block;
8use crate::process::utils::is_valid_identifier;
9use crate::process::{DefaultVisitor, NodeVisitor, ScopeVisitor};
10use crate::rules::{
11    Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
12    RulePropertyValue,
13};
14
15use std::collections::HashSet;
16use std::iter::FromIterator;
17
18pub const RENAME_VARIABLES_RULE_NAME: &str = "rename_variables";
19
20/// Rename all identifiers to small and meaningless names.
21#[derive(Debug, PartialEq, Eq)]
22pub struct RenameVariables {
23    globals: Vec<String>,
24    include_functions: bool,
25}
26
27impl RenameVariables {
28    pub fn new<I: IntoIterator<Item = String>>(iter: I) -> Self {
29        Self {
30            globals: Vec::from_iter(iter),
31            include_functions: false,
32        }
33    }
34
35    pub fn with_function_names(mut self) -> Self {
36        self.include_functions = true;
37        self
38    }
39
40    fn set_globals(&mut self, list: Vec<String>) -> Result<(), RuleConfigurationError> {
41        for value in list {
42            match value.as_str() {
43                "$default" => self
44                    .globals
45                    .extend(globals::DEFAULT.iter().map(ToString::to_string)),
46                "$roblox" => self
47                    .globals
48                    .extend(globals::ROBLOX.iter().map(ToString::to_string)),
49                identifier if !is_valid_identifier(identifier) => {
50                    return Err(RuleConfigurationError::StringExpected("".to_owned()))
51                }
52                _ => self.globals.push(value),
53            }
54        }
55
56        Ok(())
57    }
58
59    fn normalize_globals(&self) -> Vec<String> {
60        let mut globals_set: HashSet<String> = self.globals.iter().cloned().collect();
61
62        let mut result = Vec::new();
63
64        if globals::DEFAULT
65            .iter()
66            .all(|identifier| globals_set.contains(*identifier))
67        {
68            globals::DEFAULT.iter().for_each(|identifier| {
69                globals_set.remove(*identifier);
70            });
71            result.push("$default".to_owned());
72        }
73
74        if globals::ROBLOX
75            .iter()
76            .all(|identifier| globals_set.contains(*identifier))
77        {
78            globals::ROBLOX.iter().for_each(|identifier| {
79                globals_set.remove(*identifier);
80            });
81            result.push("$roblox".to_owned());
82        }
83
84        result.extend(globals_set);
85        result.sort();
86        result
87    }
88}
89
90impl Default for RenameVariables {
91    fn default() -> Self {
92        Self::new(globals::DEFAULT.iter().map(|string| (*string).to_owned()))
93    }
94}
95
96impl FlawlessRule for RenameVariables {
97    fn flawless_process(&self, block: &mut Block, _: &Context) {
98        let avoid_identifiers = if self.include_functions {
99            Vec::new()
100        } else {
101            let mut collect_functions = function_names::CollectFunctionNames::default();
102            DefaultVisitor::visit_block(block, &mut collect_functions);
103            collect_functions.into()
104        };
105
106        let mut processor = RenameProcessor::new(
107            self.globals.clone().into_iter().chain(avoid_identifiers),
108            self.include_functions,
109        );
110        ScopeVisitor::visit_block(block, &mut processor);
111    }
112}
113
114impl RuleConfiguration for RenameVariables {
115    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
116        for (key, value) in properties {
117            match key.as_str() {
118                "globals" => {
119                    self.set_globals(value.expect_string_list(&key)?)?;
120                }
121                "include_functions" => {
122                    self.include_functions = value.expect_bool(&key)?;
123                }
124                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
125            }
126        }
127
128        Ok(())
129    }
130
131    fn get_name(&self) -> &'static str {
132        RENAME_VARIABLES_RULE_NAME
133    }
134
135    fn serialize_to_properties(&self) -> RuleProperties {
136        let mut properties = RuleProperties::new();
137
138        let globals = self.normalize_globals();
139        if !(globals.len() == 1 && globals.contains(&"$default".to_owned())) {
140            properties.insert(
141                "globals".to_owned(),
142                RulePropertyValue::StringList(self.normalize_globals()),
143            );
144        }
145
146        if self.include_functions {
147            properties.insert(
148                "include_functions".to_owned(),
149                RulePropertyValue::Boolean(self.include_functions),
150            );
151        }
152
153        properties
154    }
155}
156
157#[cfg(test)]
158mod test {
159    use super::*;
160    use crate::rules::Rule;
161
162    use insta::assert_json_snapshot;
163    use std::iter::empty;
164
165    fn new_rule() -> Box<dyn Rule> {
166        Box::<RenameVariables>::default()
167    }
168
169    #[test]
170    fn serialize_default_rule() {
171        assert_json_snapshot!("default_rename_variables", new_rule());
172    }
173
174    #[test]
175    fn serialize_no_globals_rule() {
176        assert_json_snapshot!(
177            "no_globals_rename_variables",
178            Box::new(RenameVariables::new(empty())) as Box<dyn Rule>
179        );
180    }
181
182    #[test]
183    fn serialize_roblox_globals_rule() {
184        let rule = Box::new(RenameVariables::new(
185            globals::ROBLOX.iter().map(ToString::to_string),
186        ));
187
188        assert_json_snapshot!("roblox_globals_rename_variables", rule as Box<dyn Rule>);
189    }
190
191    #[test]
192    fn serialize_with_function_names() {
193        let rule = Box::new(
194            RenameVariables::new(globals::DEFAULT.iter().map(ToString::to_string))
195                .with_function_names(),
196        );
197
198        assert_json_snapshot!(
199            "rename_variables_with_function_names",
200            rule as Box<dyn Rule>
201        );
202    }
203
204    #[test]
205    fn serialize_skip_functions() {
206        let rule = Box::new(RenameVariables::new(
207            globals::ROBLOX.iter().map(ToString::to_string),
208        ));
209
210        assert_json_snapshot!("roblox_globals_rename_variables", rule as Box<dyn Rule>);
211    }
212}