1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! Tests for variable declarations (my/our/local/state) inside function call
//! argument lists.
use super::*;
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
/// Helper: parse code and return the full AST.
fn parse_program(code: &str) -> Node {
let mut parser = Parser::new(code);
parser.parse().unwrap_or_else(|e| panic!("Parse failed for `{code}`: {e:?}"))
}
/// Helper: assert that the AST sexp contains no ERROR nodes.
fn assert_no_errors(code: &str) {
let ast = parse_program(code);
let sexp = ast.to_sexp();
assert!(!sexp.contains("ERROR"), "Parse of `{code}` produced ERROR nodes: {sexp}",);
}
/// Helper: parse code and return the first statement node.
fn first_stmt(code: &str) -> Node {
let ast = parse_program(code);
match ast.kind {
NodeKind::Program { mut statements } if !statements.is_empty() => {
statements.swap_remove(0)
}
_ => panic!("Expected Program with statements, got: {}", ast.to_sexp()),
}
}
// ---------------------------------------------------------------
// Basic: `foo(my $x, $y)`
// ---------------------------------------------------------------
#[test]
fn my_scalar_in_call_args() {
let code = "foo(my $x, $y);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
// Sexp format uses `my_declaration` for VariableDeclaration with declarator "my"
assert!(sexp.contains("my_declaration"), "Expected my_declaration in sexp, got: {sexp}",);
}
// ---------------------------------------------------------------
// With initializer: `sort(my @arr = @data)`
// ---------------------------------------------------------------
#[test]
fn my_array_with_initializer_in_call_args() {
let code = "sort(my @arr = @data);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(sexp.contains("my_declaration"), "Expected my_declaration in sexp, got: {sexp}",);
}
// ---------------------------------------------------------------
// Multiple args with declaration: `push(my @arr, 1, 2, 3)`
// ---------------------------------------------------------------
#[test]
fn my_array_with_trailing_args() {
let code = "push(my @arr, 1, 2, 3);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(sexp.contains("my_declaration"), "Expected my_declaration in sexp, got: {sexp}",);
// Verify that 1, 2, 3 are separate args, not consumed into the declaration
assert!(
sexp.contains("1") && sexp.contains("2") && sexp.contains("3"),
"Expected numeric args 1, 2, 3 in sexp, got: {sexp}",
);
}
// ---------------------------------------------------------------
// our keyword: `print(our $fh, "hello")`
// ---------------------------------------------------------------
#[test]
fn our_scalar_in_call_args() {
let code = "print(our $fh, \"hello\");";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(sexp.contains("our_declaration"), "Expected our_declaration in sexp, got: {sexp}",);
}
// ---------------------------------------------------------------
// state keyword: `foo(state $count = 0)`
// ---------------------------------------------------------------
#[test]
fn state_scalar_with_initializer() {
let code = "foo(state $count = 0);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(
sexp.contains("state_declaration"),
"Expected state_declaration in sexp, got: {sexp}",
);
}
// ---------------------------------------------------------------
// local keyword: `foo(local $var)`
// ---------------------------------------------------------------
#[test]
fn local_scalar_in_call_args() {
let code = "foo(local $var);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(
sexp.contains("local_declaration"),
"Expected local_declaration in sexp, got: {sexp}",
);
}
#[test]
fn local_parenthesized_lvalue_in_call_args() {
let code = "foo(local($ENV{PATH}) = '/tmp/bin', $next);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(
sexp.contains("local_declaration"),
"Expected local_declaration in sexp, got: {sexp}",
);
assert!(sexp.contains("PATH"), "Expected localized hash element key in sexp, got: {sexp}",);
assert!(
sexp.contains("(variable $ next)"),
"Expected trailing argument to stay separate, got: {sexp}",
);
}
// ---------------------------------------------------------------
// Declaration with initializer and trailing args:
// `foo(my $x = 1, $y)`
// ---------------------------------------------------------------
#[test]
fn my_scalar_initializer_then_more_args() {
let code = "foo(my $x = 1, $y);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
assert!(sexp.contains("my_declaration"), "Expected my_declaration in sexp, got: {sexp}",);
// $y must be a separate argument, not part of the declaration initializer
// The sexp should contain both the declaration and a separate variable for $y
assert!(
sexp.contains("(variable $ y)"),
"Expected separate (variable $ y) in sexp, got: {sexp}",
);
}
// ---------------------------------------------------------------
// List declaration in args: `foo(my ($a, $b) = @_)`
// ---------------------------------------------------------------
#[test]
fn my_list_declaration_in_call_args() {
let code = "foo(my ($a, $b) = @_);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
// VariableListDeclaration sexp format should contain the declarator
assert!(sexp.contains("my"), "Expected 'my' in sexp, got: {sexp}",);
assert!(sexp.contains("(variable $ a)"), "Expected variable $a in sexp, got: {sexp}",);
assert!(sexp.contains("(variable $ b)"), "Expected variable $b in sexp, got: {sexp}",);
}
// ---------------------------------------------------------------
// Regression: local($x, $y) multi-variable form must not regress
// to single-var path (would only localize $x and fail on the comma)
// ---------------------------------------------------------------
#[test]
fn local_list_in_call_args() {
let code = "foo(local($x, $y), $z);";
assert_no_errors(code);
let stmt = first_stmt(code);
let sexp = stmt.to_sexp();
// Both $x and $y must appear in the local declaration
assert!(sexp.contains("(variable $ x)"), "Expected localized $x in sexp, got: {sexp}",);
assert!(sexp.contains("(variable $ y)"), "Expected localized $y in sexp, got: {sexp}",);
// $z must be a separate argument, not inside the local()
assert!(
sexp.contains("(variable $ z)"),
"Expected trailing $z as separate arg, got: {sexp}",
);
}
}