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