use std::sync::Arc;
use gdscript_base::TextRange;
use gdscript_syntax::ast::{self, AstNode};
use gdscript_syntax::{GdNode, SyntaxKind};
use smol_str::SmolStr;
use crate::cst::{self, AstPtr};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ItemTree {
pub class_name: Option<SmolStr>,
pub extends: Option<ExtendsRef>,
pub members: Vec<Member>,
}
impl ItemTree {
#[must_use]
pub fn member(&self, name: &str) -> Option<&Member> {
self.members.iter().find(|m| m.name() == Some(name))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtendsRef {
Name(SmolStr),
Path(SmolStr),
ScriptPath(SmolStr),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Member {
Func(FuncItem),
Var(VarItem),
Const(ConstItem),
Signal(SignalItem),
Enum(EnumItem),
Class(InnerClassItem),
}
impl Member {
#[must_use]
pub fn name(&self) -> Option<&str> {
match self {
Self::Func(f) => Some(&f.name),
Self::Var(v) => Some(&v.name),
Self::Const(c) => Some(&c.name),
Self::Signal(s) => Some(&s.name),
Self::Enum(e) => e.name.as_deref(),
Self::Class(c) => Some(&c.name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamItem {
pub name: SmolStr,
pub type_ref: Option<SmolStr>,
pub has_default: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuncItem {
pub name: SmolStr,
pub params: Vec<ParamItem>,
pub return_type: Option<SmolStr>,
pub is_static: bool,
pub ptr: AstPtr,
pub range: TextRange,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VarItem {
pub name: SmolStr,
pub type_ref: Option<SmolStr>,
pub is_static: bool,
pub has_init: bool,
pub is_inferred: bool,
pub ptr: AstPtr,
pub range: TextRange,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstItem {
pub name: SmolStr,
pub type_ref: Option<SmolStr>,
pub ptr: AstPtr,
pub range: TextRange,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignalItem {
pub name: SmolStr,
pub params: Vec<ParamItem>,
pub range: TextRange,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumItem {
pub name: Option<SmolStr>,
pub variants: Vec<SmolStr>,
pub range: TextRange,
pub name_range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InnerClassItem {
pub name: SmolStr,
pub tree: ItemTree,
pub range: TextRange,
pub name_range: TextRange,
}
#[must_use]
pub fn item_tree(root: &GdNode) -> Arc<ItemTree> {
let Some(file) = ast::SourceFile::cast(root.clone()) else {
return Arc::new(ItemTree::default());
};
Arc::new(lower_class(root, file.decls()))
}
fn lower_class(container: &GdNode, decls: impl Iterator<Item = ast::Decl>) -> ItemTree {
let mut tree = ItemTree {
extends: find_extends(container),
..ItemTree::default()
};
for decl in decls {
match decl {
ast::Decl::ClassName(d) => {
if let Some(name) = decl_name(d.name()) {
tree.class_name = Some(name);
}
}
ast::Decl::Func(d) => tree.members.push(Member::Func(lower_func(&d))),
ast::Decl::Var(d) => tree.members.push(Member::Var(lower_var(&d))),
ast::Decl::Const(d) => tree.members.push(Member::Const(lower_const(&d))),
ast::Decl::Signal(d) => tree.members.push(Member::Signal(lower_signal(&d))),
ast::Decl::Enum(d) => tree.members.push(Member::Enum(lower_enum(&d))),
ast::Decl::Class(d) => {
if let Some(item) = lower_inner_class(&d) {
tree.members.push(Member::Class(item));
}
}
}
}
tree
}
fn lower_func(d: &ast::FuncDecl) -> FuncItem {
let node = d.syntax();
FuncItem {
name: decl_name(d.name()).unwrap_or_default(),
params: d
.param_list()
.map(|pl| lower_params(&pl))
.unwrap_or_default(),
return_type: d.return_type().and_then(|t| t.text()).map(SmolStr::new),
is_static: d.is_static(),
ptr: AstPtr::of(node),
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
}
}
fn lower_var(d: &ast::VarDecl) -> VarItem {
let node = d.syntax();
VarItem {
name: decl_name(d.name()).unwrap_or_default(),
type_ref: d.type_ref().and_then(|t| t.text()).map(SmolStr::new),
is_static: d.is_static(),
has_init: cst::first_child_expr(node).is_some(),
is_inferred: cst::has_token(node, SyntaxKind::ColonEq),
ptr: AstPtr::of(node),
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
}
}
fn lower_const(d: &ast::ConstDecl) -> ConstItem {
let node = d.syntax();
let type_ref = cst::first_child(node, |k| k == SyntaxKind::TypeRef)
.and_then(ast::TypeRef::cast)
.and_then(|t| t.text())
.map(SmolStr::new);
ConstItem {
name: decl_name(d.name()).unwrap_or_default(),
type_ref,
ptr: AstPtr::of(node),
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
}
}
fn lower_signal(d: &ast::SignalDecl) -> SignalItem {
let node = d.syntax();
SignalItem {
name: decl_name(d.name()).unwrap_or_default(),
params: d
.param_list()
.map(|pl| lower_params(&pl))
.unwrap_or_default(),
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
}
}
fn lower_enum(d: &ast::EnumDecl) -> EnumItem {
let node = d.syntax();
EnumItem {
name: decl_name(d.name()),
variants: d
.variants()
.filter_map(|v| v.text())
.map(SmolStr::new)
.collect(),
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
}
}
fn lower_inner_class(d: &ast::InnerClassDecl) -> Option<InnerClassItem> {
let node = d.syntax();
let name = decl_name(d.name())?;
let mut tree = d
.body()
.map(|b| lower_class(b.syntax(), b.decls()))
.unwrap_or_default();
tree.extends = find_extends(node);
Some(InnerClassItem {
name,
tree,
range: cst::text_range_of(node),
name_range: name_range(d.name(), node),
})
}
fn lower_params(pl: &ast::ParamList) -> Vec<ParamItem> {
pl.params()
.map(|p| ParamItem {
name: decl_name(p.name()).unwrap_or_default(),
type_ref: p.type_ref().and_then(|t| t.text()).map(SmolStr::new),
has_default: cst::has_token(p.syntax(), SyntaxKind::ColonEq)
|| cst::has_token(p.syntax(), SyntaxKind::Eq)
|| cst::first_child_expr(p.syntax()).is_some(),
})
.collect()
}
fn find_extends(container: &GdNode) -> Option<ExtendsRef> {
if let Some(clause) = cst::first_child(container, |k| k == SyntaxKind::ExtendsClause) {
return parse_extends_tokens(&clause);
}
if cst::has_token(container, SyntaxKind::ExtendsKw) {
return parse_extends_tokens(container);
}
None
}
fn parse_extends_tokens(node: &GdNode) -> Option<ExtendsRef> {
if let Some(s) = cst::child_token_text(node, SyntaxKind::String) {
return Some(ExtendsRef::ScriptPath(SmolStr::new(
s.trim_matches(['"', '\'']),
)));
}
let idents: Vec<String> = node
.children_with_tokens()
.filter_map(cstree::util::NodeOrToken::into_token)
.filter(|t| t.kind() == SyntaxKind::Ident)
.map(|t| t.text().to_owned())
.collect();
match idents.len() {
0 => None,
1 => Some(ExtendsRef::Name(SmolStr::new(&idents[0]))),
_ => Some(ExtendsRef::Path(SmolStr::new(idents.join(".")))),
}
}
fn decl_name(name: Option<ast::Name>) -> Option<SmolStr> {
name.and_then(|n| n.text()).map(SmolStr::new)
}
fn name_range(name: Option<ast::Name>, decl: &GdNode) -> TextRange {
name.map_or_else(
|| cst::text_range_of(decl),
|n| trimmed_name_range(n.syntax()),
)
}
fn trimmed_name_range(name_node: &GdNode) -> TextRange {
let r = cst::text_range_of(name_node);
let text = name_node.text().to_string();
let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
let len = u32::try_from(text.trim().len()).unwrap_or(0);
TextRange::new(r.start + lead, r.start + lead + len)
}
#[cfg(test)]
mod tests {
use super::*;
use gdscript_syntax::parse;
fn tree_of(src: &str) -> Arc<ItemTree> {
item_tree(&parse(src).syntax_node())
}
#[test]
fn class_header_and_members() {
let tree = tree_of(
"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",
);
assert_eq!(tree.class_name.as_deref(), Some("Foo"));
assert_eq!(tree.extends, Some(ExtendsRef::Name(SmolStr::new("Node2D"))));
let names: Vec<_> = tree.members.iter().filter_map(Member::name).collect();
assert_eq!(names, vec!["K", "x", "s", "hit", "E", "f"]);
}
#[test]
fn func_signature() {
let tree = tree_of("func add(a: int, b := 1) -> int:\n\treturn a + b\n");
let Member::Func(f) = &tree.members[0] else {
panic!("expected func")
};
assert_eq!(f.name, "add");
assert_eq!(f.return_type.as_deref(), Some("int"));
assert_eq!(f.params.len(), 2);
assert_eq!(f.params[0].type_ref.as_deref(), Some("int"));
assert!(!f.params[0].has_default);
assert!(f.params[1].has_default);
}
#[test]
fn var_init_and_inference_flags() {
let tree = tree_of("var a: int = 1\nvar b := 2\nvar c\nvar d = 3\n");
let vars: Vec<&VarItem> = tree
.members
.iter()
.filter_map(|m| match m {
Member::Var(v) => Some(v),
_ => None,
})
.collect();
assert_eq!(vars[0].type_ref.as_deref(), Some("int"));
assert!(vars[0].has_init && !vars[0].is_inferred);
assert!(vars[1].type_ref.is_none() && vars[1].has_init && vars[1].is_inferred);
assert!(!vars[2].has_init && vars[2].type_ref.is_none());
assert!(vars[3].has_init && !vars[3].is_inferred && vars[3].type_ref.is_none());
}
#[test]
fn extends_script_path() {
let tree = tree_of("extends \"res://player.gd\"\n");
assert_eq!(
tree.extends,
Some(ExtendsRef::ScriptPath(SmolStr::new("res://player.gd")))
);
}
#[test]
fn anonymous_enum_has_no_name_but_variants() {
let tree = tree_of("enum { RED, GREEN, BLUE }\n");
let Member::Enum(e) = &tree.members[0] else {
panic!("expected enum")
};
assert!(e.name.is_none());
assert_eq!(
e.variants,
vec![
SmolStr::new("RED"),
SmolStr::new("GREEN"),
SmolStr::new("BLUE")
]
);
}
#[test]
fn inner_class_members_and_extends() {
let tree = tree_of("class Inner extends RefCounted:\n\tvar y = 2\n\tfunc m():\n\t\tpass\n");
let Member::Class(inner) = &tree.members[0] else {
panic!("expected inner class")
};
assert_eq!(inner.name, "Inner");
let names: Vec<_> = inner.tree.members.iter().filter_map(Member::name).collect();
assert_eq!(names, vec!["y", "m"]);
assert_eq!(
inner.tree.extends,
Some(ExtendsRef::Name(SmolStr::new("RefCounted")))
);
}
#[test]
fn ptr_round_trips_to_node() {
let parse = parse("func f():\n\tpass\n");
let root = parse.syntax_node();
let tree = item_tree(&root);
let Member::Func(f) = &tree.members[0] else {
panic!()
};
let node = f.ptr.to_node(&root).expect("func node recovered");
assert_eq!(node.kind(), SyntaxKind::FuncDecl);
}
}