Skip to content

Commit f79212f

Browse files
committed
Fix line length calculation for complex expressions
Close #342
1 parent ca8445e commit f79212f

7 files changed

Lines changed: 222 additions & 25 deletions

‎CHANGELOG.md‎

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

99
- Fixed format and reorder commands having a different result with blank lines around regions containing functions (#337). The formatter will now apply blank line rules across region markers.
1010
- Fix long parenthesized expression chained with a method call in an assignment, not wrapping past the max line length (#338).
11+
- Fixed line length calculation for long expression chains with subscript accesses and other cases like attribute access with parenthesized expressions (#342)
1112

1213
## Release 0.25.0 (2026-09-06)
1314

‎src/formatter.rs‎

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -350,12 +350,19 @@ fn process_node(
350350
finish_group(render_elements, group_index);
351351
}
352352
GDScriptNodeKind::SetGet => process_setget(input, node, render_elements),
353-
GDScriptNodeKind::ParenthesizedExpression => {
354-
process_parenthesized_expression(input, node, render_elements)
353+
GDScriptNodeKind::ParenthesizedExpression
354+
| GDScriptNodeKind::Attribute
355+
| GDScriptNodeKind::Subscript
356+
| GDScriptNodeKind::Call => {
357+
// Wrapping the entire expression in a group helps with calculating
358+
// the length of code segments and knowing where to break lines for
359+
// vertical spacing in the renderer.
360+
let group_index = begin_group(render_elements);
361+
process_expression_content(input, node, render_elements);
362+
finish_group(render_elements, group_index);
355363
}
356364
GDScriptNodeKind::BinaryOperator => process_binary_operator(input, node, render_elements),
357365
GDScriptNodeKind::Condition => process_conditional_expression(input, node, render_elements),
358-
GDScriptNodeKind::Attribute => process_attribute(input, node, render_elements),
359366
_ => process_children_with_spacing(input, node, render_elements),
360367
}
361368
}
@@ -1890,16 +1897,37 @@ fn process_container(
18901897
}
18911898
}
18921899

1893-
/// Formats ParenthesizedExpression nodes with a Group. The group lets a long
1894-
/// expression use its parentheses as a safe break boundary instead of breaking
1895-
/// an enclosing line at an operator inside the expression. Inner constructs
1896-
/// that already handle their own indentation (lambdas, arrays, dicts) keep
1897-
/// their specialized formatting.
1898-
fn process_parenthesized_expression(
1900+
/// Processes part of an expression and appends render elements, without
1901+
/// creating a new group. This recursively processes child nodes as an
1902+
/// expression can contain sub-expressions, operators, lambda functions, etc.
1903+
///
1904+
/// The IR for everything visited is added to `render_elements`.
1905+
fn process_expression_content(
18991906
input: &ParseInput,
19001907
node: tree_sitter::Node,
19011908
render_elements: &mut Vec<RenderElement>,
19021909
) {
1910+
match GDScriptNodeKind::get_kind_from_ast_node(node) {
1911+
GDScriptNodeKind::Attribute => {
1912+
process_attribute(input, node, render_elements);
1913+
return;
1914+
}
1915+
GDScriptNodeKind::Subscript | GDScriptNodeKind::Call => {
1916+
process_children_with_spacing(input, node, render_elements);
1917+
return;
1918+
}
1919+
GDScriptNodeKind::ParenthesizedExpression => {}
1920+
// As we dive down the AST, we can stumble upon anything. For example,
1921+
// an attribute call can be a method call that within the arguments has
1922+
// a lambda function wrapped in parentheses or anything else. When we
1923+
// stumble upon something like that, we need to process those nodes
1924+
// recursively.
1925+
_ => {
1926+
process_node(input, node, render_elements);
1927+
return;
1928+
}
1929+
}
1930+
19031931
let child_count = node.child_count();
19041932
if child_count < 3 {
19051933
process_children_with_spacing(input, node, render_elements);
@@ -1944,8 +1972,6 @@ fn process_parenthesized_expression(
19441972
return;
19451973
}
19461974

1947-
let group_index = begin_group(render_elements);
1948-
19491975
if let Some(open) = node.child(0) {
19501976
process_node(input, open, render_elements);
19511977
}
@@ -1979,8 +2005,6 @@ fn process_parenthesized_expression(
19792005
if let Some(close) = node.child((child_count - 1) as u32) {
19802006
process_node(input, close, render_elements);
19812007
}
1982-
1983-
finish_group(render_elements, group_index);
19842008
}
19852009

19862010
/// Finds and returns the unnamed operator token between a binary expression's
@@ -2459,14 +2483,24 @@ fn process_attribute(
24592483
render_elements: &mut Vec<RenderElement>,
24602484
) {
24612485
let child_count = node.child_count();
2462-
// Only handle dot-access chains (child_count >= 5: at least 2 method calls).
2463-
// Single method calls like a.foo() go through process_children_with_spacing.
2464-
let is_dot_chain = if let Some(c) = node.child(1) {
2465-
GDScriptNodeKind::get_kind_from_ast_node(c) == GDScriptNodeKind::TokenDot
2466-
} else {
2467-
false
2468-
};
2469-
if child_count < 5 || !is_dot_chain {
2486+
// An attribute node is an expression followed by dots using the dot
2487+
// accessor and accessing members or calling methods.
2488+
//
2489+
// If the chain has multiple dots, we need to specifically handle
2490+
// continuation lines. So we count the dots in the attribute chain and
2491+
// format accordingly.
2492+
let mut dot_count = 0;
2493+
let mut child_index = 0;
2494+
while child_index < child_count {
2495+
if let Some(child) = node.child(child_index as u32)
2496+
&& GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::TokenDot
2497+
{
2498+
dot_count += 1;
2499+
}
2500+
child_index += 1;
2501+
}
2502+
let has_multiple_dot_accesses = dot_count >= 2;
2503+
if !has_multiple_dot_accesses {
24702504
process_children_with_spacing(input, node, render_elements);
24712505
return;
24722506
}
@@ -2530,8 +2564,6 @@ fn process_attribute(
25302564
child_index += 1;
25312565
}
25322566

2533-
let group_index = begin_group(render_elements);
2534-
25352567
if let Some(expr) = node.child(0) {
25362568
process_node(input, expr, render_elements);
25372569
}
@@ -2652,7 +2684,6 @@ fn process_attribute(
26522684

26532685
attribute_index += 2;
26542686
}
2655-
finish_group(render_elements, group_index);
26562687
}
26572688

26582689
/// Builds a method call inside a dot-access chain. Its argument container is
@@ -3046,7 +3077,18 @@ fn process_children_with_spacing(
30463077
index += 1;
30473078
continue;
30483079
}
3049-
process_node(input, child, render_elements);
3080+
if index == 0
3081+
&& matches!(
3082+
parent_kind,
3083+
GDScriptNodeKind::Attribute
3084+
| GDScriptNodeKind::Subscript
3085+
| GDScriptNodeKind::Call
3086+
)
3087+
{
3088+
process_expression_content(input, child, render_elements);
3089+
} else {
3090+
process_node(input, child, render_elements);
3091+
}
30503092
previous = Some(child);
30513093
}
30523094
index += 1;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# This file tests long and complex expressions, especially parenthesized
2+
# expressions, subscripts, and nested expressions. This directly tests a line
3+
# length calculation for these expressions as there are just over 100 characters
4+
# (counting tab indents as = 4 spaces)
5+
func test_expressions():
6+
stored_direction_with_a_long_name = (
7+
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
8+
).normalized()
9+
stored_direction_with_a_long_name += (
10+
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
11+
)[index]
12+
return (
13+
first_direction_with_a_long_name + second_direction_with_a_long_name
14+
)[index].normalized()
15+
16+
consume(
17+
(first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized(),
18+
other_value,
19+
)
20+
var directions = [
21+
(first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized(),
22+
other_value,
23+
]
24+
var direction_by_name = {
25+
"forward": (
26+
first_direction_with_a_long_name + second_direction_with_a_long_name
27+
)[index].normalized()
28+
}
29+
if (
30+
first_direction_with_a_long_name + second_direction_with_a_long_name
31+
)[selected_index].is_normalized():
32+
pass
33+
34+
var result = (
35+
value
36+
).transform(
37+
first_argument_with_a_long_name,
38+
second_argument_with_a_long_name,
39+
third_argument_with_a_long_name,
40+
)
41+
var indexed = (
42+
value
43+
)[
44+
first_index_with_a_long_name + second_index_with_a_long_name
45+
+ third_index_with_an_extremely_long_name
46+
]
47+
var values = ([
48+
first_element_with_a_long_name,
49+
second_element_with_a_long_name,
50+
third_element_with_a_long_name,
51+
]).duplicate()
52+
var properties = ({
53+
"first": first_value_with_a_long_name,
54+
"second": second_value_with_a_long_name,
55+
}).duplicate()
56+
var doubled = (
57+
(first_direction_with_a_long_name + second_direction_with_a_long_name)
58+
)[index].normalized()
59+
var repeated_index = (
60+
first_direction_with_a_long_name + second_direction_with_a_long_name
61+
)[first_index][second_index]
62+
var selected = values[
63+
(
64+
first_index_with_a_long_name + second_index_with_a_long_name
65+
+ third_index_with_a_long_name
66+
)
67+
]
68+
var size = create_values(first_argument_with_a_long_name, second_argument_with_a_long_name)[
69+
index
70+
].size()
71+
var mapped = (values).map(
72+
func(value):
73+
return value * 2,
74+
)
75+
76+
var transformed = (first_direction + second_direction) \
77+
.first_transformation() \
78+
.second_transformation() \
79+
.third_transformation()
80+
var explicit = (first_direction + second_direction) \
81+
.first_transformation() \
82+
.second_transformation() \
83+
.third_transformation()

‎tests/expected/issue_338_parenthesized_expression.gd‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@ func _physics_process(delta: float) -> void:
33
var direction_with_a_long_name: Vector3 = (
44
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
55
).normalized()
6+
var basis_property_with_an_even_longer_name = (
7+
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
8+
).x
9+
var indexed_value_with_a_long_name = (
10+
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
11+
)[index]
12+
var indexed_direction_with_a_long_name = (
13+
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
14+
)[index].normalized()

‎tests/input/expression_chains.gd‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file tests long and complex expressions, especially parenthesized
2+
# expressions, subscripts, and nested expressions. This directly tests a line
3+
# length calculation for these expressions as there are just over 100 characters
4+
# (counting tab indents as = 4 spaces)
5+
func test_expressions():
6+
stored_direction_with_a_long_name = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
7+
stored_direction_with_a_long_name += (transform.basis * Vector3(input_dir.x, 0, input_dir.y))[index]
8+
return (first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized()
9+
10+
consume((first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized(), other_value)
11+
var directions = [(first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized(), other_value]
12+
var direction_by_name = {"forward": (first_direction_with_a_long_name + second_direction_with_a_long_name)[index].normalized()}
13+
if (first_direction_with_a_long_name + second_direction_with_a_long_name)[selected_index].is_normalized():
14+
pass
15+
16+
var result = (value).transform(first_argument_with_a_long_name, second_argument_with_a_long_name, third_argument_with_a_long_name)
17+
var indexed = (value)[first_index_with_a_long_name + second_index_with_a_long_name + third_index_with_an_extremely_long_name]
18+
var values = ([first_element_with_a_long_name, second_element_with_a_long_name, third_element_with_a_long_name]).duplicate()
19+
var properties = ({"first": first_value_with_a_long_name, "second": second_value_with_a_long_name}).duplicate()
20+
var doubled = ((first_direction_with_a_long_name + second_direction_with_a_long_name))[index].normalized()
21+
var repeated_index = (first_direction_with_a_long_name + second_direction_with_a_long_name)[first_index][second_index]
22+
var selected = values[(first_index_with_a_long_name + second_index_with_a_long_name + third_index_with_a_long_name)]
23+
var size = create_values(first_argument_with_a_long_name, second_argument_with_a_long_name)[index].size()
24+
var mapped = (values).map(func(value): return value * 2)
25+
26+
var transformed = (first_direction + second_direction).first_transformation().second_transformation().third_transformation()
27+
var explicit = (first_direction + second_direction) \
28+
.first_transformation() \
29+
.second_transformation() \
30+
.third_transformation()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# A long parenthesized expression with a method call would not wrap as expected.
22
func _physics_process(delta: float) -> void:
33
var direction_with_a_long_name: Vector3 = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
4+
var basis_property_with_an_even_longer_name = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).x
5+
var indexed_value_with_a_long_name = (transform.basis * Vector3(input_dir.x, 0, input_dir.y))[index]
6+
var indexed_direction_with_a_long_name = (transform.basis * Vector3(input_dir.x, 0, input_dir.y))[index].normalized()

‎tests/renderer_tests.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,35 @@ fn break_group_when_too_long() {
109109
assert_eq!(out, "aaaa\nbbbb\ncccc\ndddd\n");
110110
}
111111

112+
#[test]
113+
fn complete_line_width_includes_prefix_and_suffix() {
114+
let render_elements = vec![
115+
RenderElement::TextStatic("prefix = "),
116+
group(2, 9),
117+
RenderElement::TextStatic("("),
118+
RenderElement::SoftLine,
119+
indent(1, 5, 6),
120+
RenderElement::TextStatic("expression"),
121+
RenderElement::SoftLine,
122+
RenderElement::TextStatic(")"),
123+
RenderElement::TextStatic(".suffix()"),
124+
];
125+
let fits_config = PrinterConfiguration {
126+
max_line_length: 30,
127+
..get_default_printer_configuration()
128+
};
129+
let mut out = String::new();
130+
render(&render_elements, "", &fits_config, &mut out);
131+
assert_eq!(out, "prefix = (expression).suffix()\n");
132+
133+
let wraps_config = PrinterConfiguration {
134+
max_line_length: 29,
135+
..get_default_printer_configuration()
136+
};
137+
render(&render_elements, "", &wraps_config, &mut out);
138+
assert_eq!(out, "prefix = (\n expression\n).suffix()\n");
139+
}
140+
112141
#[test]
113142
fn balanced_group_distributes_segments_evenly() {
114143
let render_elements = vec![

0 commit comments

Comments
 (0)