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}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Member {
55 Func(FuncItem),
57 Var(VarItem),
59 Const(ConstItem),
61 Signal(SignalItem),
63 Enum(EnumItem),
65 Class(InnerClassItem),
67}
68
69impl Member {
70 #[must_use]
72 pub fn name(&self) -> Option<&str> {
73 match self {
74 Self::Func(f) => Some(&f.name),
75 Self::Var(v) => Some(&v.name),
76 Self::Const(c) => Some(&c.name),
77 Self::Signal(s) => Some(&s.name),
78 Self::Enum(e) => e.name.as_deref(),
79 Self::Class(c) => Some(&c.name),
80 }
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ParamItem {
87 pub name: SmolStr,
89 pub type_ref: Option<SmolStr>,
91 pub has_default: bool,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct FuncItem {
98 pub name: SmolStr,
100 pub params: Vec<ParamItem>,
102 pub return_type: Option<SmolStr>,
104 pub is_static: bool,
106 pub ptr: AstPtr,
108 pub range: TextRange,
110 pub name_range: TextRange,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct VarItem {
117 pub name: SmolStr,
119 pub type_ref: Option<SmolStr>,
121 pub is_static: bool,
123 pub has_init: bool,
125 pub is_inferred: bool,
127 pub ptr: AstPtr,
129 pub range: TextRange,
131 pub name_range: TextRange,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ConstItem {
138 pub name: SmolStr,
140 pub type_ref: Option<SmolStr>,
142 pub ptr: AstPtr,
144 pub range: TextRange,
146 pub name_range: TextRange,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct SignalItem {
153 pub name: SmolStr,
155 pub params: Vec<ParamItem>,
157 pub range: TextRange,
159 pub name_range: TextRange,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct EnumItem {
166 pub name: Option<SmolStr>,
169 pub variants: Vec<SmolStr>,
171 pub range: TextRange,
173 pub name_range: TextRange,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct InnerClassItem {
180 pub name: SmolStr,
182 pub tree: ItemTree,
184 pub range: TextRange,
186 pub name_range: TextRange,
188}
189
190#[must_use]
192pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
193 let Some(file) = ast::SourceFile::cast(root.clone()) else {
194 return Arc::new(ItemTree::default());
195 };
196 Arc::new(lower_class(root, file.decls()))
197}
198
199fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
202 let mut tree = ItemTree {
203 extends: find_extends(container),
204 ..ItemTree::default()
205 };
206 for decl in decls {
207 match decl {
208 ast::Decl::ClassName(d) => {
209 if let Some(name) = decl_name(d.name()) {
210 tree.class_name = Some(name);
211 }
212 }
213 ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
214 ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
215 ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
216 ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
217 ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
218 ast::Decl::Class(d) => {
219 if let Some(item) = lower_inner_class(&d) {
220 tree.members.push(Member::Class(item));
221 }
222 }
223 }
224 }
225 tree
226}
227
228fn lower_func(d: &ast::FuncDecl) -> FuncItem {
229 let node = d.syntax();
230 FuncItem {
231 name: decl_name(d.name()).unwrap_or_default(),
232 params: d
233 .param_list()
234 .map(|pl| lower_params(&pl))
235 .unwrap_or_default(),
236 return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
237 is_static: d.is_static(),
238 ptr: AstPtr::of(node),
239 range: cst::text_range_of(node),
240 name_range: name_range(d.name(), node),
241 }
242}
243
244fn lower_var(d: &ast::VarDecl) -> VarItem {
245 let node = d.syntax();
246 VarItem {
247 name: decl_name(d.name()).unwrap_or_default(),
248 type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
249 is_static: d.is_static(),
250 has_init: cst::first_child_expr(node).is_some(),
251 is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
252 ptr: AstPtr::of(node),
253 range: cst::text_range_of(node),
254 name_range: name_range(d.name(), node),
255 }
256}
257
258fn lower_const(d: &ast::ConstDecl) -> ConstItem {
259 let node = d.syntax();
260 let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
263 .and_then(ast::TypeRef::cast)
264 .and_then(|t| t.text())
265 .map(SmolStr::new);
266 ConstItem {
267 name: decl_name(d.name()).unwrap_or_default(),
268 type_ref,
269 ptr: AstPtr::of(node),
270 range: cst::text_range_of(node),
271 name_range: name_range(d.name(), node),
272 }
273}
274
275fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
276 let node = d.syntax();
277 SignalItem {
278 name: decl_name(d.name()).unwrap_or_default(),
279 params: d
280 .param_list()
281 .map(|pl| lower_params(&pl))
282 .unwrap_or_default(),
283 range: cst::text_range_of(node),
284 name_range: name_range(d.name(), node),
285 }
286}
287
288fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
289 let node = d.syntax();
290 EnumItem {
291 name: decl_name(d.name()),
292 variants: d
293 .variants()
294 .filter_map(|v| v.text())
295 .map(SmolStr::new)
296 .collect(),
297 range: cst::text_range_of(node),
298 name_range: name_range(d.name(), node),
299 }
300}
301
302fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
303 let node = d.syntax();
304 let name = decl_name(d.name())?;
305 let mut tree = d
306 .body()
307 .map(|b| lower_class(b.syntax(), b.decls()))
308 .unwrap_or_default();
309 tree.extends = find_extends(node);
312 Some(InnerClassItem {
313 name,
314 tree,
315 range: cst::text_range_of(node),
316 name_range: name_range(d.name(), node),
317 })
318}
319
320fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
321 pl.params()
322 .map(|p| ParamItem {
323 name: decl_name(p.name()).unwrap_or_default(),
324 type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
325 has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
326 || cst::has_token(p.syntax(), SyntaxKind::Eq)
327 || cst::first_child_expr(p.syntax()).is_some(),
328 })
329 .collect()
330}
331
332fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
338 if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
339 return parse_extends_tokens(&clause);
340 }
341 if cst::has_token(container, SyntaxKind::ExtendsKw) {
342 return parse_extends_tokens(container);
343 }
344 None
345}
346
347fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
349 if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
351 return Some(ExtendsRef::ScriptPath(SmolStr::new(
352 s.trim_matches(['"', '\'']),
353 )));
354 }
355 let idents: Vec<String> = node
357 .children_with_tokens()
358 .filter_map(cstree::util::NodeOrToken::into_token)
359 .filter(|t| t.kind() == SyntaxKind::Ident)
360 .map(|t| t.text().to_owned())
361 .collect();
362 match idents.len() {
363 0 => None,
364 1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
365 _ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
366 }
367}
368
369fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
370 name.and_then(|n| n.text()).map(SmolStr::new)
371}
372
373fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
381 name.map_or_else(
382 || cst::text_range_of(decl),
383 |n| trimmed_name_range(n.syntax()),
384 )
385}
386
387fn trimmed_name_range(name_node: &GdNode) -> TextRange {
390 let r = cst::text_range_of(name_node);
391 let text = name_node.text().to_string();
392 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
393 let len = u32::try_from(text.trim().len()).unwrap_or(0);
394 TextRange::new(r.start + lead, r.start + lead + len)
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use gdscript_syntax::parse;
401
402 fn tree_of(src: &str) -> Arc<ItemTree> {
403 item_tree(&parse(src).syntax_node())
404 }
405
406 #[test]
407 fn class_header_and_members() {
408 let tree = tree_of(
409 "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",
410 );
411 assert_eq!(tree.class_name.as_deref(), Some("Foo"));
412 assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
413 let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
414 assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
415 }
416
417 #[test]
418 fn func_signature() {
419 let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
420 let Member::Func(f) = &tree.members[0] else {
421 panic!("expected func")
422 };
423 assert_eq!(f.name, "add");
424 assert_eq!(f.return_type.as_deref(), Some("int"));
425 assert_eq!(f.params.len(), 2);
426 assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
427 assert!(!f.params[0].has_default);
428 assert!(f.params[1].has_default);
429 }
430
431 #[test]
432 fn var_init_and_inference_flags() {
433 let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
434 let vars: Vec<&VarItem> = tree
435 .members
436 .iter()
437 .filter_map(|m| match m {
438 Member::Var(v) => Some(v),
439 _ => None,
440 })
441 .collect();
442 assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
444 assert!(vars[0].has_init && !vars[0].is_inferred);
445 assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
447 assert!(!vars[2].has_init && vars[2].type_ref.is_none());
449 assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
451 }
452
453 #[test]
454 fn extends_script_path() {
455 let tree = tree_of("extends \"res://player.gd\"\n");
456 assert_eq!(
457 tree.extends,
458 Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
459 );
460 }
461
462 #[test]
463 fn anonymous_enum_has_no_name_but_variants() {
464 let tree = tree_of("enum { RED, GREEN, BLUE }\n");
465 let Member::Enum(e) = &tree.members[0] else {
466 panic!("expected enum")
467 };
468 assert!(e.name.is_none());
469 assert_eq!(
470 e.variants,
471 vec![
472 SmolStr::new("RED"),
473 SmolStr::new("GREEN"),
474 SmolStr::new("BLUE")
475 ]
476 );
477 }
478
479 #[test]
480 fn inner_class_members_and_extends() {
481 let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
482 let Member::Class(inner) = &tree.members[0] else {
483 panic!("expected inner class")
484 };
485 assert_eq!(inner.name, "Inner");
486 let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
487 assert_eq!(names, vec!["y", "m"]);
488 assert_eq!(
489 inner.tree.extends,
490 Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
491 );
492 }
493
494 #[test]
495 fn ptr_round_trips_to_node() {
496 let parse = parse("func f():\n\tpass\n");
497 let root = parse.syntax_node();
498 let tree = item_tree(&root);
499 let Member::Func(f) = &tree.members[0] else {
500 panic!()
501 };
502 let node = f.ptr.to_node(&root).expect("func node recovered");
503 assert_eq!(node.kind(), SyntaxKind::FuncDecl);
504 }
505}