1use gdscript_base::{FileId, FilePosition, TextRange};
18use gdscript_db::{Db, FileText, parse};
19use gdscript_syntax::{GdNode, GdToken, SyntaxKind, ast};
20use smol_str::SmolStr;
21
22use crate::cst;
23use crate::ty::Ty;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum GodotDef {
29 Global {
31 decl_file: FileId,
33 name: SmolStr,
35 },
36 Member {
39 owner_file: FileId,
41 name: SmolStr,
43 },
44 Local {
48 body_file: FileId,
50 body_range: TextRange,
52 decl_name_range: TextRange,
54 },
55 Autoload {
57 name: SmolStr,
59 target_file: Option<FileId>,
61 },
62 Engine {
66 name: SmolStr,
68 },
69}
70
71impl GodotDef {
72 #[must_use]
74 pub fn name(&self) -> &str {
75 match self {
76 Self::Global { name, .. }
77 | Self::Member { name, .. }
78 | Self::Autoload { name, .. }
79 | Self::Engine { name } => name,
80 Self::Local { .. } => "", }
82 }
83
84 #[must_use]
86 pub fn is_renameable(&self) -> bool {
87 !matches!(self, Self::Engine { .. })
88 }
89}
90
91#[must_use]
95pub fn classify(db: &dyn Db, pos: FilePosition) -> Option<GodotDef> {
96 let ft = db.file_text(pos.file)?;
97 let root = parse(db, ft).syntax_node();
98 let tok = ast::token_at(&root, pos.offset.into())?;
99 let parent = tok.parent();
100 let is_name_tok = tok.kind() == SyntaxKind::Ident
105 || (matches!(tok.kind(), SyntaxKind::MatchKw | SyntaxKind::WhenKw)
106 && matches!(parent.kind(), SyntaxKind::Name | SyntaxKind::NameRef));
107 if !is_name_tok {
108 return None; }
110 let name = SmolStr::new(tok.text());
111 let tok_range = cst::token_range(&tok);
112
113 if parent.kind() == SyntaxKind::Name
115 && let Some(def) = classify_decl(db, ft, pos.file, parent, &name, tok_range)
116 {
117 return Some(def);
118 }
119 if let Some(head) = cst::extends_head_token(parent)
125 && cst::token_range(&head) == tok_range
126 {
127 return classify_type_name(db, &name);
128 }
129 if parent.kind() == SyntaxKind::EnumVariant && in_anon_enum(parent) {
135 return Some(GodotDef::Member {
136 owner_file: pos.file,
137 name,
138 });
139 }
140 if has_ancestor(&tok, SyntaxKind::TypeRef) {
143 return classify_type_name(db, &name);
144 }
145 classify_body_ref(db, ft, pos.file, pos.offset, &name)
148}
149
150fn classify_decl(
152 db: &dyn Db,
153 ft: FileText,
154 file: FileId,
155 name_node: &GdNode,
156 name: &SmolStr,
157 tok_range: TextRange,
158) -> Option<GodotDef> {
159 let decl = name_node.parent()?;
160 let in_body = node_has_ancestor(decl, SyntaxKind::FuncDecl)
164 || node_has_ancestor(decl, SyntaxKind::Getter)
165 || node_has_ancestor(decl, SyntaxKind::Setter)
166 || node_has_ancestor(decl, SyntaxKind::LambdaExpr);
167 let in_inner_class = node_has_ancestor(decl, SyntaxKind::InnerClassDecl);
174 match decl.kind() {
175 SyntaxKind::ClassNameDecl => Some(GodotDef::Global {
176 decl_file: file,
177 name: name.clone(),
178 }),
179 SyntaxKind::Param | SyntaxKind::ForStmt | SyntaxKind::PatternBind => {
181 local_def(db, ft, file, tok_range)
182 }
183 SyntaxKind::VarDecl | SyntaxKind::ConstDecl if in_body => {
185 local_def(db, ft, file, tok_range)
186 }
187 SyntaxKind::FuncDecl
190 | SyntaxKind::SignalDecl
191 | SyntaxKind::EnumDecl
192 | SyntaxKind::InnerClassDecl
193 | SyntaxKind::VarDecl
194 | SyntaxKind::ConstDecl
195 if !in_inner_class =>
196 {
197 Some(GodotDef::Member {
198 owner_file: file,
199 name: name.clone(),
200 })
201 }
202 _ => None,
203 }
204}
205
206fn local_def(db: &dyn Db, ft: FileText, file: FileId, tok_range: TextRange) -> Option<GodotDef> {
211 let fi = crate::queries::analyze_file(db, ft);
212 let unit = fi.unit_at(tok_range.start)?;
213 let binding = unit.result.binding_at(tok_range.start)?;
214 Some(GodotDef::Local {
215 body_file: file,
216 body_range: unit.range,
217 decl_name_range: trim_range(ft.text(db), binding.name_range),
218 })
219}
220
221fn trim_range(text: &str, nr: TextRange) -> TextRange {
224 match text.get(nr.start as usize..nr.end as usize) {
225 Some(s) => {
226 let lead = u32::try_from(s.len() - s.trim_start().len()).unwrap_or(0);
227 let len = u32::try_from(s.trim().len()).unwrap_or(0);
228 TextRange::new(nr.start + lead, nr.start + lead + len)
229 }
230 None => nr,
231 }
232}
233
234fn classify_type_name(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
236 let api = db.engine()?;
237 match crate::resolve::resolve_type_name(db, api, name) {
238 Ty::ScriptRef(sref) => Some(GodotDef::Global {
239 decl_file: FileId(sref.0),
240 name: name.clone(),
241 }),
242 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
243 _ => None,
244 }
245}
246
247fn classify_body_ref(
249 db: &dyn Db,
250 ft: FileText,
251 file: FileId,
252 offset: u32,
253 name: &SmolStr,
254) -> Option<GodotDef> {
255 let fi = crate::queries::analyze_file(db, ft);
256 let unit = fi.unit_at(offset)?;
257 let eid = unit.body.source_map.expr_at_offset(offset)?;
258 match unit.body.expr(eid) {
259 crate::body::Expr::Name(n) if n == name => {
260 resolve_name_to_def(db, ft, file, offset, unit, name)
261 }
262 crate::body::Expr::Field {
263 receiver,
264 name: fname,
265 name_range,
266 } if fname == name && name_range.start <= offset && offset < name_range.end => {
267 if matches!(unit.body.expr(*receiver), crate::body::Expr::SelfExpr) {
270 return member_owner(db, crate::ty::ScriptRefId(file.0), name, 0).map(|owner| {
271 GodotDef::Member {
272 owner_file: owner,
273 name: name.clone(),
274 }
275 });
276 }
277 let recv_ty = unit.result.type_of(*receiver)?;
278 match recv_ty {
279 Ty::ScriptRef(sref) => {
280 member_owner(db, *sref, name, 0).map(|owner| GodotDef::Member {
281 owner_file: owner,
282 name: name.clone(),
283 })
284 }
285 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
286 _ => None, }
288 }
289 _ => None,
290 }
291}
292
293fn resolve_name_to_def(
297 db: &dyn Db,
298 ft: FileText,
299 file: FileId,
300 offset: u32,
301 unit: &crate::infer::Unit,
302 name: &SmolStr,
303) -> Option<GodotDef> {
304 let text = ft.text(db);
311 let mut best: Option<TextRange> = None;
312 for b in &unit.result.bindings {
313 if !matches!(
314 b.kind,
315 crate::infer::BindingKind::Var
316 | crate::infer::BindingKind::Param
317 | crate::infer::BindingKind::ForVar
318 | crate::infer::BindingKind::MatchBind
319 ) {
320 continue;
321 }
322 let nr = trim_range(text, b.name_range);
323 if text.get(nr.start as usize..nr.end as usize) != Some(name.as_str()) {
324 continue;
325 }
326 if nr.start <= offset && best.is_none_or(|cur| nr.start >= cur.start) {
327 best = Some(nr);
328 }
329 }
330 if let Some(nr) = best {
331 return Some(GodotDef::Local {
332 body_file: file,
333 body_range: unit.range,
334 decl_name_range: nr,
335 });
336 }
337 if let Some(owner) = member_owner(db, crate::ty::ScriptRefId(file.0), name, 0) {
339 return Some(GodotDef::Member {
340 owner_file: owner,
341 name: name.clone(),
342 });
343 }
344 if let Some(api) = db.engine()
347 && crate::resolve::resolve_global(api, name).is_some()
348 {
349 return Some(GodotDef::Engine { name: name.clone() });
350 }
351 if let Some(root) = db.source_root()
353 && let Some(decl) = crate::queries::global_registry(db, root).resolve(name)
354 {
355 return Some(GodotDef::Global {
356 decl_file: decl.file_id(db),
357 name: name.clone(),
358 });
359 }
360 if let Some(config) = db.project_config()
362 && let Some(path) = crate::queries::autoload_registry(db, config)
363 .resolve_path(name)
364 .cloned()
365 {
366 let target = db.source_root().and_then(|root| {
367 crate::queries::res_path_registry(db, root)
368 .get(path.as_str())
369 .copied()
370 });
371 return Some(GodotDef::Autoload {
372 name: name.clone(),
373 target_file: target,
374 });
375 }
376 None
377}
378
379fn member_owner(
382 db: &dyn Db,
383 sref: crate::ty::ScriptRefId,
384 name: &str,
385 depth: u32,
386) -> Option<FileId> {
387 if depth > 32 {
388 return None;
389 }
390 let file = db.file_text(FileId(sref.0))?;
391 let tree = crate::queries::item_tree(db, file);
392 if tree.member(name).is_some() || anon_enum_has_variant(&tree, name) {
395 return Some(file.file_id(db));
396 }
397 match crate::queries::script_class(db, file).base() {
398 Ty::ScriptRef(base) => member_owner(db, *base, name, depth + 1),
399 _ => None, }
401}
402
403fn anon_enum_has_variant(tree: &crate::item_tree::ItemTree, name: &str) -> bool {
407 tree.members.iter().any(|m| {
408 matches!(m, crate::item_tree::Member::Enum(e)
409 if e.name.is_none() && e.variants.iter().any(|v| v == name))
410 })
411}
412
413fn in_anon_enum(enum_variant: &GdNode) -> bool {
415 enum_variant.parent().is_some_and(|enum_decl| {
416 enum_decl.kind() == SyntaxKind::EnumDecl
417 && !enum_decl.children().any(|c| c.kind() == SyntaxKind::Name)
418 })
419}
420
421fn has_ancestor(tok: &GdToken, kind: SyntaxKind) -> bool {
423 node_has_ancestor_or_self(tok.parent(), kind)
424}
425
426fn node_has_ancestor(node: &GdNode, kind: SyntaxKind) -> bool {
428 node.parent()
429 .is_some_and(|p| node_has_ancestor_or_self(p, kind))
430}
431
432fn node_has_ancestor_or_self(node: &GdNode, kind: SyntaxKind) -> bool {
433 let mut cur = Some(node.clone());
434 while let Some(n) = cur {
435 if n.kind() == kind {
436 return true;
437 }
438 cur = n.parent().cloned();
439 }
440 false
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct NodePathTarget {
449 pub scene: FileId,
451 pub node_name: SmolStr,
453 pub header_span: TextRange,
455 pub name_span: TextRange,
457}
458
459#[must_use]
462pub fn node_path_target(db: &dyn Db, pos: FilePosition) -> Option<NodePathTarget> {
463 let ft = db.file_text(pos.file)?;
464 let fi = crate::queries::analyze_file(db, ft);
465 let unit = fi.unit_at(pos.offset)?;
466 let eid = unit.body.source_map.expr_at_offset(pos.offset)?;
467 let crate::body::Expr::GetNode {
468 path: Some(path),
469 unique,
470 } = unit.body.expr(eid)
471 else {
472 return None;
473 };
474 let ctx = crate::queries::scene_context(db, ft)?;
475 let idx = if *unique {
476 ctx.model.resolve_unique(path)
477 } else {
478 ctx.model.resolve_path_from(ctx.attach, path)
479 }?;
480 let node = ctx.model.node(idx)?;
481 Some(NodePathTarget {
482 scene: ctx.scene,
483 node_name: node.name.clone(),
484 header_span: node.header_span,
485 name_span: node.name_span,
486 })
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use gdscript_db::RootDatabase;
493 use salsa::Durability;
494
495 fn db_with(files: &[(u32, &str)]) -> RootDatabase {
496 let mut db = RootDatabase::default();
497 for (id, src) in files {
498 db.set_file_text(FileId(*id), src, Durability::LOW);
499 }
500 db.sync_source_root();
501 db
502 }
503
504 fn at(db: &RootDatabase, file: u32, needle: &str, src: &str) -> Option<GodotDef> {
505 let offset = u32::try_from(src.find(needle).expect("needle")).unwrap();
506 classify(
507 db,
508 FilePosition {
509 file: FileId(file),
510 offset,
511 },
512 )
513 }
514
515 fn at_nth(db: &RootDatabase, file: u32, needle: &str, n: usize, src: &str) -> Option<GodotDef> {
517 let off = src.match_indices(needle).nth(n).expect("nth needle").0;
518 classify(
519 db,
520 FilePosition {
521 file: FileId(file),
522 offset: u32::try_from(off).unwrap(),
523 },
524 )
525 }
526
527 #[test]
528 fn two_unrelated_locals_are_distinct() {
529 let src =
530 "func a():\n\tvar i := 1\n\tvar ra := i\nfunc b():\n\tvar i := 2\n\tvar rb := i\n";
531 let db = db_with(&[(0, src)]);
532 let off_a = u32::try_from(src.match_indices(":= i").next().unwrap().0 + 3).unwrap();
534 let off_b = u32::try_from(src.match_indices(":= i").nth(1).unwrap().0 + 3).unwrap();
535 let da = classify(
536 &db,
537 FilePosition {
538 file: FileId(0),
539 offset: off_a,
540 },
541 )
542 .unwrap();
543 let dbf = classify(
544 &db,
545 FilePosition {
546 file: FileId(0),
547 offset: off_b,
548 },
549 )
550 .unwrap();
551 assert!(matches!(da, GodotDef::Local { .. }), "{da:?}");
552 assert!(matches!(dbf, GodotDef::Local { .. }), "{dbf:?}");
553 assert_ne!(da, dbf, "two unrelated `i`s must be distinct locals");
554 }
555
556 #[test]
557 fn local_shadowing_a_member_is_distinct() {
558 let src = "var pos := 1\nfunc f():\n\tvar pos := 2\n\tprint(pos)\n";
559 let db = db_with(&[(0, src)]);
560 let member = at_nth(&db, 0, "pos", 0, src).unwrap();
562 let local = at_nth(&db, 0, "pos", 1, src).unwrap();
563 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
564 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
565 assert_ne!(member, local);
566 let r = at_nth(&db, 0, "pos", 2, src).unwrap();
568 assert_eq!(r, local);
569 }
570
571 #[test]
572 fn same_named_members_of_different_classes_are_distinct() {
573 let a = "class_name A\nfunc update():\n\tpass\n";
574 let b = "class_name B\nfunc update():\n\tpass\n";
575 let db = db_with(&[(0, a), (1, b)]);
576 let ua = at(&db, 0, "update", a).unwrap();
577 let ub = at(&db, 1, "update", b).unwrap();
578 assert!(matches!(ua, GodotDef::Member { .. }));
579 assert!(matches!(ub, GodotDef::Member { .. }));
580 assert_ne!(ua, ub, "A.update and B.update must be distinct");
581 }
582
583 #[test]
584 fn class_name_decl_and_reference_classify_to_the_same_global() {
585 let widget = "class_name Widget\nfunc make() -> int:\n\treturn 1\n";
586 let user = "func f():\n\tvar w: Widget\n\tvar x := Widget.new()\n";
587 let db = db_with(&[(0, widget), (1, user)]);
588 let decl = at(&db, 0, "Widget", widget).unwrap();
589 let ann = at(&db, 1, "Widget\n", user).unwrap(); let ctor = at(&db, 1, "Widget.new", user).unwrap();
591 assert!(matches!(
592 decl,
593 GodotDef::Global {
594 decl_file: FileId(0),
595 ..
596 }
597 ));
598 assert_eq!(decl, ann, "annotation must resolve to the class_name def");
599 assert_eq!(
600 decl, ctor,
601 "`Widget.new()` must resolve to the class_name def"
602 );
603 }
604
605 #[test]
606 fn extends_user_class_classifies_to_the_global() {
607 let base = "class_name Base\nfunc m():\n\tpass\n";
611 let derived = "class_name Derived\nextends Base\n";
612 let db = db_with(&[(0, base), (1, derived)]);
613 let decl = at(&db, 0, "Base", base).unwrap();
614 let ext = at(&db, 1, "Base", derived).unwrap(); assert!(matches!(
616 decl,
617 GodotDef::Global {
618 decl_file: FileId(0),
619 ..
620 }
621 ));
622 assert_eq!(
623 decl, ext,
624 "`extends Base` must classify to Base's class_name def"
625 );
626 }
627
628 #[test]
629 fn inherited_member_resolves_to_the_declaring_base() {
630 let base = "class_name Base\nfunc base_m() -> int:\n\treturn 1\n";
631 let derived = "class_name Derived\nextends Base\nfunc use_it():\n\tself.base_m()\n";
632 let db = db_with(&[(0, base), (1, derived)]);
633 let decl = at(&db, 0, "base_m", base).unwrap();
634 let call = at(&db, 1, "base_m()", derived).unwrap();
635 assert!(matches!(
636 decl,
637 GodotDef::Member {
638 owner_file: FileId(0),
639 ..
640 }
641 ));
642 assert_eq!(
643 decl, call,
644 "inherited call must resolve to the base's member def"
645 );
646 }
647
648 #[test]
649 fn inner_class_member_is_out_of_scope() {
650 let src =
653 "class_name A\nfunc update():\n\tpass\nclass Inner:\n\tfunc update():\n\t\tpass\n";
654 let db = db_with(&[(0, src)]);
655 let top = at_nth(&db, 0, "update", 0, src).unwrap();
656 let inner = at_nth(&db, 0, "update", 1, src);
657 assert!(matches!(top, GodotDef::Member { .. }), "{top:?}");
658 assert_eq!(
659 inner, None,
660 "an inner-class member must not classify (out of scope), got {inner:?}"
661 );
662 }
663
664 #[test]
665 fn match_capture_classifies_as_local_distinct_from_member() {
666 let src = "var cap := 0\nfunc f(v):\n\tmatch v:\n\t\tvar cap:\n\t\t\tprint(cap)\n";
669 let db = db_with(&[(0, src)]);
670 let member = at_nth(&db, 0, "cap", 0, src).unwrap();
671 let capture = at_nth(&db, 0, "cap", 1, src).unwrap();
672 let usage = at_nth(&db, 0, "cap", 2, src).unwrap();
673 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
674 assert!(matches!(capture, GodotDef::Local { .. }), "{capture:?}");
675 assert_eq!(
676 usage, capture,
677 "`print(cap)` must resolve to the match capture"
678 );
679 assert_ne!(usage, member);
680 }
681
682 #[test]
683 fn accessor_body_local_is_not_a_member() {
684 let src = "var hp: int:\n\tget:\n\t\tvar tmp = 2\n\t\treturn tmp\n";
686 let db = db_with(&[(0, src)]);
687 let tmp = at_nth(&db, 0, "tmp", 0, src);
688 assert!(
689 !matches!(tmp, Some(GodotDef::Member { .. })),
690 "a local in a get/set body must not be a Member, got {tmp:?}"
691 );
692 }
693
694 #[test]
695 fn anon_enum_variant_classifies_as_member() {
696 let src = "enum { FIRE, ICE }\nfunc f():\n\tprint(FIRE)\n";
699 let db = db_with(&[(0, src)]);
700 let decl = at_nth(&db, 0, "FIRE", 0, src).unwrap(); let usage = at_nth(&db, 0, "FIRE", 1, src).unwrap(); assert!(matches!(decl, GodotDef::Member { .. }), "{decl:?}");
703 assert_eq!(
704 decl, usage,
705 "an anon-enum variant decl and use share identity"
706 );
707 }
708
709 #[test]
710 fn shadowed_local_reference_resolves_to_the_nearest_declaration() {
711 let src = "func f(x):\n\tvar x := 2\n\tprint(x)\n";
714 let db = db_with(&[(0, src)]);
715 let param = at_nth(&db, 0, "x", 0, src).unwrap();
716 let local = at_nth(&db, 0, "x", 1, src).unwrap();
717 let usage = at_nth(&db, 0, "x", 2, src).unwrap();
718 assert!(matches!(param, GodotDef::Local { .. }), "{param:?}");
719 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
720 assert_ne!(param, local, "param x and local x are distinct");
721 assert_eq!(
722 usage, local,
723 "the reference resolves to the nearest (local) declaration"
724 );
725 }
726}