darklua_core/rules/
mod.rs

1//! A module that contains the different rules that mutates a Lua block.
2
3mod append_text_comment;
4pub mod bundle;
5mod call_parens;
6mod compute_expression;
7mod configuration_error;
8mod convert_index_to_field;
9mod convert_require;
10mod empty_do;
11mod filter_early_return;
12mod group_local;
13mod inject_value;
14mod method_def;
15mod no_local_function;
16mod remove_assertions;
17mod remove_call_match;
18mod remove_comments;
19mod remove_compound_assign;
20mod remove_continue;
21mod remove_debug_profiling;
22mod remove_floor_division;
23mod remove_if_expression;
24mod remove_interpolated_string;
25mod remove_nil_declarations;
26mod remove_spaces;
27mod remove_types;
28mod remove_unused_variable;
29mod rename_variables;
30mod replace_referenced_tokens;
31pub(crate) mod require;
32mod rule_property;
33mod shift_token_line;
34mod unused_if_branch;
35mod unused_while;
36
37pub use append_text_comment::*;
38pub use call_parens::*;
39pub use compute_expression::*;
40pub use configuration_error::RuleConfigurationError;
41pub use convert_index_to_field::*;
42pub use convert_require::*;
43pub use empty_do::*;
44pub use filter_early_return::*;
45pub use group_local::*;
46pub use inject_value::*;
47pub use method_def::*;
48pub use no_local_function::*;
49pub use remove_assertions::*;
50pub use remove_comments::*;
51pub use remove_compound_assign::*;
52pub use remove_continue::*;
53pub use remove_debug_profiling::*;
54pub use remove_floor_division::*;
55pub use remove_if_expression::*;
56pub use remove_interpolated_string::*;
57pub use remove_nil_declarations::*;
58pub use remove_spaces::*;
59pub use remove_types::*;
60pub use remove_unused_variable::*;
61pub use rename_variables::*;
62pub(crate) use replace_referenced_tokens::*;
63pub use rule_property::*;
64pub(crate) use shift_token_line::*;
65pub use unused_if_branch::*;
66pub use unused_while::*;
67
68use crate::nodes::Block;
69use crate::Resources;
70
71use serde::de::{self, MapAccess, Visitor};
72use serde::ser::SerializeMap;
73use serde::{Deserialize, Deserializer, Serialize, Serializer};
74use std::collections::HashMap;
75use std::fmt;
76use std::path::{Path, PathBuf};
77use std::str::FromStr;
78
79#[derive(Debug, Clone)]
80pub struct ContextBuilder<'a, 'resources, 'code> {
81    path: PathBuf,
82    resources: &'resources Resources,
83    original_code: &'code str,
84    blocks: HashMap<PathBuf, &'a Block>,
85    project_location: Option<PathBuf>,
86}
87
88impl<'a, 'resources, 'code> ContextBuilder<'a, 'resources, 'code> {
89    pub fn new(
90        path: impl Into<PathBuf>,
91        resources: &'resources Resources,
92        original_code: &'code str,
93    ) -> Self {
94        Self {
95            path: path.into(),
96            resources,
97            original_code,
98            blocks: Default::default(),
99            project_location: None,
100        }
101    }
102
103    pub fn with_project_location(mut self, path: impl Into<PathBuf>) -> Self {
104        self.project_location = Some(path.into());
105        self
106    }
107
108    pub fn build(self) -> Context<'a, 'resources, 'code> {
109        Context {
110            path: self.path,
111            resources: self.resources,
112            original_code: self.original_code,
113            blocks: self.blocks,
114            project_location: self.project_location,
115            dependencies: Default::default(),
116        }
117    }
118
119    pub fn insert_block<'block: 'a>(&mut self, path: impl Into<PathBuf>, block: &'block Block) {
120        self.blocks.insert(path.into(), block);
121    }
122}
123
124/// The intent of this struct is to hold data shared across all rules applied to a file.
125#[derive(Debug, Clone)]
126pub struct Context<'a, 'resources, 'code> {
127    path: PathBuf,
128    resources: &'resources Resources,
129    original_code: &'code str,
130    blocks: HashMap<PathBuf, &'a Block>,
131    project_location: Option<PathBuf>,
132    dependencies: std::cell::RefCell<Vec<PathBuf>>,
133}
134
135impl Context<'_, '_, '_> {
136    pub fn block(&self, path: impl AsRef<Path>) -> Option<&Block> {
137        self.blocks.get(path.as_ref()).copied()
138    }
139
140    pub fn current_path(&self) -> &Path {
141        self.path.as_ref()
142    }
143
144    pub fn add_file_dependency(&self, path: PathBuf) {
145        if let Ok(mut dependencies) = self.dependencies.try_borrow_mut() {
146            log::trace!("add file dependency {}", path.display());
147            dependencies.push(path);
148        } else {
149            log::warn!("unable to submit file dependency (internal error)");
150        }
151    }
152
153    pub fn into_dependencies(self) -> impl Iterator<Item = PathBuf> {
154        self.dependencies.into_inner().into_iter()
155    }
156
157    fn resources(&self) -> &Resources {
158        self.resources
159    }
160
161    fn original_code(&self) -> &str {
162        self.original_code
163    }
164
165    fn project_location(&self) -> &Path {
166        self.project_location.as_deref().unwrap_or_else(|| {
167            let source = self.current_path();
168            source.parent().unwrap_or_else(|| {
169                log::warn!(
170                    "unexpected file path `{}` (unable to extract parent path)",
171                    source.display()
172                );
173                source
174            })
175        })
176    }
177}
178
179pub type RuleProcessResult = Result<(), String>;
180
181/// Defines an interface that will be used to mutate blocks and how to serialize and deserialize
182/// the rule configuration.
183pub trait Rule: RuleConfiguration + fmt::Debug {
184    /// This method should mutate the given block to apply the rule
185    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult;
186
187    /// Return the list of paths to Lua files that is necessary to apply this rule. This will load
188    /// each AST block from these files into the context object.
189    fn require_content(&self, _current_source: &Path, _current_block: &Block) -> Vec<PathBuf> {
190        Vec::new()
191    }
192}
193
194pub trait RuleConfiguration {
195    /// The rule deserializer will construct the default rule and then send the properties through
196    /// this method to modify the behavior of the rule.
197    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError>;
198    /// This method should return the unique name of the rule.
199    fn get_name(&self) -> &'static str;
200    /// For implementing the serialize trait on the Rule trait, this method should return all
201    /// properties that differs from their default value.
202    fn serialize_to_properties(&self) -> RuleProperties;
203    /// Returns `true` if the rule has at least one property.
204    fn has_properties(&self) -> bool {
205        !self.serialize_to_properties().is_empty()
206    }
207}
208
209pub trait FlawlessRule {
210    fn flawless_process(&self, block: &mut Block, context: &Context);
211}
212
213impl<T: FlawlessRule + RuleConfiguration + fmt::Debug> Rule for T {
214    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
215        self.flawless_process(block, context);
216        Ok(())
217    }
218}
219
220/// A function to get the default rule stack for darklua. All the rules here must preserve all the
221/// functionalities of the original code after being applied. They must guarantee that the
222/// processed block will work as much as the original one.
223pub fn get_default_rules() -> Vec<Box<dyn Rule>> {
224    vec![
225        Box::<RemoveSpaces>::default(),
226        Box::<RemoveComments>::default(),
227        Box::<ComputeExpression>::default(),
228        Box::<RemoveUnusedIfBranch>::default(),
229        Box::<RemoveUnusedWhile>::default(),
230        Box::<FilterAfterEarlyReturn>::default(),
231        Box::<RemoveEmptyDo>::default(),
232        Box::<RemoveUnusedVariable>::default(),
233        Box::<RemoveMethodDefinition>::default(),
234        Box::<ConvertIndexToField>::default(),
235        Box::<RemoveNilDeclaration>::default(),
236        Box::<RenameVariables>::default(),
237        Box::<RemoveFunctionCallParens>::default(),
238    ]
239}
240
241pub fn get_all_rule_names() -> Vec<&'static str> {
242    vec![
243        APPEND_TEXT_COMMENT_RULE_NAME,
244        COMPUTE_EXPRESSIONS_RULE_NAME,
245        CONVERT_INDEX_TO_FIELD_RULE_NAME,
246        CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME,
247        CONVERT_REQUIRE_RULE_NAME,
248        FILTER_AFTER_EARLY_RETURN_RULE_NAME,
249        GROUP_LOCAL_ASSIGNMENT_RULE_NAME,
250        INJECT_GLOBAL_VALUE_RULE_NAME,
251        REMOVE_ASSERTIONS_RULE_NAME,
252        REMOVE_COMMENTS_RULE_NAME,
253        REMOVE_COMPOUND_ASSIGNMENT_RULE_NAME,
254        REMOVE_DEBUG_PROFILING_RULE_NAME,
255        REMOVE_EMPTY_DO_RULE_NAME,
256        REMOVE_FUNCTION_CALL_PARENS_RULE_NAME,
257        REMOVE_INTERPOLATED_STRING_RULE_NAME,
258        REMOVE_METHOD_DEFINITION_RULE_NAME,
259        REMOVE_NIL_DECLARATION_RULE_NAME,
260        REMOVE_SPACES_RULE_NAME,
261        REMOVE_TYPES_RULE_NAME,
262        REMOVE_UNUSED_IF_BRANCH_RULE_NAME,
263        REMOVE_UNUSED_VARIABLE_RULE_NAME,
264        REMOVE_UNUSED_WHILE_RULE_NAME,
265        RENAME_VARIABLES_RULE_NAME,
266        REMOVE_IF_EXPRESSION_RULE_NAME,
267        REMOVE_CONTINUE_RULE_NAME,
268    ]
269}
270
271impl FromStr for Box<dyn Rule> {
272    type Err = String;
273
274    fn from_str(string: &str) -> Result<Self, Self::Err> {
275        let rule: Box<dyn Rule> = match string {
276            APPEND_TEXT_COMMENT_RULE_NAME => Box::<AppendTextComment>::default(),
277            COMPUTE_EXPRESSIONS_RULE_NAME => Box::<ComputeExpression>::default(),
278            CONVERT_INDEX_TO_FIELD_RULE_NAME => Box::<ConvertIndexToField>::default(),
279            CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME => {
280                Box::<ConvertLocalFunctionToAssign>::default()
281            }
282            CONVERT_REQUIRE_RULE_NAME => Box::<ConvertRequire>::default(),
283            FILTER_AFTER_EARLY_RETURN_RULE_NAME => Box::<FilterAfterEarlyReturn>::default(),
284            GROUP_LOCAL_ASSIGNMENT_RULE_NAME => Box::<GroupLocalAssignment>::default(),
285            INJECT_GLOBAL_VALUE_RULE_NAME => Box::<InjectGlobalValue>::default(),
286            REMOVE_ASSERTIONS_RULE_NAME => Box::<RemoveAssertions>::default(),
287            REMOVE_COMMENTS_RULE_NAME => Box::<RemoveComments>::default(),
288            REMOVE_COMPOUND_ASSIGNMENT_RULE_NAME => Box::<RemoveCompoundAssignment>::default(),
289            REMOVE_DEBUG_PROFILING_RULE_NAME => Box::<RemoveDebugProfiling>::default(),
290            REMOVE_EMPTY_DO_RULE_NAME => Box::<RemoveEmptyDo>::default(),
291            REMOVE_FLOOR_DIVISION_RULE_NAME => Box::<RemoveFloorDivision>::default(),
292            REMOVE_FUNCTION_CALL_PARENS_RULE_NAME => Box::<RemoveFunctionCallParens>::default(),
293            REMOVE_INTERPOLATED_STRING_RULE_NAME => Box::<RemoveInterpolatedString>::default(),
294            REMOVE_METHOD_DEFINITION_RULE_NAME => Box::<RemoveMethodDefinition>::default(),
295            REMOVE_NIL_DECLARATION_RULE_NAME => Box::<RemoveNilDeclaration>::default(),
296            REMOVE_SPACES_RULE_NAME => Box::<RemoveSpaces>::default(),
297            REMOVE_TYPES_RULE_NAME => Box::<RemoveTypes>::default(),
298            REMOVE_UNUSED_IF_BRANCH_RULE_NAME => Box::<RemoveUnusedIfBranch>::default(),
299            REMOVE_UNUSED_VARIABLE_RULE_NAME => Box::<RemoveUnusedVariable>::default(),
300            REMOVE_UNUSED_WHILE_RULE_NAME => Box::<RemoveUnusedWhile>::default(),
301            RENAME_VARIABLES_RULE_NAME => Box::<RenameVariables>::default(),
302            REMOVE_IF_EXPRESSION_RULE_NAME => Box::<RemoveIfExpression>::default(),
303            REMOVE_CONTINUE_RULE_NAME => Box::<RemoveContinue>::default(),
304            _ => return Err(format!("invalid rule name: {}", string)),
305        };
306
307        Ok(rule)
308    }
309}
310
311impl Serialize for dyn Rule {
312    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
313        let properties = self.serialize_to_properties();
314        let property_count = properties.len();
315        let rule_name = self.get_name();
316
317        if property_count == 0 {
318            serializer.serialize_str(rule_name)
319        } else {
320            let mut map = serializer.serialize_map(Some(property_count + 1))?;
321
322            map.serialize_entry("rule", rule_name)?;
323
324            let mut ordered: Vec<(String, RulePropertyValue)> = properties.into_iter().collect();
325
326            ordered.sort_by(|a, b| a.0.cmp(&b.0));
327
328            for (key, value) in ordered {
329                map.serialize_entry(&key, &value)?;
330            }
331
332            map.end()
333        }
334    }
335}
336
337impl<'de> Deserialize<'de> for Box<dyn Rule> {
338    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Box<dyn Rule>, D::Error> {
339        struct StringOrStruct;
340
341        impl<'de> Visitor<'de> for StringOrStruct {
342            type Value = Box<dyn Rule>;
343
344            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
345                formatter.write_str("rule name or rule object")
346            }
347
348            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
349            where
350                E: de::Error,
351            {
352                let mut rule: Self::Value = FromStr::from_str(value).map_err(de::Error::custom)?;
353
354                rule.configure(RuleProperties::new())
355                    .map_err(de::Error::custom)?;
356
357                Ok(rule)
358            }
359
360            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
361            where
362                M: MapAccess<'de>,
363            {
364                let mut rule_name = None;
365                let mut properties = HashMap::new();
366
367                while let Some(key) = map.next_key::<String>()? {
368                    match key.as_str() {
369                        "rule" => {
370                            if rule_name.is_none() {
371                                rule_name.replace(map.next_value::<String>()?);
372                            } else {
373                                return Err(de::Error::duplicate_field("rule"));
374                            }
375                        }
376                        property => {
377                            let value = map.next_value::<RulePropertyValue>()?;
378
379                            if properties.insert(property.to_owned(), value).is_some() {
380                                return Err(de::Error::custom(format!(
381                                    "duplicate field {} in rule object",
382                                    property
383                                )));
384                            }
385                        }
386                    }
387                }
388
389                if let Some(rule_name) = rule_name {
390                    let mut rule: Self::Value =
391                        FromStr::from_str(&rule_name).map_err(de::Error::custom)?;
392
393                    rule.configure(properties).map_err(de::Error::custom)?;
394
395                    Ok(rule)
396                } else {
397                    Err(de::Error::missing_field("rule"))
398                }
399            }
400        }
401
402        deserializer.deserialize_any(StringOrStruct)
403    }
404}
405
406fn verify_no_rule_properties(properties: &RuleProperties) -> Result<(), RuleConfigurationError> {
407    if let Some((key, _value)) = properties.iter().next() {
408        return Err(RuleConfigurationError::UnexpectedProperty(key.to_owned()));
409    }
410    Ok(())
411}
412
413fn verify_required_properties(
414    properties: &RuleProperties,
415    names: &[&str],
416) -> Result<(), RuleConfigurationError> {
417    for name in names.iter() {
418        if !properties.contains_key(*name) {
419            return Err(RuleConfigurationError::MissingProperty(name.to_string()));
420        }
421    }
422    Ok(())
423}
424
425fn verify_required_any_properties(
426    properties: &RuleProperties,
427    names: &[&str],
428) -> Result<(), RuleConfigurationError> {
429    if names.iter().any(|name| properties.contains_key(*name)) {
430        Ok(())
431    } else {
432        Err(RuleConfigurationError::MissingAnyProperty(
433            names.iter().map(ToString::to_string).collect(),
434        ))
435    }
436}
437
438fn verify_property_collisions(
439    properties: &RuleProperties,
440    names: &[&str],
441) -> Result<(), RuleConfigurationError> {
442    let mut exists = false;
443    for name in names.iter() {
444        if properties.contains_key(*name) {
445            if exists {
446                return Err(RuleConfigurationError::PropertyCollision(
447                    names.iter().map(ToString::to_string).collect(),
448                ));
449            } else {
450                exists = true;
451            }
452        }
453    }
454    Ok(())
455}
456
457#[cfg(test)]
458mod test {
459    use super::*;
460
461    use insta::assert_json_snapshot;
462
463    #[test]
464    fn snapshot_default_rules() {
465        let rules = get_default_rules();
466
467        assert_json_snapshot!("default_rules", rules);
468    }
469
470    #[test]
471    fn snapshot_all_rules() {
472        let rule_names = get_all_rule_names();
473
474        assert_json_snapshot!("all_rule_names", rule_names);
475    }
476
477    #[test]
478    fn verify_no_rule_properties_is_ok_when_empty() {
479        let empty_properties = RuleProperties::default();
480
481        assert_eq!(verify_no_rule_properties(&empty_properties), Ok(()));
482    }
483
484    #[test]
485    fn verify_no_rule_properties_is_unexpected_rule_err() {
486        let mut properties = RuleProperties::default();
487        let some_rule_name = "rule name";
488        properties.insert(some_rule_name.to_owned(), RulePropertyValue::None);
489
490        assert_eq!(
491            verify_no_rule_properties(&properties),
492            Err(RuleConfigurationError::UnexpectedProperty(
493                some_rule_name.to_owned()
494            ))
495        );
496    }
497
498    #[test]
499    fn get_all_rule_names_are_deserializable() {
500        for name in get_all_rule_names() {
501            let rule: Result<Box<dyn Rule>, _> = name.parse();
502            assert!(rule.is_ok(), "unable to deserialize `{}`", name);
503        }
504    }
505
506    #[test]
507    fn get_all_rule_names_are_serializable() {
508        for name in get_all_rule_names() {
509            let rule: Box<dyn Rule> = name
510                .parse()
511                .unwrap_or_else(|_| panic!("unable to deserialize `{}`", name));
512            assert!(json5::to_string(&rule).is_ok());
513        }
514    }
515}