Skip to main content

gdck_format/
lib.rs

1//! GDScript formatter.
2//!
3//! Formatting runs in two stages. Lowering turns the concrete syntax tree
4//! into a document describing where lines *may* break, and the renderer decides
5//! where they *do*, given the configured width. Keeping those apart means the
6//! style-guide rules live in one place instead of being spread across string
7//! concatenation.
8//!
9//! # What the guide asks for
10//!
11//! Most of it falls out of the document IR: the 100-column wrap, one space
12//! around operators and after commas, two blank lines around top-level
13//! definitions and one inside a class, trailing commas on collections that
14//! break, and two indent levels on continuation lines against one inside
15//! arrays, dictionaries and enums.
16//!
17//! The rest is explicit: quote style chosen to minimise escapes, lowercase
18//! hexadecimal, a digit either side of a float's point, single-line inner
19//! class declarations, and redundant parentheses dropped. Those live in
20//! the `literal` and `lower` modules.
21//!
22//! # Safety checks
23//!
24//! Before returning, the formatter re-parses its own output and checks that it
25//! still parses, that the tree still means the same thing, that no comment was
26//! dropped, and that a second pass is a no-op. A formatter that silently eats code is far
27//! worse than one that refuses to run, so these are on by default;
28//! [`FormatConfig::safety_checks`] turns them off.
29
30mod 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/// Why formatting could not be completed.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum FormatError {
46    /// The input could not be parsed, so there is nothing safe to rewrite.
47    Unparseable,
48    /// Formatting changed the meaning of the code. Always a bug in `gdck`.
49    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
65/// Format a parsed GDScript file.
66///
67/// # Errors
68///
69/// Returns [`FormatError::Unparseable`] if the tree holds syntax errors, or
70/// [`FormatError::SafetyCheckFailed`] if the output does not survive the
71/// checks described on this module.
72pub 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
109/// Format source text directly, parsing it first.
110///
111/// # Errors
112///
113/// As [`format()`].
114pub 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    // A document always ends with the file's final break; collapse whatever
125    // that produced to exactly one line feed.
126    while output.ends_with('\n') {
127        output.pop();
128    }
129    if !output.is_empty() {
130        output.push('\n');
131    }
132    output
133}
134
135/// One step of a tree's canonical form. See [`canonical`].
136#[derive(Debug, Clone, PartialEq, Eq)]
137enum Step {
138    Enter(SyntaxKind),
139    Token(SyntaxKind, String),
140    /// The operator of an initializer, spelled the same however it was written.
141    Operator(&'static str),
142}
143
144/// A canonical form capturing what the program *means*.
145///
146/// Comparing flat token streams would be simpler, but it would reject the
147/// rewrites the style guide asks for: hoisting an inner class's `extends` onto
148/// the declaration line moves tokens, and dropping a redundant parenthesis
149/// removes them. Comparing tree shape instead is both weaker in the right
150/// places and stronger in the important one — grouping is encoded by the
151/// nesting, so a parenthesis that actually mattered shows up as a differently
152/// shaped expression rather than as two missing tokens.
153///
154/// Elided deliberately:
155///
156/// * `ParenExpr`, which only ever expressed grouping the tree already records.
157/// * Commas and semicolons, which separate siblings the tree already orders.
158/// * The position of an inner class's `extends`, canonicalised to the header.
159/// * Literal spelling, since quote style and hexadecimal case may change.
160fn 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        // Transparent: its only contribution was grouping, which is now the
169        // shape of the tree around it.
170        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            // `:=`, `: =` and `=` all reduce to which of the two forms it is.
183            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
204/// Emit a class declaration with its `extends` in the header position.
205///
206/// GDScript allows the parent either there or as the body's first statement,
207/// and the formatter moves it, so the comparison has to see both spellings as
208/// the same program.
209fn 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                // Separators the sibling order already records.
249                | 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
261/// Compare literals by value rather than spelling, since the formatter is
262/// allowed to change quote style and hexadecimal case.
263fn 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    /// Formatting an already-formatted file must change nothing.
287    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        // "Use line feed (LF) characters to break lines, not CRLF or CR."
308        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        // The guide: "For inner classes, use single-line declarations".
328        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        // Godot's rule: a `##` block "must immediately precede a script member,
338        // or for script descriptions, be placed at the top of the script". The
339        // blank line under it is what says this one is the script's rather than
340        // the function's, so the guide's two blank lines go *after* it.
341        //
342        // It used to be carried along as the function's leading comment, which
343        // put two blank lines above it and left the author's one below it —
344        // immediately preceding nothing, and no longer at the top either.
345        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        // The other half of the same rule. With no blank line the comment does
355        // immediately precede the function, so it documents the function and
356        // the spacing belongs above the pair.
357        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        // Nothing about this is specific to the top of the file: a note written
366        // between two definitions is neither one's, and stays where it was put.
367        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        // The counterpart rule: at file level the two are separate lines.
375        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        // `gdformat` enforces neither form, so a project can be uniformly on
385        // this one without having chosen it. Both inputs still converge, or
386        // the setting would only be preserving whatever it was given.
387        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        // Nothing to join: `extends` alone is one line under either setting.
400        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        // It has nowhere to go on a joined line, so it keeps the two apart
409        // whatever the setting says. Before this was handled the comment was
410        // dropped, and only the safety check noticed.
411        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        // How the Godot documentation writes them: `@rpc(...)` above the func,
426        // `@export_range(...)` beside the var.
427        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        // `@abstract` is a modifier, and the language reference writes it
434        // inline: `@abstract func draw()`.
435        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        // The guide's own example ends with exactly this shape.
463        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        // An array the author spread over several lines stays that way, and
507        // gains the trailing comma the guide asks for.
508        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        // A comment above a definition belongs to it, so the two blank lines
519        // go before the comment rather than between it and the function.
520        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        // The guide: continuation lines use 2 indent levels so they cannot be
544        // mistaken for the block that follows.
545        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    /// The trailing comma above is not cosmetic.
558    ///
559    /// A lambda body is the one place inside brackets where Godot still tracks
560    /// indentation, and it stops again at whatever ends the lambda. Without the
561    /// comma the closing bracket's line is the first line after the body, and
562    /// Godot then demands it sit at the enclosing statement's indent. One level
563    /// of nesting can satisfy that by accident; two cannot, and the file stops
564    /// compiling with "Unindent doesn't match the previous indentation level".
565    ///
566    /// So the comma goes in whenever the list breaks and ends in a lambda
567    /// block, rather than only in the arrangement that would otherwise break.
568    /// The alternative is output whose validity depends on how deeply the call
569    /// happens to be nested.
570    #[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        // It never opens a block, so nothing has to close one.
587        check_stable("func f():\n\tbutton.pressed.connect(func(): do_something())\n");
588    }
589
590    /// Parentheses around a lambda block close on the body's last line, for the
591    /// same reason the comma above exists: what ends the lambda has to sit
592    /// there, because Godot is still tracking indentation until it does and a
593    /// closing paren on its own line would dedent to a continuation's level
594    /// rather than the statement's.
595    #[test]
596    fn parens_around_a_lambda_block_close_on_its_last_line() {
597        // A lambda block opens the call out however short it is, so the paren
598        // is always the last thing on the body's line rather than the first on
599        // the next one.
600        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    /// A standalone annotation opens or closes a region rather than saying
623    /// something about the declaration under it, so there is nothing for it to
624    /// sit beside. Godot rejects the attempt with "Expected newline after a
625    /// standalone annotation", which is what moving these up beside a `var`
626    /// used to produce.
627    #[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    /// The ones that do describe the declaration below them still move up onto
646    /// its line, which is how the Godot documentation writes them.
647    #[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    /// Godot decides which property form it is reading from the first accessor
662    /// and separates the two differently, so the comma is not a matter of
663    /// taste. In `set = f, get = g` it is what carries the parser on to the
664    /// second accessor: drop it and the property is over, and the `get` line
665    /// is rejected with "Expected end of indented block for property".
666    #[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    /// The other form takes no comma at all — Godot never looks for one there,
673    /// so emitting it would be a syntax error rather than a redundancy.
674    #[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        // Nothing should trip this; the test exists so the wiring is exercised
682        // rather than merely present.
683        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        // A comment between `=` and its value cannot stay there: anything
690        // after it on the line would be commented out, including the comma.
691        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
705    #[test]
706    fn a_trailing_comment_does_not_make_the_value_wrap() {
707        // The line is over the limit only because of the comment, which the
708        // formatter can neither reflow nor move. Parenthesising the value to
709        // make room restructures code around prose and does not address what
710        // is actually too long; `line-too-long` reports that instead.
711        let source = concat!(
712            "const WATER_COST_MULT: StringName = &\"water_cost_mult\"",
713            " ## Scales the energy a watering swing costs, and then some more.\n"
714        );
715        check(source, source);
716    }
717
718    #[test]
719    fn code_too_long_on_its_own_still_wraps_around_a_comment() {
720        // The other half of the rule: the comment is excused, the code is not.
721        let source = concat!(
722            "const LONG = alpha_value + beta_value + gamma_value + delta_value",
723            " + epsilon_value + zeta_value + eta_value ## ok\n"
724        );
725        let expected = concat!(
726            "const LONG = (\n",
727            "\t\talpha_value\n",
728            "\t\t+ beta_value\n",
729            "\t\t+ gamma_value\n",
730            "\t\t+ delta_value\n",
731            "\t\t+ epsilon_value\n",
732            "\t\t+ zeta_value\n",
733            "\t\t+ eta_value\n",
734            ") ## ok\n"
735        );
736        check(source, expected);
737    }
738
739    #[test]
740    fn a_comment_inside_brackets_still_forces_them_open() {
741        // Only a declaration's own trailing comment is excused. Inside
742        // brackets the width still decides, because there a comment governs
743        // whether the brackets open rather than whether code is rewritten.
744        let source = "var d = {\n\t\"k\": 1, # why\n\t\"j\": 2,\n}\n";
745        check(source, source);
746    }
747}