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