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