1use std::sync::Arc;
12
13use gdscript_base::TextRange;
14use gdscript_syntax::ast::{self, AstNode};
15use gdscript_syntax::{GdNode, SyntaxKind};
16use smol_str::SmolStr;
17
18use crate::cst::{self, AstPtr};
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct ItemTree {
23 pub class_name: Option<SmolStr>,
26 pub extends: Option<ExtendsRef>,
28 pub annotations: Vec<AnnotationItem>,
33 pub members: Vec<Member>,
35}
36
37impl ItemTree {
38 #[must_use]
40 pub fn member(&self, name: &str) -> Option<&Member> {
41 self.members.iter().find(|m| m.name() == Some(name))
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum ExtendsRef {
49 Name(SmolStr),
51 Path(SmolStr),
53 ScriptPath(SmolStr),
55 ScriptPathInner(SmolStr),
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Member {
65 Func(FuncItem),
67 Var(VarItem),
69 Const(ConstItem),
71 Signal(SignalItem),
73 Enum(EnumItem),
75 Class(InnerClassItem),
77}
78
79impl Member {
80 #[must_use]
82 pub fn name(&self) -> Option<&str> {
83 match self {
84 Self::Func(f) => Some(&f.name),
85 Self::Var(v) => Some(&v.name),
86 Self::Const(c) => Some(&c.name),
87 Self::Signal(s) => Some(&s.name),
88 Self::Enum(e) => e.name.as_deref(),
89 Self::Class(c) => Some(&c.name),
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ParamItem {
97 pub name: SmolStr,
99 pub type_ref: Option<SmolStr>,
101 pub has_default: bool,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct AnnotationItem {
111 pub name: SmolStr,
113 pub range: TextRange,
115}
116
117#[must_use]
119pub fn has_annotation(annotations: &[AnnotationItem], name: &str) -> bool {
120 annotations.iter().any(|a| a.name == name)
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct FuncItem {
126 pub name: SmolStr,
128 pub params: Vec<ParamItem>,
130 pub return_type: Option<SmolStr>,
132 pub tuple_return: Option<Vec<SmolStr>>,
139 pub is_static: bool,
141 pub is_vararg: bool,
145 pub annotations: Vec<AnnotationItem>,
147 pub ptr: AstPtr,
149 pub range: TextRange,
151 pub name_range: TextRange,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157#[allow(
158 clippy::struct_excessive_bools,
159 reason = "independent declaration facts of a `var` (static / exported / has-init / inferred); not a state machine to encode as an enum"
160)]
161pub struct VarItem {
162 pub name: SmolStr,
164 pub type_ref: Option<SmolStr>,
166 pub is_static: bool,
168 pub is_exported: bool,
172 pub annotations: Vec<AnnotationItem>,
174 pub has_init: bool,
176 pub is_inferred: bool,
178 pub ptr: AstPtr,
180 pub range: TextRange,
182 pub name_range: TextRange,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct ConstItem {
189 pub name: SmolStr,
191 pub type_ref: Option<SmolStr>,
193 pub preload_path: Option<SmolStr>,
199 pub annotations: Vec<AnnotationItem>,
201 pub ptr: AstPtr,
203 pub range: TextRange,
205 pub name_range: TextRange,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct SignalItem {
212 pub name: SmolStr,
214 pub params: Vec<ParamItem>,
216 pub annotations: Vec<AnnotationItem>,
218 pub range: TextRange,
220 pub name_range: TextRange,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct EnumItem {
227 pub name: Option<SmolStr>,
230 pub variants: Vec<SmolStr>,
232 pub range: TextRange,
234 pub name_range: TextRange,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct InnerClassItem {
241 pub name: SmolStr,
243 pub tree: ItemTree,
245 pub range: TextRange,
247 pub name_range: TextRange,
249}
250
251#[must_use]
253pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
254 let Some(file) = ast::SourceFile::cast(root.clone()) else {
255 return Arc::new(ItemTree::default());
256 };
257 Arc::new(lower_class(root, file.decls()))
258}
259
260fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
263 let mut tree = ItemTree {
264 extends: find_extends(container),
265 annotations: container_annotations(container),
266 ..ItemTree::default()
267 };
268 for decl in decls {
269 match decl {
270 ast::Decl::ClassName(d) => {
271 if let Some(name) = decl_name(d.name()) {
272 tree.class_name = Some(name);
273 }
274 }
275 ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
276 ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
277 ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
278 ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
279 ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
280 ast::Decl::Class(d) => {
281 if let Some(item) = lower_inner_class(&d) {
282 tree.members.push(Member::Class(item));
283 }
284 }
285 }
286 }
287 tree
288}
289
290fn lower_func(d: &ast::FuncDecl) -> FuncItem {
291 let node = d.syntax();
292 FuncItem {
293 name: decl_name(d.name()).unwrap_or_default(),
294 params: d
295 .param_list()
296 .map(|pl| lower_params(&pl))
297 .unwrap_or_default(),
298 return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
299 tuple_return: doc_tuple_return(node),
300 is_static: d.is_static(),
301 is_vararg: d.param_list().is_some_and(|pl| {
302 pl.syntax()
303 .children()
304 .any(|c| c.kind() == SyntaxKind::VarargParam)
305 }),
306 annotations: preceding_annotations(node),
307 ptr: AstPtr::of(node),
308 range: cst::text_range_of(node),
309 name_range: name_range(d.name(), node),
310 }
311}
312
313fn doc_tuple_return(node: &GdNode) -> Option<Vec<SmolStr>> {
318 use cstree::util::NodeOrToken;
319 for el in node.children_with_tokens() {
320 match el {
321 NodeOrToken::Token(t) if t.kind() == SyntaxKind::DocComment => {
322 if let Some(names) = parse_return_tuple_tag(t.text()) {
323 return Some(names);
324 }
325 }
326 NodeOrToken::Token(t) if !t.kind().is_trivia() && t.kind() != SyntaxKind::Newline => {
330 break;
331 }
332 NodeOrToken::Node(_) => break,
333 NodeOrToken::Token(_) => {}
334 }
335 }
336 None
337}
338
339fn parse_return_tuple_tag(text: &str) -> Option<Vec<SmolStr>> {
342 let at = text.find("@return-tuple(")?;
343 let rest = &text[at + "@return-tuple(".len()..];
344 let inner = &rest[..rest.find(')')?];
345 let names: Vec<SmolStr> = inner
346 .split(',')
347 .map(str::trim)
348 .filter(|s| !s.is_empty())
349 .map(SmolStr::new)
350 .collect();
351 (names.len() >= 2
353 && names.iter().all(|n| {
354 n.chars()
355 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
356 }))
357 .then_some(names)
358}
359
360fn lower_var(d: &ast::VarDecl) -> VarItem {
361 let node = d.syntax();
362 let annotations = preceding_annotations(node);
363 VarItem {
364 name: decl_name(d.name()).unwrap_or_default(),
365 type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
366 is_static: d.is_static(),
367 is_exported: is_exported(&annotations),
368 annotations,
369 has_init: cst::first_child_expr(node).is_some(),
370 is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
371 ptr: AstPtr::of(node),
372 range: cst::text_range_of(node),
373 name_range: name_range(d.name(), node),
374 }
375}
376
377fn is_exported(annotations: &[AnnotationItem]) -> bool {
380 annotations
381 .iter()
382 .any(|a| a.name == "export" || a.name.starts_with("export_"))
383}
384
385fn preceding_annotations(node: &GdNode) -> Vec<AnnotationItem> {
389 let mut out = Vec::new();
390 let mut sib = node.prev_sibling();
391 while let Some(s) = sib {
392 if s.kind() != SyntaxKind::Annotation {
393 break;
394 }
395 if let Some(item) = annotation_item(s) {
396 out.push(item);
397 }
398 sib = s.prev_sibling();
399 }
400 out.reverse(); out
402}
403
404fn container_annotations(container: &GdNode) -> Vec<AnnotationItem> {
407 container
408 .children()
409 .filter(|c| c.kind() == SyntaxKind::Annotation)
410 .filter_map(annotation_item)
411 .collect()
412}
413
414fn annotation_item(ann: &GdNode) -> Option<AnnotationItem> {
416 use cstree::util::NodeOrToken;
417 ann.children_with_tokens()
418 .filter_map(NodeOrToken::into_token)
419 .find(|t| t.kind() == SyntaxKind::Ident)
420 .map(|t| AnnotationItem {
421 name: SmolStr::new(t.text()),
422 range: cst::token_range(t),
423 })
424}
425
426fn lower_const(d: &ast::ConstDecl) -> ConstItem {
427 let node = d.syntax();
428 let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
431 .and_then(ast::TypeRef::cast)
432 .and_then(|t| t.text())
433 .map(SmolStr::new);
434 ConstItem {
435 name: decl_name(d.name()).unwrap_or_default(),
436 type_ref,
437 preload_path: const_preload_path(node),
438 annotations: preceding_annotations(node),
439 ptr: AstPtr::of(node),
440 range: cst::text_range_of(node),
441 name_range: name_range(d.name(), node),
442 }
443}
444
445fn const_preload_path(const_decl: &GdNode) -> Option<SmolStr> {
450 let preload = cst::first_child(const_decl, |k| k == SyntaxKind::PreloadExpr)?;
451 let arg = cst::first_child(&preload, |k| k == SyntaxKind::ArgList)
452 .and_then(|al| cst::first_child_expr(&al))?;
453 if arg.kind() != SyntaxKind::Literal {
454 return None;
455 }
456 cst::child_token_text(&arg, SyntaxKind::String)
457 .map(|s| SmolStr::new(s.trim_matches(['"', '\''])))
458}
459
460fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
461 let node = d.syntax();
462 SignalItem {
463 name: decl_name(d.name()).unwrap_or_default(),
464 params: d
465 .param_list()
466 .map(|pl| lower_params(&pl))
467 .unwrap_or_default(),
468 annotations: preceding_annotations(node),
469 range: cst::text_range_of(node),
470 name_range: name_range(d.name(), node),
471 }
472}
473
474fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
475 let node = d.syntax();
476 EnumItem {
477 name: decl_name(d.name()),
478 variants: d
479 .variants()
480 .filter_map(|v| v.text())
481 .map(SmolStr::new)
482 .collect(),
483 range: cst::text_range_of(node),
484 name_range: name_range(d.name(), node),
485 }
486}
487
488fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
489 let node = d.syntax();
490 let name = decl_name(d.name())?;
491 let mut tree = d
492 .body()
493 .map(|b| lower_class(b.syntax(), b.decls()))
494 .unwrap_or_default();
495 tree.extends = find_extends(node);
498 Some(InnerClassItem {
499 name,
500 tree,
501 range: cst::text_range_of(node),
502 name_range: name_range(d.name(), node),
503 })
504}
505
506fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
507 pl.params()
508 .map(|p| ParamItem {
509 name: decl_name(p.name()).unwrap_or_default(),
510 type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
511 has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
512 || cst::has_token(p.syntax(), SyntaxKind::Eq)
513 || cst::first_child_expr(p.syntax()).is_some(),
514 })
515 .collect()
516}
517
518fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
524 if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
525 return parse_extends_tokens(&clause);
526 }
527 if cst::has_token(container, SyntaxKind::ExtendsKw) {
528 return parse_extends_tokens(container);
529 }
530 None
531}
532
533fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
535 let idents: Vec<String> = node
538 .children_with_tokens()
539 .filter_map(cstree::util::NodeOrToken::into_token)
540 .filter(|t| t.kind() == SyntaxKind::Ident)
541 .map(|t| t.text().to_owned())
542 .collect();
543 if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
547 let path = SmolStr::new(s.trim_matches(['"', '\'']));
548 return Some(if idents.is_empty() {
549 ExtendsRef::ScriptPath(path)
550 } else {
551 ExtendsRef::ScriptPathInner(path)
552 });
553 }
554 match idents.len() {
556 0 => None,
557 1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
558 _ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
559 }
560}
561
562fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
563 name.and_then(|n| n.text()).map(SmolStr::new)
564}
565
566fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
574 name.map_or_else(
575 || cst::text_range_of(decl),
576 |n| trimmed_name_range(n.syntax()),
577 )
578}
579
580fn trimmed_name_range(name_node: &GdNode) -> TextRange {
583 let r = cst::text_range_of(name_node);
584 let text = name_node.text().to_string();
585 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
586 let len = u32::try_from(text.trim().len()).unwrap_or(0);
587 TextRange::new(r.start + lead, r.start + lead + len)
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use gdscript_syntax::parse;
594
595 fn tree_of(src: &str) -> Arc<ItemTree> {
596 item_tree(&parse(src).syntax_node())
597 }
598
599 #[test]
600 fn class_header_and_members() {
601 let tree = tree_of(
602 "class_name Foo\nextends Node2D\nconst K = 1\nvar x: int\nstatic var s := 2\nsignal hit(dmg: int)\nenum E { A, B }\nfunc f(a: int, b := 1) -> void:\n\tpass\n",
603 );
604 assert_eq!(tree.class_name.as_deref(), Some("Foo"));
605 assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
606 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
607 assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
608 }
609
610 #[test]
611 fn func_signature() {
612 let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
613 let Member::Func(f) = &tree.members[0] else {
614 panic!("expected func")
615 };
616 assert_eq!(f.name, "add");
617 assert_eq!(f.return_type.as_deref(), Some("int"));
618 assert_eq!(f.params.len(), 2);
619 assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
620 assert!(!f.params[0].has_default);
621 assert!(f.params[1].has_default);
622 }
623
624 #[test]
625 fn soft_keyword_names_are_not_dropped() {
626 let tree =
630 tree_of("var when := 1\nfunc match(when: int):\n\tpass\nenum E { match, when }\n");
631 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
632 assert_eq!(names, vec!["when", "match", "E"]);
633 let Some(Member::Func(f)) = tree.member("match") else {
634 panic!("expected a func named `match`")
635 };
636 assert_eq!(f.params[0].name, "when");
637 let Some(Member::Enum(e)) = tree.member("E") else {
638 panic!("expected enum E")
639 };
640 assert_eq!(
641 e.variants,
642 vec![SmolStr::new("match"), SmolStr::new("when")]
643 );
644 }
645
646 #[test]
647 fn var_init_and_inference_flags() {
648 let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
649 let vars: Vec<&VarItem> = tree
650 .members
651 .iter()
652 .filter_map(|m| match m {
653 Member::Var(v) => Some(v),
654 _ => None,
655 })
656 .collect();
657 assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
659 assert!(vars[0].has_init && !vars[0].is_inferred);
660 assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
662 assert!(!vars[2].has_init && vars[2].type_ref.is_none());
664 assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
666 }
667
668 #[test]
669 fn extends_script_path() {
670 let tree = tree_of("extends \"res://player.gd\"\n");
671 assert_eq!(
672 tree.extends,
673 Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
674 );
675 }
676
677 #[test]
678 fn extends_script_path_with_inner_class_is_distinguished() {
679 let tree = tree_of("extends \"res://base.gd\".Inner\n");
682 assert_eq!(
683 tree.extends,
684 Some(ExtendsRef::ScriptPathInner(SmolStr::new("res://base.gd"))),
685 "the trailing .Inner must be detected, not dropped"
686 );
687 }
688
689 #[test]
690 fn anonymous_enum_has_no_name_but_variants() {
691 let tree = tree_of("enum { RED, GREEN, BLUE }\n");
692 let Member::Enum(e) = &tree.members[0] else {
693 panic!("expected enum")
694 };
695 assert!(e.name.is_none());
696 assert_eq!(
697 e.variants,
698 vec![
699 SmolStr::new("RED"),
700 SmolStr::new("GREEN"),
701 SmolStr::new("BLUE")
702 ]
703 );
704 }
705
706 #[test]
707 fn inner_class_members_and_extends() {
708 let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
709 let Member::Class(inner) = &tree.members[0] else {
710 panic!("expected inner class")
711 };
712 assert_eq!(inner.name, "Inner");
713 let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
714 assert_eq!(names, vec!["y", "m"]);
715 assert_eq!(
716 inner.tree.extends,
717 Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
718 );
719 }
720
721 #[test]
722 fn annotations_are_captured_first_class() {
723 let tree = tree_of(
724 "@tool\nextends Node\n@export var speed = 5\n@onready var label = null\n@rpc(\"any_peer\")\nfunc ping():\n\tpass\n",
725 );
726 assert!(
728 has_annotation(&tree.annotations, "tool"),
729 "{:?}",
730 tree.annotations
731 );
732 let var = |name: &str| {
733 tree.members.iter().find_map(|m| match m {
734 Member::Var(v) if v.name == name => Some(v),
735 _ => None,
736 })
737 };
738 let speed = var("speed").unwrap();
739 assert!(has_annotation(&speed.annotations, "export"));
740 assert!(speed.is_exported, "@export derives is_exported");
741 assert!(has_annotation(
742 &var("label").unwrap().annotations,
743 "onready"
744 ));
745 let ping = tree.members.iter().find_map(|m| match m {
747 Member::Func(f) if f.name == "ping" => Some(f),
748 _ => None,
749 });
750 assert!(has_annotation(&ping.unwrap().annotations, "rpc"));
751 }
752
753 #[test]
754 fn ptr_round_trips_to_node() {
755 let parse = parse("func f():\n\tpass\n");
756 let root = parse.syntax_node();
757 let tree = item_tree(&root);
758 let Member::Func(f) = &tree.members[0] else {
759 panic!()
760 };
761 let node = f.ptr.to_node(&root).expect("func node recovered");
762 assert_eq!(node.kind(), SyntaxKind::FuncDecl);
763 }
764}