darklua_core/rules/
mod.rs

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