darklua_core/frontend/
worker.rs

1use std::path::Path;
2
3use super::{
4    configuration::Configuration,
5    resources::Resources,
6    utils::maybe_plural,
7    work_cache::WorkCache,
8    work_item::{WorkData, WorkItem, WorkProgress, WorkStatus},
9    DarkluaError, DarkluaResult, Options, ProcessResult,
10};
11
12use crate::{
13    nodes::Block,
14    rules::{bundle::Bundler, ContextBuilder, Rule, RuleConfiguration},
15    utils::{normalize_path, Timer},
16    GeneratorParameters,
17};
18
19const DEFAULT_CONFIG_PATHS: [&str; 2] = [".darklua.json", ".darklua.json5"];
20
21#[derive(Debug)]
22pub(crate) struct Worker<'a> {
23    resources: &'a Resources,
24    cache: WorkCache<'a>,
25    configuration: Configuration,
26    cached_bundler: Option<Bundler>,
27}
28
29impl<'a> Worker<'a> {
30    pub fn new(resources: &'a Resources) -> Self {
31        Self {
32            resources,
33            cache: WorkCache::new(resources),
34            configuration: Configuration::default(),
35            cached_bundler: None,
36        }
37    }
38
39    pub fn process(
40        mut self,
41        work_items: impl Iterator<Item = Result<WorkItem, DarkluaError>>,
42        mut options: Options,
43    ) -> Result<ProcessResult, ProcessResult> {
44        let configuration_setup_timer = Timer::now();
45
46        if let Some(config) = options.take_configuration() {
47            self.configuration = config;
48            if let Some(config_path) = options.configuration_path() {
49                log::warn!(
50                    concat!(
51                        "the provided options contained both a configuration object and ",
52                        "a path to a configuration file (`{}`). the provided configuration ",
53                        "takes precedence, so it is best to avoid confusion by providing ",
54                        "only the configuration itself or a path to a configuration"
55                    ),
56                    config_path.display()
57                );
58            }
59        } else if let Some(config) = options.configuration_path() {
60            if self.resources.exists(config)? {
61                self.configuration = self.read_configuration(config)?;
62                log::info!("using configuration file `{}`", config.display());
63            } else {
64                return Err(DarkluaError::resource_not_found(config)
65                    .context("expected to find configuration file as provided by the options")
66                    .into());
67            }
68        } else {
69            let mut configuration_files = Vec::new();
70            for path in DEFAULT_CONFIG_PATHS.iter().map(Path::new) {
71                if self.resources.exists(path)? {
72                    configuration_files.push(path);
73                }
74            }
75
76            match configuration_files.len() {
77                0 => {
78                    log::info!("using default configuration");
79                }
80                1 => {
81                    let configuration_file_path = configuration_files.first().unwrap();
82                    self.configuration = self
83                        .read_configuration(configuration_file_path)
84                        .map_err(element_to_vec)?;
85                    log::info!(
86                        "using configuration file `{}`",
87                        configuration_file_path.display()
88                    );
89                }
90                _ => {
91                    return Err(DarkluaError::multiple_configuration_found(
92                        configuration_files.into_iter().map(Path::to_path_buf),
93                    )
94                    .into())
95                }
96            }
97        };
98
99        if let Some(generator) = options.generator_override() {
100            log::trace!(
101                "override with {} generator",
102                match generator {
103                    GeneratorParameters::RetainLines => "`retain_lines`".to_owned(),
104                    GeneratorParameters::Dense { column_span } =>
105                        format!("dense ({})", column_span),
106                    GeneratorParameters::Readable { column_span } =>
107                        format!("readable ({})", column_span),
108                }
109            );
110            self.configuration = self.configuration.with_generator(generator.clone());
111        }
112
113        log::trace!(
114            "configuration setup in {}",
115            configuration_setup_timer.duration_label()
116        );
117        log::debug!(
118            "using configuration: {}",
119            json5::to_string(&self.configuration).unwrap_or_else(|err| {
120                format!("? (unable to serialize configuration: {})", err)
121            })
122        );
123
124        log::trace!("start collecting work");
125        let collect_work_timer = Timer::now();
126
127        let collect_work_result: Result<Vec<_>, _> = work_items.collect();
128        let mut work_items = collect_work_result.map_err(element_to_vec)?;
129
130        log::trace!("work collected in {}", collect_work_timer.duration_label());
131
132        let mut errors = Vec::new();
133        let mut success_count = 0;
134
135        let work_timer = Timer::now();
136        let mut created_files = Vec::new();
137
138        'work_loop: while !work_items.is_empty() {
139            let work_length = work_items.len();
140            log::trace!(
141                "working on batch of {} task{}",
142                work_length,
143                maybe_plural(work_length)
144            );
145
146            let mut work_left = Vec::new();
147
148            for work in work_items.into_iter() {
149                let work_source_display = work.source().display().to_string();
150
151                let created_path = work.get_created_file_path();
152
153                match self.do_work(work) {
154                    Ok(None) => {
155                        success_count += 1;
156                        if let Some(new_file) = created_path {
157                            created_files.push(new_file.to_path_buf());
158                        }
159                        log::info!("successfully processed `{}`", work_source_display);
160                    }
161                    Ok(Some(next_work)) => {
162                        log::trace!("work on `{}` has not completed", work_source_display);
163                        work_left.push(next_work);
164                    }
165                    Err(err) => {
166                        errors.push(err);
167                        if options.should_fail_fast() {
168                            log::debug!(
169                                "dropping all work because the fail-fast option is enabled"
170                            );
171                            break 'work_loop;
172                        }
173                    }
174                }
175            }
176
177            if work_left.len() >= work_length {
178                errors.push(DarkluaError::cyclic_work(work_left));
179                return ProcessResult::new(success_count, created_files, errors).into();
180            }
181
182            work_items = work_left;
183        }
184
185        log::info!("executed work in {}", work_timer.duration_label());
186
187        ProcessResult::new(success_count, created_files, errors).into()
188    }
189
190    fn read_configuration(&self, config: &Path) -> DarkluaResult<Configuration> {
191        let config_content = self.resources.get(config)?;
192        json5::from_str(&config_content)
193            .map_err(|err| {
194                DarkluaError::invalid_configuration_file(config).context(err.to_string())
195            })
196            .map(|configuration: Configuration| {
197                configuration.with_location({
198                    config.parent().unwrap_or_else(|| {
199                        log::warn!(
200                            "unexpected configuration path `{}` (unable to extract parent path)",
201                            config.display()
202                        );
203                        config
204                    })
205                })
206            })
207    }
208
209    fn do_work(&mut self, work: WorkItem) -> DarkluaResult<Option<WorkItem>> {
210        let (status, data) = work.extract();
211        match status {
212            WorkStatus::NotStarted => {
213                let source_display = data.source().display();
214
215                let source = data.source();
216                let content = self.resources.get(source)?;
217
218                let parser = self.configuration.build_parser();
219
220                log::debug!("beginning work on `{}`", source_display);
221
222                let parser_timer = Timer::now();
223
224                let mut block = parser
225                    .parse(&content)
226                    .map_err(|parser_error| DarkluaError::parser_error(source, parser_error))?;
227
228                let parser_time = parser_timer.duration_label();
229                log::debug!("parsed `{}` in {}", source_display, parser_time);
230
231                self.bundle(&mut block, source, &content)?;
232
233                self.apply_rules(data, WorkProgress::new(content, block))
234            }
235            WorkStatus::InProgress(progress) => self.apply_rules(data, *progress),
236        }
237    }
238
239    fn apply_rules(
240        &mut self,
241        data: WorkData,
242        progress: WorkProgress,
243    ) -> DarkluaResult<Option<WorkItem>> {
244        let (content, mut progress) = progress.extract();
245
246        let source_display = data.source().display();
247        let normalized_source = normalize_path(data.source());
248
249        progress.duration().start();
250
251        for (index, rule) in self
252            .configuration
253            .rules()
254            .enumerate()
255            .skip(progress.next_rule())
256        {
257            let mut context_builder = self.create_rule_context(data.source(), &content);
258            log::trace!(
259                "[{}] apply rule `{}`{}",
260                source_display,
261                rule.get_name(),
262                if rule.has_properties() {
263                    format!(" {:?}", rule.serialize_to_properties())
264                } else {
265                    "".to_owned()
266                }
267            );
268            let mut required_content: Vec<_> = rule
269                .require_content(&normalized_source, progress.block())
270                .into_iter()
271                .map(normalize_path)
272                .filter(|path| {
273                    if *path == normalized_source {
274                        log::debug!("filtering out currently processing path");
275                        false
276                    } else {
277                        true
278                    }
279                })
280                .collect();
281            required_content.sort();
282            required_content.dedup();
283
284            if !required_content.is_empty() {
285                if required_content
286                    .iter()
287                    .all(|path| self.cache.contains(path))
288                {
289                    let parser = self.configuration.build_parser();
290                    for path in required_content.iter() {
291                        let block = self.cache.get_block(path, &parser)?;
292                        context_builder.insert_block(path, block);
293                    }
294                } else {
295                    progress.duration().pause();
296                    log::trace!(
297                        "queue work for `{}` at rule `{}` (#{}) because it requires:{}",
298                        source_display,
299                        rule.get_name(),
300                        index,
301                        if required_content.len() == 1 {
302                            format!(" {}", required_content.first().unwrap().display())
303                        } else {
304                            format!(
305                                "\n- {}",
306                                required_content
307                                    .iter()
308                                    .map(|path| format!("- {}", path.display()))
309                                    .collect::<Vec<_>>()
310                                    .join("\n")
311                            )
312                        }
313                    );
314                    return Ok(Some(
315                        data.with_status(
316                            progress
317                                .at_rule(index)
318                                .with_required_content(required_content)
319                                .with_content(content),
320                        ),
321                    ));
322                }
323            }
324
325            let context = context_builder.build();
326            let block = progress.mutate_block();
327            let rule_timer = Timer::now();
328            rule.process(block, &context).map_err(|rule_error| {
329                let error = DarkluaError::rule_error(data.source(), rule, index, rule_error);
330
331                log::trace!(
332                    "[{}] rule `{}` errored: {}",
333                    source_display,
334                    rule.get_name(),
335                    error
336                );
337
338                error
339            })?;
340            let rule_duration = rule_timer.duration_label();
341            log::trace!(
342                "[{}] ⨽completed `{}` in {}",
343                source_display,
344                rule.get_name(),
345                rule_duration
346            );
347        }
348
349        let rule_time = progress.duration().duration_label();
350        let total_rules = self.configuration.rules_len();
351        log::debug!(
352            "{} rule{} applied in {} for `{}`",
353            total_rules,
354            maybe_plural(total_rules),
355            rule_time,
356            source_display,
357        );
358
359        log::trace!("begin generating code for `{}`", source_display);
360
361        if cfg!(test) || (cfg!(debug_assertions) && log::log_enabled!(log::Level::Trace)) {
362            log::trace!(
363                "generate AST debugging view at `{}`",
364                data.output().display()
365            );
366            self.resources
367                .write(data.output(), &format!("{:#?}", progress.block()))?;
368        }
369
370        let generator_timer = Timer::now();
371
372        let lua_code = self.configuration.generate_lua(progress.block(), &content);
373
374        let generator_time = generator_timer.duration_label();
375        log::debug!(
376            "generated code for `{}` in {}",
377            source_display,
378            generator_time,
379        );
380
381        self.resources.write(data.output(), &lua_code)?;
382
383        self.cache
384            .link_source_to_output(normalized_source, data.output());
385
386        Ok(None)
387    }
388
389    fn create_rule_context<'block, 'src>(
390        &self,
391        source: &Path,
392        original_code: &'src str,
393    ) -> ContextBuilder<'block, 'a, 'src> {
394        let builder = ContextBuilder::new(normalize_path(source), self.resources, original_code);
395        if let Some(project_location) = self.configuration.location() {
396            builder.with_project_location(project_location)
397        } else {
398            builder
399        }
400    }
401
402    fn bundle(
403        &mut self,
404        block: &mut Block,
405        source: &Path,
406        original_code: &str,
407    ) -> DarkluaResult<()> {
408        if self.cached_bundler.is_none() {
409            if let Some(bundler) = self.configuration.bundle() {
410                self.cached_bundler = Some(bundler);
411            }
412        }
413        let bundler = match self.cached_bundler.as_ref() {
414            Some(bundler) => bundler,
415            None => return Ok(()),
416        };
417
418        log::debug!("beginning bundling from `{}`", source.display());
419
420        let bundle_timer = Timer::now();
421
422        let context = self.create_rule_context(source, original_code).build();
423
424        bundler.process(block, &context).map_err(|rule_error| {
425            let error = DarkluaError::orphan_rule_error(source, bundler, rule_error);
426
427            log::trace!(
428                "[{}] rule `{}` errored: {}",
429                source.display(),
430                bundler.get_name(),
431                error
432            );
433
434            error
435        })?;
436
437        let bundle_time = bundle_timer.duration_label();
438        log::debug!("bundled `{}` in {}", source.display(), bundle_time);
439
440        Ok(())
441    }
442}
443
444#[inline]
445fn element_to_vec<T>(element: impl Into<T>) -> Vec<T> {
446    vec![element.into()]
447}