Skip to content

Commit 997cc2c

Browse files
committed
Fix export annotation detection to avoid false positives
Fix #308
1 parent 4738e00 commit 997cc2c

4 files changed

Lines changed: 79 additions & 60 deletions

File tree

CHANGELOG.md

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

1313
- Fixed an extra comma being inserted after a trailing comment in a lambda function argument (#304)
14+
- fixed certain export annotations being moved out of their respective groups (#308)
1415

1516
## Release 0.24.0 (2026-07-25)
1617

src/reorder.rs

Lines changed: 46 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,6 @@ pub enum MethodType {
7676
Custom, // all other user methods
7777
}
7878

79-
/// Checks whether a single `annotation` node's `identifier` child has text
80-
/// equal to `expected_name`.
81-
fn annotation_identifier_matches(
82-
annotation_node: Node,
83-
content: &str,
84-
expected_name: &str,
85-
) -> bool {
86-
let count = annotation_node.child_count();
87-
let mut child_index = 0;
88-
while child_index < count {
89-
if let Some(child) = annotation_node.child(child_index as u32) {
90-
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Identifier {
91-
return get_node_text(child, content) == expected_name;
92-
}
93-
}
94-
child_index += 1;
95-
}
96-
false
97-
}
98-
9979
/// Slice the source string at a node's byte range.
10080
/// Tree-sitter byte offsets are always on UTF-8 char boundaries.
10181
fn get_node_text<'a>(node: Node<'a>, content: &'a str) -> &'a str {
@@ -496,11 +476,55 @@ fn classify_child<'a>(node: Node<'a>, content: &'a str) -> ChildClassification<'
496476
}
497477

498478
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+
499493
let name = extract_name(node, content).unwrap_or("unknown_var");
500494

501-
if has_annotation_with_name(node, content, "export") {
495+
let mut has_export_annotation = false;
496+
let mut has_onready_annotation = false;
497+
for child_index in 0..node.child_count() {
498+
let Some(child) = node.child(child_index as u32) else {
499+
continue;
500+
};
501+
if GDScriptNodeKind::get_kind_from_ast_node(child) != GDScriptNodeKind::Annotations {
502+
continue;
503+
}
504+
505+
for annotation_index in 0..child.child_count() {
506+
let Some(annotation) = child.child(annotation_index as u32) else {
507+
continue;
508+
};
509+
if GDScriptNodeKind::get_kind_from_ast_node(annotation) != GDScriptNodeKind::Annotation
510+
{
511+
continue;
512+
}
513+
514+
let Some(annotation_name) = get_annotation_identifier(annotation, content) else {
515+
continue;
516+
};
517+
if annotation_name.starts_with("export") {
518+
has_export_annotation = true;
519+
} else if annotation_name == "onready" {
520+
has_onready_annotation = true;
521+
}
522+
}
523+
}
524+
525+
if has_export_annotation {
502526
ChildClassification::new(DeclarationKind::ExportVariable, name)
503-
} else if has_annotation_with_name(node, content, "onready") {
527+
} else if has_onready_annotation {
504528
ChildClassification::new(DeclarationKind::OnReadyVariable, name)
505529
} else if has_static_keyword_child(node) {
506530
ChildClassification::new(DeclarationKind::StaticVariable, name)
@@ -554,44 +578,6 @@ fn has_static_keyword_child(node: Node) -> bool {
554578
false
555579
}
556580

557-
/// Returns true if `node` has an `annotations` child containing an
558-
/// `annotation` whose identifier matches `annotation_name`.
559-
///
560-
/// This replaces fragile source-text checks like `text.contains("@export")`
561-
/// which can falsely match comments or strings containing those substrings.
562-
fn has_annotation_with_name(node: Node, content: &str, annotation_name: &str) -> bool {
563-
let count = node.child_count();
564-
let mut child_index = 0;
565-
while child_index < count {
566-
if let Some(child) = node.child(child_index as u32) {
567-
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Annotations
568-
&& annotations_contain_name(child, content, annotation_name)
569-
{
570-
return true;
571-
}
572-
}
573-
child_index += 1;
574-
}
575-
false
576-
}
577-
578-
/// Walks the children of an `annotations` container node and checks whether
579-
/// any `annotation` child has an `identifier` whose text matches `name`.
580-
fn annotations_contain_name(annotations_node: Node, content: &str, name: &str) -> bool {
581-
let count = annotations_node.child_count();
582-
let mut child_index = 0;
583-
while child_index < count {
584-
if let Some(child) = annotations_node.child(child_index as u32) {
585-
if GDScriptNodeKind::get_kind_from_ast_node(child) == GDScriptNodeKind::Annotation
586-
&& annotation_identifier_matches(child, content, name)
587-
{
588-
return true;
589-
}
590-
}
591-
child_index += 1;
592-
}
593-
false
594-
}
595581
/// Maps built-in virtual method names to Godot lifecycle priority.
596582
fn get_builtin_virtual_priority(method_name: &str) -> u8 {
597583
match method_name {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class_name StatsData
2+
extends Resource
3+
4+
@export_category("Survival")
5+
@export_range(1, 999) var max_health: int
6+
7+
@export_category("Movement")
8+
@export var can_move: bool
9+
@export var movement_acceleration: float
10+
@export_range(0.01, 10.0) var movement_speed: float
11+
12+
@export_category("Perception")
13+
@export_group("Vision", "vision")
14+
@export var can_see: bool
15+
@export_subgroup("Details")
16+
@export_flags_2d_physics var vision_collision_layer: int
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class_name StatsData
2+
extends Resource
3+
4+
@export_category("Survival")
5+
@export_range(1, 999) var max_health: int
6+
7+
@export_category("Movement")
8+
@export var can_move: bool
9+
@export var movement_acceleration: float
10+
@export_range(0.01, 10.0) var movement_speed: float
11+
12+
@export_category("Perception")
13+
@export_group("Vision", "vision")
14+
@export var can_see: bool
15+
@export_subgroup("Details")
16+
@export_flags_2d_physics var vision_collision_layer: int

0 commit comments

Comments
 (0)