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}