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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#![cfg(any(
not(any(
feature = "parser_tests",
feature = "analyzer_tests",
feature = "codegen_tests",
feature = "interpreter_tests",
feature = "conformance_tests",
feature = "integration_tests",
)),
feature = "analyzer_tests",
))]
/// TDD Test: Read-only non-Copy parameters should be inferred as borrowed (&T)
///
/// Bug: Functions that only READ their non-Copy parameters (Vec, String, Custom structs)
/// generate owned types in the function signature (e.g., `data: Vec<f32>`), but call
/// sites may pass references (e.g., `&self.frame_times`), causing E0308 mismatched types.
///
/// Root Cause: The analyzer's `infer_parameter_ownership` defaults to `Owned` for all
/// parameters, and `build_signature` forces `Owned` for non-Copy types like Vec, String,
/// Custom. This means read-only parameters are never inferred as `Borrowed`.
///
/// Fix: When a parameter is only read (not mutated, returned, stored, iterated, or used
/// in binary ops), infer `Borrowed`. Update `build_signature` to respect `Borrowed` for
/// non-Copy types instead of forcing `Owned`.
///
/// Discovered via dogfooding: windjammer-game-editor has 6+ E0308 errors from this pattern.
/// Files affected: panels/profiler.rs, panels/hierarchy.rs, panels/inspector.rs
#[path = "common/test_utils.rs"]
mod test_utils;
// ============================================================================
// TEST 1: Method with read-only Vec parameter
//
// Real game code (profiler.wj):
// fn render_graph(self, data: Vec<f32>, ...) -> String { ... reads data ... }
// self.render_graph(&self.frame_times, ...)
//
// The function only reads data (len, indexing). The generated Rust should have
// `data: &Vec<f32>`, not `data: Vec<f32>`, so the call site `&self.frame_times`
// matches.
// ============================================================================
#[test]
#[cfg_attr(tarpaulin, ignore)]
fn test_readonly_vec_param_inferred_as_borrowed() {
let source = r#"
pub struct Profiler {
pub frame_times: Vec<f32>,
pub fps_history: Vec<f32>,
}
impl Profiler {
pub fn render_graph(self, data: Vec<f32>, label: string) -> string {
if data.len() < 2 {
return "No data".to_string()
}
let first = data[0]
format!("{}: {} points, first={}", label, data.len(), first)
}
pub fn render(self) -> string {
let graph1 = self.render_graph(self.frame_times, "Frame Times".to_string())
let graph2 = self.render_graph(self.fps_history, "FPS".to_string())
format!("{}\n{}", graph1, graph2)
}
}
"#;
let (generated, _stderr) = test_utils::compile_via_cli_with_stderr(source);
// render_graph should take data as &Vec<f32> (borrowed) because it only reads
assert!(
generated.contains("data: &Vec<f32>"),
"COMPILER BUG: render_graph only reads 'data' (len, indexing). \
Should be 'data: &Vec<f32>', not 'data: Vec<f32>'.\n\
This causes E0308 when call sites pass &self.frame_times.\n\
Generated:\n{}",
generated
);
// WINDJAMMER DESIGN: Read-only String params infer to &str (not &String!)
let render_graph_line = generated.lines().find(|l| l.contains("fn render_graph"));
if let Some(line) = render_graph_line {
assert!(
line.contains("label: &str"),
"Read-only String params should become &str (idiomatic Rust).\n\
Line: {}",
line
);
}
}
// ============================================================================
// TEST 2: Method with read-only Custom struct parameter
//
// Real game code (hierarchy.wj):
// fn render_node(self, object: SceneObject, depth: i32) -> String { ... }
// self.render_node(obj, 0) // where obj is &SceneObject from map.get()
//
// The function only reads object fields. Should be `object: &SceneObject`.
// ============================================================================
#[test]
#[cfg_attr(tarpaulin, ignore)]
fn test_readonly_custom_struct_param_inferred_as_borrowed() {
let source = r#"
pub struct SceneObject {
pub id: string,
pub name: string,
pub visible: bool,
}
pub struct HierarchyPanel {
pub filter_text: string,
}
impl HierarchyPanel {
pub fn render_node(self, object: SceneObject, depth: i32) -> string {
let indent = depth * 20
format!("<div style='margin-left:{}px'>{} ({})</div>", indent, object.name, object.id)
}
pub fn render(self, objects: Vec<SceneObject>) -> string {
let mut html = "".to_string()
for obj in &objects {
html = html + self.render_node(obj, 0)
}
html
}
}
"#;
let (generated, _stderr) = test_utils::compile_via_cli_with_stderr(source);
// render_node should take object as &SceneObject (borrowed) because it only reads fields
assert!(
generated.contains("object: &SceneObject"),
"COMPILER BUG: render_node only reads 'object' fields (name, id). \
Should be 'object: &SceneObject', not 'object: SceneObject'.\n\
This causes E0308 when call sites pass &SceneObject from iterators.\n\
Generated:\n{}",
generated
);
// depth is i32 (Copy type) - should remain owned (pass by value)
let render_node_line = generated.lines().find(|l| l.contains("fn render_node"));
if let Some(line) = render_node_line {
assert!(
line.contains("depth: i32"),
"Copy type 'depth: i32' should remain owned (pass by value).\n\
Line: {}",
line
);
}
}
// ============================================================================
// TEST 3: Parameters that ARE consumed should stay owned
//
// Ensure the fix doesn't break parameters that need to be owned:
// - Parameters stored in struct fields
// - Parameters returned from the function
// - Parameters passed to functions that consume them
// ============================================================================
#[test]
#[cfg_attr(tarpaulin, ignore)]
fn test_consumed_params_stay_owned() {
let source = r#"
pub struct Config {
pub name: string,
pub items: Vec<i32>,
}
impl Config {
pub fn new(name: string, items: Vec<i32>) -> Config {
Config { name: name, items: items }
}
}
pub fn get_name(config: Config) -> string {
config.name
}
"#;
let (generated, _stderr) = test_utils::compile_via_cli_with_stderr(source);
// Config::new stores both params - they MUST stay owned
assert!(
generated.contains("name: String") && !generated.contains("name: &String"),
"Config::new stores 'name' in struct - must be owned String, not &String.\n\
Generated:\n{}",
generated
);
// get_name returns config.name (moves it out) - config must be owned or the
// field access on a reference would need clone. The analyzer should detect
// that the return value comes from the parameter.
// NOTE: This could also work as &Config with config.name.clone(), but
// is_returned should catch this case.
}
// ============================================================================
// TEST 4: String parameters stay owned (avoiding &String vs &str mismatches)
//
// String parameters are a special case. In Rust, &String doesn't accept &str
// literals ("hello"), so borrowing String params would cause type errors at
// call sites passing string literals. We keep String params Owned.
// Future: could generate &str for borrowed String params (more idiomatic Rust).
// ============================================================================
#[test]
#[cfg_attr(tarpaulin, ignore)]
fn test_string_param_infers_to_str() {
let source = r#"
pub struct Logger {
pub prefix: string,
}
impl Logger {
pub fn log_message(self, message: string) {
let output = format!("[{}] {}", self.prefix, message)
}
}
"#;
let (generated, _stderr) = test_utils::compile_via_cli_with_stderr(source);
// WINDJAMMER DESIGN: Read-only String params infer to &str (idiomatic Rust!)
let log_line = generated.lines().find(|l| l.contains("fn log_message"));
if let Some(line) = log_line {
assert!(
line.contains("message: &str"),
"Read-only String params should infer to &str (not &String, not String).\n\
Line: {}\n\
Generated:\n{}",
line,
generated
);
}
}