darklua_core/rules/bundle/
mod.rs

1pub(crate) mod path_require_mode;
2mod rename_type_declaration;
3mod require_mode;
4
5use std::path::Path;
6
7use crate::nodes::Block;
8use crate::rules::{
9    Context, Rule, RuleConfiguration, RuleConfigurationError, RuleProcessResult, RuleProperties,
10};
11use crate::Parser;
12
13pub(crate) use rename_type_declaration::RenameTypeDeclarationProcessor;
14pub use require_mode::BundleRequireMode;
15use wax::Pattern;
16
17pub const BUNDLER_RULE_NAME: &str = "bundler";
18
19#[derive(Debug)]
20pub(crate) struct BundleOptions {
21    parser: Parser,
22    modules_identifier: String,
23    excludes: Option<wax::Any<'static>>,
24}
25
26impl BundleOptions {
27    fn new<'a>(
28        parser: Parser,
29        modules_identifier: impl Into<String>,
30        excludes: impl Iterator<Item = &'a str>,
31    ) -> Self {
32        let excludes: Vec<_> = excludes
33            .filter_map(|exclusion| match wax::Glob::new(exclusion) {
34                Ok(glob) => Some(glob.into_owned()),
35                Err(err) => {
36                    log::warn!(
37                        "unable to create exclude matcher from `{}`: {}",
38                        exclusion,
39                        err.to_string()
40                    );
41                    None
42                }
43            })
44            .collect();
45        Self {
46            parser,
47            modules_identifier: modules_identifier.into(),
48            excludes: if excludes.is_empty() {
49                None
50            } else {
51                let any_pattern = wax::any::<wax::Glob, _>(excludes)
52                    .expect("exclude globs errors should be filtered and only emit a warning");
53                Some(any_pattern)
54            },
55        }
56    }
57
58    fn parser(&self) -> &Parser {
59        &self.parser
60    }
61
62    fn modules_identifier(&self) -> &str {
63        &self.modules_identifier
64    }
65
66    fn is_excluded(&self, require: &Path) -> bool {
67        self.excludes
68            .as_ref()
69            .map(|any| any.is_match(require))
70            .unwrap_or(false)
71    }
72}
73
74/// A rule that inlines required modules
75#[derive(Debug)]
76pub(crate) struct Bundler {
77    require_mode: BundleRequireMode,
78    options: BundleOptions,
79}
80
81impl Bundler {
82    pub(crate) fn new<'a>(
83        parser: Parser,
84        require_mode: BundleRequireMode,
85        excludes: impl Iterator<Item = &'a str>,
86    ) -> Self {
87        Self {
88            require_mode,
89            options: BundleOptions::new(parser, DEFAULT_MODULE_IDENTIFIER, excludes),
90        }
91    }
92
93    pub(crate) fn with_modules_identifier(mut self, modules_identifier: impl Into<String>) -> Self {
94        self.options.modules_identifier = modules_identifier.into();
95        self
96    }
97}
98
99impl Rule for Bundler {
100    fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
101        self.require_mode
102            .process_block(block, context, &self.options)
103    }
104}
105
106impl RuleConfiguration for Bundler {
107    fn configure(&mut self, _properties: RuleProperties) -> Result<(), RuleConfigurationError> {
108        Err(RuleConfigurationError::InternalUsageOnly(
109            self.get_name().to_owned(),
110        ))
111    }
112
113    fn get_name(&self) -> &'static str {
114        BUNDLER_RULE_NAME
115    }
116
117    fn serialize_to_properties(&self) -> RuleProperties {
118        RuleProperties::new()
119    }
120}
121
122const DEFAULT_MODULE_IDENTIFIER: &str = "__DARKLUA_BUNDLE_MODULES";
123
124#[cfg(test)]
125mod test {
126    use super::*;
127    use crate::rules::{require::PathRequireMode, Rule};
128
129    use insta::assert_json_snapshot;
130
131    fn new_rule() -> Bundler {
132        Bundler::new(
133            Parser::default(),
134            BundleRequireMode::default(),
135            std::iter::empty(),
136        )
137    }
138
139    fn new_rule_with_require_mode(mode: impl Into<BundleRequireMode>) -> Bundler {
140        Bundler::new(Parser::default(), mode.into(), std::iter::empty())
141    }
142
143    // the bundler rule should only be used internally by darklua
144    // so there is no need for it to serialize properly. The
145    // implementation exist just make sure it does not panic
146
147    #[test]
148    fn serialize_default_rule() {
149        let rule: Box<dyn Rule> = Box::new(new_rule());
150
151        assert_json_snapshot!("default_bundler", rule);
152    }
153
154    #[test]
155    fn serialize_path_require_mode_with_custom_module_folder_name() {
156        let rule: Box<dyn Rule> =
157            Box::new(new_rule_with_require_mode(PathRequireMode::new("__init__")));
158
159        assert_json_snapshot!("default_bundler", rule);
160    }
161
162    #[test]
163    fn serialize_path_require_mode_with_custom_module_folder_name_and_modules_identifier() {
164        let rule: Box<dyn Rule> = Box::new(
165            new_rule_with_require_mode(PathRequireMode::new("__init__"))
166                .with_modules_identifier("_CUSTOM_VAR"),
167        );
168
169        assert_json_snapshot!("default_bundler", rule);
170    }
171
172    #[test]
173    fn serialize_with_custom_modules_identifier() {
174        let rule: Box<dyn Rule> = Box::new(new_rule().with_modules_identifier("_CUSTOM_VAR"));
175
176        assert_json_snapshot!("default_bundler", rule);
177    }
178}