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