1use std::path::Path;
33
34use anyhow::{Context, Result};
35use tree_sitter::Parser;
36
37pub struct RewriteOutput {
40 pub source: Vec<u8>,
42 pub changed: bool,
44}
45
46pub fn rewrite_file(path: &Path, original_res_path: &str) -> Result<RewriteOutput> {
47 let source = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
48 Ok(rewrite_source(&source, original_res_path))
49}
50
51pub fn rewrite_source(source: &[u8], original_res_path: &str) -> RewriteOutput {
52 let Some(asserts) = find_asserts(source) else {
53 return RewriteOutput {
54 source: source.to_vec(),
55 changed: false,
56 };
57 };
58 if asserts.is_empty() {
59 return RewriteOutput {
60 source: source.to_vec(),
61 changed: false,
62 };
63 }
64
65 let mut replacements: Vec<(usize, usize, String)> = Vec::new();
67 for a in &asserts {
68 if let Some(repl) = build_replacement(a, source, original_res_path) {
69 replacements.push((a.start_byte, a.end_byte, repl));
70 }
71 }
72 if replacements.is_empty() {
73 return RewriteOutput {
74 source: source.to_vec(),
75 changed: false,
76 };
77 }
78
79 replacements.sort_by_key(|r| std::cmp::Reverse(r.0));
80 let mut result = source.to_vec();
81 for (start, end, replacement) in &replacements {
82 result.splice(start..end, replacement.bytes());
83 }
84 RewriteOutput {
85 source: result,
86 changed: true,
87 }
88}
89
90struct AssertInfo {
93 start_byte: usize,
95 end_byte: usize,
96 line: u32,
98 condition_text: String,
100 binary: Option<BinaryInfo>,
102}
103
104struct BinaryInfo {
105 op: BinaryOp,
106 left_text: String,
107 right_text: String,
108}
109
110#[derive(Clone, Copy)]
111enum BinaryOp {
112 Eq,
113 Ne,
114 Lt,
115 Lte,
116 Gt,
117 Gte,
118}
119
120impl BinaryOp {
121 fn gdt_suffix(self) -> &'static str {
122 match self {
123 BinaryOp::Eq => "eq",
124 BinaryOp::Ne => "ne",
125 BinaryOp::Lt => "lt",
126 BinaryOp::Lte => "lte",
127 BinaryOp::Gt => "gt",
128 BinaryOp::Gte => "gte",
129 }
130 }
131 fn from_text(op: &str) -> Option<Self> {
132 match op {
133 "==" => Some(BinaryOp::Eq),
134 "!=" => Some(BinaryOp::Ne),
135 "<" => Some(BinaryOp::Lt),
136 "<=" => Some(BinaryOp::Lte),
137 ">" => Some(BinaryOp::Gt),
138 ">=" => Some(BinaryOp::Gte),
139 _ => None,
140 }
141 }
142}
143
144fn find_asserts(source: &[u8]) -> Option<Vec<AssertInfo>> {
147 let mut parser = Parser::new();
148 parser
149 .set_language(&tree_sitter_gdscript::LANGUAGE.into())
150 .ok()?;
151 let tree = parser.parse(source, None)?;
152 let mut out = Vec::new();
153 collect_assert_calls(tree.root_node(), source, &mut out);
154 Some(out)
155}
156
157fn collect_assert_calls(node: tree_sitter::Node, source: &[u8], out: &mut Vec<AssertInfo>) {
158 if node.kind() == "call"
159 && let Some(info) = parse_assert_call(node, source)
160 {
161 out.push(info);
162 return; }
164 for i in 0..node.child_count() {
165 if let Some(child) = node.child(i as u32) {
166 collect_assert_calls(child, source, out);
167 }
168 }
169}
170
171fn parse_assert_call(node: tree_sitter::Node, source: &[u8]) -> Option<AssertInfo> {
173 let func_node = node.child(0)?;
175 if func_node.kind() != "identifier" {
176 return None;
177 }
178 if func_node.utf8_text(source).ok()? != "assert" {
179 return None;
180 }
181
182 if node.start_position().row != node.end_position().row {
184 return None;
185 }
186
187 let args_node = node.child(1)?;
189 if args_node.kind() != "arguments" {
190 return None;
191 }
192
193 let mut condition_node: Option<tree_sitter::Node> = None;
196 for i in 0..args_node.child_count() {
197 let child = args_node.child(i as u32)?;
198 if child.kind() == "(" || child.kind() == ")" || child.kind() == "," {
199 continue;
200 }
201 if condition_node.is_none() {
202 condition_node = Some(child);
203 break; }
205 }
206
207 let condition_node = condition_node?;
208 let condition_text = condition_node.utf8_text(source).ok()?.to_string();
209 let binary = try_parse_binary(condition_node, source);
210
211 Some(AssertInfo {
212 start_byte: node.start_byte(),
213 end_byte: node.end_byte(),
214 line: node.start_position().row as u32 + 1,
215 condition_text,
216 binary,
217 })
218}
219
220fn try_parse_binary(node: tree_sitter::Node, source: &[u8]) -> Option<BinaryInfo> {
222 if node.kind() != "binary_operator" {
223 return None;
224 }
225
226 let mut left: Option<tree_sitter::Node> = None;
229 let mut right: Option<tree_sitter::Node> = None;
230 let mut op: Option<BinaryOp> = None;
231
232 for i in 0..node.child_count() {
233 let child = node.child(i as u32)?;
234 if child.is_named() {
235 if left.is_none() {
236 left = Some(child);
237 } else {
238 right = Some(child);
239 }
240 } else if let Ok(txt) = child.utf8_text(source)
241 && let Some(parsed) = BinaryOp::from_text(txt)
242 {
243 op = Some(parsed);
244 }
245 }
246
247 Some(BinaryInfo {
248 op: op?,
249 left_text: left?.utf8_text(source).ok()?.to_string(),
250 right_text: right?.utf8_text(source).ok()?.to_string(),
251 })
252}
253
254fn build_replacement(info: &AssertInfo, source: &[u8], original_res_path: &str) -> Option<String> {
257 let indent = leading_whitespace(info.start_byte, source).to_string();
258 let file = escape_gdstring(original_res_path);
259 let line = info.line;
260
261 let repl = if let Some(bin) = &info.binary {
262 let expr = escape_gdstring(&info.condition_text);
263 format!(
264 r#"{indent}Gdt._rewrite_{suffix}({lhs}, {rhs}, "{expr}", "{file}", {line})"#,
265 suffix = bin.op.gdt_suffix(),
266 lhs = bin.left_text,
267 rhs = bin.right_text,
268 )
269 } else {
270 let expr = escape_gdstring(&info.condition_text);
271 format!(
272 r#"{indent}Gdt._rewrite_true({cond}, "{expr}", "{file}", {line})"#,
273 cond = info.condition_text,
274 )
275 };
276
277 Some(repl)
278}
279
280fn leading_whitespace(pos: usize, source: &[u8]) -> &str {
281 let line_start = source[..pos]
282 .iter()
283 .rposition(|&b| b == b'\n')
284 .map(|p| p + 1)
285 .unwrap_or(0);
286 let prefix = &source[line_start..pos];
287 let ws_end = prefix
288 .iter()
289 .take_while(|&&b| b == b' ' || b == b'\t')
290 .count();
291 std::str::from_utf8(&prefix[..ws_end]).unwrap_or("")
292}
293
294fn escape_gdstring(s: &str) -> String {
295 s.replace('\\', "\\\\").replace('"', "\\\"")
296}
297
298#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn dump_assert_tree() {
306 let src = b"extends RefCounted\n\nfunc test_foo():\n\tassert(x == y)\n\tassert(player.hp == 100, \"hp check\")\n\tassert(flag)\n";
307 let mut parser = Parser::new();
308 parser
309 .set_language(&tree_sitter_gdscript::LANGUAGE.into())
310 .unwrap();
311 let tree = parser.parse(src, None).unwrap();
312 fn dump(node: tree_sitter::Node, src: &[u8], indent: usize) {
313 let preview: String = node
314 .utf8_text(src)
315 .unwrap_or("?")
316 .chars()
317 .take(40)
318 .collect::<String>()
319 .replace('\n', "↵");
320 println!(
321 "{}{} [{}:{}-{}:{}] {:?}",
322 " ".repeat(indent),
323 node.kind(),
324 node.start_position().row + 1,
325 node.start_position().column,
326 node.end_position().row + 1,
327 node.end_position().column,
328 preview
329 );
330 for i in 0..node.child_count() {
331 if let Some(c) = node.child(i as u32) {
332 dump(c, src, indent + 1);
333 }
334 }
335 }
336 dump(tree.root_node(), src, 0);
337 }
338
339 #[test]
340 fn rewrites_eq_comparison() {
341 let src = b"extends RefCounted\n\nfunc test_foo():\n\tassert(player.hp == 100)\n";
342 let out = rewrite_source(src, "res://test_player.gd");
343 assert!(out.changed, "expected rewrite");
344 let s = std::str::from_utf8(&out.source).unwrap();
345 assert!(s.contains("Gdt._rewrite_eq"), "missing _rewrite_eq:\n{s}");
346 assert!(s.contains("player.hp"), "missing left operand:\n{s}");
347 assert!(s.contains("100"), "missing right operand:\n{s}");
348 assert!(s.contains("test_player.gd"), "missing original file:\n{s}");
349 assert!(
350 s.contains("\"player.hp == 100\""),
351 "missing expr text:\n{s}"
352 );
353 }
354
355 #[test]
356 fn rewrites_ne() {
357 let src = b"func test_foo():\n\tassert(a != b)\n";
358 let out = rewrite_source(src, "res://test.gd");
359 assert!(out.changed);
360 let s = std::str::from_utf8(&out.source).unwrap();
361 assert!(s.contains("_rewrite_ne"), "{s}");
362 }
363
364 #[test]
365 fn rewrites_lt_lte_gt_gte() {
366 for (op, suffix) in [("<", "lt"), ("<=", "lte"), (">", "gt"), (">=", "gte")] {
367 let src = format!("func test_foo():\n\tassert(a {op} b)\n");
368 let out = rewrite_source(src.as_bytes(), "res://test.gd");
369 assert!(out.changed, "op={op}");
370 let s = std::str::from_utf8(&out.source).unwrap();
371 assert!(
372 s.contains(&format!("_rewrite_{suffix}")),
373 "op={op} missing suffix:\n{s}"
374 );
375 }
376 }
377
378 #[test]
379 fn rewrites_bool_assert() {
380 let src = b"func test_foo():\n\tassert(is_alive)\n";
381 let out = rewrite_source(src, "res://test.gd");
382 assert!(out.changed);
383 let s = std::str::from_utf8(&out.source).unwrap();
384 assert!(s.contains("Gdt._rewrite_true"), "{s}");
385 assert!(s.contains("is_alive"), "{s}");
386 }
387
388 #[test]
389 fn no_rewrite_multiline() {
390 let src = b"func test_foo():\n\tassert(\n\t\tx == y\n\t)\n";
391 let out = rewrite_source(src, "res://test.gd");
392 assert!(!out.changed, "multi-line assert must not be rewritten");
393 }
394
395 #[test]
396 fn preserves_indentation() {
397 let src = b"func test_foo():\n\tassert(x == y)\n";
398 let out = rewrite_source(src, "res://test.gd");
399 let s = std::str::from_utf8(&out.source).unwrap();
400 let line = s.lines().find(|l| l.contains("_rewrite_")).unwrap();
401 assert!(line.starts_with('\t'), "indentation lost: {line:?}");
402 }
403
404 #[test]
405 fn no_assert_unchanged() {
406 let src = b"func test_foo():\n\tvar x = 1\n";
407 let out = rewrite_source(src, "res://test.gd");
408 assert!(!out.changed);
409 assert_eq!(out.source, src);
410 }
411
412 #[test]
413 fn multiple_asserts_in_one_function() {
414 let src = b"func test_foo():\n\tassert(a == b)\n\tassert(c != d)\n\tassert(flag)\n";
415 let out = rewrite_source(src, "res://test.gd");
416 assert!(out.changed);
417 let s = std::str::from_utf8(&out.source).unwrap();
418 assert!(s.contains("_rewrite_eq"), "{s}");
419 assert!(s.contains("_rewrite_ne"), "{s}");
420 assert!(s.contains("_rewrite_true"), "{s}");
421 }
422
423 #[test]
424 fn line_number_embedded() {
425 let src = b"extends RefCounted\n\nfunc test_foo():\n\tassert(x == y)\n";
427 let out = rewrite_source(src, "res://test.gd");
428 let s = std::str::from_utf8(&out.source).unwrap();
429 assert!(
431 s.contains(", 4)"),
432 "line number not embedded correctly:\n{s}"
433 );
434 }
435
436 #[test]
437 fn does_not_rewrite_non_assert_calls() {
438 let src = b"func test_foo():\n\tmy_assert(x == y)\n\tassert_eq(a, b)\n";
439 let out = rewrite_source(src, "res://test.gd");
440 assert!(
442 !out.changed,
443 "should not rewrite non-assert calls:\n{}",
444 std::str::from_utf8(&out.source).unwrap()
445 );
446 }
447}