Skip to content

Commit 2bd4328

Browse files
committed
Fix: Prevent line breaks inside generic type annotations
Fix #283
1 parent 7a34c68 commit 2bd4328

6 files changed

Lines changed: 100 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This file documents the changes made to the formatter with each release.
77
### Fixed
88

99
- Make sure to keep tool at the top of the script, above class_name and extends (#285)
10+
- Fixed generic type parameters breaking across lines (#283)
1011

1112
## Release 0.22.0
1213

src/formatter.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,46 @@ fn process_node(
258258
| GDScriptNodeKind::Dictionary
259259
| GDScriptNodeKind::EnumeratorList
260260
| GDScriptNodeKind::Parameters
261-
| GDScriptNodeKind::Arguments
262-
| GDScriptNodeKind::SubscriptArguments => process_container(input, node, render_elements),
261+
| GDScriptNodeKind::Arguments => process_container(input, node, render_elements),
262+
GDScriptNodeKind::SubscriptArguments => {
263+
// Anything like a[b] is parsed as a `subscript` node, but this may
264+
// be a dictionary access which can wrap across lines or a type hint
265+
// like `Dictionary[String, String]` which must stay on one line;
266+
// Official GDScript parser cannot parse line returns in there. So
267+
// we walk up the AST to check if we are inside a `type` node.
268+
// The type hint `Dictionary[String, String]` is parsed like this:
269+
//
270+
// ```text
271+
// (type
272+
// (subscript
273+
// (identifier) ; Dictionary
274+
// arguments: (subscript_arguments
275+
// (identifier) ; String
276+
// (identifier))) ; String
277+
// ```
278+
//
279+
// Which is why we walk up the AST to check if we are inside a `type` node.
280+
let mut is_type_with_subscript = false;
281+
let mut ancestor = node.parent();
282+
while let Some(current) = ancestor {
283+
let current_kind = GDScriptNodeKind::get_kind_from_ast_node(current);
284+
if current_kind == GDScriptNodeKind::Type {
285+
is_type_with_subscript = true;
286+
break;
287+
}
288+
if current_kind != GDScriptNodeKind::Subscript
289+
&& current_kind != GDScriptNodeKind::Other
290+
{
291+
break;
292+
}
293+
ancestor = current.parent();
294+
}
295+
if is_type_with_subscript {
296+
process_children_with_spacing(input, node, render_elements);
297+
} else {
298+
process_container(input, node, render_elements);
299+
}
300+
}
263301
GDScriptNodeKind::Body | GDScriptNodeKind::ClassBody | GDScriptNodeKind::MatchBody => {
264302
process_body(input, node, render_elements)
265303
}

src/node_kind.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ pub enum GDScriptNodeKind {
109109
KeywordStatic,
110110

111111
// Type annotations
112+
Type,
113+
Subscript,
112114
InferredType,
113115

114116
// Meta
@@ -237,6 +239,8 @@ const MAP_TREE_SITTER_TO_GDSCRIPT_NODE_KIND: &[(&str, GDScriptNodeKind)] = &[
237239
("parameters", GDScriptNodeKind::Parameters),
238240
("arguments", GDScriptNodeKind::Arguments),
239241
("subscript_arguments", GDScriptNodeKind::SubscriptArguments),
242+
("subscript", GDScriptNodeKind::Subscript),
243+
("type", GDScriptNodeKind::Type),
240244
("condition", GDScriptNodeKind::Condition),
241245
("conditional_expression", GDScriptNodeKind::Condition),
242246
("lambda", GDScriptNodeKind::Lambda),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generic type parameters must never break across lines, even when they would go
2+
# past max line length. Breaking brackets like Dictionary[ String, String] would
3+
# produce invalid GDScript.
4+
func return_nested_generic() -> Dictionary[String, Array[int]]:
5+
return { "a": [1, 2, 3], "b": [4, 5, 6] }
6+
7+
8+
func typed_variable() -> void:
9+
var d: Dictionary[String, int] = { }
10+
print(d)
11+
12+
13+
func parameterized() -> void:
14+
pass
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generic type parameters must never break across lines, even when they would go
2+
# past max line length. Breaking brackets like Dictionary[ String, String] would
3+
# produce invalid GDScript.
4+
func return_nested_generic() -> Dictionary[String, Array[int]]:
5+
return { "a": [1, 2, 3], "b": [4, 5, 6] }
6+
7+
8+
func typed_variable() -> void:
9+
var d: Dictionary[String, int] = {}
10+
print(d)
11+
12+
13+
func parameterized() -> void:
14+
pass

tests/integration_tests.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
/// matches the expected output file. See files in the ./input and ./expected
44
/// folders.
55
use gdscript_formatter::linter::{GDScriptLinter, LinterConfig};
6-
use gdscript_formatter::{FormatterConfiguration, QuoteStyle, format_gdscript};
6+
use gdscript_formatter::{
7+
FormatterConfiguration, PrinterConfiguration, QuoteStyle, format_gdscript,
8+
};
79
use similar::{ChangeTag, TextDiff};
810
use std::fs;
911
use std::path::Path;
@@ -192,6 +194,30 @@ lines"""
192194
assert_eq!(format_gdscript(input, &config).unwrap(), expected);
193195
}
194196

197+
#[test]
198+
fn generic_type_parameters_never_break() {
199+
// Type-level generic parameters like Dictionary[String, String] must
200+
// stay on one line even when max_line_length would otherwise force a
201+
// break. Splitting the brackets produces invalid GDScript.
202+
let input = "func test() -> Dictionary[String, String]:\n return {}\n";
203+
let config = FormatterConfiguration {
204+
printer: PrinterConfiguration {
205+
max_line_length: 10,
206+
..Default::default()
207+
},
208+
..Default::default()
209+
};
210+
let output = format_gdscript(input, &config).unwrap();
211+
// The generic type must remain on a single line.
212+
let expected_line_0 = "func test() -> Dictionary[String, String]:";
213+
assert!(
214+
output.lines().next() == Some(expected_line_0),
215+
"Expected first line to be '{}' but got '{}'",
216+
expected_line_0,
217+
output.lines().next().unwrap_or("")
218+
);
219+
}
220+
195221
#[test]
196222
fn editorconfig_applies_quote_style() {
197223
let mut config = FormatterConfiguration::default();

0 commit comments

Comments
 (0)