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