1mod doc;
31pub mod literal;
32mod lower;
33mod trivia;
34
35use std::fmt;
36
37use gdck_config::FormatConfig;
38use gdck_syntax::{Element, SyntaxKind, SyntaxNode, SyntaxTree};
39
40use crate::lower::Lowerer;
41use crate::trivia::Trivia;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum FormatError {
46 Unparseable,
48 SafetyCheckFailed(&'static str),
50}
51
52impl fmt::Display for FormatError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::Unparseable => f.write_str("cannot format a file with syntax errors"),
56 Self::SafetyCheckFailed(what) => {
57 write!(f, "formatting was rejected by a safety check: {what}")
58 }
59 }
60 }
61}
62
63impl std::error::Error for FormatError {}
64
65pub fn format(tree: &SyntaxTree, config: &FormatConfig) -> Result<String, FormatError> {
73 if tree.has_errors() {
74 return Err(FormatError::Unparseable);
75 }
76
77 let output = render(tree, config);
78
79 if !config.safety_checks {
80 return Ok(output);
81 }
82
83 let reparsed = gdck_syntax::parse(&output);
84 if reparsed.has_errors() {
85 return Err(FormatError::SafetyCheckFailed(
86 "the formatted output does not parse",
87 ));
88 }
89 if canonical(tree) != canonical(&reparsed) {
90 return Err(FormatError::SafetyCheckFailed(
91 "formatting changed the code",
92 ));
93 }
94 let before = Trivia::collect(tree);
95 let after = Trivia::collect(&reparsed);
96 if before.all_comments() != after.all_comments() {
97 return Err(FormatError::SafetyCheckFailed("a comment was lost"));
98 }
99 let second = render(&reparsed, config);
100 if second != output {
101 return Err(FormatError::SafetyCheckFailed(
102 "formatting is not idempotent",
103 ));
104 }
105
106 Ok(output)
107}
108
109pub fn format_source(source: &str, config: &FormatConfig) -> Result<String, FormatError> {
115 format(&gdck_syntax::parse(source), config)
116}
117
118fn render(tree: &SyntaxTree, config: &FormatConfig) -> String {
119 let trivia = Trivia::collect(tree);
120 let lowerer = Lowerer::new(tree, &trivia, config.class_declaration);
121 let document = lowerer.source_file(tree.root());
122 let mut output = doc::render(&document, config.line_length as usize, config.indent);
123
124 while output.ends_with('\n') {
127 output.pop();
128 }
129 if !output.is_empty() {
130 output.push('\n');
131 }
132 output
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137enum Step {
138 Enter(SyntaxKind),
139 Token(SyntaxKind, String),
140 Operator(&'static str),
142}
143
144fn canonical(tree: &SyntaxTree) -> Vec<Step> {
161 let mut steps = Vec::new();
162 walk(tree.root(), tree.text(), &mut steps);
163 steps
164}
165
166fn walk(node: SyntaxNode<'_>, source: &str, steps: &mut Vec<Step>) {
167 match node.kind() {
168 SyntaxKind::ParenExpr => {
171 for child in node.child_nodes() {
172 walk(child, source, steps);
173 }
174 return;
175 }
176 SyntaxKind::ClassDecl => {
177 walk_class_decl(node, source, steps);
178 return;
179 }
180 SyntaxKind::Initializer => {
181 steps.push(Step::Enter(SyntaxKind::Initializer));
182 let inferred = node
184 .child_tokens()
185 .any(|token| matches!(token.kind, SyntaxKind::ColonEq | SyntaxKind::Colon));
186 steps.push(Step::Operator(if inferred { ":=" } else { "=" }));
187 for child in node.child_nodes() {
188 walk(child, source, steps);
189 }
190 return;
191 }
192 _ => {}
193 }
194
195 steps.push(Step::Enter(node.kind()));
196 for element in node.children() {
197 match element {
198 Element::Node(id) => walk(node.tree().node(id), source, steps),
199 Element::Token(token) => push_token(token, source, steps),
200 }
201 }
202}
203
204fn walk_class_decl(node: SyntaxNode<'_>, source: &str, steps: &mut Vec<Step>) {
210 steps.push(Step::Enter(SyntaxKind::ClassDecl));
211
212 let block = node.child_node_of(SyntaxKind::Block);
213 let mut members: Vec<SyntaxNode<'_>> = block
214 .map(|block| block.child_nodes().collect())
215 .unwrap_or_default();
216
217 let mut extends = node.child_node_of(SyntaxKind::ExtendsDecl);
218 if extends.is_none() {
219 let body_level = members
220 .iter()
221 .position(|member| member.kind() == SyntaxKind::ExtendsDecl);
222 if let Some(index) = body_level {
223 extends = Some(members.remove(index));
224 }
225 }
226
227 for token in node.child_tokens() {
228 push_token(token, source, steps);
229 }
230 if let Some(extends) = extends {
231 walk(extends, source, steps);
232 }
233 if block.is_some() {
234 steps.push(Step::Enter(SyntaxKind::Block));
235 for member in members {
236 walk(member, source, steps);
237 }
238 }
239}
240
241fn push_token(token: gdck_syntax::Token, source: &str, steps: &mut Vec<Step>) {
242 if token.kind.is_trivia()
243 || matches!(
244 token.kind,
245 SyntaxKind::Indent
246 | SyntaxKind::Dedent
247 | SyntaxKind::Eof
248 | SyntaxKind::Comma
250 | SyntaxKind::Semicolon
251 )
252 {
253 return;
254 }
255 steps.push(Step::Token(
256 token.kind,
257 normalize_for_comparison(token, source),
258 ));
259}
260
261fn normalize_for_comparison(token: gdck_syntax::Token, source: &str) -> String {
264 let text = token.text(source);
265 match token.kind {
266 SyntaxKind::Int | SyntaxKind::Float => literal::normalize_number(text),
267 SyntaxKind::Str
268 | SyntaxKind::StringName
269 | SyntaxKind::NodePath
270 | SyntaxKind::GetNode
271 | SyntaxKind::UniqueNode => literal::normalize_string(text),
272 _ => text.to_string(),
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn check(source: &str, expected: &str) {
281 let formatted = format_source(source, &FormatConfig::default())
282 .unwrap_or_else(|error| panic!("failed to format {source:?}: {error}"));
283 assert_eq!(formatted, expected, "\ninput was:\n{source}");
284 }
285
286 fn check_stable(source: &str) {
288 check(source, source);
289 }
290
291 #[test]
292 fn refuses_to_format_unparseable_input() {
293 let tree = gdck_syntax::parse("func f(:\n");
294 assert_eq!(
295 format(&tree, &FormatConfig::default()),
296 Err(FormatError::Unparseable)
297 );
298 }
299
300 #[test]
301 fn an_empty_file_stays_empty() {
302 check("", "");
303 }
304
305 #[test]
306 fn carriage_returns_are_normalised_away() {
307 check("var x = 1\r\nvar y = 2\r\n", "var x = 1\nvar y = 2\n");
309 }
310
311 #[test]
312 fn a_file_ends_with_exactly_one_newline() {
313 check("var x = 1", "var x = 1\n");
314 check("var x = 1\n\n\n", "var x = 1\n");
315 }
316
317 #[test]
318 fn operators_get_one_space_and_commas_one_after() {
319 check(
320 "func f():\n\tposition.x=5\n\tmy_array = [4,5,6]\n\tdict [\"key\"] = 5\n\tprint ( \"foo\" )\n",
321 "func f():\n\tposition.x = 5\n\tmy_array = [4, 5, 6]\n\tdict[\"key\"] = 5\n\tprint(\"foo\")\n",
322 );
323 }
324
325 #[test]
326 fn an_inner_class_declares_its_parent_on_one_line() {
327 check_stable("class Child extends Parent:\n\tpass\n");
329 check(
330 "class Child:\n\textends Parent\n\tpass\n",
331 "class Child extends Parent:\n\tpass\n",
332 );
333 }
334
335 #[test]
336 fn a_script_description_stays_with_the_header_it_documents() {
337 check(
346 "extends Node\n## Doc.\n\nfunc f() -> void:\n\tpass\n",
347 "extends Node\n## Doc.\n\n\nfunc f() -> void:\n\tpass\n",
348 );
349 check_stable("extends Node\n## Doc.\n\n\nfunc f() -> void:\n\tpass\n");
350 }
351
352 #[test]
353 fn a_comment_touching_its_declaration_still_belongs_to_it() {
354 check(
358 "extends Node\n## Doc.\nfunc f() -> void:\n\tpass\n",
359 "extends Node\n\n\n## Doc.\nfunc f() -> void:\n\tpass\n",
360 );
361 }
362
363 #[test]
364 fn a_detached_comment_between_two_functions_keeps_its_distance() {
365 check_stable(
368 "extends Node\n\n\nfunc a() -> void:\n\tpass\n\n\n## A note.\n\n\nfunc b() -> void:\n\tpass\n",
369 );
370 }
371
372 #[test]
373 fn a_file_level_class_declares_its_parent_on_the_next_line() {
374 check(
376 "class_name Player extends Node\n",
377 "class_name Player\nextends Node\n",
378 );
379 check_stable("class_name Player\nextends Node\n");
380 }
381
382 #[test]
383 fn a_project_can_keep_the_joined_class_declaration() {
384 let mut joined = FormatConfig::default();
388 joined.class_declaration = gdck_config::ClassDeclaration::SingleLine;
389 for source in [
390 "class_name Player extends Node\n",
391 "class_name Player\nextends Node\n",
392 ] {
393 assert_eq!(
394 format_source(source, &joined).expect("formats"),
395 "class_name Player extends Node\n",
396 "\ninput was:\n{source}"
397 );
398 }
399 assert_eq!(
401 format_source("extends Node\n", &joined).expect("formats"),
402 "extends Node\n"
403 );
404 }
405
406 #[test]
407 fn a_comment_between_class_name_and_extends_survives() {
408 let source = "class_name Player\n# why we extend\nextends Node\n";
412 check_stable(source);
413 let mut joined = FormatConfig::default();
414 joined.class_declaration = gdck_config::ClassDeclaration::SingleLine;
415 assert_eq!(format_source(source, &joined).expect("formats"), source);
416 }
417
418 #[test]
419 fn abstract_stays_on_the_inner_class_line() {
420 check_stable("@abstract class MyNode extends Node:\n\tpass\n");
421 }
422
423 #[test]
424 fn a_functions_annotations_take_a_line_each() {
425 check_stable("@rpc(\"any_peer\")\nfunc ping() -> void:\n\tpass\n");
428 check(
429 "@rpc(\"any_peer\") func ping() -> void:\n\tpass\n",
430 "@rpc(\"any_peer\")\nfunc ping() -> void:\n\tpass\n",
431 );
432 check_stable("@export_range(0, 10) var lives = 3\n");
433 check_stable("@abstract\nclass_name Shape\n\n\n@abstract func area() -> float\n");
436 }
437
438 #[test]
439 fn one_statement_per_line() {
440 check(
441 "func f():\n\tif flag: print(\"flagged\")\n",
442 "func f():\n\tif flag:\n\t\tprint(\"flagged\")\n",
443 );
444 check("var a = 1; var b = 2\n", "var a = 1\nvar b = 2\n");
445 }
446
447 #[test]
448 fn the_ternary_operator_is_the_exception_to_that() {
449 check_stable("func f():\n\tnext_state = \"idle\" if is_on_floor() else \"fall\"\n");
450 }
451
452 #[test]
453 fn definitions_get_two_blank_lines_at_file_level() {
454 check(
455 "func a():\n\tpass\nfunc b():\n\tpass\n",
456 "func a():\n\tpass\n\n\nfunc b():\n\tpass\n",
457 );
458 }
459
460 #[test]
461 fn definitions_get_one_blank_line_inside_a_class() {
462 check_stable("class State:\n\tvar foo = 0\n\n\tfunc _init():\n\t\tprint(\"Hello!\")\n");
464 }
465
466 #[test]
467 fn blank_line_runs_collapse_to_one() {
468 check("var a = 1\n\n\n\nvar b = 2\n", "var a = 1\n\nvar b = 2\n");
469 }
470
471 #[test]
472 fn redundant_parentheses_are_dropped() {
473 check(
474 "func f():\n\tif (is_colliding()):\n\t\tqueue_free()\n",
475 "func f():\n\tif is_colliding():\n\t\tqueue_free()\n",
476 );
477 }
478
479 #[test]
480 fn parentheses_that_carry_meaning_are_kept() {
481 check_stable("var x = (a + b) * c\n");
482 check_stable("func f():\n\tif (foo and bar) or not baz:\n\t\tprint(\"yes\")\n");
483 }
484
485 #[test]
486 fn a_single_line_dictionary_gets_spaces_inside_its_braces() {
487 check(
488 "var my_dictionary = {key = \"value\"}\n",
489 "var my_dictionary = { key = \"value\" }\n",
490 );
491 check_stable("var empty = {}\n");
492 }
493
494 #[test]
495 fn collections_take_one_indent_level_and_a_trailing_comma() {
496 let long = "var party = [\"Godot\", \"Godette\", \"Steve\", \"a name quite long indeed\", \"and one more that certainly pushes it over\"]\n";
497 check(
498 long,
499 "var party = [\n\t\"Godot\",\n\t\"Godette\",\n\t\"Steve\",\n\t\"a name quite long indeed\",\n\t\"and one more that certainly pushes it over\",\n]\n",
500 );
501 }
502
503 #[test]
504 fn a_short_collection_stays_on_one_line_without_a_trailing_comma() {
505 check("var array = [1, 2, 3,]\n", "var array = [1, 2, 3]\n");
506 check(
509 "var array = [\n\t1,\n\t2\n]\n",
510 "var array = [\n\t1,\n\t2,\n]\n",
511 );
512 }
513
514 #[test]
515 fn comments_stay_with_what_they_document() {
516 check_stable("# Sets things up.\nfunc _ready():\n\tpass\n");
517 check_stable("var x = 1 # why\n");
518 check(
521 "var a = 1\n# Documents f.\nfunc f():\n\tpass\n",
522 "var a = 1\n\n\n# Documents f.\nfunc f():\n\tpass\n",
523 );
524 }
525
526 #[test]
527 fn a_trailing_comment_keeps_one_space_before_it() {
528 check("var x = 1 # why\n", "var x = 1 # why\n");
529 }
530
531 #[test]
532 fn comments_at_the_end_of_a_file_survive() {
533 check_stable("var x = 1\n\n# the end\n");
534 }
535
536 #[test]
537 fn a_lambda_written_inline_stays_inline() {
538 check_stable("var double = func(x): return x * 2\n");
539 }
540
541 #[test]
542 fn wrapped_expressions_take_two_indent_levels() {
543 check_stable(
546 "var position = Vector2(250, 350)\n\n\nfunc f():\n\tif (\n\t\t\tposition.x > 200\n\t\t\tand position.x < 400\n\t\t\tand position.y > 300\n\t\t\tand position.y < 400\n\t):\n\t\tpass\n",
547 );
548 }
549
550 #[test]
551 fn a_multi_line_lambda_keeps_its_block() {
552 check_stable(
553 "func f():\n\tbutton.pressed.connect(\n\t\t\tfunc() -> void:\n\t\t\t\tdo_something(),\n\t)\n",
554 );
555 }
556
557 #[test]
571 fn a_lambda_closing_a_nested_call_ends_with_a_comma() {
572 check_stable(
573 "func f():\n\
574 \tbox.add_child(\n\
575 \t\t\tmake_button(\n\
576 \t\t\t\t\t\"a long label here to force the formatter to wrap this\",\n\
577 \t\t\t\t\tfunc() -> void:\n\
578 \t\t\t\t\t\tdo_something(),\n\
579 \t\t\t)\n\
580 \t)\n",
581 );
582 }
583
584 #[test]
585 fn a_single_line_lambda_gains_no_comma() {
586 check_stable("func f():\n\tbutton.pressed.connect(func(): do_something())\n");
588 }
589
590 #[test]
596 fn parens_around_a_lambda_block_close_on_its_last_line() {
597 check(
601 "func f():\n\tassert((func() -> bool:\n\t\treturn check_something_here()).call())\n",
602 "func f():\n\
603 \tassert(\n\
604 \t\t\t(func() -> bool:\n\
605 \t\t\t\treturn check_something_here()).call()\n\
606 \t)\n",
607 );
608 check(
609 "func f():\n\
610 \tassert((func() -> bool:\n\
611 \t\tvar ok: bool = probe_the_thing_for_a_while(argument_one, argument_two)\n\
612 \t\treturn ok).call())\n",
613 "func f():\n\
614 \tassert(\n\
615 \t\t\t(func() -> bool:\n\
616 \t\t\t\tvar ok: bool = probe_the_thing_for_a_while(argument_one, argument_two)\n\
617 \t\t\t\treturn ok).call()\n\
618 \t)\n",
619 );
620 }
621
622 #[test]
628 fn a_standalone_annotation_keeps_its_own_line() {
629 check_stable(
630 "func f():\n\
631 \t@warning_ignore_start(\"integer_division\")\n\
632 \tvar halved := total / 2\n\
633 \t@warning_ignore_restore(\"integer_division\")\n\
634 \treturn halved\n",
635 );
636 check_stable(
637 "@export_category(\"Stats\")\n\
638 @export_group(\"Health\", \"health_\")\n\
639 var health_max := 10\n\
640 @export_subgroup(\"Regen\")\n\
641 var health_regen := 1.0\n",
642 );
643 }
644
645 #[test]
648 fn an_annotation_about_a_variable_stays_beside_it() {
649 check(
650 "@export_range(0, 10)\nvar lives := 3\n",
651 "@export_range(0, 10) var lives := 3\n",
652 );
653 }
654
655 #[test]
656 fn accessors_keep_the_form_they_were_written_in() {
657 check_stable("var health = max_health:\n\tset(new_health):\n\t\thealth = new_health\n");
658 check_stable("var is_active = true:\n\tset = set_is_active\n");
659 }
660
661 #[test]
667 fn a_setget_property_keeps_the_comma_between_its_accessors() {
668 check_stable("var p:\n\tset = __set,\n\tget = __get\n");
669 check_stable("var p:\n\tget = __get,\n\tset = __set\n");
670 }
671
672 #[test]
675 fn block_bodied_accessors_are_not_comma_separated() {
676 check_stable("var p:\n\tset(x):\n\t\t_p = x\n\tget:\n\t\treturn _p\n");
677 }
678
679 #[test]
680 fn the_safety_check_catches_a_lost_comment() {
681 let tree = gdck_syntax::parse("# a\nvar x = 1 # b\n## c\nfunc f():\n\tpass\n");
684 assert!(format(&tree, &FormatConfig::default()).is_ok());
685 }
686
687 #[test]
688 fn a_comment_moved_onto_its_own_line_keeps_no_inline_space() {
689 check(
692 "var x = {\n\tname = # why\n\t1\n}\n",
693 "var x = {\n\t# why\n\tname = 1,\n}\n",
694 );
695 }
696
697 #[test]
698 fn formatting_is_idempotent_on_awkward_input() {
699 let source = "class_name A extends B\nvar x={'k':1,}\nfunc f(a,b=2):\n\tif (a): return\n";
700 let first = format_source(source, &FormatConfig::default()).expect("formats");
701 let second = format_source(&first, &FormatConfig::default()).expect("formats");
702 assert_eq!(first, second);
703 }
704}