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