Repository: seaofvoices/darklua (rust)

Issue:
Strip Luau types
Once darklua is able to process Luau, create a rule that strips the type information.

Related to #40

Pull request:
Add rule to remove Luau types
Closes #34

- [x] add entry to the changelog


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f51f6c0..7ecd7e2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+* add rule to remove Luau types ([#130](https://github.com/seaofvoices/darklua/pull/130))
 * add support for Luau types ([#129](https://github.com/seaofvoices/darklua/pull/129))
 
 ## 0.10.3
diff --git a/site/content/rules/remove_types.md b/site/content/rules/remove_types.md
new file mode 100644
index 0000000..1a0b9b7
--- /dev/null
+++ b/site/content/rules/remove_types.md
@@ -0,0 +1,19 @@
+---
+description: Removes types
+added_in: "unreleased"
+parameters: []
+examples:
+  - content: "local var: number? = nil"
+  - content: "type Array<T> = { T }"
+  - content: "return value :: string"
+  - content: |
+      local function getAverage(array: { string }): number
+          local sum: number = 0
+          for _, element: number in array do
+              sum += tonumber(element) :: number
+          end
+          return sum / #array
+      end
+---
+
+This rule removes all Luau type declarations and annotations.
diff --git a/src/nodes/expressions/function.rs b/src/nodes/expressions/function.rs
index d1c0519..e4fd54b 100644
--- a/src/nodes/expressions/function.rs
+++ b/src/nodes/expressions/function.rs
@@ -184,6 +184,18 @@ impl FunctionExpression {
         !self.parameters.is_empty()
     }
 
+    pub fn clear_types(&mut self) {
+        self.return_type.take();
+        self.variadic_type.take();
+        self.generic_parameters.take();
+        for parameter in &mut self.parameters {
+            parameter.remove_type();
+        }
+        if let Some(tokens) = &mut self.tokens {
+            tokens.variable_arguments_colon.take();
+        }
+    }
+
     pub fn clear_comments(&mut self) {
         self.parameters
             .iter_mut()
diff --git a/src/nodes/statements/function.rs b/src/nodes/statements/function.rs
index 00432e7..e3a98c2 100644
--- a/src/nodes/statements/function.rs
+++ b/src/nodes/statements/function.rs
@@ -373,6 +373,18 @@ impl FunctionStatement {
         !self.parameters.is_empty()
     }
 
+    pub fn clear_types(&mut self) {
+        self.return_type.take();
+        self.variadic_type.take();
+        self.generic_parameters.take();
+        for parameter in &mut self.parameters {
+            parameter.remove_type();
+        }
+        if let Some(tokens) = &mut self.tokens {
+            tokens.variable_arguments_colon.take();
+        }
+    }
+
     pub fn clear_comments(&mut self) {
         self.name.clear_comments();
         self.parameters
diff --git a/src/nodes/statements/generic_for.rs b/src/nodes/statements/generic_for.rs
index 6816413..f8c644c 100644
--- a/src/nodes/statements/generic_for.rs
+++ b/src/nodes/statements/generic_for.rs
@@ -149,6 +149,12 @@ impl GenericForStatement {
         self.expressions.len()
     }
 
+    pub fn clear_types(&mut self) {
+        for identifier in &mut self.identifiers {
+            identifier.remove_type();
+        }
+    }
+
     pub fn clear_comments(&mut self) {
         if let Some(tokens) = &mut self.tokens {
             tokens.clear_comments();
diff --git a/src/nodes/statements/local_assign.rs b/src/nodes/statements/local_assign.rs
index 86d19a9..24b1b18 100644
--- a/src/nodes/statements/local_assign.rs
+++ b/src/nodes/statements/local_assign.rs
@@ -253,6 +253,12 @@ impl LocalAssignStatement {
         !self.values.is_empty()
     }
 
+    pub fn clear_types(&mut self) {
+        for variable in &mut self.variables {
+            variable.remove_type();
+        }
+    }
+
     pub fn clear_comments(&mut self) {
         self.variables
             .iter_mut()
diff --git a/src/nodes/statements/local_function.rs b/src/nodes/statements/local_function.rs
index 698c475..858f782 100644
--- a/src/nodes/statements/local_function.rs
+++ b/src/nodes/statements/local_function.rs
@@ -240,6 +240,18 @@ impl LocalFunctionStatement {
         self.parameters.len()
     }
 
+    pub fn clear_types(&mut self) {
+        self.return_type.take();
+        self.variadic_type.take();
+        self.generic_parameters.take();
+        for parameter in &mut self.parameters {
+            parameter.remove_type();
+        }
+        if let Some(tokens) = &mut self.tokens {
+            tokens.variable_arguments_colon.take();
+        }
+    }
+
     pub fn clear_comments(&mut self) {
         self.identifier.clear_comments();
         self.parameters
diff --git a/src/nodes/statements/numeric_for.rs b/src/nodes/statements/numeric_for.rs
index 6ebb7e7..cb26ff4 100644
--- a/src/nodes/statements/numeric_for.rs
+++ b/src/nodes/statements/numeric_for.rs
@@ -159,6 +159,10 @@ impl NumericForStatement {
         self.identifier = identifier.into();
     }
 
+    pub fn clear_types(&mut self) {
+        self.identifier.remove_type();
+    }
+
     pub fn clear_comments(&mut self) {
         self.identifier.clear_comments();
         if let Some(tokens) = &mut self.tokens {
diff --git a/src/nodes/typed_identifier.rs b/src/nodes/typed_identifier.rs
index 330d7dc..0333f41 100644
--- a/src/nodes/typed_identifier.rs
+++ b/src/nodes/typed_identifier.rs
@@ -56,7 +56,7 @@ impl TypedIdentifier {
         self.r#type.as_mut()
     }
 
-    pub fn remove_type(mut self) -> Option<Type> {
+    pub fn remove_type(&mut self) -> Option<Type> {
         self.token.take();
         self.r#type.take()
     }
diff --git a/src/rules/mod.rs b/src/rules/mod.rs
index 43e1d61..21f6138 100644
--- a/src/rules/mod.rs
+++ b/src/rules/mod.rs
@@ -16,6 +16,7 @@ mod remove_comments;
 mod remove_compound_assign;
 mod remove_nil_declarations;
 mod remove_spaces;
+mod remove_types;
 mod rename_variables;
 mod replace_referenced_tokens;
 pub(crate) mod require;
@@ -39,6 +40,7 @@ pub use remove_comments::*;
 pub use remove_compound_assign::*;
 pub use remove_nil_declarations::*;
 pub use remove_spaces::*;
+pub use remove_types::*;
 pub use rename_variables::*;
 pub(crate) use replace_referenced_tokens::*;
 pub use rule_property::*;
@@ -219,6 +221,7 @@ pub fn get_all_rule_names() -> Vec<&'static str> {
         REMOVE_METHOD_DEFINITION_RULE_NAME,
         REMOVE_NIL_DECLARATION_RULE_NAME,
         REMOVE_SPACES_RULE_NAME,
+        REMOVE_TYPES_RULE_NAME,
         REMOVE_UNUSED_IF_BRANCH_RULE_NAME,
         REMOVE_UNUSED_WHILE_RULE_NAME,
         RENAME_VARIABLES_RULE_NAME,
@@ -246,6 +249,7 @@ impl FromStr for Box<dyn Rule> {
             REMOVE_METHOD_DEFINITION_RULE_NAME => Box::<RemoveMethodDefinition>::default(),
             REMOVE_NIL_DECLARATION_RULE_NAME => Box::<RemoveNilDeclaration>::default(),
             REMOVE_SPACES_RULE_NAME => Box::<RemoveSpaces>::default(),
+            REMOVE_TYPES_RULE_NAME => Box::<RemoveTypes>::default(),
             REMOVE_UNUSED_IF_BRANCH_RULE_NAME => Box::<RemoveUnusedIfBranch>::default(),
             REMOVE_UNUSED_WHILE_RULE_NAME => Box::<RemoveUnusedWhile>::default(),
             RENAME_VARIABLES_RULE_NAME => Box::<RenameVariables>::default(),
diff --git a/src/rules/remove_types.rs b/src/rules/remove_types.rs
new file mode 100644
index 0000000..400d3af
--- /dev/null
+++ b/src/rules/remove_types.rs
@@ -0,0 +1,110 @@
+use crate::nodes::*;
+use crate::process::{DefaultVisitor, NodeProcessor, NodeVisitor};
+use crate::rules::{
+    Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
+};
+
+use super::verify_no_rule_properties;
+
+#[derive(Default)]
+struct RemoveTypesProcessor {}
+
+impl NodeProcessor for RemoveTypesProcessor {
+    fn process_block(&mut self, block: &mut Block) {
+        block.filter_statements(|statement| !matches!(statement, Statement::TypeDeclaration(_)));
+    }
+
+    fn process_local_assign_statement(&mut self, local_assign: &mut LocalAssignStatement) {
+        local_assign.clear_types();
+    }
+
+    fn process_numeric_for_statement(&mut self, numeric_for: &mut NumericForStatement) {
+        numeric_for.clear_types();
+    }
+
+    fn process_generic_for_statement(&mut self, generic_for: &mut GenericForStatement) {
+        generic_for.clear_types();
+    }
+
+    fn process_function_statement(&mut self, function: &mut FunctionStatement) {
+        function.clear_types();
+    }
+
+    fn process_local_function_statement(&mut self, function: &mut LocalFunctionStatement) {
+        function.clear_types();
+    }
+
+    fn process_function_expression(&mut self, function: &mut FunctionExpression) {
+        function.clear_types();
+    }
+
+    fn process_expression(&mut self, expression: &mut Expression) {
+        match expression {
+            Expression::TypeCast(type_cast) => {
+                *expression = type_cast.get_expression().clone();
+            }
+            Expression::Function(function) => {
+                function.clear_types();
+            }
+            _ => {}
+        }
+    }
+}
+
+pub const REMOVE_TYPES_RULE_NAME: &str = "remove_types";
+
+/// A rule that removes Luau types from all AST nodes.
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct RemoveTypes {}
+
+impl FlawlessRule for RemoveTypes {
+    fn flawless_process(&self, block: &mut Block, _: &Context) {
+        let mut processor = RemoveTypesProcessor::default();
+        DefaultVisitor::visit_block(block, &mut processor);
+    }
+}
+
+impl RuleConfiguration for RemoveTypes {
+    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
+        verify_no_rule_properties(&properties)?;
+        Ok(())
+    }
+
+    fn get_name(&self) -> &'static str {
+        REMOVE_TYPES_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() -> RemoveTypes {
+        RemoveTypes::default()
+    }
+
+    #[test]
+    fn serialize_default_rule() {
+        let rule: Box<dyn Rule> = Box::new(new_rule());
+
+        assert_json_snapshot!("default_remove_types", rule);
+    }
+
+    #[test]
+    fn configure_with_extra_field_error() {
+        let result = json5::from_str::<Box<dyn Rule>>(
+            r#"{
+            rule: 'remove_types',
+            prop: "something",
+        }"#,
+        );
+        pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
+    }
+}


Test patch:
diff --git a/src/rules/snapshots/darklua_core__rules__remove_types__test__default_remove_types.snap b/src/rules/snapshots/darklua_core__rules__remove_types__test__default_remove_types.snap
new file mode 100644
index 0000000..bb40cf0
--- /dev/null
+++ b/src/rules/snapshots/darklua_core__rules__remove_types__test__default_remove_types.snap
@@ -0,0 +1,5 @@
+---
+source: src/rules/remove_types.rs
+expression: rule
+---
+"remove_types"
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 9cc5276..65d28d5 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
@@ -17,6 +17,7 @@ expression: rule_names
   "remove_method_definition",
   "remove_nil_declaration",
   "remove_spaces",
+  "remove_types",
   "remove_unused_if_branch",
   "remove_unused_while",
   "rename_variables"
diff --git a/tests/rule_tests/mod.rs b/tests/rule_tests/mod.rs
index 6387991..2d3da53 100644
--- a/tests/rule_tests/mod.rs
+++ b/tests/rule_tests/mod.rs
@@ -223,6 +223,7 @@ mod remove_compound_assignment;
 mod remove_empty_do;
 mod remove_method_definition;
 mod remove_nil_declaration;
+mod remove_types;
 mod remove_unused_if_branch;
 mod remove_unused_while;
 mod rename_variables;
diff --git a/tests/rule_tests/remove_types.rs b/tests/rule_tests/remove_types.rs
new file mode 100644
index 0000000..27ecf21
--- /dev/null
+++ b/tests/rule_tests/remove_types.rs
@@ -0,0 +1,103 @@
+use std::time::Duration;
+
+use darklua_core::{
+    generator::{LuaGenerator, ReadableLuaGenerator},
+    nodes::Type,
+    process::{DefaultVisitor, NodeProcessor, NodeVisitor},
+    rules::{ContextBuilder, FlawlessRule, RemoveTypes, Rule},
+    Resources,
+};
+
+use crate::{
+    ast_fuzzer::{AstFuzzer, FuzzBudget},
+    utils,
+};
+
+test_rule!(
+    remove_types,
+    RemoveTypes::default(),
+    remove_type_declaration("type T = string | number") => "",
+    remove_exported_type_declaration("export type T = { Node }") => "",
+    remove_type_in_local_assign("local var: boolean = true") => "local var = true",
+    remove_type_in_numeric_for("for i: number=a, b do end") => "for i=a, b do end",
+    remove_types_in_generic_for("for k: string, v: boolean in pairs({}) do end")
+        => "for k, v in pairs({}) do end",
+    remove_type_cast("return value :: string") => "return value",
+    remove_types_in_local_function_param("local function foo(param: T) end")
+        => "local function foo(param) end",
+    remove_types_in_local_function_variadic_param("local function foo(...: string) end")
+        => "local function foo(...) end",
+    remove_types_in_local_function_return_type("local function foo(): boolean end")
+        => "local function foo() end",
+    remove_types_in_function_statement_param("function test(param: T) end")
+        => "function test(param) end",
+    remove_types_in_function_statement_variadic_param("function foo(...: string) end")
+        => "function foo(...) end",
+    remove_types_in_function_statement_return_type("function foo(): boolean end")
+        => "function foo() end",
+    remove_types_in_function_expression_param("return function(param: T) end")
+        => "return function(param) end",
+    remove_types_in_function_expression_variadic_param("return function(...: string) end")
+        => "return function(...) end",
+    remove_types_in_function_expression_return_type("return function(): boolean end")
+        => "return function() end",
+);
+
+#[test]
+fn deserialize_from_object_notation() {
+    json5::from_str::<Box<dyn Rule>>(
+        r#"{
+        rule: 'remove_types',
+    }"#,
+    )
+    .unwrap();
+}
+
+#[test]
+fn deserialize_from_string() {
+    json5::from_str::<Box<dyn Rule>>("'remove_types'").unwrap();
+}
+
+#[test]
+fn fuzz_bundle() {
+    struct HasTypeProcessor {
+        found_type: bool,
+    }
+
+    impl HasTypeProcessor {
+        fn new() -> Self {
+            Self { found_type: false }
+        }
+    }
+
+    impl NodeProcessor for HasTypeProcessor {
+        fn process_type(&mut self, _: &mut Type) {
+            if !self.found_type {
+                self.found_type = true;
+            }
+        }
+    }
+
+    utils::run_for_minimum_time(Duration::from_millis(250), || {
+        let fuzz_budget = FuzzBudget::new(20, 40).with_types(40);
+        let mut block = AstFuzzer::new(fuzz_budget).fuzz_block();
+
+        RemoveTypes::default().flawless_process(
+            &mut block,
+            &ContextBuilder::new("test.lua", &Resources::from_memory(), "").build(),
+        );
+
+        let mut generator = ReadableLuaGenerator::new(80);
+
+        generator.write_block(&block);
+
+        let block_content = generator.into_string();
+
+        let mut result_block = utils::parse_input(&block_content);
+
+        let mut processor = HasTypeProcessor::new();
+        DefaultVisitor::visit_block(&mut result_block, &mut processor);
+
+        assert!(!processor.found_type);
+    })
+}
diff --git a/tests/rules.rs b/tests/rules.rs
index cab1ca9..992ee0f 100644
--- a/tests/rules.rs
+++ b/tests/rules.rs
@@ -1,2 +1,3 @@
+mod ast_fuzzer;
 mod rule_tests;
 mod utils;