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 is_static: bool,
134 pub annotations: Vec<AnnotationItem>,
136 pub ptr: AstPtr,
138 pub range: TextRange,
140 pub name_range: TextRange,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146#[allow(
147 clippy::struct_excessive_bools,
148 reason = "independent declaration facts of a `var` (static / exported / has-init / inferred); not a state machine to encode as an enum"
149)]
150pub struct VarItem {
151 pub name: SmolStr,
153 pub type_ref: Option<SmolStr>,
155 pub is_static: bool,
157 pub is_exported: bool,
161 pub annotations: Vec<AnnotationItem>,
163 pub has_init: bool,
165 pub is_inferred: bool,
167 pub ptr: AstPtr,
169 pub range: TextRange,
171 pub name_range: TextRange,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct ConstItem {
178 pub name: SmolStr,
180 pub type_ref: Option<SmolStr>,
182 pub preload_path: Option<SmolStr>,
188 pub annotations: Vec<AnnotationItem>,
190 pub ptr: AstPtr,
192 pub range: TextRange,
194 pub name_range: TextRange,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct SignalItem {
201 pub name: SmolStr,
203 pub params: Vec<ParamItem>,
205 pub annotations: Vec<AnnotationItem>,
207 pub range: TextRange,
209 pub name_range: TextRange,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct EnumItem {
216 pub name: Option<SmolStr>,
219 pub variants: Vec<SmolStr>,
221 pub range: TextRange,
223 pub name_range: TextRange,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct InnerClassItem {
230 pub name: SmolStr,
232 pub tree: ItemTree,
234 pub range: TextRange,
236 pub name_range: TextRange,
238}
239
240#[must_use]
242pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
243 let Some(file) = ast::SourceFile::cast(root.clone()) else {
244 return Arc::new(ItemTree::default());
245 };
246 Arc::new(lower_class(root, file.decls()))
247}
248
249fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
252 let mut tree = ItemTree {
253 extends: find_extends(container),
254 annotations: container_annotations(container),
255 ..ItemTree::default()
256 };
257 for decl in decls {
258 match decl {
259 ast::Decl::ClassName(d) => {
260 if let Some(name) = decl_name(d.name()) {
261 tree.class_name = Some(name);
262 }
263 }
264 ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
265 ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
266 ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
267 ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
268 ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
269 ast::Decl::Class(d) => {
270 if let Some(item) = lower_inner_class(&d) {
271 tree.members.push(Member::Class(item));
272 }
273 }
274 }
275 }
276 tree
277}
278
279fn lower_func(d: &ast::FuncDecl) -> FuncItem {
280 let node = d.syntax();
281 FuncItem {
282 name: decl_name(d.name()).unwrap_or_default(),
283 params: d
284 .param_list()
285 .map(|pl| lower_params(&pl))
286 .unwrap_or_default(),
287 return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
288 is_static: d.is_static(),
289 annotations: preceding_annotations(node),
290 ptr: AstPtr::of(node),
291 range: cst::text_range_of(node),
292 name_range: name_range(d.name(), node),
293 }
294}
295
296fn lower_var(d: &ast::VarDecl) -> VarItem {
297 let node = d.syntax();
298 let annotations = preceding_annotations(node);
299 VarItem {
300 name: decl_name(d.name()).unwrap_or_default(),
301 type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
302 is_static: d.is_static(),
303 is_exported: is_exported(&annotations),
304 annotations,
305 has_init: cst::first_child_expr(node).is_some(),
306 is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
307 ptr: AstPtr::of(node),
308 range: cst::text_range_of(node),
309 name_range: name_range(d.name(), node),
310 }
311}
312
313fn is_exported(annotations: &[AnnotationItem]) -> bool {
316 annotations
317 .iter()
318 .any(|a| a.name == "export" || a.name.starts_with("export_"))
319}
320
321fn preceding_annotations(node: &GdNode) -> Vec<AnnotationItem> {
325 let mut out = Vec::new();
326 let mut sib = node.prev_sibling();
327 while let Some(s) = sib {
328 if s.kind() != SyntaxKind::Annotation {
329 break;
330 }
331 if let Some(item) = annotation_item(s) {
332 out.push(item);
333 }
334 sib = s.prev_sibling();
335 }
336 out.reverse(); out
338}
339
340fn container_annotations(container: &GdNode) -> Vec<AnnotationItem> {
343 container
344 .children()
345 .filter(|c| c.kind() == SyntaxKind::Annotation)
346 .filter_map(annotation_item)
347 .collect()
348}
349
350fn annotation_item(ann: &GdNode) -> Option<AnnotationItem> {
352 use cstree::util::NodeOrToken;
353 ann.children_with_tokens()
354 .filter_map(NodeOrToken::into_token)
355 .find(|t| t.kind() == SyntaxKind::Ident)
356 .map(|t| AnnotationItem {
357 name: SmolStr::new(t.text()),
358 range: cst::token_range(t),
359 })
360}
361
362fn lower_const(d: &ast::ConstDecl) -> ConstItem {
363 let node = d.syntax();
364 let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
367 .and_then(ast::TypeRef::cast)
368 .and_then(|t| t.text())
369 .map(SmolStr::new);
370 ConstItem {
371 name: decl_name(d.name()).unwrap_or_default(),
372 type_ref,
373 preload_path: const_preload_path(node),
374 annotations: preceding_annotations(node),
375 ptr: AstPtr::of(node),
376 range: cst::text_range_of(node),
377 name_range: name_range(d.name(), node),
378 }
379}
380
381fn const_preload_path(const_decl: &GdNode) -> Option<SmolStr> {
386 let preload = cst::first_child(const_decl, |k| k == SyntaxKind::PreloadExpr)?;
387 let arg = cst::first_child(&preload, |k| k == SyntaxKind::ArgList)
388 .and_then(|al| cst::first_child_expr(&al))?;
389 if arg.kind() != SyntaxKind::Literal {
390 return None;
391 }
392 cst::child_token_text(&arg, SyntaxKind::String)
393 .map(|s| SmolStr::new(s.trim_matches(['"', '\''])))
394}
395
396fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
397 let node = d.syntax();
398 SignalItem {
399 name: decl_name(d.name()).unwrap_or_default(),
400 params: d
401 .param_list()
402 .map(|pl| lower_params(&pl))
403 .unwrap_or_default(),
404 annotations: preceding_annotations(node),
405 range: cst::text_range_of(node),
406 name_range: name_range(d.name(), node),
407 }
408}
409
410fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
411 let node = d.syntax();
412 EnumItem {
413 name: decl_name(d.name()),
414 variants: d
415 .variants()
416 .filter_map(|v| v.text())
417 .map(SmolStr::new)
418 .collect(),
419 range: cst::text_range_of(node),
420 name_range: name_range(d.name(), node),
421 }
422}
423
424fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
425 let node = d.syntax();
426 let name = decl_name(d.name())?;
427 let mut tree = d
428 .body()
429 .map(|b| lower_class(b.syntax(), b.decls()))
430 .unwrap_or_default();
431 tree.extends = find_extends(node);
434 Some(InnerClassItem {
435 name,
436 tree,
437 range: cst::text_range_of(node),
438 name_range: name_range(d.name(), node),
439 })
440}
441
442fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
443 pl.params()
444 .map(|p| ParamItem {
445 name: decl_name(p.name()).unwrap_or_default(),
446 type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
447 has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
448 || cst::has_token(p.syntax(), SyntaxKind::Eq)
449 || cst::first_child_expr(p.syntax()).is_some(),
450 })
451 .collect()
452}
453
454fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
460 if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
461 return parse_extends_tokens(&clause);
462 }
463 if cst::has_token(container, SyntaxKind::ExtendsKw) {
464 return parse_extends_tokens(container);
465 }
466 None
467}
468
469fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
471 let idents: Vec<String> = node
474 .children_with_tokens()
475 .filter_map(cstree::util::NodeOrToken::into_token)
476 .filter(|t| t.kind() == SyntaxKind::Ident)
477 .map(|t| t.text().to_owned())
478 .collect();
479 if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
483 let path = SmolStr::new(s.trim_matches(['"', '\'']));
484 return Some(if idents.is_empty() {
485 ExtendsRef::ScriptPath(path)
486 } else {
487 ExtendsRef::ScriptPathInner(path)
488 });
489 }
490 match idents.len() {
492 0 => None,
493 1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
494 _ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
495 }
496}
497
498fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
499 name.and_then(|n| n.text()).map(SmolStr::new)
500}
501
502fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
510 name.map_or_else(
511 || cst::text_range_of(decl),
512 |n| trimmed_name_range(n.syntax()),
513 )
514}
515
516fn trimmed_name_range(name_node: &GdNode) -> TextRange {
519 let r = cst::text_range_of(name_node);
520 let text = name_node.text().to_string();
521 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
522 let len = u32::try_from(text.trim().len()).unwrap_or(0);
523 TextRange::new(r.start + lead, r.start + lead + len)
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use gdscript_syntax::parse;
530
531 fn tree_of(src: &str) -> Arc<ItemTree> {
532 item_tree(&parse(src).syntax_node())
533 }
534
535 #[test]
536 fn class_header_and_members() {
537 let tree = tree_of(
538 "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",
539 );
540 assert_eq!(tree.class_name.as_deref(), Some("Foo"));
541 assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
542 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
543 assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
544 }
545
546 #[test]
547 fn func_signature() {
548 let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
549 let Member::Func(f) = &tree.members[0] else {
550 panic!("expected func")
551 };
552 assert_eq!(f.name, "add");
553 assert_eq!(f.return_type.as_deref(), Some("int"));
554 assert_eq!(f.params.len(), 2);
555 assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
556 assert!(!f.params[0].has_default);
557 assert!(f.params[1].has_default);
558 }
559
560 #[test]
561 fn soft_keyword_names_are_not_dropped() {
562 let tree =
566 tree_of("var when := 1\nfunc match(when: int):\n\tpass\nenum E { match, when }\n");
567 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
568 assert_eq!(names, vec!["when", "match", "E"]);
569 let Some(Member::Func(f)) = tree.member("match") else {
570 panic!("expected a func named `match`")
571 };
572 assert_eq!(f.params[0].name, "when");
573 let Some(Member::Enum(e)) = tree.member("E") else {
574 panic!("expected enum E")
575 };
576 assert_eq!(
577 e.variants,
578 vec![SmolStr::new("match"), SmolStr::new("when")]
579 );
580 }
581
582 #[test]
583 fn var_init_and_inference_flags() {
584 let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
585 let vars: Vec<&VarItem> = tree
586 .members
587 .iter()
588 .filter_map(|m| match m {
589 Member::Var(v) => Some(v),
590 _ => None,
591 })
592 .collect();
593 assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
595 assert!(vars[0].has_init && !vars[0].is_inferred);
596 assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
598 assert!(!vars[2].has_init && vars[2].type_ref.is_none());
600 assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
602 }
603
604 #[test]
605 fn extends_script_path() {
606 let tree = tree_of("extends \"res://player.gd\"\n");
607 assert_eq!(
608 tree.extends,
609 Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
610 );
611 }
612
613 #[test]
614 fn extends_script_path_with_inner_class_is_distinguished() {
615 let tree = tree_of("extends \"res://base.gd\".Inner\n");
618 assert_eq!(
619 tree.extends,
620 Some(ExtendsRef::ScriptPathInner(SmolStr::new("res://base.gd"))),
621 "the trailing .Inner must be detected, not dropped"
622 );
623 }
624
625 #[test]
626 fn anonymous_enum_has_no_name_but_variants() {
627 let tree = tree_of("enum { RED, GREEN, BLUE }\n");
628 let Member::Enum(e) = &tree.members[0] else {
629 panic!("expected enum")
630 };
631 assert!(e.name.is_none());
632 assert_eq!(
633 e.variants,
634 vec![
635 SmolStr::new("RED"),
636 SmolStr::new("GREEN"),
637 SmolStr::new("BLUE")
638 ]
639 );
640 }
641
642 #[test]
643 fn inner_class_members_and_extends() {
644 let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
645 let Member::Class(inner) = &tree.members[0] else {
646 panic!("expected inner class")
647 };
648 assert_eq!(inner.name, "Inner");
649 let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
650 assert_eq!(names, vec!["y", "m"]);
651 assert_eq!(
652 inner.tree.extends,
653 Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
654 );
655 }
656
657 #[test]
658 fn annotations_are_captured_first_class() {
659 let tree = tree_of(
660 "@tool\nextends Node\n@export var speed = 5\n@onready var label = null\n@rpc(\"any_peer\")\nfunc ping():\n\tpass\n",
661 );
662 assert!(
664 has_annotation(&tree.annotations, "tool"),
665 "{:?}",
666 tree.annotations
667 );
668 let var = |name: &str| {
669 tree.members.iter().find_map(|m| match m {
670 Member::Var(v) if v.name == name => Some(v),
671 _ => None,
672 })
673 };
674 let speed = var("speed").unwrap();
675 assert!(has_annotation(&speed.annotations, "export"));
676 assert!(speed.is_exported, "@export derives is_exported");
677 assert!(has_annotation(
678 &var("label").unwrap().annotations,
679 "onready"
680 ));
681 let ping = tree.members.iter().find_map(|m| match m {
683 Member::Func(f) if f.name == "ping" => Some(f),
684 _ => None,
685 });
686 assert!(has_annotation(&ping.unwrap().annotations, "rpc"));
687 }
688
689 #[test]
690 fn ptr_round_trips_to_node() {
691 let parse = parse("func f():\n\tpass\n");
692 let root = parse.syntax_node();
693 let tree = item_tree(&root);
694 let Member::Func(f) = &tree.members[0] else {
695 panic!()
696 };
697 let node = f.ptr.to_node(&root).expect("func node recovered");
698 assert_eq!(node.kind(), SyntaxKind::FuncDecl);
699 }
700}