darklua_core/rules/bundle/path_require_mode/
mod.rs

1mod module_definitions;
2
3use module_definitions::BuildModuleDefinitions;
4
5use std::collections::{HashMap, HashSet};
6use std::ops::{Deref, DerefMut};
7use std::path::{Path, PathBuf};
8use std::{iter, mem};
9
10use serde::Serialize;
11
12use crate::frontend::DarkluaResult;
13use crate::nodes::{
14    Block, DoStatement, Expression, FunctionCall, LocalAssignStatement, Prefix, Statement,
15    StringExpression,
16};
17use crate::process::{
18    to_expression, DefaultVisitor, IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor,
19};
20use crate::rules::require::{
21    is_require_call, match_path_require_call, PathRequireMode, RequirePathLocator,
22};
23use crate::rules::{
24    Context, ContextBuilder, FlawlessRule, ReplaceReferencedTokens, RuleProcessResult,
25};
26use crate::utils::Timer;
27use crate::{DarkluaError, Resources};
28
29use super::BundleOptions;
30
31pub(crate) enum RequiredResource {
32    Block(Block),
33    Expression(Expression),
34}
35
36#[derive(Debug)]
37struct RequirePathProcessor<'a, 'b, 'resources, 'code> {
38    options: &'a BundleOptions,
39    identifier_tracker: IdentifierTracker,
40    path_locator: RequirePathLocator<'b, 'code, 'resources>,
41    module_definitions: BuildModuleDefinitions,
42    source: PathBuf,
43    module_cache: HashMap<PathBuf, Expression>,
44    require_stack: Vec<PathBuf>,
45    skip_module_paths: HashSet<PathBuf>,
46    resources: &'resources Resources,
47    errors: Vec<String>,
48}
49
50impl<'a, 'b, 'code, 'resources> RequirePathProcessor<'a, 'b, 'code, 'resources> {
51    fn new<'context>(
52        context: &'context Context<'b, 'resources, 'code>,
53        options: &'a BundleOptions,
54        path_require_mode: &'b PathRequireMode,
55    ) -> Self
56    where
57        'context: 'b,
58        'context: 'resources,
59        'context: 'code,
60    {
61        Self {
62            options,
63            identifier_tracker: IdentifierTracker::new(),
64            path_locator: RequirePathLocator::new(
65                path_require_mode,
66                context.project_location(),
67                context.resources(),
68            ),
69            module_definitions: BuildModuleDefinitions::new(options.modules_identifier()),
70            source: context.current_path().to_path_buf(),
71            module_cache: Default::default(),
72            require_stack: Default::default(),
73            skip_module_paths: Default::default(),
74            resources: context.resources(),
75            errors: Vec::new(),
76        }
77    }
78
79    fn apply(self, block: &mut Block, context: &Context) -> RuleProcessResult {
80        self.module_definitions.apply(block, context);
81        match self.errors.len() {
82            0 => Ok(()),
83            1 => Err(self.errors.first().unwrap().to_string()),
84            _ => Err(format!("- {}", self.errors.join("\n- "))),
85        }
86    }
87
88    fn require_call(&self, call: &FunctionCall) -> Option<PathBuf> {
89        if is_require_call(call, self) {
90            match_path_require_call(call)
91        } else {
92            None
93        }
94    }
95
96    fn try_inline_call(&mut self, call: &FunctionCall) -> Option<Expression> {
97        let literal_require_path = self.require_call(call)?;
98
99        if self.options.is_excluded(&literal_require_path) {
100            log::info!(
101                "exclude `{}` from bundle [from `{}`]",
102                literal_require_path.display(),
103                self.source.display()
104            );
105            return None;
106        }
107
108        let require_path = match self
109            .path_locator
110            .find_require_path(&literal_require_path, &self.source)
111        {
112            Ok(path) => path,
113            Err(err) => {
114                self.errors.push(err.to_string());
115                return None;
116            }
117        };
118
119        log::debug!(
120            "found require call to path `{}` (normalized `{}`)",
121            literal_require_path.display(),
122            require_path.display()
123        );
124
125        if self.skip_module_paths.contains(&require_path) {
126            log::trace!(
127                "skip `{}` because it previously errored",
128                require_path.display()
129            );
130            return None;
131        }
132
133        match self.inline_require(&require_path, call) {
134            Ok(expression) => Some(expression),
135            Err(error) => {
136                self.errors.push(error.to_string());
137                self.skip_module_paths.insert(require_path);
138                None
139            }
140        }
141    }
142
143    fn inline_require(
144        &mut self,
145        require_path: &Path,
146        call: &FunctionCall,
147    ) -> DarkluaResult<Expression> {
148        if let Some(expression) = self.module_cache.get(require_path) {
149            Ok(expression.clone())
150        } else {
151            if let Some(i) = self
152                .require_stack
153                .iter()
154                .enumerate()
155                .find(|(_, path)| **path == require_path)
156                .map(|(i, _)| i)
157            {
158                let require_stack_paths: Vec<_> = self
159                    .require_stack
160                    .iter()
161                    .skip(i)
162                    .map(|path| path.display().to_string())
163                    .chain(iter::once(require_path.display().to_string()))
164                    .collect();
165
166                return Err(DarkluaError::custom(format!(
167                    "cyclic require detected with `{}`",
168                    require_stack_paths.join("` > `")
169                )));
170            }
171
172            self.require_stack.push(require_path.to_path_buf());
173            let required_resource = self.require_resource(require_path);
174            self.require_stack.pop();
175
176            let module_value = self.module_definitions.build_module_from_resource(
177                required_resource?,
178                require_path,
179                call,
180            )?;
181
182            self.module_cache
183                .insert(require_path.to_path_buf(), module_value.clone());
184
185            Ok(module_value)
186        }
187    }
188
189    fn require_resource(&mut self, path: impl AsRef<Path>) -> DarkluaResult<RequiredResource> {
190        let path = path.as_ref();
191        log::trace!("look for resource `{}`", path.display());
192        let content = self.resources.get(path).map_err(DarkluaError::from)?;
193
194        match path.extension() {
195            Some(extension) => match extension.to_string_lossy().as_ref() {
196                "lua" | "luau" => {
197                    let parser_timer = Timer::now();
198                    let mut block =
199                        self.options
200                            .parser()
201                            .parse(&content)
202                            .map_err(|parser_error| {
203                                DarkluaError::parser_error(path.to_path_buf(), parser_error)
204                            })?;
205                    log::debug!(
206                        "parsed `{}` in {}",
207                        path.display(),
208                        parser_timer.duration_label()
209                    );
210
211                    if self.options.parser().is_preserving_tokens() {
212                        log::trace!("replacing token references of {}", path.display());
213                        let context = ContextBuilder::new(path, self.resources, &content).build();
214                        // run `replace_referenced_tokens` rule to avoid generating invalid code
215                        // when using the token-based generator
216                        let replace_tokens = ReplaceReferencedTokens::default();
217
218                        let apply_replace_tokens_timer = Timer::now();
219
220                        replace_tokens.flawless_process(&mut block, &context);
221
222                        log::trace!(
223                            "replaced token references for `{}` in {}",
224                            path.display(),
225                            apply_replace_tokens_timer.duration_label()
226                        );
227                    }
228
229                    let current_source = mem::replace(&mut self.source, path.to_path_buf());
230
231                    let apply_processor_timer = Timer::now();
232                    DefaultVisitor::visit_block(&mut block, self);
233
234                    log::debug!(
235                        "processed `{}` into bundle in {}",
236                        path.display(),
237                        apply_processor_timer.duration_label()
238                    );
239
240                    self.source = current_source;
241
242                    Ok(RequiredResource::Block(block))
243                }
244                "json" | "json5" => {
245                    transcode("json", path, json5::from_str::<serde_json::Value>, &content)
246                }
247                "yml" | "yaml" => transcode(
248                    "yaml",
249                    path,
250                    serde_yaml::from_str::<serde_yaml::Value>,
251                    &content,
252                ),
253                "toml" => transcode("toml", path, toml::from_str::<toml::Value>, &content),
254                "txt" => Ok(RequiredResource::Expression(
255                    StringExpression::from_value(content).into(),
256                )),
257                _ => Err(DarkluaError::invalid_resource_extension(path)),
258            },
259            None => unreachable!("extension should be defined"),
260        }
261    }
262}
263
264impl<'a, 'b, 'resources, 'code> Deref for RequirePathProcessor<'a, 'b, 'resources, 'code> {
265    type Target = IdentifierTracker;
266
267    fn deref(&self) -> &Self::Target {
268        &self.identifier_tracker
269    }
270}
271
272impl<'a, 'b, 'resources, 'code> DerefMut for RequirePathProcessor<'a, 'b, 'resources, 'code> {
273    fn deref_mut(&mut self) -> &mut Self::Target {
274        &mut self.identifier_tracker
275    }
276}
277
278fn transcode<'a, T, E>(
279    label: &'static str,
280    path: &Path,
281    deserialize_value: impl Fn(&'a str) -> Result<T, E>,
282    content: &'a str,
283) -> Result<RequiredResource, DarkluaError>
284where
285    T: Serialize,
286    E: Into<DarkluaError>,
287{
288    log::trace!("transcode {} data to Lua from `{}`", label, path.display());
289    let transcode_duration = Timer::now();
290    let value = deserialize_value(content).map_err(E::into)?;
291    let expression = to_expression(&value)
292        .map(RequiredResource::Expression)
293        .map_err(DarkluaError::from);
294    log::debug!(
295        "transcoded {} data to Lua from `{}` in {}",
296        label,
297        path.display(),
298        transcode_duration.duration_label()
299    );
300    expression
301}
302
303impl<'a, 'b, 'resources, 'code> NodeProcessor for RequirePathProcessor<'a, 'b, 'resources, 'code> {
304    fn process_expression(&mut self, expression: &mut Expression) {
305        if let Expression::Call(call) = expression {
306            if let Some(replace_with) = self.try_inline_call(call) {
307                *expression = replace_with;
308            }
309        }
310    }
311
312    fn process_prefix_expression(&mut self, prefix: &mut Prefix) {
313        if let Prefix::Call(call) = prefix {
314            if let Some(replace_with) = self.try_inline_call(call) {
315                *prefix = replace_with.into();
316            }
317        }
318    }
319
320    fn process_statement(&mut self, statement: &mut Statement) {
321        if let Statement::Call(call) = statement {
322            if let Some(replace_with) = self.try_inline_call(call) {
323                if let Expression::Call(replace_with) = replace_with {
324                    *call = *replace_with;
325                } else {
326                    *statement = convert_expression_to_statement(replace_with);
327                }
328            }
329        }
330    }
331}
332
333fn convert_expression_to_statement(expression: Expression) -> Statement {
334    DoStatement::new(
335        Block::default()
336            .with_statement(LocalAssignStatement::from_variable("_").with_value(expression)),
337    )
338    .into()
339}
340
341pub(crate) fn process_block(
342    block: &mut Block,
343    context: &Context,
344    options: &BundleOptions,
345    path_require_mode: &PathRequireMode,
346) -> Result<(), String> {
347    if options.parser().is_preserving_tokens() {
348        log::trace!(
349            "replacing token references of {}",
350            context.current_path().display()
351        );
352        let replace_tokens = ReplaceReferencedTokens::default();
353
354        let apply_replace_tokens_timer = Timer::now();
355
356        replace_tokens.flawless_process(block, context);
357
358        log::trace!(
359            "replaced token references for `{}` in {}",
360            context.current_path().display(),
361            apply_replace_tokens_timer.duration_label()
362        );
363    }
364
365    let mut processor = RequirePathProcessor::new(context, options, path_require_mode);
366    ScopeVisitor::visit_block(block, &mut processor);
367    processor.apply(block, context)
368}