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