diff --git a/Cargo.lock b/Cargo.lock index 63dbf806..3c301b3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -106,6 +112,19 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 1.0.0", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -311,6 +330,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cpufeatures" version = "0.2.14" @@ -422,6 +447,7 @@ version = "0.14.0" dependencies = [ "anstyle", "assert_cmd", + "blake3", "clap", "criterion", "ctrlc", @@ -429,6 +455,7 @@ dependencies = [ "elsa", "env_logger", "full_moon", + "hex", "include_dir", "insta", "json5", @@ -446,6 +473,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_yaml", + "strfmt", "tempfile", "toml", "tracing", @@ -692,6 +720,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "humantime" version = "2.1.0" @@ -1498,6 +1532,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strfmt" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index c1e4fc71..8e82859c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,13 @@ tracing = ["dep:tracing"] [dependencies] anstyle = "1.0.6" +blake3 = "1.5.4" clap = { version = "4.5.3", features = ["derive"] } durationfmt = "0.1.1" elsa = "1.10.0" env_logger = "0.11.3" full_moon = { version = "1.0.0", features = ["roblox"] } +hex = "0.4.3" json5 = "0.4.1" log = "0.4.21" pathdiff = "0.2.1" @@ -39,6 +41,7 @@ regex = "1.10.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.114" serde_yaml = "0.9.32" +strfmt = "0.2.4" toml = "0.8.11" tracing = { version = "0.1", optional = true } wax = "0.5.0" diff --git a/src/nodes/block.rs b/src/nodes/block.rs index 6306c331..8a73c43a 100644 --- a/src/nodes/block.rs +++ b/src/nodes/block.rs @@ -167,6 +167,11 @@ impl Block { self.statements.iter_mut() } + #[inline] + pub fn mutate_statements(&mut self) -> &mut Vec { + &mut self.statements + } + #[inline] pub fn first_statement(&self) -> Option<&Statement> { self.statements.first() diff --git a/src/nodes/statements/generic_for.rs b/src/nodes/statements/generic_for.rs index eac78872..76027420 100644 --- a/src/nodes/statements/generic_for.rs +++ b/src/nodes/statements/generic_for.rs @@ -94,6 +94,11 @@ impl GenericForStatement { self.expressions.iter_mut() } + #[inline] + pub fn mutate_expressions(&mut self) -> &mut Vec { + &mut self.expressions + } + #[inline] pub fn mutate_block(&mut self) -> &mut Block { &mut self.block diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 3390d87b..f5e5ce33 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -19,6 +19,7 @@ mod remove_comments; mod remove_compound_assign; mod remove_debug_profiling; mod remove_if_expression; +mod remove_generalized_iteration; mod remove_interpolated_string; mod remove_nil_declarations; mod remove_spaces; @@ -28,6 +29,7 @@ mod rename_variables; mod replace_referenced_tokens; pub(crate) mod require; mod rule_property; +pub mod runtime_identifier; mod shift_token_line; mod unused_if_branch; mod unused_while; @@ -49,6 +51,7 @@ pub use remove_comments::*; pub use remove_compound_assign::*; pub use remove_debug_profiling::*; pub use remove_if_expression::*; +pub use remove_generalized_iteration::*; pub use remove_interpolated_string::*; pub use remove_nil_declarations::*; pub use remove_spaces::*; @@ -216,6 +219,7 @@ pub fn get_default_rules() -> Vec> { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), ] } @@ -245,6 +249,7 @@ pub fn get_all_rule_names() -> Vec<&'static str> { REMOVE_UNUSED_WHILE_RULE_NAME, RENAME_VARIABLES_RULE_NAME, REMOVE_IF_EXPRESSION_RULE_NAME, + REMOVE_GENERALIZED_ITERATION_RULE_NAME, ] } @@ -279,6 +284,7 @@ impl FromStr for Box { REMOVE_UNUSED_WHILE_RULE_NAME => Box::::default(), RENAME_VARIABLES_RULE_NAME => Box::::default(), REMOVE_IF_EXPRESSION_RULE_NAME => Box::::default(), + REMOVE_GENERALIZED_ITERATION_RULE_NAME => Box::::default(), _ => return Err(format!("invalid rule name: {}", string)), }; diff --git a/src/rules/remove_generalized_iteration.rs b/src/rules/remove_generalized_iteration.rs new file mode 100644 index 00000000..7fbe9820 --- /dev/null +++ b/src/rules/remove_generalized_iteration.rs @@ -0,0 +1,253 @@ +use crate::nodes::{ + AssignStatement, BinaryExpression, BinaryOperator, Block, DoStatement, Expression, + FieldExpression, FunctionCall, Identifier, IfBranch, IfStatement, LocalAssignStatement, Prefix, + Statement, StringExpression, TupleArguments, TypedIdentifier, Variable, +}; +use crate::process::{DefaultVisitor, NodeProcessor, NodeVisitor}; +use crate::rules::{Context, RuleConfiguration, RuleConfigurationError, RuleProperties}; + +use super::runtime_identifier::RuntimeIdentifierBuilder; +use super::{Rule, RuleProcessResult}; + +const METATABLE_VARIABLE_NAME: &str = "m"; + +struct Processor { + iterator_identifier: String, + invariant_identifier: String, + control_identifier: String, + skip_block_once: bool, +} + +fn get_type_condition(arg: Expression, type_name: &str) -> Box { + let type_call = Box::new(FunctionCall::new( + Prefix::from_name("type"), + TupleArguments::new(vec![arg]).into(), + None, + )); + Box::new(BinaryExpression::new( + BinaryOperator::Equal, + Expression::Call(type_call), + Expression::String(StringExpression::from_value(type_name)), + )) +} + +impl Processor { + fn process_into_do(&self, block: &mut Block) -> Option<(usize, Statement)> { + let block_stmts = block.mutate_statements(); + for (i, stmt) in block_stmts.iter_mut().enumerate() { + if let Statement::GenericFor(generic_for) = stmt { + let exps = generic_for.mutate_expressions(); + if exps.len() == 1 { + let mut stmts: Vec = Vec::new(); + let iterator_typed_identifier = + TypedIdentifier::new(self.iterator_identifier.as_str()); + let iterator_identifier = iterator_typed_identifier.get_identifier().clone(); + + let invariant_typed_identifier = + TypedIdentifier::new(self.invariant_identifier.as_str()); + let invariant_identifier = invariant_typed_identifier.get_identifier().clone(); + + let control_typed_identifier = + TypedIdentifier::new(self.control_identifier.as_str()); + let control_identifier = control_typed_identifier.get_identifier().clone(); + + let iterator_local_assign = LocalAssignStatement::new( + vec![iterator_typed_identifier], + vec![exps[0].to_owned()], + ); + let invar_control_local_assign = LocalAssignStatement::new( + vec![invariant_typed_identifier, control_typed_identifier], + Vec::new(), + ); + + let iterator_exp = Expression::Identifier(iterator_identifier.clone()); + exps[0] = iterator_exp.clone(); + let invariant_exp = Expression::Identifier(invariant_identifier.clone()); + exps.push(invariant_exp); + let control_exp = Expression::Identifier(control_identifier.clone()); + exps.push(control_exp); + + let if_table_condition = get_type_condition(iterator_exp.clone(), "table"); + + let mt_typed_identifier = TypedIdentifier::new(METATABLE_VARIABLE_NAME); + let mt_identifier = mt_typed_identifier.get_identifier().clone(); + + let get_mt_call = FunctionCall::new( + Prefix::from_name("getmetatable"), + TupleArguments::new(vec![iterator_exp.clone()]).into(), + None, + ); + let mt_local_assign = LocalAssignStatement::new( + vec![mt_typed_identifier], + vec![get_mt_call.into()], + ); + + let if_mt_table_condition = + get_type_condition(mt_identifier.clone().into(), "table"); + let mt_iter = FieldExpression::new( + Prefix::Identifier(mt_identifier), + Identifier::new("__iter"), + ); + let if_mt_iter_function_condition = + get_type_condition(mt_iter.clone().into(), "function"); + + let mt_iter_call = FunctionCall::from_prefix(Prefix::Field(Box::new(mt_iter))); + let assign_from_iter = AssignStatement::new( + vec![ + Variable::Identifier(iterator_identifier.clone()), + Variable::Identifier(invariant_identifier.clone()), + Variable::Identifier(control_identifier.clone()), + ], + vec![mt_iter_call.into()], + ); + + let pairs_call = FunctionCall::new( + Prefix::from_name("pairs"), + TupleArguments::new(vec![iterator_identifier.clone().into()]).into(), + None, + ); + let assign_from_pairs = AssignStatement::new( + vec![ + Variable::Identifier(iterator_identifier), + Variable::Identifier(invariant_identifier), + Variable::Identifier(control_identifier), + ], + vec![pairs_call.into()], + ); + + let if_mt_table_block = Block::new(vec![assign_from_iter.into()], None); + let if_not_mt_table_block = Block::new(vec![assign_from_pairs.into()], None); + let if_mt_table_branch = IfBranch::new( + Expression::Binary(Box::new(BinaryExpression::new( + BinaryOperator::And, + Expression::Binary(if_mt_table_condition), + Expression::Binary(if_mt_iter_function_condition), + ))), + if_mt_table_block, + ); + let if_mt_table_stmt = + IfStatement::new(vec![if_mt_table_branch], Some(if_not_mt_table_block)); + + let if_table_block = + Block::new(vec![mt_local_assign.into(), if_mt_table_stmt.into()], None); + let if_table_branch = + IfBranch::new(Expression::Binary(if_table_condition), if_table_block); + let if_table_stmt = IfStatement::new(vec![if_table_branch], None); + + stmts.push(iterator_local_assign.into()); + stmts.push(invar_control_local_assign.into()); + stmts.push(if_table_stmt.into()); + stmts.push(generic_for.clone().into()); + + block_stmts.remove(i); + + return Some((i, DoStatement::new(Block::new(stmts, None)).into())); + } + } + } + None + } +} + +impl NodeProcessor for Processor { + fn process_block(&mut self, block: &mut Block) { + if self.skip_block_once { + self.skip_block_once = false; + return; + } + let do_stmt = self.process_into_do(block); + if let Some((i, stmt)) = do_stmt { + self.skip_block_once = true; + block.insert_statement(i, stmt); + } + } +} + +pub const REMOVE_GENERALIZED_ITERATION_RULE_NAME: &str = "remove_generalized_iteration"; + +/// A rule that removes generalized iteration. +#[derive(Debug, PartialEq, Eq)] +pub struct RemoveGeneralizedIteration { + runtime_identifier_format: String, +} + +impl Default for RemoveGeneralizedIteration { + fn default() -> Self { + Self { + runtime_identifier_format: "_DARKLUA_REMOVE_GENERALIZED_ITERATION_{name}{hash}" + .to_string(), + } + } +} + +impl Rule for RemoveGeneralizedIteration { + fn process(&self, block: &mut Block, _: &Context) -> RuleProcessResult { + let var_builder = RuntimeIdentifierBuilder::new( + self.runtime_identifier_format.as_str(), + format!("{block:?}").as_bytes(), + Some(vec![METATABLE_VARIABLE_NAME.to_string()]), + )?; + let mut processor = Processor { + iterator_identifier: var_builder.build("iter")?, + invariant_identifier: var_builder.build("invar")?, + control_identifier: var_builder.build("control")?, + skip_block_once: false, + }; + DefaultVisitor::visit_block(block, &mut processor); + Ok(()) + } +} + +impl RuleConfiguration for RemoveGeneralizedIteration { + fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> { + for (key, value) in properties { + match key.as_str() { + "runtime_identifier_format" => { + self.runtime_identifier_format = value.expect_string(&key)?; + } + _ => return Err(RuleConfigurationError::UnexpectedProperty(key)), + } + } + + Ok(()) + } + + fn get_name(&self) -> &'static str { + REMOVE_GENERALIZED_ITERATION_RULE_NAME + } + + fn serialize_to_properties(&self) -> RuleProperties { + RuleProperties::new() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::rules::Rule; + + use insta::assert_json_snapshot; + + fn new_rule() -> RemoveGeneralizedIteration { + RemoveGeneralizedIteration::default() + } + + #[test] + fn serialize_default_rule() { + let rule: Box = Box::new(new_rule()); + + assert_json_snapshot!("default_remove_generalized_iteration", rule); + } + + #[test] + fn configure_with_extra_field_error() { + let result = json5::from_str::>( + r#"{ + rule: 'remove_generalized_iteration', + runtime_identifier_format: '{name}', + prop: "something", + }"#, + ); + pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'"); + } +} diff --git a/src/rules/runtime_identifier.rs b/src/rules/runtime_identifier.rs new file mode 100644 index 00000000..8bfe5504 --- /dev/null +++ b/src/rules/runtime_identifier.rs @@ -0,0 +1,45 @@ +use blake3; +use hex; +use std::collections::HashMap; +use strfmt::strfmt; + +pub struct RuntimeIdentifierBuilder { + format: String, + hash: String, + keywords: Option>, +} + +impl RuntimeIdentifierBuilder { + pub fn new( + format: impl Into, + identifier: &[u8], + keywords: Option>, + ) -> Result { + let format: String = format.into(); + if !format.as_str().contains("{name}") { + return Err("`name` field is required for runtime identifier".to_string()); + } + let hash = blake3::hash(identifier); + Ok(Self { + format, + hash: hex::encode(&hash.as_bytes()[..8]), + keywords, + }) + } + + pub fn build(&self, name: &str) -> Result { + let mut vars = HashMap::new(); + vars.insert("name".to_owned(), name); + vars.insert("hash".to_owned(), self.hash.as_str()); + + let name = strfmt(&self.format, &vars).map_err(|err| err.to_string())?; + + if let Some(keywords) = &self.keywords { + if keywords.contains(&name) { + Err(format!("Runtime variable `{name}` cannot be set because it contains a reserved keyword."))?; + } + } + + Ok(name) + } +} diff --git a/src/rules/snapshots/darklua_core__rules__remove_generalized_iteration__test__default_remove_generalized_iteration.snap b/src/rules/snapshots/darklua_core__rules__remove_generalized_iteration__test__default_remove_generalized_iteration.snap new file mode 100644 index 00000000..6926519f --- /dev/null +++ b/src/rules/snapshots/darklua_core__rules__remove_generalized_iteration__test__default_remove_generalized_iteration.snap @@ -0,0 +1,5 @@ +--- +source: src/rules/remove_generalized_iteration.rs +expression: rule +--- +"remove_generalized_iteration" diff --git a/src/rules/snapshots/darklua_core__rules__test__all_rule_names.snap b/src/rules/snapshots/darklua_core__rules__test__all_rule_names.snap index b940d30f..6ab1a57f 100644 --- a/src/rules/snapshots/darklua_core__rules__test__all_rule_names.snap +++ b/src/rules/snapshots/darklua_core__rules__test__all_rule_names.snap @@ -26,5 +26,6 @@ expression: rule_names "remove_unused_variable", "remove_unused_while", "rename_variables", - "remove_if_expression" + "remove_if_expression", + "remove_generalized_iteration" ] diff --git a/src/rules/snapshots/darklua_core__rules__test__default_rules.snap b/src/rules/snapshots/darklua_core__rules__test__default_rules.snap index d87f8de3..df034bb0 100644 --- a/src/rules/snapshots/darklua_core__rules__test__default_rules.snap +++ b/src/rules/snapshots/darklua_core__rules__test__default_rules.snap @@ -15,5 +15,6 @@ expression: rules "convert_index_to_field", "remove_nil_declaration", "rename_variables", - "remove_function_call_parens" + "remove_function_call_parens", + "remove_generalized_iteration" ] diff --git a/tests/rule_tests/mod.rs b/tests/rule_tests/mod.rs index 8e39528d..04c305a5 100644 --- a/tests/rule_tests/mod.rs +++ b/tests/rule_tests/mod.rs @@ -305,6 +305,7 @@ mod remove_compound_assignment; mod remove_debug_profiling; mod remove_empty_do; mod remove_if_expression; +mod remove_generalized_iteration; mod remove_interpolated_string; mod remove_method_definition; mod remove_nil_declaration; diff --git a/tests/rule_tests/remove_generalized_iteration.rs b/tests/rule_tests/remove_generalized_iteration.rs new file mode 100644 index 00000000..f1abe753 --- /dev/null +++ b/tests/rule_tests/remove_generalized_iteration.rs @@ -0,0 +1,24 @@ +use darklua_core::rules::Rule; + +test_rule!( + remove_generalized_iteration, + json5::from_str::>( + r#"{ + rule: 'remove_generalized_iteration', + runtime_identifier_format: '{name}' + }"# + ).unwrap(), + generic_for("for i,v in {1,2,3} do end") + => "do local iter={1,2,3} local invar,control if type(iter)=='table' then local m=getmetatable(iter) if type(m)=='table' and type(m.__iter)=='function' then iter,invar,control=m.__iter() else iter,invar,control=pairs(iter) end end for i,v in iter,invar,control do end end" +); + +#[test] +fn deserialize_from_object_notation() { + json5::from_str::>( + r#"{ + rule: 'remove_generalized_iteration', + runtime_identifier_format: '{name}' + }"#, + ) + .unwrap(); +}