99use crate :: node_kind:: GDScriptNodeKind ;
1010use tree_sitter:: Node ;
1111
12- // Public types
13-
1412#[ derive( Debug , Clone ) ]
1513pub 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.
81109fn 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.
537578fn 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
617658fn 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