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 members: Vec<Member>,
30}
31
32impl ItemTree {
33 #[must_use]
35 pub fn member(&self, name: &str) -> Option<&Member> {
36 self.members.iter().find(|m| m.name() == Some(name))
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ExtendsRef {
44 Name(SmolStr),
46 Path(SmolStr),
48 ScriptPath(SmolStr),
50 ScriptPathInner(SmolStr),
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Member {
60 Func(FuncItem),
62 Var(VarItem),
64 Const(ConstItem),
66 Signal(SignalItem),
68 Enum(EnumItem),
70 Class(InnerClassItem),
72}
73
74impl Member {
75 #[must_use]
77 pub fn name(&self) -> Option<&str> {
78 match self {
79 Self::Func(f) => Some(&f.name),
80 Self::Var(v) => Some(&v.name),
81 Self::Const(c) => Some(&c.name),
82 Self::Signal(s) => Some(&s.name),
83 Self::Enum(e) => e.name.as_deref(),
84 Self::Class(c) => Some(&c.name),
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct ParamItem {
92 pub name: SmolStr,
94 pub type_ref: Option<SmolStr>,
96 pub has_default: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FuncItem {
103 pub name: SmolStr,
105 pub params: Vec<ParamItem>,
107 pub return_type: Option<SmolStr>,
109 pub is_static: bool,
111 pub ptr: AstPtr,
113 pub range: TextRange,
115 pub name_range: TextRange,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct VarItem {
122 pub name: SmolStr,
124 pub type_ref: Option<SmolStr>,
126 pub is_static: bool,
128 pub has_init: bool,
130 pub is_inferred: bool,
132 pub ptr: AstPtr,
134 pub range: TextRange,
136 pub name_range: TextRange,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct ConstItem {
143 pub name: SmolStr,
145 pub type_ref: Option<SmolStr>,
147 pub preload_path: Option<SmolStr>,
153 pub ptr: AstPtr,
155 pub range: TextRange,
157 pub name_range: TextRange,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct SignalItem {
164 pub name: SmolStr,
166 pub params: Vec<ParamItem>,
168 pub range: TextRange,
170 pub name_range: TextRange,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct EnumItem {
177 pub name: Option<SmolStr>,
180 pub variants: Vec<SmolStr>,
182 pub range: TextRange,
184 pub name_range: TextRange,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct InnerClassItem {
191 pub name: SmolStr,
193 pub tree: ItemTree,
195 pub range: TextRange,
197 pub name_range: TextRange,
199}
200
201#[must_use]
203pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
204 let Some(file) = ast::SourceFile::cast(root.clone()) else {
205 return Arc::new(ItemTree::default());
206 };
207 Arc::new(lower_class(root, file.decls()))
208}
209
210fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
213 let mut tree = ItemTree {
214 extends: find_extends(container),
215 ..ItemTree::default()
216 };
217 for decl in decls {
218 match decl {
219 ast::Decl::ClassName(d) => {
220 if let Some(name) = decl_name(d.name()) {
221 tree.class_name = Some(name);
222 }
223 }
224 ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
225 ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
226 ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
227 ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
228 ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
229 ast::Decl::Class(d) => {
230 if let Some(item) = lower_inner_class(&d) {
231 tree.members.push(Member::Class(item));
232 }
233 }
234 }
235 }
236 tree
237}
238
239fn lower_func(d: &ast::FuncDecl) -> FuncItem {
240 let node = d.syntax();
241 FuncItem {
242 name: decl_name(d.name()).unwrap_or_default(),
243 params: d
244 .param_list()
245 .map(|pl| lower_params(&pl))
246 .unwrap_or_default(),
247 return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
248 is_static: d.is_static(),
249 ptr: AstPtr::of(node),
250 range: cst::text_range_of(node),
251 name_range: name_range(d.name(), node),
252 }
253}
254
255fn lower_var(d: &ast::VarDecl) -> VarItem {
256 let node = d.syntax();
257 VarItem {
258 name: decl_name(d.name()).unwrap_or_default(),
259 type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
260 is_static: d.is_static(),
261 has_init: cst::first_child_expr(node).is_some(),
262 is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
263 ptr: AstPtr::of(node),
264 range: cst::text_range_of(node),
265 name_range: name_range(d.name(), node),
266 }
267}
268
269fn lower_const(d: &ast::ConstDecl) -> ConstItem {
270 let node = d.syntax();
271 let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
274 .and_then(ast::TypeRef::cast)
275 .and_then(|t| t.text())
276 .map(SmolStr::new);
277 ConstItem {
278 name: decl_name(d.name()).unwrap_or_default(),
279 type_ref,
280 preload_path: const_preload_path(node),
281 ptr: AstPtr::of(node),
282 range: cst::text_range_of(node),
283 name_range: name_range(d.name(), node),
284 }
285}
286
287fn const_preload_path(const_decl: &GdNode) -> Option<SmolStr> {
292 let preload = cst::first_child(const_decl, |k| k == SyntaxKind::PreloadExpr)?;
293 let arg = cst::first_child(&preload, |k| k == SyntaxKind::ArgList)
294 .and_then(|al| cst::first_child_expr(&al))?;
295 if arg.kind() != SyntaxKind::Literal {
296 return None;
297 }
298 cst::child_token_text(&arg, SyntaxKind::String)
299 .map(|s| SmolStr::new(s.trim_matches(['"', '\''])))
300}
301
302fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
303 let node = d.syntax();
304 SignalItem {
305 name: decl_name(d.name()).unwrap_or_default(),
306 params: d
307 .param_list()
308 .map(|pl| lower_params(&pl))
309 .unwrap_or_default(),
310 range: cst::text_range_of(node),
311 name_range: name_range(d.name(), node),
312 }
313}
314
315fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
316 let node = d.syntax();
317 EnumItem {
318 name: decl_name(d.name()),
319 variants: d
320 .variants()
321 .filter_map(|v| v.text())
322 .map(SmolStr::new)
323 .collect(),
324 range: cst::text_range_of(node),
325 name_range: name_range(d.name(), node),
326 }
327}
328
329fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
330 let node = d.syntax();
331 let name = decl_name(d.name())?;
332 let mut tree = d
333 .body()
334 .map(|b| lower_class(b.syntax(), b.decls()))
335 .unwrap_or_default();
336 tree.extends = find_extends(node);
339 Some(InnerClassItem {
340 name,
341 tree,
342 range: cst::text_range_of(node),
343 name_range: name_range(d.name(), node),
344 })
345}
346
347fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
348 pl.params()
349 .map(|p| ParamItem {
350 name: decl_name(p.name()).unwrap_or_default(),
351 type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
352 has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
353 || cst::has_token(p.syntax(), SyntaxKind::Eq)
354 || cst::first_child_expr(p.syntax()).is_some(),
355 })
356 .collect()
357}
358
359fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
365 if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
366 return parse_extends_tokens(&clause);
367 }
368 if cst::has_token(container, SyntaxKind::ExtendsKw) {
369 return parse_extends_tokens(container);
370 }
371 None
372}
373
374fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
376 let idents: Vec<String> = node
379 .children_with_tokens()
380 .filter_map(cstree::util::NodeOrToken::into_token)
381 .filter(|t| t.kind() == SyntaxKind::Ident)
382 .map(|t| t.text().to_owned())
383 .collect();
384 if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
388 let path = SmolStr::new(s.trim_matches(['"', '\'']));
389 return Some(if idents.is_empty() {
390 ExtendsRef::ScriptPath(path)
391 } else {
392 ExtendsRef::ScriptPathInner(path)
393 });
394 }
395 match idents.len() {
397 0 => None,
398 1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
399 _ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
400 }
401}
402
403fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
404 name.and_then(|n| n.text()).map(SmolStr::new)
405}
406
407fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
415 name.map_or_else(
416 || cst::text_range_of(decl),
417 |n| trimmed_name_range(n.syntax()),
418 )
419}
420
421fn trimmed_name_range(name_node: &GdNode) -> TextRange {
424 let r = cst::text_range_of(name_node);
425 let text = name_node.text().to_string();
426 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
427 let len = u32::try_from(text.trim().len()).unwrap_or(0);
428 TextRange::new(r.start + lead, r.start + lead + len)
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use gdscript_syntax::parse;
435
436 fn tree_of(src: &str) -> Arc<ItemTree> {
437 item_tree(&parse(src).syntax_node())
438 }
439
440 #[test]
441 fn class_header_and_members() {
442 let tree = tree_of(
443 "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",
444 );
445 assert_eq!(tree.class_name.as_deref(), Some("Foo"));
446 assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
447 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
448 assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
449 }
450
451 #[test]
452 fn func_signature() {
453 let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
454 let Member::Func(f) = &tree.members[0] else {
455 panic!("expected func")
456 };
457 assert_eq!(f.name, "add");
458 assert_eq!(f.return_type.as_deref(), Some("int"));
459 assert_eq!(f.params.len(), 2);
460 assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
461 assert!(!f.params[0].has_default);
462 assert!(f.params[1].has_default);
463 }
464
465 #[test]
466 fn soft_keyword_names_are_not_dropped() {
467 let tree =
471 tree_of("var when := 1\nfunc match(when: int):\n\tpass\nenum E { match, when }\n");
472 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
473 assert_eq!(names, vec!["when", "match", "E"]);
474 let Some(Member::Func(f)) = tree.member("match") else {
475 panic!("expected a func named `match`")
476 };
477 assert_eq!(f.params[0].name, "when");
478 let Some(Member::Enum(e)) = tree.member("E") else {
479 panic!("expected enum E")
480 };
481 assert_eq!(
482 e.variants,
483 vec![SmolStr::new("match"), SmolStr::new("when")]
484 );
485 }
486
487 #[test]
488 fn var_init_and_inference_flags() {
489 let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
490 let vars: Vec<&VarItem> = tree
491 .members
492 .iter()
493 .filter_map(|m| match m {
494 Member::Var(v) => Some(v),
495 _ => None,
496 })
497 .collect();
498 assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
500 assert!(vars[0].has_init && !vars[0].is_inferred);
501 assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
503 assert!(!vars[2].has_init && vars[2].type_ref.is_none());
505 assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
507 }
508
509 #[test]
510 fn extends_script_path() {
511 let tree = tree_of("extends \"res://player.gd\"\n");
512 assert_eq!(
513 tree.extends,
514 Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
515 );
516 }
517
518 #[test]
519 fn extends_script_path_with_inner_class_is_distinguished() {
520 let tree = tree_of("extends \"res://base.gd\".Inner\n");
523 assert_eq!(
524 tree.extends,
525 Some(ExtendsRef::ScriptPathInner(SmolStr::new("res://base.gd"))),
526 "the trailing .Inner must be detected, not dropped"
527 );
528 }
529
530 #[test]
531 fn anonymous_enum_has_no_name_but_variants() {
532 let tree = tree_of("enum { RED, GREEN, BLUE }\n");
533 let Member::Enum(e) = &tree.members[0] else {
534 panic!("expected enum")
535 };
536 assert!(e.name.is_none());
537 assert_eq!(
538 e.variants,
539 vec![
540 SmolStr::new("RED"),
541 SmolStr::new("GREEN"),
542 SmolStr::new("BLUE")
543 ]
544 );
545 }
546
547 #[test]
548 fn inner_class_members_and_extends() {
549 let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
550 let Member::Class(inner) = &tree.members[0] else {
551 panic!("expected inner class")
552 };
553 assert_eq!(inner.name, "Inner");
554 let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
555 assert_eq!(names, vec!["y", "m"]);
556 assert_eq!(
557 inner.tree.extends,
558 Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
559 );
560 }
561
562 #[test]
563 fn ptr_round_trips_to_node() {
564 let parse = parse("func f():\n\tpass\n");
565 let root = parse.syntax_node();
566 let tree = item_tree(&root);
567 let Member::Func(f) = &tree.members[0] else {
568 panic!()
569 };
570 let node = f.ptr.to_node(&root).expect("func node recovered");
571 assert_eq!(node.kind(), SyntaxKind::FuncDecl);
572 }
573}