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 if tok.kind() != SyntaxKind::Ident {
100 return None; }
102 let name = SmolStr::new(tok.text());
103 let tok_range = cst::token_range(&tok);
104 let parent = tok.parent();
105
106 if parent.kind() == SyntaxKind::Name
108 && let Some(def) = classify_decl(db, ft, pos.file, parent, &name, tok_range)
109 {
110 return Some(def);
111 }
112 if let Some(head) = cst::extends_head_token(parent)
118 && cst::token_range(&head) == tok_range
119 {
120 return classify_type_name(db, &name);
121 }
122 if parent.kind() == SyntaxKind::EnumVariant && in_anon_enum(parent) {
128 return Some(GodotDef::Member {
129 owner_file: pos.file,
130 name,
131 });
132 }
133 if has_ancestor(&tok, SyntaxKind::TypeRef) {
136 return classify_type_name(db, &name);
137 }
138 classify_body_ref(db, ft, pos.file, pos.offset, &name)
141}
142
143fn classify_decl(
145 db: &dyn Db,
146 ft: FileText,
147 file: FileId,
148 name_node: &GdNode,
149 name: &SmolStr,
150 tok_range: TextRange,
151) -> Option<GodotDef> {
152 let decl = name_node.parent()?;
153 let in_body = node_has_ancestor(decl, SyntaxKind::FuncDecl)
157 || node_has_ancestor(decl, SyntaxKind::Getter)
158 || node_has_ancestor(decl, SyntaxKind::Setter)
159 || node_has_ancestor(decl, SyntaxKind::LambdaExpr);
160 let in_inner_class = node_has_ancestor(decl, SyntaxKind::InnerClassDecl);
167 match decl.kind() {
168 SyntaxKind::ClassNameDecl => Some(GodotDef::Global {
169 decl_file: file,
170 name: name.clone(),
171 }),
172 SyntaxKind::Param | SyntaxKind::ForStmt | SyntaxKind::PatternBind => {
174 local_def(db, ft, file, tok_range)
175 }
176 SyntaxKind::VarDecl | SyntaxKind::ConstDecl if in_body => {
178 local_def(db, ft, file, tok_range)
179 }
180 SyntaxKind::FuncDecl
183 | SyntaxKind::SignalDecl
184 | SyntaxKind::EnumDecl
185 | SyntaxKind::InnerClassDecl
186 | SyntaxKind::VarDecl
187 | SyntaxKind::ConstDecl
188 if !in_inner_class =>
189 {
190 Some(GodotDef::Member {
191 owner_file: file,
192 name: name.clone(),
193 })
194 }
195 _ => None,
196 }
197}
198
199fn local_def(db: &dyn Db, ft: FileText, file: FileId, tok_range: TextRange) -> Option<GodotDef> {
204 let fi = crate::queries::analyze_file(db, ft);
205 let unit = fi.unit_at(tok_range.start)?;
206 let binding = unit.result.binding_at(tok_range.start)?;
207 Some(GodotDef::Local {
208 body_file: file,
209 body_range: unit.range,
210 decl_name_range: trim_range(ft.text(db), binding.name_range),
211 })
212}
213
214fn trim_range(text: &str, nr: TextRange) -> TextRange {
217 match text.get(nr.start as usize..nr.end as usize) {
218 Some(s) => {
219 let lead = u32::try_from(s.len() - s.trim_start().len()).unwrap_or(0);
220 let len = u32::try_from(s.trim().len()).unwrap_or(0);
221 TextRange::new(nr.start + lead, nr.start + lead + len)
222 }
223 None => nr,
224 }
225}
226
227fn classify_type_name(db: &dyn Db, name: &SmolStr) -> Option<GodotDef> {
229 let api = db.engine()?;
230 match crate::resolve::resolve_type_name(db, api, name) {
231 Ty::ScriptRef(sref) => Some(GodotDef::Global {
232 decl_file: FileId(sref.0),
233 name: name.clone(),
234 }),
235 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
236 _ => None,
237 }
238}
239
240fn classify_body_ref(
242 db: &dyn Db,
243 ft: FileText,
244 file: FileId,
245 offset: u32,
246 name: &SmolStr,
247) -> Option<GodotDef> {
248 let fi = crate::queries::analyze_file(db, ft);
249 let unit = fi.unit_at(offset)?;
250 let eid = unit.body.source_map.expr_at_offset(offset)?;
251 match unit.body.expr(eid) {
252 crate::body::Expr::Name(n) if n == name => {
253 resolve_name_to_def(db, ft, file, offset, unit, name)
254 }
255 crate::body::Expr::Field {
256 receiver,
257 name: fname,
258 name_range,
259 } if fname == name && name_range.start <= offset && offset < name_range.end => {
260 if matches!(unit.body.expr(*receiver), crate::body::Expr::SelfExpr) {
263 return member_owner(db, crate::ty::ScriptRefId(file.0), name, 0).map(|owner| {
264 GodotDef::Member {
265 owner_file: owner,
266 name: name.clone(),
267 }
268 });
269 }
270 let recv_ty = unit.result.type_of(*receiver)?;
271 match recv_ty {
272 Ty::ScriptRef(sref) => {
273 member_owner(db, *sref, name, 0).map(|owner| GodotDef::Member {
274 owner_file: owner,
275 name: name.clone(),
276 })
277 }
278 Ty::Object(_) | Ty::Builtin(_) => Some(GodotDef::Engine { name: name.clone() }),
279 _ => None, }
281 }
282 _ => None,
283 }
284}
285
286fn resolve_name_to_def(
290 db: &dyn Db,
291 ft: FileText,
292 file: FileId,
293 offset: u32,
294 unit: &crate::infer::Unit,
295 name: &SmolStr,
296) -> Option<GodotDef> {
297 let text = ft.text(db);
304 let mut best: Option<TextRange> = None;
305 for b in &unit.result.bindings {
306 if !matches!(
307 b.kind,
308 crate::infer::BindingKind::Var
309 | crate::infer::BindingKind::Param
310 | crate::infer::BindingKind::ForVar
311 | crate::infer::BindingKind::MatchBind
312 ) {
313 continue;
314 }
315 let nr = trim_range(text, b.name_range);
316 if text.get(nr.start as usize..nr.end as usize) != Some(name.as_str()) {
317 continue;
318 }
319 if nr.start <= offset && best.is_none_or(|cur| nr.start >= cur.start) {
320 best = Some(nr);
321 }
322 }
323 if let Some(nr) = best {
324 return Some(GodotDef::Local {
325 body_file: file,
326 body_range: unit.range,
327 decl_name_range: nr,
328 });
329 }
330 if let Some(owner) = member_owner(db, crate::ty::ScriptRefId(file.0), name, 0) {
332 return Some(GodotDef::Member {
333 owner_file: owner,
334 name: name.clone(),
335 });
336 }
337 if let Some(api) = db.engine()
340 && crate::resolve::resolve_global(api, name).is_some()
341 {
342 return Some(GodotDef::Engine { name: name.clone() });
343 }
344 if let Some(root) = db.source_root()
346 && let Some(decl) = crate::queries::global_registry(db, root).resolve(name)
347 {
348 return Some(GodotDef::Global {
349 decl_file: decl.file_id(db),
350 name: name.clone(),
351 });
352 }
353 if let Some(config) = db.project_config()
355 && let Some(path) = crate::queries::autoload_registry(db, config)
356 .resolve_path(name)
357 .cloned()
358 {
359 let target = db.source_root().and_then(|root| {
360 crate::queries::res_path_registry(db, root)
361 .get(path.as_str())
362 .copied()
363 });
364 return Some(GodotDef::Autoload {
365 name: name.clone(),
366 target_file: target,
367 });
368 }
369 None
370}
371
372fn member_owner(
375 db: &dyn Db,
376 sref: crate::ty::ScriptRefId,
377 name: &str,
378 depth: u32,
379) -> Option<FileId> {
380 if depth > 32 {
381 return None;
382 }
383 let file = db.file_text(FileId(sref.0))?;
384 let tree = crate::queries::item_tree(db, file);
385 if tree.member(name).is_some() || anon_enum_has_variant(&tree, name) {
388 return Some(file.file_id(db));
389 }
390 match crate::queries::script_class(db, file).base() {
391 Ty::ScriptRef(base) => member_owner(db, *base, name, depth + 1),
392 _ => None, }
394}
395
396fn anon_enum_has_variant(tree: &crate::item_tree::ItemTree, name: &str) -> bool {
400 tree.members.iter().any(|m| {
401 matches!(m, crate::item_tree::Member::Enum(e)
402 if e.name.is_none() && e.variants.iter().any(|v| v == name))
403 })
404}
405
406fn in_anon_enum(enum_variant: &GdNode) -> bool {
408 enum_variant.parent().is_some_and(|enum_decl| {
409 enum_decl.kind() == SyntaxKind::EnumDecl
410 && !enum_decl.children().any(|c| c.kind() == SyntaxKind::Name)
411 })
412}
413
414fn has_ancestor(tok: &GdToken, kind: SyntaxKind) -> bool {
416 node_has_ancestor_or_self(tok.parent(), kind)
417}
418
419fn node_has_ancestor(node: &GdNode, kind: SyntaxKind) -> bool {
421 node.parent()
422 .is_some_and(|p| node_has_ancestor_or_self(p, kind))
423}
424
425fn node_has_ancestor_or_self(node: &GdNode, kind: SyntaxKind) -> bool {
426 let mut cur = Some(node.clone());
427 while let Some(n) = cur {
428 if n.kind() == kind {
429 return true;
430 }
431 cur = n.parent().cloned();
432 }
433 false
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct NodePathTarget {
442 pub scene: FileId,
444 pub node_name: SmolStr,
446 pub header_span: TextRange,
448 pub name_span: TextRange,
450}
451
452#[must_use]
455pub fn node_path_target(db: &dyn Db, pos: FilePosition) -> Option<NodePathTarget> {
456 let ft = db.file_text(pos.file)?;
457 let fi = crate::queries::analyze_file(db, ft);
458 let unit = fi.unit_at(pos.offset)?;
459 let eid = unit.body.source_map.expr_at_offset(pos.offset)?;
460 let crate::body::Expr::GetNode {
461 path: Some(path),
462 unique,
463 } = unit.body.expr(eid)
464 else {
465 return None;
466 };
467 let ctx = crate::queries::scene_context(db, ft)?;
468 let idx = if *unique {
469 ctx.model.resolve_unique(path)
470 } else {
471 ctx.model.resolve_path_from(ctx.attach, path)
472 }?;
473 let node = ctx.model.node(idx)?;
474 Some(NodePathTarget {
475 scene: ctx.scene,
476 node_name: node.name.clone(),
477 header_span: node.header_span,
478 name_span: node.name_span,
479 })
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use gdscript_db::RootDatabase;
486 use salsa::Durability;
487
488 fn db_with(files: &[(u32, &str)]) -> RootDatabase {
489 let mut db = RootDatabase::default();
490 for (id, src) in files {
491 db.set_file_text(FileId(*id), src, Durability::LOW);
492 }
493 db.sync_source_root();
494 db
495 }
496
497 fn at(db: &RootDatabase, file: u32, needle: &str, src: &str) -> Option<GodotDef> {
498 let offset = u32::try_from(src.find(needle).expect("needle")).unwrap();
499 classify(
500 db,
501 FilePosition {
502 file: FileId(file),
503 offset,
504 },
505 )
506 }
507
508 fn at_nth(db: &RootDatabase, file: u32, needle: &str, n: usize, src: &str) -> Option<GodotDef> {
510 let off = src.match_indices(needle).nth(n).expect("nth needle").0;
511 classify(
512 db,
513 FilePosition {
514 file: FileId(file),
515 offset: u32::try_from(off).unwrap(),
516 },
517 )
518 }
519
520 #[test]
521 fn two_unrelated_locals_are_distinct() {
522 let src =
523 "func a():\n\tvar i := 1\n\tvar ra := i\nfunc b():\n\tvar i := 2\n\tvar rb := i\n";
524 let db = db_with(&[(0, src)]);
525 let off_a = u32::try_from(src.match_indices(":= i").next().unwrap().0 + 3).unwrap();
527 let off_b = u32::try_from(src.match_indices(":= i").nth(1).unwrap().0 + 3).unwrap();
528 let da = classify(
529 &db,
530 FilePosition {
531 file: FileId(0),
532 offset: off_a,
533 },
534 )
535 .unwrap();
536 let dbf = classify(
537 &db,
538 FilePosition {
539 file: FileId(0),
540 offset: off_b,
541 },
542 )
543 .unwrap();
544 assert!(matches!(da, GodotDef::Local { .. }), "{da:?}");
545 assert!(matches!(dbf, GodotDef::Local { .. }), "{dbf:?}");
546 assert_ne!(da, dbf, "two unrelated `i`s must be distinct locals");
547 }
548
549 #[test]
550 fn local_shadowing_a_member_is_distinct() {
551 let src = "var pos := 1\nfunc f():\n\tvar pos := 2\n\tprint(pos)\n";
552 let db = db_with(&[(0, src)]);
553 let member = at_nth(&db, 0, "pos", 0, src).unwrap();
555 let local = at_nth(&db, 0, "pos", 1, src).unwrap();
556 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
557 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
558 assert_ne!(member, local);
559 let r = at_nth(&db, 0, "pos", 2, src).unwrap();
561 assert_eq!(r, local);
562 }
563
564 #[test]
565 fn same_named_members_of_different_classes_are_distinct() {
566 let a = "class_name A\nfunc update():\n\tpass\n";
567 let b = "class_name B\nfunc update():\n\tpass\n";
568 let db = db_with(&[(0, a), (1, b)]);
569 let ua = at(&db, 0, "update", a).unwrap();
570 let ub = at(&db, 1, "update", b).unwrap();
571 assert!(matches!(ua, GodotDef::Member { .. }));
572 assert!(matches!(ub, GodotDef::Member { .. }));
573 assert_ne!(ua, ub, "A.update and B.update must be distinct");
574 }
575
576 #[test]
577 fn class_name_decl_and_reference_classify_to_the_same_global() {
578 let widget = "class_name Widget\nfunc make() -> int:\n\treturn 1\n";
579 let user = "func f():\n\tvar w: Widget\n\tvar x := Widget.new()\n";
580 let db = db_with(&[(0, widget), (1, user)]);
581 let decl = at(&db, 0, "Widget", widget).unwrap();
582 let ann = at(&db, 1, "Widget\n", user).unwrap(); let ctor = at(&db, 1, "Widget.new", user).unwrap();
584 assert!(matches!(
585 decl,
586 GodotDef::Global {
587 decl_file: FileId(0),
588 ..
589 }
590 ));
591 assert_eq!(decl, ann, "annotation must resolve to the class_name def");
592 assert_eq!(
593 decl, ctor,
594 "`Widget.new()` must resolve to the class_name def"
595 );
596 }
597
598 #[test]
599 fn extends_user_class_classifies_to_the_global() {
600 let base = "class_name Base\nfunc m():\n\tpass\n";
604 let derived = "class_name Derived\nextends Base\n";
605 let db = db_with(&[(0, base), (1, derived)]);
606 let decl = at(&db, 0, "Base", base).unwrap();
607 let ext = at(&db, 1, "Base", derived).unwrap(); assert!(matches!(
609 decl,
610 GodotDef::Global {
611 decl_file: FileId(0),
612 ..
613 }
614 ));
615 assert_eq!(
616 decl, ext,
617 "`extends Base` must classify to Base's class_name def"
618 );
619 }
620
621 #[test]
622 fn inherited_member_resolves_to_the_declaring_base() {
623 let base = "class_name Base\nfunc base_m() -> int:\n\treturn 1\n";
624 let derived = "class_name Derived\nextends Base\nfunc use_it():\n\tself.base_m()\n";
625 let db = db_with(&[(0, base), (1, derived)]);
626 let decl = at(&db, 0, "base_m", base).unwrap();
627 let call = at(&db, 1, "base_m()", derived).unwrap();
628 assert!(matches!(
629 decl,
630 GodotDef::Member {
631 owner_file: FileId(0),
632 ..
633 }
634 ));
635 assert_eq!(
636 decl, call,
637 "inherited call must resolve to the base's member def"
638 );
639 }
640
641 #[test]
642 fn inner_class_member_is_out_of_scope() {
643 let src =
646 "class_name A\nfunc update():\n\tpass\nclass Inner:\n\tfunc update():\n\t\tpass\n";
647 let db = db_with(&[(0, src)]);
648 let top = at_nth(&db, 0, "update", 0, src).unwrap();
649 let inner = at_nth(&db, 0, "update", 1, src);
650 assert!(matches!(top, GodotDef::Member { .. }), "{top:?}");
651 assert_eq!(
652 inner, None,
653 "an inner-class member must not classify (out of scope), got {inner:?}"
654 );
655 }
656
657 #[test]
658 fn match_capture_classifies_as_local_distinct_from_member() {
659 let src = "var cap := 0\nfunc f(v):\n\tmatch v:\n\t\tvar cap:\n\t\t\tprint(cap)\n";
662 let db = db_with(&[(0, src)]);
663 let member = at_nth(&db, 0, "cap", 0, src).unwrap();
664 let capture = at_nth(&db, 0, "cap", 1, src).unwrap();
665 let usage = at_nth(&db, 0, "cap", 2, src).unwrap();
666 assert!(matches!(member, GodotDef::Member { .. }), "{member:?}");
667 assert!(matches!(capture, GodotDef::Local { .. }), "{capture:?}");
668 assert_eq!(
669 usage, capture,
670 "`print(cap)` must resolve to the match capture"
671 );
672 assert_ne!(usage, member);
673 }
674
675 #[test]
676 fn accessor_body_local_is_not_a_member() {
677 let src = "var hp: int:\n\tget:\n\t\tvar tmp = 2\n\t\treturn tmp\n";
679 let db = db_with(&[(0, src)]);
680 let tmp = at_nth(&db, 0, "tmp", 0, src);
681 assert!(
682 !matches!(tmp, Some(GodotDef::Member { .. })),
683 "a local in a get/set body must not be a Member, got {tmp:?}"
684 );
685 }
686
687 #[test]
688 fn anon_enum_variant_classifies_as_member() {
689 let src = "enum { FIRE, ICE }\nfunc f():\n\tprint(FIRE)\n";
692 let db = db_with(&[(0, src)]);
693 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:?}");
696 assert_eq!(
697 decl, usage,
698 "an anon-enum variant decl and use share identity"
699 );
700 }
701
702 #[test]
703 fn shadowed_local_reference_resolves_to_the_nearest_declaration() {
704 let src = "func f(x):\n\tvar x := 2\n\tprint(x)\n";
707 let db = db_with(&[(0, src)]);
708 let param = at_nth(&db, 0, "x", 0, src).unwrap();
709 let local = at_nth(&db, 0, "x", 1, src).unwrap();
710 let usage = at_nth(&db, 0, "x", 2, src).unwrap();
711 assert!(matches!(param, GodotDef::Local { .. }), "{param:?}");
712 assert!(matches!(local, GodotDef::Local { .. }), "{local:?}");
713 assert_ne!(param, local, "param x and local x are distinct");
714 assert_eq!(
715 usage, local,
716 "the reference resolves to the nearest (local) declaration"
717 );
718 }
719}