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 group_local;
19mod inject_value;
20mod method_def;
21mod no_local_function;
22mod remove_assertions;
23mod remove_call_match;
24mod remove_comments;
25mod remove_compound_assign;
26mod remove_continue;
27mod remove_debug_profiling;
28mod remove_floor_division;
29mod remove_if_expression;
30mod remove_interpolated_string;
31mod remove_method_call;
32mod remove_nil_declarations;
33mod remove_spaces;
34mod remove_types;
35mod remove_unused_variable;
36mod rename_variables;
37mod replace_referenced_tokens;
38pub(crate) mod require;
39mod rule_property;
40mod shift_token_line;
41mod unused_if_branch;
42mod unused_while;
43
44pub use append_text_comment::*;
45pub use call_parens::*;
46pub use compute_expression::*;
47pub use configuration_error::RuleConfigurationError;
48pub use convert_index_to_field::*;
49pub use convert_luau_number::*;
50pub use convert_require::*;
51pub use convert_square_root_call::*;
52pub use empty_do::*;
53pub use filter_early_return::*;
54pub use group_local::*;
55pub use inject_value::*;
56pub use method_def::*;
57pub use no_local_function::*;
58pub use remove_assertions::*;
59pub use remove_comments::*;
60pub use remove_compound_assign::*;
61pub use remove_continue::*;
62pub use remove_debug_profiling::*;
63pub use remove_floor_division::*;
64pub use remove_if_expression::*;
65pub use remove_interpolated_string::*;
66pub use remove_method_call::*;
67pub use remove_nil_declarations::*;
68pub use remove_spaces::*;
69pub use remove_types::*;
70pub use remove_unused_variable::*;
71pub use rename_variables::*;
72pub(crate) use replace_referenced_tokens::*;
73pub use require::PathRequireMode;
74pub use rule_property::*;
75pub(crate) use shift_token_line::*;
76pub use unused_if_branch::*;
77pub use unused_while::*;
78
79use crate::nodes::Block;
80use crate::Resources;
81
82use serde::de::{self, MapAccess, Visitor};
83use serde::ser::SerializeMap;
84use serde::{Deserialize, Deserializer, Serialize, Serializer};
85use std::collections::HashMap;
86use std::fmt;
87use std::path::{Path, PathBuf};
88use std::str::FromStr;
89
90#[derive(Debug, Clone)]
95pub struct ContextBuilder<'a, 'resources, 'code> {
96 path: PathBuf,
97 resources: &'resources Resources,
98 original_code: &'code str,
99 blocks: HashMap<PathBuf, &'a Block>,
100 project_location: Option<PathBuf>,
101}
102
103impl<'a, 'resources, 'code> ContextBuilder<'a, 'resources, 'code> {
104 pub fn new(
106 path: impl Into<PathBuf>,
107 resources: &'resources Resources,
108 original_code: &'code str,
109 ) -> Self {
110 Self {
111 path: path.into(),
112 resources,
113 original_code,
114 blocks: Default::default(),
115 project_location: None,
116 }
117 }
118
119 pub fn with_project_location(mut self, path: impl Into<PathBuf>) -> Self {
121 self.project_location = Some(path.into());
122 self
123 }
124
125 pub fn build(self) -> Context<'a, 'resources, 'code> {
127 Context {
128 path: self.path,
129 resources: self.resources,
130 original_code: self.original_code,
131 blocks: self.blocks,
132 project_location: self.project_location,
133 dependencies: Default::default(),
134 }
135 }
136
137 pub fn insert_block<'block: 'a>(&mut self, path: impl Into<PathBuf>, block: &'block Block) {
139 self.blocks.insert(path.into(), block);
140 }
141}
142
143#[derive(Debug, Clone)]
148pub struct Context<'a, 'resources, 'code> {
149 path: PathBuf,
150 resources: &'resources Resources,
151 original_code: &'code str,
152 blocks: HashMap<PathBuf, &'a Block>,
153 project_location: Option<PathBuf>,
154 dependencies: std::cell::RefCell<Vec<PathBuf>>,
155}
156
157impl Context<'_, '_, '_> {
158 pub fn block(&self, path: impl AsRef<Path>) -> Option<&Block> {
160 self.blocks.get(path.as_ref()).copied()
161 }
162
163 pub fn current_path(&self) -> &Path {
165 self.path.as_ref()
166 }
167
168 pub fn add_file_dependency(&self, path: PathBuf) {
172 if let Ok(mut dependencies) = self.dependencies.try_borrow_mut() {
173 log::trace!("add file dependency {}", path.display());
174 dependencies.push(path);
175 } else {
176 log::warn!("unable to submit file dependency (internal error)");
177 }
178 }
179
180 pub fn into_dependencies(self) -> impl Iterator<Item = PathBuf> {
182 self.dependencies.into_inner().into_iter()
183 }
184
185 fn resources(&self) -> &Resources {
186 self.resources
187 }
188
189 fn original_code(&self) -> &str {
190 self.original_code
191 }
192
193 fn project_location(&self) -> &Path {
194 self.project_location.as_deref().unwrap_or_else(|| {
195 let source = self.current_path();
196 source.parent().unwrap_or_else(|| {
197 log::warn!(
198 "unexpected file path `{}` (unable to extract parent path)",
199 source.display()
200 );
201 source
202 })
203 })
204 }
205}
206
207pub type RuleProcessResult = Result<(), String>;
209
210pub trait Rule: RuleConfiguration + fmt::Debug {
215 fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult;
219
220 fn require_content(&self, _current_source: &Path, _current_block: &Block) -> Vec<PathBuf> {
224 Vec::new()
225 }
226}
227
228pub trait RuleConfiguration {
233 fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError>;
237
238 fn get_name(&self) -> &'static str;
240
241 fn serialize_to_properties(&self) -> RuleProperties;
245
246 fn has_properties(&self) -> bool {
248 !self.serialize_to_properties().is_empty()
249 }
250}
251
252pub trait FlawlessRule {
257 fn flawless_process(&self, block: &mut Block, context: &Context);
259}
260
261impl<T: FlawlessRule + RuleConfiguration + fmt::Debug> Rule for T {
262 fn process(&self, block: &mut Block, context: &Context) -> RuleProcessResult {
263 self.flawless_process(block, context);
264 Ok(())
265 }
266}
267
268pub fn get_default_rules() -> Vec<Box<dyn Rule>> {
273 vec![
274 Box::<RemoveSpaces>::default(),
275 Box::<RemoveComments>::default(),
276 Box::<ComputeExpression>::default(),
277 Box::<RemoveUnusedIfBranch>::default(),
278 Box::<RemoveUnusedWhile>::default(),
279 Box::<FilterAfterEarlyReturn>::default(),
280 Box::<RemoveEmptyDo>::default(),
281 Box::<RemoveUnusedVariable>::default(),
282 Box::<RemoveMethodDefinition>::default(),
283 Box::<ConvertIndexToField>::default(),
284 Box::<RemoveNilDeclaration>::default(),
285 Box::<RenameVariables>::default(),
286 Box::<RemoveFunctionCallParens>::default(),
287 ]
288}
289
290pub fn get_all_rule_names() -> Vec<&'static str> {
294 vec![
295 APPEND_TEXT_COMMENT_RULE_NAME,
296 COMPUTE_EXPRESSIONS_RULE_NAME,
297 CONVERT_INDEX_TO_FIELD_RULE_NAME,
298 CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME,
299 CONVERT_LUAU_NUMBER_RULE_NAME,
300 CONVERT_REQUIRE_RULE_NAME,
301 CONVERT_SQUARE_ROOT_CALL_RULE_NAME,
302 FILTER_AFTER_EARLY_RETURN_RULE_NAME,
303 GROUP_LOCAL_ASSIGNMENT_RULE_NAME,
304 INJECT_GLOBAL_VALUE_RULE_NAME,
305 REMOVE_ASSERTIONS_RULE_NAME,
306 REMOVE_COMMENTS_RULE_NAME,
307 REMOVE_COMPOUND_ASSIGNMENT_RULE_NAME,
308 REMOVE_DEBUG_PROFILING_RULE_NAME,
309 REMOVE_EMPTY_DO_RULE_NAME,
310 REMOVE_FUNCTION_CALL_PARENS_RULE_NAME,
311 REMOVE_INTERPOLATED_STRING_RULE_NAME,
312 REMOVE_METHOD_CALL_RULE_NAME,
313 REMOVE_METHOD_DEFINITION_RULE_NAME,
314 REMOVE_NIL_DECLARATION_RULE_NAME,
315 REMOVE_SPACES_RULE_NAME,
316 REMOVE_TYPES_RULE_NAME,
317 REMOVE_UNUSED_IF_BRANCH_RULE_NAME,
318 REMOVE_UNUSED_VARIABLE_RULE_NAME,
319 REMOVE_UNUSED_WHILE_RULE_NAME,
320 RENAME_VARIABLES_RULE_NAME,
321 REMOVE_IF_EXPRESSION_RULE_NAME,
322 REMOVE_CONTINUE_RULE_NAME,
323 ]
324}
325
326impl FromStr for Box<dyn Rule> {
327 type Err = String;
328
329 fn from_str(string: &str) -> Result<Self, Self::Err> {
330 let rule: Box<dyn Rule> = match string {
331 APPEND_TEXT_COMMENT_RULE_NAME => Box::<AppendTextComment>::default(),
332 COMPUTE_EXPRESSIONS_RULE_NAME => Box::<ComputeExpression>::default(),
333 CONVERT_INDEX_TO_FIELD_RULE_NAME => Box::<ConvertIndexToField>::default(),
334 CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME => {
335 Box::<ConvertLocalFunctionToAssign>::default()
336 }
337 CONVERT_LUAU_NUMBER_RULE_NAME => Box::<ConvertLuauNumber>::default(),
338 CONVERT_REQUIRE_RULE_NAME => Box::<ConvertRequire>::default(),
339 CONVERT_SQUARE_ROOT_CALL_RULE_NAME => Box::<ConvertSquareRootCall>::default(),
340 FILTER_AFTER_EARLY_RETURN_RULE_NAME => Box::<FilterAfterEarlyReturn>::default(),
341 GROUP_LOCAL_ASSIGNMENT_RULE_NAME => Box::<GroupLocalAssignment>::default(),
342 INJECT_GLOBAL_VALUE_RULE_NAME => Box::<InjectGlobalValue>::default(),
343 REMOVE_ASSERTIONS_RULE_NAME => Box::<RemoveAssertions>::default(),
344 REMOVE_COMMENTS_RULE_NAME => Box::<RemoveComments>::default(),
345 REMOVE_COMPOUND_ASSIGNMENT_RULE_NAME => Box::<RemoveCompoundAssignment>::default(),
346 REMOVE_DEBUG_PROFILING_RULE_NAME => Box::<RemoveDebugProfiling>::default(),
347 REMOVE_EMPTY_DO_RULE_NAME => Box::<RemoveEmptyDo>::default(),
348 REMOVE_FLOOR_DIVISION_RULE_NAME => Box::<RemoveFloorDivision>::default(),
349 REMOVE_FUNCTION_CALL_PARENS_RULE_NAME => Box::<RemoveFunctionCallParens>::default(),
350 REMOVE_INTERPOLATED_STRING_RULE_NAME => Box::<RemoveInterpolatedString>::default(),
351 REMOVE_METHOD_CALL_RULE_NAME => Box::<RemoveMethodCall>::default(),
352 REMOVE_METHOD_DEFINITION_RULE_NAME => Box::<RemoveMethodDefinition>::default(),
353 REMOVE_NIL_DECLARATION_RULE_NAME => Box::<RemoveNilDeclaration>::default(),
354 REMOVE_SPACES_RULE_NAME => Box::<RemoveSpaces>::default(),
355 REMOVE_TYPES_RULE_NAME => Box::<RemoveTypes>::default(),
356 REMOVE_UNUSED_IF_BRANCH_RULE_NAME => Box::<RemoveUnusedIfBranch>::default(),
357 REMOVE_UNUSED_VARIABLE_RULE_NAME => Box::<RemoveUnusedVariable>::default(),
358 REMOVE_UNUSED_WHILE_RULE_NAME => Box::<RemoveUnusedWhile>::default(),
359 RENAME_VARIABLES_RULE_NAME => Box::<RenameVariables>::default(),
360 REMOVE_IF_EXPRESSION_RULE_NAME => Box::<RemoveIfExpression>::default(),
361 REMOVE_CONTINUE_RULE_NAME => Box::<RemoveContinue>::default(),
362 _ => return Err(format!("invalid rule name: {}", string)),
363 };
364
365 Ok(rule)
366 }
367}
368
369impl Serialize for dyn Rule {
370 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
371 let properties = self.serialize_to_properties();
372 let property_count = properties.len();
373 let rule_name = self.get_name();
374
375 if property_count == 0 {
376 serializer.serialize_str(rule_name)
377 } else {
378 let mut map = serializer.serialize_map(Some(property_count + 1))?;
379
380 map.serialize_entry("rule", rule_name)?;
381
382 let mut ordered: Vec<(String, RulePropertyValue)> = properties.into_iter().collect();
383
384 ordered.sort_by(|a, b| a.0.cmp(&b.0));
385
386 for (key, value) in ordered {
387 map.serialize_entry(&key, &value)?;
388 }
389
390 map.end()
391 }
392 }
393}
394
395impl<'de> Deserialize<'de> for Box<dyn Rule> {
396 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Box<dyn Rule>, D::Error> {
397 struct StringOrStruct;
398
399 impl<'de> Visitor<'de> for StringOrStruct {
400 type Value = Box<dyn Rule>;
401
402 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
403 formatter.write_str("rule name or rule object")
404 }
405
406 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
407 where
408 E: de::Error,
409 {
410 let mut rule: Self::Value = FromStr::from_str(value).map_err(de::Error::custom)?;
411
412 rule.configure(RuleProperties::new())
413 .map_err(de::Error::custom)?;
414
415 Ok(rule)
416 }
417
418 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
419 where
420 M: MapAccess<'de>,
421 {
422 let mut rule_name = None;
423 let mut properties = HashMap::new();
424
425 while let Some(key) = map.next_key::<String>()? {
426 match key.as_str() {
427 "rule" => {
428 if rule_name.is_none() {
429 rule_name.replace(map.next_value::<String>()?);
430 } else {
431 return Err(de::Error::duplicate_field("rule"));
432 }
433 }
434 property => {
435 let value = map.next_value::<RulePropertyValue>()?;
436
437 if properties.insert(property.to_owned(), value).is_some() {
438 return Err(de::Error::custom(format!(
439 "duplicate field {} in rule object",
440 property
441 )));
442 }
443 }
444 }
445 }
446
447 if let Some(rule_name) = rule_name {
448 let mut rule: Self::Value =
449 FromStr::from_str(&rule_name).map_err(de::Error::custom)?;
450
451 rule.configure(properties).map_err(de::Error::custom)?;
452
453 Ok(rule)
454 } else {
455 Err(de::Error::missing_field("rule"))
456 }
457 }
458 }
459
460 deserializer.deserialize_any(StringOrStruct)
461 }
462}
463
464fn verify_no_rule_properties(properties: &RuleProperties) -> Result<(), RuleConfigurationError> {
465 if let Some((key, _value)) = properties.iter().next() {
466 return Err(RuleConfigurationError::UnexpectedProperty(key.to_owned()));
467 }
468 Ok(())
469}
470
471fn verify_required_properties(
472 properties: &RuleProperties,
473 names: &[&str],
474) -> Result<(), RuleConfigurationError> {
475 for name in names.iter() {
476 if !properties.contains_key(*name) {
477 return Err(RuleConfigurationError::MissingProperty(name.to_string()));
478 }
479 }
480 Ok(())
481}
482
483fn verify_required_any_properties(
484 properties: &RuleProperties,
485 names: &[&str],
486) -> Result<(), RuleConfigurationError> {
487 if names.iter().any(|name| properties.contains_key(*name)) {
488 Ok(())
489 } else {
490 Err(RuleConfigurationError::MissingAnyProperty(
491 names.iter().map(ToString::to_string).collect(),
492 ))
493 }
494}
495
496fn verify_property_collisions(
497 properties: &RuleProperties,
498 names: &[&str],
499) -> Result<(), RuleConfigurationError> {
500 let mut exists: Option<&str> = None;
501 for name in names.iter() {
502 if properties.contains_key(*name) {
503 if let Some(existing_name) = &exists {
504 return Err(RuleConfigurationError::PropertyCollision(vec![
505 existing_name.to_string(),
506 name.to_string(),
507 ]));
508 } else {
509 exists = Some(*name);
510 }
511 }
512 }
513 Ok(())
514}
515
516#[cfg(test)]
517mod test {
518 use super::*;
519
520 use insta::assert_json_snapshot;
521
522 #[test]
523 fn snapshot_default_rules() {
524 let rules = get_default_rules();
525
526 assert_json_snapshot!("default_rules", rules);
527 }
528
529 #[test]
530 fn snapshot_all_rules() {
531 let rule_names = get_all_rule_names();
532
533 assert_json_snapshot!("all_rule_names", rule_names);
534 }
535
536 #[test]
537 fn verify_no_rule_properties_is_ok_when_empty() {
538 let empty_properties = RuleProperties::default();
539
540 assert_eq!(verify_no_rule_properties(&empty_properties), Ok(()));
541 }
542
543 #[test]
544 fn verify_no_rule_properties_is_unexpected_rule_err() {
545 let mut properties = RuleProperties::default();
546 let some_rule_name = "rule name";
547 properties.insert(some_rule_name.to_owned(), RulePropertyValue::None);
548
549 assert_eq!(
550 verify_no_rule_properties(&properties),
551 Err(RuleConfigurationError::UnexpectedProperty(
552 some_rule_name.to_owned()
553 ))
554 );
555 }
556
557 #[test]
558 fn get_all_rule_names_are_deserializable() {
559 for name in get_all_rule_names() {
560 let rule: Result<Box<dyn Rule>, _> = name.parse();
561 assert!(rule.is_ok(), "unable to deserialize `{}`", name);
562 }
563 }
564
565 #[test]
566 fn get_all_rule_names_are_serializable() {
567 for name in get_all_rule_names() {
568 let rule: Box<dyn Rule> = name
569 .parse()
570 .unwrap_or_else(|_| panic!("unable to deserialize `{}`", name));
571 assert!(json5::to_string(&rule).is_ok());
572 }
573 }
574}