Skip to main content

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