Skip to content

Commit efb7c63

Browse files
Add convert_function_to_assignment rule (#317)
1 parent d1972d8 commit efb7c63

File tree

7 files changed

+191
-0
lines changed

7 files changed

+191
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
* add `convert_function_to_assignment` rule ([#317](https://github.com/seaofvoices/darklua/pull/317))
34
* fix module types when bundling code ([#300](https://github.com/seaofvoices/darklua/pull/300))
45
* improve the `compute_expression` rule to compute the length of strings ([#316](https://github.com/seaofvoices/darklua/pull/316))
56
* improve `append_text_comment` rule to support multiple comments being defined in a single config (this also fix a bug in the code generator related to how multiline comments were written) ([#314](https://github.com/seaofvoices/darklua/pull/314))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
description: Convert global function definitions to assignment statements
3+
added_in: "unreleased"
4+
parameters: []
5+
examples:
6+
- content: |
7+
function foo(a, b)
8+
return a + b
9+
end
10+
- content: |
11+
function obj:method(value)
12+
return self.field + value
13+
end
14+
---
15+
16+
Global function declarations will be transformed into assignment statements. This rule handles simple global functions, functions with field access (`module.function`), and method definitions (`object:method`).
17+
18+
When converting method syntax (`:method`), the rule automatically adds `self` as the first parameter to maintain the same behavior.
19+
20+
Note that, depending on your Lua runtime implementation, you may no longer be able to use reflection-like APIs (eg `debug.info`) to acquire the name of the function, or the function name may be missing from stack traces of `error` invocations.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use crate::nodes::{
2+
AssignStatement, Block, FieldExpression, FunctionExpression, FunctionStatement, Identifier,
3+
Statement, Variable,
4+
};
5+
use crate::process::{DefaultVisitor, NodeProcessor, NodeVisitor};
6+
use crate::rules::{
7+
Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
8+
};
9+
10+
use serde::ser::{Serialize, Serializer};
11+
use std::mem;
12+
13+
use super::verify_no_rule_properties;
14+
15+
struct Processor;
16+
17+
impl Processor {
18+
fn convert(&self, function: &mut FunctionStatement) -> Statement {
19+
let mut function_expression = FunctionExpression::default();
20+
function_expression.set_variadic(function.is_variadic());
21+
mem::swap(function_expression.mutate_block(), function.mutate_block());
22+
mem::swap(
23+
function_expression.mutate_parameters(),
24+
function.mutate_parameters(),
25+
);
26+
27+
let name = function.get_name();
28+
29+
let base = name.get_name().clone();
30+
31+
let fields = name.get_field_names();
32+
33+
let variable = if fields.is_empty() {
34+
if let Some(method) = name.get_method() {
35+
Variable::from(FieldExpression::new(base, method.clone()))
36+
} else {
37+
Variable::from(base)
38+
}
39+
} else {
40+
let mut fields_iter = fields.iter().chain(name.get_method()).map(Clone::clone);
41+
let mut current = FieldExpression::new(base, fields_iter.next().unwrap());
42+
for field in fields_iter {
43+
current = FieldExpression::new(current, field.clone());
44+
}
45+
Variable::from(current)
46+
};
47+
48+
if name.has_method() {
49+
function_expression
50+
.mutate_parameters()
51+
.insert(0, Identifier::new("self").into());
52+
}
53+
54+
AssignStatement::from_variable(variable, function_expression).into()
55+
}
56+
}
57+
58+
impl NodeProcessor for Processor {
59+
fn process_statement(&mut self, statement: &mut Statement) {
60+
if let Statement::Function(function) = statement {
61+
let mut assign = self.convert(function);
62+
mem::swap(statement, &mut assign)
63+
};
64+
}
65+
}
66+
67+
pub const CONVERT_FUNCTION_TO_ASSIGNMENT_RULE_NAME: &str = "convert_function_to_assignment";
68+
69+
/// Convert function statements into regular assignments.
70+
#[derive(Debug, Default, PartialEq, Eq)]
71+
pub struct ConvertFunctionToAssign {}
72+
73+
impl FlawlessRule for ConvertFunctionToAssign {
74+
fn flawless_process(&self, block: &mut Block, _: &Context) {
75+
let mut processor = Processor;
76+
DefaultVisitor::visit_block(block, &mut processor);
77+
}
78+
}
79+
80+
impl RuleConfiguration for ConvertFunctionToAssign {
81+
fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
82+
verify_no_rule_properties(&properties)?;
83+
84+
Ok(())
85+
}
86+
87+
fn get_name(&self) -> &'static str {
88+
CONVERT_FUNCTION_TO_ASSIGNMENT_RULE_NAME
89+
}
90+
91+
fn serialize_to_properties(&self) -> RuleProperties {
92+
RuleProperties::new()
93+
}
94+
}
95+
96+
impl Serialize for ConvertFunctionToAssign {
97+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98+
serializer.serialize_str(CONVERT_FUNCTION_TO_ASSIGNMENT_RULE_NAME)
99+
}
100+
}
101+
102+
#[cfg(test)]
103+
mod test {
104+
use super::*;
105+
106+
use crate::rules::Rule;
107+
108+
use insta::assert_json_snapshot;
109+
110+
fn new_rule() -> ConvertFunctionToAssign {
111+
ConvertFunctionToAssign::default()
112+
}
113+
114+
#[test]
115+
fn serialize_default_rule() {
116+
assert_json_snapshot!(new_rule(), @r###""convert_function_to_assignment""###);
117+
}
118+
119+
#[test]
120+
fn configure_with_extra_field_error() {
121+
let result = json5::from_str::<Box<dyn Rule>>(
122+
r#"{
123+
rule: 'convert_function_to_assignment',
124+
prop: "something",
125+
}"#,
126+
);
127+
pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
128+
}
129+
}

src/rules/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod convert_require;
1515
mod convert_square_root_call;
1616
mod empty_do;
1717
mod filter_early_return;
18+
mod global_function_to_assign;
1819
mod group_local;
1920
mod inject_value;
2021
mod method_def;
@@ -51,6 +52,7 @@ pub use convert_require::*;
5152
pub use convert_square_root_call::*;
5253
pub use empty_do::*;
5354
pub use filter_early_return::*;
55+
pub use global_function_to_assign::*;
5456
pub use group_local::*;
5557
pub use inject_value::*;
5658
pub use method_def::*;
@@ -294,6 +296,7 @@ pub fn get_all_rule_names() -> Vec<&'static str> {
294296
vec![
295297
APPEND_TEXT_COMMENT_RULE_NAME,
296298
COMPUTE_EXPRESSIONS_RULE_NAME,
299+
CONVERT_FUNCTION_TO_ASSIGNMENT_RULE_NAME,
297300
CONVERT_INDEX_TO_FIELD_RULE_NAME,
298301
CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME,
299302
CONVERT_LUAU_NUMBER_RULE_NAME,
@@ -331,6 +334,7 @@ impl FromStr for Box<dyn Rule> {
331334
let rule: Box<dyn Rule> = match string {
332335
APPEND_TEXT_COMMENT_RULE_NAME => Box::<AppendTextComment>::default(),
333336
COMPUTE_EXPRESSIONS_RULE_NAME => Box::<ComputeExpression>::default(),
337+
CONVERT_FUNCTION_TO_ASSIGNMENT_RULE_NAME => Box::<ConvertFunctionToAssign>::default(),
334338
CONVERT_INDEX_TO_FIELD_RULE_NAME => Box::<ConvertIndexToField>::default(),
335339
CONVERT_LOCAL_FUNCTION_TO_ASSIGN_RULE_NAME => {
336340
Box::<ConvertLocalFunctionToAssign>::default()

src/rules/snapshots/darklua_core__rules__test__all_rule_names.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ expression: rule_names
55
[
66
"append_text_comment",
77
"compute_expression",
8+
"convert_function_to_assignment",
89
"convert_index_to_field",
910
"convert_local_function_to_assign",
1011
"convert_luau_number",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use darklua_core::rules::{ConvertFunctionToAssign, Rule};
2+
3+
test_rule!(
4+
convert_function_to_assignment,
5+
ConvertFunctionToAssign::default(),
6+
empty_function("function foo() end") => "foo = function() end",
7+
empty_function_with_arguments("function foo(a, b) end") => "foo = function(a, b) end",
8+
empty_variadic_function("function foo(...) end") => "foo = function(...) end",
9+
empty_variadic_function_with_arguments("function foo(a, b, c, ...) end") => "foo = function(a, b, c, ...) end",
10+
function_with_block("function foo() return true end") => "foo = function() return true end",
11+
function_with_field("function foo.bar() end") => "foo.bar = function() end",
12+
function_with_field_and_arguments("function foo.bar(a, b) end") => "foo.bar = function(a, b) end",
13+
function_with_nested_fields("function foo.bar.baz() end") => "foo.bar.baz = function() end",
14+
function_with_method("function foo:bar() end") => "foo.bar = function(self) end",
15+
function_with_method_and_arguments("function foo:bar(a, b) end") => "foo.bar = function(self, a, b) end",
16+
function_with_method_and_variadic("function foo:bar(...) end") => "foo.bar = function(self, ...) end",
17+
function_with_nested_fields_and_method("function foo.bar:baz() end") => "foo.bar.baz = function(self) end",
18+
function_with_body("function foo() local x = 1 return x end") => "foo = function() local x = 1 return x end",
19+
recursive_function("function foo() foo() end") => "foo = function() foo() end"
20+
);
21+
22+
#[test]
23+
fn deserialize_from_object_notation() {
24+
json5::from_str::<Box<dyn Rule>>(
25+
r#"{
26+
rule: 'convert_function_to_assignment',
27+
}"#,
28+
)
29+
.unwrap();
30+
}
31+
32+
#[test]
33+
fn deserialize_from_string() {
34+
json5::from_str::<Box<dyn Rule>>("'convert_function_to_assignment'").unwrap();
35+
}

tests/rule_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ mod convert_luau_number;
539539
mod convert_require;
540540
mod convert_square_root_call;
541541
mod filter_early_return;
542+
mod global_function_to_assign;
542543
mod group_local_assignment;
543544
mod inject_value;
544545
mod no_local_function;

0 commit comments

Comments
 (0)