1use gdscript_base::{FileId, FilePosition, TextRange};
18use gdscript_db::{Db, FileText, parse};
19use gdscript_scene::NodeIdx;
20use gdscript_syntax::{GdNode, GdToken, SyntaxKind, ast};
21use smol_str::SmolStr;
22
23use crate::cst;
24use crate::ty::Ty;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum GodotDef {
30 Global {
32 decl_file: FileId,
34 name: SmolStr,
36 },
37 Member {
40 owner_file: FileId,
42 name: SmolStr,
44 },
45 Local {
49 body_file: FileId,
51 body_range: TextRange,
53 decl_name_range: TextRange,
55 },
56 Autoload {
58 name: SmolStr,
60 target_file: Option<FileId>,
62 },
63 Engine {
67 name: SmolStr,
69 },
70 SceneNode {
75 scene: FileId,
77 path: SmolStr,
79 },
80}
81
82impl GodotDef {
83 #[must_use]
85 pub fn name(&self) -> &str {
86 match self {
87 Self::Global { name, .. }
88 | Self::Member { name, .. }
89 | Self::Autoload { name, .. }
90 | Self::Engine { name } => name,
91 Self::SceneNode { path, .. } => path.rsplit('/').next().unwrap_or(path),
93 Self::Local { .. } => "", }
95 }
96
97 #[must_use]
99 pub fn is_renameable(&self) -> bool {
100 !matches!(self, Self::Engine { .. })
101 }
102}
103
104#[must_use]
108pub fn classify(db: &dyn Db, pos: FilePosition) -> Option<GodotDef> {
109 let ft = db.file_text(pos.file)?;
110 if ft
113 .res_path(db)
114 .as_deref()
115 .is_some_and(crate::queries::is_scene_path)
116 {
117 return classify_scene_name(db, ft, pos.file, pos.offset);
118 }
119 let root = parse(db, ft).syntax_node();
120 let tok = ast::token_at(&root, pos.offset.into())?;
121 if let Some(def) = classify_node_path_at(db, ft, pos.file, &tok, pos.offset) {
124 return Some(def);
125 }
126 if let Some(def) = classify_get_node_call_at(db, ft, &tok, pos.offset) {
128 return Some(def);
129 }
130 let parent = tok.parent();
131 let is_name_tok = tok.kind() == SyntaxKind::Ident
136 || (matches!(tok.kind(), SyntaxKind::MatchKw | SyntaxKind::WhenKw)
137 && matches!(parent.kind(), SyntaxKind::Name | SyntaxKind::NameRef));
138 if !is_name_tok {
139 return None; }
141 let name = SmolStr::new(tok.text());
142 let tok_range = cst::token_range(&tok);
143
144 if parent.kind() == SyntaxKind::Name
146 && let Some(def) = classify_decl(db, ft, pos.file, parent, &name, tok_range)
147 {
148 return Some(def);
149 }
150 if let Some(head) = cst::extends_head_token(parent)
156 && cst::token_range(&head) == tok_range
157 {
158 return classify_type_name(db, &name);
159 }
160 if parent.kind() == SyntaxKind::EnumVariant && in_anon_enum(parent) {
166 return Some(GodotDef::Member {
167 owner_file: pos.file,
168 name,
169 });
170 }
171 if has_ancestor(&tok, SyntaxKind::TypeRef) {
174 return classify_type_name(db, &name);
175 }
176 if has_ancestor(&tok, SyntaxKind::InnerClassDecl) {
184 return None;
185 }
186 classify_body_ref(db, ft, pos.file, pos.offset, &name)
189}
190
191fn classify_decl(
193 db: &dyn Db,
194 ft: FileText,
195 file: FileId,
196 name_node: &GdNode,
197 name: &SmolStr,
198 tok_range: TextRange,
199) -> Option<GodotDef> {
200 let decl = name_node.parent()?;
201 let in_body = node_has_ancestor(decl, SyntaxKind::FuncDecl)
205 || node_has_ancestor(decl, SyntaxKind::Getter)
206 || node_has_ancestor(decl, SyntaxKind::Setter)
207 || node_has_ancestor(decl, SyntaxKind::LambdaExpr);
208 let in_inner_class = node_has_ancestor(decl, SyntaxKind::InnerClassDecl);
215 match decl.kind() {
216 SyntaxKind::ClassNameDecl => Some(GodotDef::Global {
217 decl_file: file,
218 name: name.clone(),
219 }),
220 SyntaxKind::Param | SyntaxKind::ForStmt | SyntaxKind::PatternBind => {
222 local_def(db, ft, file, tok_range)
223 }
224 SyntaxKind::VarDecl | SyntaxKind::ConstDecl if in_body => {
226 local_def(db, ft, file, tok_range)
227 }
228 SyntaxKind::FuncDecl
231 | SyntaxKind::SignalDecl
232 | SyntaxKind::EnumDecl
233 | SyntaxKind::InnerClassDecl
234 | SyntaxKind::VarDecl
235 | SyntaxKind::ConstDecl
236 if !in_inner_class =>
237 {
238 Some(GodotDef::Member {
239 owner_file: file,
240 name: name.clone(),
241 })
242 }
243 _ => None,
244 }
245}
246
247fn local_def(db: &dyn Db, ft: FileText, file: FileId, tok_range: TextRange) -> Option<GodotDef> {
252 let fi = crate::queries::analyze_file(db, ft);
253 let unit = fi.unit_at(tok_range.start)?;
254 let binding = unit.result.binding_at(tok_range.start)?;
255 Some(GodotDef::Local {
256 body_file: file,
257 body_range: unit.range,
258 decl_name_range: trim_range(ft.text(db), binding.name_range),
259 })
260}
261
262fn trim_range(text: &str, nr: TextRange) -> TextRange {
265 match text.get(nr.start as usize..nr.end as usize) {
266 Some(s) => {
267 let lead = u32::try_from(s.len() - s.trim_start().len()).unwrap_or(0);
268 let len = u32::try_from(s.trim().len()).unwrap_or(0);
269 TextRange::new(nr.start + lead, nr.start + lead + len)
270 }
271 None => nr,
272 }
273}
274
275fn classify_type_name(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
277 let api = db.engine()?;
278 match crate::resolve::resolve_type_name(db, api, name) {
279 Ty::ScriptRef(sref) => Some(GodotDef::Global {
280 decl_file: FileId(sref.0),
281 name: name.clone(),
282 }),
283 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
284 _ => None,
285 }
286}
287
288fn classify_body_ref(
290 db: &dyn Db,
291 ft: FileText,
292 file: FileId,
293 offset: u32,
294 name: &SmolStr,
295) -> Option<GodotDef> {
296 let fi = crate::queries::analyze_file(db, ft);
297 let unit = fi.unit_at(offset)?;
298 let eid = unit.body.source_map.expr_at_offset(offset)?;
299 match unit.body.expr(eid) {
300 crate::body::Expr::Name(n) if n == name => {
301 resolve_name_to_def(db, ft, file, offset, unit, name)
302 }
303 crate::body::Expr::Field {
304 receiver,
305 name: fname,
306 name_range,
307 } if fname == name && name_range.start <= offset && offset < name_range.end => {
308 if matches!(unit.body.expr(*receiver), crate::body::Expr::SelfExpr) {
311 return member_owner(db, crate::ty::ScriptRefId(file.0), name, 0).map(|owner| {
312 GodotDef::Member {
313 owner_file: owner,
314 name: name.clone(),
315 }
316 });
317 }
318 let recv_ty = unit.result.type_of(*receiver)?;
319 match recv_ty {
320 Ty::ScriptRef(sref) => {
321 member_owner(db, *sref, name, 0).map(|owner| GodotDef::Member {
322 owner_file: owner,
323 name: name.clone(),
324 })
325 }
326 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
327 _ => None, }
329 }
330 _ => None,
331 }
332}
333
334fn resolve_name_to_def(
349 db: &dyn Db,
350 ft: FileText,
351 file: FileId,
352 offset: u32,
353 unit: &crate::infer::Unit,
354 name: &SmolStr,
355) -> Option<GodotDef> {
356 let text = ft.text(db);
363 let mut best: Option<TextRange> = None;
364 for b in &unit.result.bindings {
365 if !matches!(
366 b.kind,
367 crate::infer::BindingKind::Var
368 | crate::infer::BindingKind::Param
369 | crate::infer::BindingKind::ForVar
370 | crate::infer::BindingKind::MatchBind
371 ) {
372 continue;
373 }
374 let nr = trim_range(text, b.name_range);
375 if text.get(nr.start as usize..nr.end as usize) != Some(name.as_str()) {
376 continue;
377 }
378 if nr.start <= offset && best.is_none_or(|cur| nr.start >= cur.start) {
379 best = Some(nr);
380 }
381 }
382 if let Some(nr) = best {
383 return Some(GodotDef::Local {
384 body_file: file,
385 body_range: unit.range,
386 decl_name_range: nr,
387 });
388 }
389 if let Some(owner) = member_owner(db, crate::ty::ScriptRefId(file.0), name, 0) {
391 return Some(GodotDef::Member {
392 owner_file: owner,
393 name: name.clone(),
394 });
395 }
396 if let Some(api) = db.engine()
399 && crate::resolve::resolve_global(api, name).is_some()
400 {
401 return Some(GodotDef::Engine { name: name.clone() });
402 }
403 if let Some(def) = class_name_global_def(db, name) {
405 return Some(def);
406 }
407 autoload_def(db, name)
409}
410
411fn class_name_global_def(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
415 let root = db.source_root()?;
416 let decl = crate::queries::global_registry(db, root).resolve(name)?;
417 Some(GodotDef::Global {
418 decl_file: decl.file_id(db),
419 name: name.clone(),
420 })
421}
422
423fn autoload_def(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
427 let config = db.project_config()?;
428 let path = crate::queries::autoload_registry(db, config)
429 .resolve_path(name)
430 .cloned()?;
431 let target_file = db.source_root().and_then(|root| {
432 crate::queries::res_path_registry(db, root)
433 .get(path.as_str())
434 .copied()
435 });
436 Some(GodotDef::Autoload {
437 name: name.clone(),
438 target_file,
439 })
440}
441
442fn member_owner(
445 db: &dyn Db,
446 sref: crate::ty::ScriptRefId,
447 name: &str,
448 depth: u32,
449) -> Option<FileId> {
450 if depth > 32 {
451 return None;
452 }
453 let file = db.file_text(FileId(sref.0))?;
454 let tree = crate::queries::item_tree(db, file);
455 if tree.member(name).is_some() || anon_enum_has_variant(&tree, name) {
458 return Some(file.file_id(db));
459 }
460 match crate::queries::script_class(db, file).base() {
461 Ty::ScriptRef(base) => member_owner(db, *base, name, depth + 1),
462 _ => None, }
464}
465
466fn anon_enum_has_variant(tree: &crate::item_tree::ItemTree, name: &str) -> bool {
470 tree.members.iter().any(|m| {
471 matches!(m, crate::item_tree::Member::Enum(e)
472 if e.name.is_none() && e.variants.iter().any(|v| v == name))
473 })
474}
475
476fn in_anon_enum(enum_variant: &GdNode) -> bool {
478 enum_variant.parent().is_some_and(|enum_decl| {
479 enum_decl.kind() == SyntaxKind::EnumDecl
480 && !enum_decl.children().any(|c| c.kind() == SyntaxKind::Name)
481 })
482}
483
484fn has_ancestor(tok: &GdToken, kind: SyntaxKind) -> bool {
486 node_has_ancestor_or_self(tok.parent(), kind)
487}
488
489fn node_has_ancestor(node: &GdNode, kind: SyntaxKind) -> bool {
491 node.parent()
492 .is_some_and(|p| node_has_ancestor_or_self(p, kind))
493}
494
495fn node_has_ancestor_or_self(node: &GdNode, kind: SyntaxKind) -> bool {
496 let mut cur = Some(node.clone());
497 while let Some(n) = cur {
498 if n.kind() == kind {
499 return true;
500 }
501 cur = n.parent().cloned();
502 }
503 false
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
511pub struct NodePathTarget {
512 pub scene: FileId,
514 pub node_name: SmolStr,
516 pub header_span: TextRange,
518 pub name_span: TextRange,
520}
521
522#[must_use]
525pub fn node_path_target(db: &dyn Db, pos: FilePosition) -> Option<NodePathTarget> {
526 let ft = db.file_text(pos.file)?;
527 let fi = crate::queries::analyze_file(db, ft);
528 let unit = fi.unit_at(pos.offset)?;
529 let eid = unit.body.source_map.expr_at_offset(pos.offset)?;
530 let crate::body::Expr::GetNode {
531 path: Some(path),
532 unique,
533 } = unit.body.expr(eid)
534 else {
535 return None;
536 };
537 let ctx = crate::queries::scene_context(db, ft)?;
538 let idx = if *unique {
539 ctx.model.resolve_unique(path)
540 } else {
541 ctx.model.resolve_path_from(ctx.attach, path)
542 }?;
543 let node = ctx.model.node(idx)?;
544 Some(NodePathTarget {
545 scene: ctx.scene,
546 node_name: node.name.clone(),
547 header_span: node.header_span,
548 name_span: node.name_span,
549 })
550}
551
552fn classify_node_path_at(
560 db: &dyn Db,
561 ft: FileText,
562 _file: FileId,
563 tok: &GdToken,
564 offset: u32,
565) -> Option<GodotDef> {
566 let np = node_path_ancestor(tok)?;
567 let unique = np.kind() == SyntaxKind::UniqueNodeExpr;
568 let truncated = node_path_up_to_cursor(&np, offset)?;
569 let ctx = crate::queries::scene_context(db, ft)?;
570 if ctx.ambiguous {
571 return None; }
573 let idx = if unique {
574 ctx.model.resolve_unique(&truncated)
575 } else {
576 ctx.model.resolve_path_from(ctx.attach, &truncated)
577 }?;
578 Some(GodotDef::SceneNode {
579 scene: ctx.scene,
580 path: ctx.model.node_full_path(idx)?,
581 })
582}
583
584fn classify_get_node_call_at(
588 db: &dyn Db,
589 ft: FileText,
590 tok: &GdToken,
591 offset: u32,
592) -> Option<GodotDef> {
593 if tok.kind() != SyntaxKind::String {
594 return None;
595 }
596 let fi = crate::queries::analyze_file(db, ft);
597 let unit = fi.unit_at(offset)?;
598 let eid = unit.body.source_map.expr_at_offset(offset)?;
599 let crate::body::Expr::GetNode { path: Some(_), .. } = unit.body.expr(eid) else {
600 return None; };
602 let range = cst::token_range(tok);
603 let content = tok.text().trim_matches(['"', '\'']);
604 let truncated = segment_up_to(content, range.start + 1, offset)?;
605 let ctx = crate::queries::scene_context(db, ft)?;
606 if ctx.ambiguous {
607 return None;
608 }
609 Some(GodotDef::SceneNode {
611 scene: ctx.scene,
612 path: ctx
613 .model
614 .node_full_path(ctx.model.resolve_path_from(ctx.attach, &truncated)?)?,
615 })
616}
617
618fn node_path_ancestor(tok: &GdToken) -> Option<GdNode> {
620 let mut cur = Some(tok.parent().clone());
621 while let Some(n) = cur {
622 if matches!(
623 n.kind(),
624 SyntaxKind::GetNodeExpr | SyntaxKind::UniqueNodeExpr
625 ) {
626 return Some(n);
627 }
628 cur = n.parent().cloned();
629 }
630 None
631}
632
633fn node_path_up_to_cursor(np: &GdNode, offset: u32) -> Option<String> {
637 if let Some(str_tok) = np
639 .children_with_tokens()
640 .filter_map(cstree::util::NodeOrToken::into_token)
641 .find(|t| t.kind() == SyntaxKind::String)
642 {
643 let range = cst::token_range(str_tok);
644 let content = str_tok.text().trim_matches(['"', '\'']);
645 return segment_up_to(content, range.start + 1, offset); }
647 let idents: Vec<(TextRange, String)> = np
649 .children_with_tokens()
650 .filter_map(cstree::util::NodeOrToken::into_token)
651 .filter(|t| t.kind() == SyntaxKind::Ident)
652 .map(|t| (cst::token_range(t), t.text().to_owned()))
653 .collect();
654 let hit = idents
655 .iter()
656 .position(|(r, _)| offset >= r.start && offset <= r.end)
657 .or_else(|| idents.iter().rposition(|(r, _)| r.start <= offset))?;
659 Some(
660 idents[..=hit]
661 .iter()
662 .map(|(_, s)| s.as_str())
663 .collect::<Vec<_>>()
664 .join("/"),
665 )
666}
667
668fn segment_up_to(content: &str, content_start: u32, offset: u32) -> Option<String> {
674 let rel = offset.checked_sub(content_start)? as usize;
675 if rel > content.len() {
676 return None;
677 }
678 let end = content[rel.min(content.len())..]
679 .find('/')
680 .map_or(content.len(), |i| rel + i);
681 Some(content[..end].to_owned())
682}
683
684fn in_span(offset: u32, span: TextRange) -> bool {
686 offset >= span.start && offset < span.end
687}
688
689fn classify_scene_name(db: &dyn Db, ft: FileText, file: FileId, offset: u32) -> Option<GodotDef> {
693 let model = crate::queries::scene_model(db, ft);
694 let def = |idx: NodeIdx| {
695 Some(GodotDef::SceneNode {
696 scene: file,
697 path: model.node_full_path(idx)?,
698 })
699 };
700 for (i, n) in model.nodes.iter().enumerate() {
702 if in_span(offset, n.name_span) {
703 return def(NodeIdx(u32::try_from(i).ok()?));
704 }
705 }
706 for n in &model.nodes {
708 if let (Some(span), Some(path)) = (n.parent_span, n.parent_path.as_deref())
709 && in_span(offset, span)
710 {
711 let trunc = segment_up_to(path, span.start, offset)?;
712 return def(model.resolve_path(&trunc)?);
713 }
714 }
715 for c in &model.connections {
717 for (span, path) in [(c.from_span, &c.from), (c.to_span, &c.to)] {
718 if in_span(offset, span) {
719 let trunc = segment_up_to(path, span.start, offset)?;
720 return def(model.resolve_path(&trunc)?);
721 }
722 }
723 }
724 None
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730 use gdscript_db::RootDatabase;
731 use salsa::Durability;
732
733 fn db_with(files: &[(u32, &str)]) -> RootDatabase {
734 let mut db = RootDatabase::default();
735 for (id, src) in files {
736 db.set_file_text(FileId(*id), src, Durability::LOW);
737 }
738 db.sync_source_root();
739 db
740 }
741
742 fn at(db: &RootDatabase, file: u32, needle: &str, src: &str) -> Option<GodotDef> {
743 let offset = u32::try_from(src.find(needle).expect("needle")).unwrap();
744 classify(
745 db,
746 FilePosition {
747 file: FileId(file),
748 offset,
749 },
750 )
751 }
752
753 fn at_nth(db: &RootDatabase, file: u32, needle: &str, n: usize, src: &str) -> Option<GodotDef> {
755 let off = src.match_indices(needle).nth(n).expect("nth needle").0;
756 classify(
757 db,
758 FilePosition {
759 file: FileId(file),
760 offset: u32::try_from(off).unwrap(),
761 },
762 )
763 }
764
765 #[test]
766 fn two_unrelated_locals_are_distinct() {
767 let src =
768 "func a():\n\tvar i := 1\n\tvar ra := i\nfunc b():\n\tvar i := 2\n\tvar rb := i\n";
769 let db = db_with(&[(0, src)]);
770 let off_a = u32::try_from(src.match_indices(":= i").next().unwrap().0 + 3).unwrap();
772 let off_b = u32::try_from(src.match_indices(":= i").nth(1).unwrap().0 + 3).unwrap();
773 let da = classify(
774 &db,
775 FilePosition {
776 file: FileId(0),
777 offset: off_a,
778 },
779 )
780 .unwrap();
781 let dbf = classify(
782 &db,
783 FilePosition {
784 file: FileId(0),
785 offset: off_b,
786 },
787 )
788 .unwrap();
789 assert!(matches!(da, GodotDef::Local { .. }), "{da:?}");
790 assert!(matches!(dbf, GodotDef::Local { .. }), "{dbf:?}");
791 assert_ne!(da, dbf, "two unrelated `i`s must be distinct locals");
792 }
793
794 #[test]
795 fn local_shadowing_a_member_is_distinct() {
796 let src = "var pos := 1\nfunc f():\n\tvar pos := 2\n\tprint(pos)\n";
797 let db = db_with(&[(0, src)]);
798 let member = at_nth(&db, 0, "pos", 0, src).unwrap();
800 let local = at_nth(&db, 0, "pos", 1, src).unwrap();
801 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
802 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
803 assert_ne!(member, local);
804 let r = at_nth(&db, 0, "pos", 2, src).unwrap();
806 assert_eq!(r, local);
807 }
808
809 #[test]
810 fn same_named_members_of_different_classes_are_distinct() {
811 let a = "class_name A\nfunc update():\n\tpass\n";
812 let b = "class_name B\nfunc update():\n\tpass\n";
813 let db = db_with(&[(0, a), (1, b)]);
814 let ua = at(&db, 0, "update", a).unwrap();
815 let ub = at(&db, 1, "update", b).unwrap();
816 assert!(matches!(ua, GodotDef::Member { .. }));
817 assert!(matches!(ub, GodotDef::Member { .. }));
818 assert_ne!(ua, ub, "A.update and B.update must be distinct");
819 }
820
821 #[test]
822 fn class_name_decl_and_reference_classify_to_the_same_global() {
823 let widget = "class_name Widget\nfunc make() -> int:\n\treturn 1\n";
824 let user = "func f():\n\tvar w: Widget\n\tvar x := Widget.new()\n";
825 let db = db_with(&[(0, widget), (1, user)]);
826 let decl = at(&db, 0, "Widget", widget).unwrap();
827 let ann = at(&db, 1, "Widget\n", user).unwrap(); let ctor = at(&db, 1, "Widget.new", user).unwrap();
829 assert!(matches!(
830 decl,
831 GodotDef::Global {
832 decl_file: FileId(0),
833 ..
834 }
835 ));
836 assert_eq!(decl, ann, "annotation must resolve to the class_name def");
837 assert_eq!(
838 decl, ctor,
839 "`Widget.new()` must resolve to the class_name def"
840 );
841 }
842
843 #[test]
844 fn extends_user_class_classifies_to_the_global() {
845 let base = "class_name Base\nfunc m():\n\tpass\n";
849 let derived = "class_name Derived\nextends Base\n";
850 let db = db_with(&[(0, base), (1, derived)]);
851 let decl = at(&db, 0, "Base", base).unwrap();
852 let ext = at(&db, 1, "Base", derived).unwrap(); assert!(matches!(
854 decl,
855 GodotDef::Global {
856 decl_file: FileId(0),
857 ..
858 }
859 ));
860 assert_eq!(
861 decl, ext,
862 "`extends Base` must classify to Base's class_name def"
863 );
864 }
865
866 #[test]
867 fn inherited_member_resolves_to_the_declaring_base() {
868 let base = "class_name Base\nfunc base_m() -> int:\n\treturn 1\n";
869 let derived = "class_name Derived\nextends Base\nfunc use_it():\n\tself.base_m()\n";
870 let db = db_with(&[(0, base), (1, derived)]);
871 let decl = at(&db, 0, "base_m", base).unwrap();
872 let call = at(&db, 1, "base_m()", derived).unwrap();
873 assert!(matches!(
874 decl,
875 GodotDef::Member {
876 owner_file: FileId(0),
877 ..
878 }
879 ));
880 assert_eq!(
881 decl, call,
882 "inherited call must resolve to the base's member def"
883 );
884 }
885
886 #[test]
887 fn inner_class_member_is_out_of_scope() {
888 let src =
891 "class_name A\nfunc update():\n\tpass\nclass Inner:\n\tfunc update():\n\t\tpass\n";
892 let db = db_with(&[(0, src)]);
893 let top = at_nth(&db, 0, "update", 0, src).unwrap();
894 let inner = at_nth(&db, 0, "update", 1, src);
895 assert!(matches!(top, GodotDef::Member { .. }), "{top:?}");
896 assert_eq!(
897 inner, None,
898 "an inner-class member must not classify (out of scope), got {inner:?}"
899 );
900 }
901
902 #[test]
903 fn match_capture_classifies_as_local_distinct_from_member() {
904 let src = "var cap := 0\nfunc f(v):\n\tmatch v:\n\t\tvar cap:\n\t\t\tprint(cap)\n";
907 let db = db_with(&[(0, src)]);
908 let member = at_nth(&db, 0, "cap", 0, src).unwrap();
909 let capture = at_nth(&db, 0, "cap", 1, src).unwrap();
910 let usage = at_nth(&db, 0, "cap", 2, src).unwrap();
911 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
912 assert!(matches!(capture, GodotDef::Local { .. }), "{capture:?}");
913 assert_eq!(
914 usage, capture,
915 "`print(cap)` must resolve to the match capture"
916 );
917 assert_ne!(usage, member);
918 }
919
920 #[test]
921 fn accessor_body_local_is_not_a_member() {
922 let src = "var hp: int:\n\tget:\n\t\tvar tmp = 2\n\t\treturn tmp\n";
924 let db = db_with(&[(0, src)]);
925 let tmp = at_nth(&db, 0, "tmp", 0, src);
926 assert!(
927 !matches!(tmp, Some(GodotDef::Member { .. })),
928 "a local in a get/set body must not be a Member, got {tmp:?}"
929 );
930 }
931
932 #[test]
933 fn anon_enum_variant_classifies_as_member() {
934 let src = "enum { FIRE, ICE }\nfunc f():\n\tprint(FIRE)\n";
937 let db = db_with(&[(0, src)]);
938 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:?}");
941 assert_eq!(
942 decl, usage,
943 "an anon-enum variant decl and use share identity"
944 );
945 }
946
947 #[test]
948 fn shadowed_local_reference_resolves_to_the_nearest_declaration() {
949 let src = "func f(x):\n\tvar x := 2\n\tprint(x)\n";
952 let db = db_with(&[(0, src)]);
953 let param = at_nth(&db, 0, "x", 0, src).unwrap();
954 let local = at_nth(&db, 0, "x", 1, src).unwrap();
955 let usage = at_nth(&db, 0, "x", 2, src).unwrap();
956 assert!(matches!(param, GodotDef::Local { .. }), "{param:?}");
957 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
958 assert_ne!(param, local, "param x and local x are distinct");
959 assert_eq!(
960 usage, local,
961 "the reference resolves to the nearest (local) declaration"
962 );
963 }
964}