Skip to content

Commit eafd8f0

Browse files
committed
Fix reordering moving exported variables below regular variables when
their annotation is on a separate line Close #347
1 parent 7cab4d7 commit eafd8f0

2 files changed

Lines changed: 131 additions & 94 deletions

File tree

‎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
- Fix parse error when annotation and a const variable are on the same line (#329)
1010
- Fix export subgroup annotation wrapping the next property on the same line (#358)
11+
- Fix reordering moving exported variables below regular variables when their annotation is on a separate line (#347)
1112

1213
## Release 0.26.1 (2026-09-15)
1314

‎src/reorder.rs‎

Lines changed: 130 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
use crate::node_kind::GDScriptNodeKind;
1010
use tree_sitter::Node;
1111

12-
// Public types
13-
1412
#[derive(Debug, Clone)]
1513
pub struct ReorderPlan<'a> {
1614
pub items: Vec<ReorderItem<'a>>,
@@ -40,7 +38,7 @@ pub struct ReorderItem<'a> {
4038
/// Declaration name for tie-breaking within same category, borrowed from
4139
/// the source string.
4240
pub name: &'a str,
43-
pub is_private: bool,
41+
pub is_pseudo_private: bool,
4442
pub method_type: Option<MethodType>,
4543
/// When true, the class_name_statement node contains an inline extends
4644
/// child that should be skipped when building (emitted as separate item).
@@ -76,6 +74,36 @@ pub enum MethodType {
7674
Custom, // all other user methods
7775
}
7876

77+
/// Result of classifying a single child node during reorder planning.
78+
struct ChildClassification<'a> {
79+
classification: DeclarationKind,
80+
name: &'a str,
81+
method_type: Option<MethodType>,
82+
/// If true, the extends child of a class_name_statement should be split out during reorder.
83+
split_extends: bool,
84+
}
85+
86+
impl<'a> ChildClassification<'a> {
87+
/// Builds a classification with no method type and no split extends. This
88+
/// covers the common case for non-function declarations.
89+
fn new(classification: DeclarationKind, name: &'a str) -> Self {
90+
Self {
91+
classification,
92+
name,
93+
method_type: None,
94+
split_extends: false,
95+
}
96+
}
97+
}
98+
99+
/// Used to track if we found an annotation written on its own line before a
100+
/// declaration.
101+
#[derive(Default)]
102+
struct VisitedAnnotation {
103+
has_export_annotation: bool,
104+
has_onready_annotation: bool,
105+
}
106+
79107
/// Slice the source string at a node's byte range.
80108
/// Tree-sitter byte offsets are always on UTF-8 char boundaries.
81109
fn get_node_text<'a>(node: Node<'a>, content: &'a str) -> &'a str {
@@ -115,6 +143,21 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
115143
let mut is_child_attached_to_declaration = vec![false; child_count];
116144
let mut is_region_end = vec![false; child_count];
117145

146+
// We use this to track if we visited a line with an annotation before
147+
// encountering a declaration.
148+
//
149+
// We need something like this with the current code structure because in
150+
// the AST, annotations can take this form:
151+
//
152+
// ```
153+
// (annotation)
154+
// (variable_statement)
155+
// ```
156+
//
157+
// And we want to group onready and export variables separately in the class
158+
// header.
159+
let mut visited_annotations = VisitedAnnotation::default();
160+
118161
let mut child_index = 0;
119162
while child_index < child_count {
120163
let Some(child) = parent.child(child_index as u32) else {
@@ -137,20 +180,27 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
137180
has_blank_line_before: false,
138181
classification: DeclarationKind::ClassAnnotation,
139182
name: annotation_name,
140-
is_private: false,
183+
is_pseudo_private: false,
141184
method_type: None,
142185
split_extends: false,
143186
});
144187
} else {
145188
is_child_attached_to_declaration[child_index] = true;
189+
if let Some(annotation_identifier) = get_annotation_identifier(child, content) {
190+
if is_inline_export_annotation(annotation_identifier) {
191+
visited_annotations.has_export_annotation = true;
192+
} else if annotation_identifier == "onready" {
193+
visited_annotations.has_onready_annotation = true;
194+
}
195+
}
146196
}
147197
} else if kind == GDScriptNodeKind::RegionEnd {
148198
is_child_attached_to_declaration[child_index] = true;
149199
is_region_end[child_index] = true;
150200
} else if kind == GDScriptNodeKind::SemiColon {
151201
// skip; handled by builder spacing
152202
} else {
153-
let child_classification = classify_child(child, content);
203+
let child_classification = classify_child(child, content, &visited_annotations);
154204
let is_private = child_classification.name.starts_with('_');
155205
items.push(ReorderItem {
156206
child_index,
@@ -160,7 +210,7 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
160210
has_blank_line_before: false,
161211
classification: child_classification.classification,
162212
name: child_classification.name,
163-
is_private,
213+
is_pseudo_private: is_private,
164214
method_type: child_classification.method_type,
165215
split_extends: child_classification.split_extends,
166216
});
@@ -175,23 +225,21 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
175225
has_blank_line_before: false,
176226
classification: DeclarationKind::Extends,
177227
name: "",
178-
is_private: false,
228+
is_pseudo_private: false,
179229
method_type: None,
180230
split_extends: false,
181231
});
182232
}
183233
}
234+
235+
visited_annotations = VisitedAnnotation::default();
184236
}
185237
child_index += 1;
186238
}
187239

188-
// Every item pushed so far corresponds to a real declaration (possibly a
189-
// split-off extends). The docstring item, pushed below, is appended after
190-
// this point, so this count also bounds pass 2's iteration.
191240
let declaration_count = items.len();
192241

193-
// Pass 1b: find class docstring: `##` comments in the header zone
194-
// (after class_name/extends/annotations, before first signal/enum/etc).
242+
// Pass 1b: find class docstring after class_name/extends/annotations, before first signal/enum/etc.
195243
let mut docstring_indices = Vec::new();
196244
let mut last_header_child_index: Option<usize> = None;
197245
let mut last_header_end_byte: Option<usize> = None;
@@ -263,13 +311,13 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
263311
has_blank_line_before: false,
264312
classification: DeclarationKind::Docstring,
265313
name: "",
266-
is_private: false,
314+
is_pseudo_private: false,
267315
method_type: None,
268316
split_extends: false,
269317
});
270318
}
271319

272-
// Pass 2: assign source children before and after each declaration.
320+
// Pass 2: assign AST nodes before and after each declaration.
273321
let mut previous_declaration_child_index: Option<usize> = None;
274322
let mut declaration_index = 0;
275323
while declaration_index < declaration_count {
@@ -281,9 +329,10 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
281329
None
282330
};
283331

284-
// Attach every eligible source child between the previous declaration
285-
// and this one before the current declaration.
286-
let first_possible_attachment_child_index: usize = match previous_declaration_child_index {
332+
// Attach every relevant AST node between the previous declaration and
333+
// this one before the current declaration.
334+
let first_possible_attachment_child_index: usize = match
335+
previous_declaration_child_index {
287336
Some(previous_declaration_child_index) => previous_declaration_child_index + 1,
288337
None => 0,
289338
};
@@ -370,41 +419,17 @@ pub fn build_reorder_plan<'a>(parent: Node<'a>, content: &'a str) -> ReorderPlan
370419
declaration_index += 1;
371420
}
372421

373-
// Pass 3: sort.
374422
items.sort_by(compare_reorder_items);
375-
376423
ReorderPlan { items }
377424
}
378425

379-
/// Result of classifying a single child node during reorder planning.
380-
struct ChildClassification<'a> {
381-
classification: DeclarationKind,
382-
name: &'a str,
383-
method_type: Option<MethodType>,
384-
/// If true, the extends child of a class_name_statement should be split out during reorder.
385-
split_extends: bool,
386-
}
387-
388-
impl<'a> ChildClassification<'a> {
389-
/// Builds a classification with no method type and no split extends. This
390-
/// covers the common case for non-function declarations.
391-
fn new(classification: DeclarationKind, name: &'a str) -> Self {
392-
Self {
393-
classification,
394-
name,
395-
method_type: None,
396-
split_extends: false,
397-
}
398-
}
399-
}
400-
401-
fn classify_child<'a>(node: Node<'a>, content: &'a str) -> ChildClassification<'a> {
426+
fn classify_child<'a>(
427+
node: Node<'a>,
428+
content: &'a str,
429+
visited_annotations: &VisitedAnnotation,
430+
) -> ChildClassification<'a> {
402431
let kind = GDScriptNodeKind::get_kind_from_ast_node(node);
403432
match kind {
404-
GDScriptNodeKind::Annotation => {
405-
let name = get_node_text(node, content);
406-
ChildClassification::new(DeclarationKind::ClassAnnotation, name)
407-
}
408433
GDScriptNodeKind::ClassName => {
409434
let extends_index = find_extends_child_index(node);
410435
let name = extract_name(node, content).unwrap_or("unknown_class");
@@ -431,7 +456,7 @@ fn classify_child<'a>(node: Node<'a>, content: &'a str) -> ChildClassification<'
431456
let name = extract_name(node, content).unwrap_or("unknown_const");
432457
ChildClassification::new(DeclarationKind::Constant, name)
433458
}
434-
GDScriptNodeKind::Variable => classify_variable(node, content),
459+
GDScriptNodeKind::Variable => classify_variable(node, content, visited_annotations),
435460
GDScriptNodeKind::ExportVariable => {
436461
let name = extract_name(node, content).unwrap_or("unknown_var");
437462
ChildClassification::new(DeclarationKind::ExportVariable, name)
@@ -475,25 +500,15 @@ fn classify_child<'a>(node: Node<'a>, content: &'a str) -> ChildClassification<'
475500
}
476501
}
477502

478-
fn classify_variable<'a>(node: Node<'a>, content: &'a str) -> ChildClassification<'a> {
479-
fn get_annotation_identifier<'a>(annotation: Node<'a>, content: &'a str) -> Option<&'a str> {
480-
let child_count = annotation.child_count();
481-
let mut child_index = 0;
482-
while child_index < child_count {
483-
if let Some(child) = annotation.child(child_index as u32) {
484-
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Identifier {
485-
return Some(get_node_text(child, content));
486-
}
487-
}
488-
child_index += 1;
489-
}
490-
None
491-
}
492-
503+
fn classify_variable<'a>(
504+
node: Node<'a>,
505+
content: &'a str,
506+
visited_annotations: &VisitedAnnotation,
507+
) -> ChildClassification<'a> {
493508
let name = extract_name(node, content).unwrap_or("unknown_var");
494509

495-
let mut has_export_annotation = false;
496-
let mut has_onready_annotation = false;
510+
let mut has_export_annotation = visited_annotations.has_export_annotation;
511+
let mut has_onready_annotation = visited_annotations.has_onready_annotation;
497512
for child_index in 0..node.child_count() {
498513
let Some(child) = node.child(child_index as u32) else {
499514
continue;
@@ -514,7 +529,7 @@ fn classify_variable<'a>(node: Node<'a>, content: &'a str) -> ChildClassificatio
514529
let Some(annotation_name) = get_annotation_identifier(annotation, content) else {
515530
continue;
516531
};
517-
if annotation_name.starts_with("export") {
532+
if is_inline_export_annotation(annotation_name) {
518533
has_export_annotation = true;
519534
} else if annotation_name == "onready" {
520535
has_onready_annotation = true;
@@ -533,6 +548,32 @@ fn classify_variable<'a>(node: Node<'a>, content: &'a str) -> ChildClassificatio
533548
}
534549
}
535550

551+
/// Returns the identifier name of an annotation node, e.g. `export_range` for
552+
/// `@export_range(0, 100)`.
553+
fn get_annotation_identifier<'a>(annotation: Node<'a>, content: &'a str) -> Option<&'a str> {
554+
let child_count = annotation.child_count();
555+
let mut child_index = 0;
556+
while child_index < child_count {
557+
if let Some(child) = annotation.child(child_index as u32)
558+
&& GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Identifier
559+
{
560+
return Some(get_node_text(child, content));
561+
}
562+
child_index += 1;
563+
}
564+
None
565+
}
566+
567+
/// Returns true when an annotation exports the variable it annotates. The
568+
/// `@export_group` and `@export_subgroup` annotations only mark groups of
569+
/// following properties and must not make an unannotated variable look
570+
/// exported.
571+
fn is_inline_export_annotation(annotation_name: &str) -> bool {
572+
annotation_name.starts_with("export")
573+
&& annotation_name != "export_group"
574+
&& annotation_name != "export_subgroup"
575+
}
576+
536577
/// Extract the "name" field child from a declaration node.
537578
fn extract_name<'a>(node: Node<'a>, content: &'a str) -> Option<&'a str> {
538579
let count = node.child_count();
@@ -615,49 +656,44 @@ fn get_builtin_virtual_priority(method_name: &str) -> u8 {
615656
}
616657

617658
fn compare_reorder_items(left: &ReorderItem, right: &ReorderItem) -> std::cmp::Ordering {
618-
// 1. DeclarationKind (numeric discriminant)
619-
let kind_cmp = (left.classification as u8).cmp(&(right.classification as u8));
620-
if kind_cmp != std::cmp::Ordering::Equal {
621-
return kind_cmp;
659+
let ordering_declaration_kind = (left.classification as u8).cmp(&(right.classification as u8));
660+
if ordering_declaration_kind != std::cmp::Ordering::Equal {
661+
return ordering_declaration_kind;
622662
}
623663

624-
// 2. MethodType sub-sorting for Method items
625664
if let (Some(method_type_left), Some(method_type_right)) = (left.method_type, right.method_type)
626665
{
627-
let type_cmp = method_type_left.cmp(&method_type_right);
628-
if type_cmp != std::cmp::Ordering::Equal {
629-
return type_cmp;
666+
let ordering_method_type = method_type_left.cmp(&method_type_right);
667+
if ordering_method_type != std::cmp::Ordering::Equal {
668+
return ordering_method_type;
630669
}
631670
}
632671

633-
// 3. Privacy: public before pseudo-private
634-
let privacy_cmp = left.is_private.cmp(&right.is_private);
635-
if privacy_cmp != std::cmp::Ordering::Equal {
636-
return privacy_cmp;
672+
let ordering_pseudo_private = left.is_pseudo_private.cmp(&right.is_pseudo_private);
673+
if ordering_pseudo_private != std::cmp::Ordering::Equal {
674+
return ordering_pseudo_private;
637675
}
638676

639-
// 4. ClassAnnotation special ordering: @tool < @icon < other
677+
// Class annotations have a specific order: @tool < @icon < other potential
678+
// annotations
640679
if left.classification == DeclarationKind::ClassAnnotation
641680
&& right.classification == DeclarationKind::ClassAnnotation
642681
{
643-
let priority_left = annotation_priority(left.name);
644-
let priority_right = annotation_priority(right.name);
645-
let annotation_cmp = priority_left.cmp(&priority_right);
646-
if annotation_cmp != std::cmp::Ordering::Equal {
647-
return annotation_cmp;
682+
fn get_annotation_priority(text: &str) -> u8 {
683+
match text {
684+
"@tool" => 0,
685+
"@icon" => 1,
686+
_ => 2,
687+
}
688+
}
689+
690+
let priority_left = get_annotation_priority(left.name);
691+
let priority_right = get_annotation_priority(right.name);
692+
let ordering_class_annotations = priority_left.cmp(&priority_right);
693+
if ordering_class_annotations != std::cmp::Ordering::Equal {
694+
return ordering_class_annotations;
648695
}
649696
}
650697

651-
// 5. Stable: original source order (child_index)
652698
left.child_index.cmp(&right.child_index)
653699
}
654-
655-
fn annotation_priority(text: &str) -> u8 {
656-
if text.starts_with("@tool") {
657-
0
658-
} else if text.starts_with("@icon") {
659-
1
660-
} else {
661-
2
662-
}
663-
}

0 commit comments

Comments
 (0)