gdscript_hir/resolve.rs
1//! Name & type resolution (Playbook §3.2/§3.5): the [`resolve_external`] Phase-3 seam, the
2//! GDScript source-annotation → [`Ty`] resolver, base-class resolution, the per-class
3//! [`ClassScope`] (the class-member tier of the binder), and global resolution.
4//!
5//! The binder's lookup order (local → class member → inherited → global) is *driven* by
6//! [`crate::infer`]; this module supplies the class-member and global tiers plus the type
7//! resolution all tiers share. Everything here is a pure function of the item tree + the
8//! `Arc`-shared [`EngineApi`] — no body, no cross-file state.
9
10use cstree::util::NodeOrToken;
11use gdscript_api::gdscript_layer::LayerTy;
12use gdscript_api::{BuiltinId, ClassId, EngineApi};
13use gdscript_db::Db;
14use gdscript_syntax::{GdNode, SyntaxKind};
15use rustc_hash::FxHashMap;
16use smol_str::SmolStr;
17
18use crate::item_tree::{ExtendsRef, ItemTree, Member};
19use crate::ty::{EnumRef, ScriptRefId, Ty};
20
21/// A reference that *would* require another file to resolve — the Phase-3 boundary. Phase 2
22/// never reaches across files, so every variant resolves to the same non-cascading
23/// [`Ty::Unknown`]; Phase 3 reimplements only [`resolve_external`], leaving every inference
24/// body unchanged (Playbook §0 — "the biggest enabler in the whole phase; protect it").
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ExternalRef {
27 /// A `class_name`-registered global from another script.
28 ClassName(SmolStr),
29 /// An `extends "res://…"` / `extends Other.Inner` target.
30 ExtendsPath(SmolStr),
31 /// A `preload(...)`/`load(...)` resource.
32 Preload(SmolStr),
33 /// A project autoload singleton.
34 Autoload(SmolStr),
35}
36
37/// **The Phase-3 seam.** Resolve a cross-file reference. In Phase 2 this is *always*
38/// [`Ty::Unknown`] — a type that never warns, never cascades a diagnostic, and is elided from
39/// hover. Funnel every "would need another file" path through here so Phase 3 has exactly one
40/// function to reimplement.
41#[must_use]
42pub fn resolve_external(db: &dyn Db, r: &ExternalRef) -> Ty {
43 match r {
44 // M1: a project-global `class_name` → its script reference.
45 ExternalRef::ClassName(name) => resolve_class_name(db, name),
46 // M3: `preload("res://x.gd")` → the declaring file's `ScriptRef` (a compile-time constant
47 // SCRIPT meta-type in Godot; `reduce_preload` — resolved by `res://` PATH, independent of
48 // `class_name`, so a script with no `class_name` is still preloadable). We reuse the
49 // `ScriptRef` representation: `X.new()` → instance, `X.member`/`X.CONST` resolve via the
50 // same `script_member_walk` as a `class_name` reference (the analyzer already collapses
51 // the meta-vs-instance distinction, like a bare `class_name`).
52 ExternalRef::Preload(path) => resolve_res_path(db, path),
53 // M3: `extends "res://x.gd"` lights up the same path map. A *relative* / dotted form
54 // (`extends "sibling.gd"`, `extends A.B`) stays the seam — relative-path anchoring is a
55 // documented follow-up (needs the importing file's dir; 0 occurrences in the corpus).
56 ExternalRef::ExtendsPath(path) if is_resource_path(path) => resolve_res_path(db, path),
57 // M4: a `*`-flagged autoload singleton's bare name → its script `ScriptRef` — a `.gd`
58 // directly, or a `.tscn` via its root node's attached script (Phase-4 scene-root sharpening).
59 ExternalRef::Autoload(name) => resolve_autoload(db, name),
60 // `load(...)` is never routed here (it stays an opaque runtime call). Dotted `extends`
61 // remains the seam.
62 ExternalRef::ExtendsPath(_) => Ty::Unknown,
63 }
64}
65
66/// Resolve a `*`-singleton autoload's bare name (M4). A `.gd` autoload resolves by **path** to its
67/// declaring file's [`Ty::ScriptRef`] (so `.member`/`.new()` walk via the script member table,
68/// even when the script has no `class_name`). A scene (`.tscn`/`.scn`) or any other resource
69/// autoload stays the **seam** ([`Ty::Unknown`]): typing it as bare `Node` would *false-warn* on
70/// the scene root script's own members (e.g. `Music.play()`), which we cannot see until Phase 4
71/// scene parsing recovers the root's real type — the conservative seam keeps zero false positives.
72/// No project config, a non-singleton name, or a dangling path is likewise the seam.
73fn resolve_autoload(db: &dyn Db, name: &str) -> Ty {
74 let Some(config) = db.project_config() else {
75 return Ty::Unknown;
76 };
77 let Some(path) = crate::queries::autoload_registry(db, config)
78 .resolve_path(name)
79 .cloned()
80 else {
81 return Ty::Unknown;
82 };
83 if is_gdscript_path(&path) {
84 resolve_res_path(db, &path)
85 } else if is_scene_path(&path) {
86 resolve_scene_autoload(db, &path)
87 } else {
88 Ty::Unknown
89 }
90}
91
92/// A `*`-autoload pointing at a scene (`.tscn`/`.tres`) resolves to its **root node's attached
93/// script** — the singleton-scene pattern (`Music="*res://music.tscn"` whose root has
94/// `script=music.gd`), so `Music.play()` checks against the real script (Phase-4 unblocked this; the
95/// scene model is now ingested). A root with no script, or an un-loaded scene, → the conservative
96/// seam. (Typing a script-less root by its native `type=` would need the engine API, which
97/// `resolve_external` doesn't carry — a follow-up; the attached-script case is the common one.)
98fn resolve_scene_autoload(db: &dyn Db, scene_path: &str) -> Ty {
99 let Some(root) = db.source_root() else {
100 return Ty::Unknown;
101 };
102 let Some(&scene_file) = crate::queries::res_path_registry(db, root).get(scene_path) else {
103 return Ty::Unknown; // the scene isn't loaded into the VFS
104 };
105 let Some(ft) = db.file_text(scene_file) else {
106 return Ty::Unknown;
107 };
108 let scene = crate::queries::scene_model(db, ft);
109 let Some(root_node) = scene.root.and_then(|idx| scene.node(idx)) else {
110 return Ty::Unknown;
111 };
112 let Some(script_path) = root_node
113 .script
114 .as_ref()
115 .and_then(|id| scene.ext_resources.get(id))
116 .and_then(|ext| ext.path.as_deref())
117 else {
118 return Ty::Unknown; // the root has no attached script
119 };
120 resolve_res_path(db, script_path)
121}
122
123/// Whether a resource path is a Godot scene/resource (`.tscn`/`.tres`).
124fn is_scene_path(p: &str) -> bool {
125 p.rsplit('.')
126 .next()
127 .is_some_and(|ext| ext.eq_ignore_ascii_case("tscn") || ext.eq_ignore_ascii_case("tres"))
128}
129
130/// Whether a resource path is a GDScript file (the `.cs` C# case is out of scope → seam). Compare
131/// the final extension rather than `ends_with` so a `.GD` (case quirk) still matches.
132fn is_gdscript_path(p: &str) -> bool {
133 p.rsplit('.')
134 .next()
135 .is_some_and(|ext| ext.eq_ignore_ascii_case("gd"))
136}
137
138/// Whether a path is an engine resource URI we resolve project-root-absolutely (no anchor
139/// needed). Godot also accepts relative `preload`/`extends` paths anchored to the importing
140/// script's directory; those are a documented follow-up (they need the importing file's path
141/// threaded into resolution, and the reference corpus has none).
142fn is_resource_path(p: &str) -> bool {
143 p.starts_with("res://") || p.starts_with("user://")
144}
145
146/// Resolve a `res://` resource path to the declaring file's [`Ty::ScriptRef`] via the project
147/// [`res_path_registry`](crate::queries::res_path_registry), or the seam ([`Ty::Unknown`]) when
148/// no project is loaded or the path maps to no known file (a dangling `preload` — imprecise, but
149/// never a false diagnostic).
150fn resolve_res_path(db: &dyn Db, path: &str) -> Ty {
151 // Only a GDScript resource has a script `ScriptRef`. A `.tscn`/`.tres`/`.png`/… resolves to a
152 // PackedScene/Resource, not a script — typing it as a `ScriptRef` would wrongly accept
153 // `X.new()` and member access on it (scene-root typing is Phase 4). The `res_path_registry`
154 // only indexes `.gd` files today, but gate defensively so a future scene-ingesting loader
155 // cannot mis-type `preload("res://x.tscn")`. Non-`.gd` → the conservative seam.
156 if !is_gdscript_path(path) {
157 return Ty::Unknown;
158 }
159 let Some(root) = db.source_root() else {
160 return Ty::Unknown;
161 };
162 match crate::queries::res_path_registry(db, root).get(path) {
163 Some(file) => Ty::ScriptRef(ScriptRefId(file.0)),
164 None => Ty::Unknown,
165 }
166}
167
168/// Resolve a global `class_name` against the project registry (M1): the script's
169/// [`Ty::ScriptRef`], or the seam ([`Ty::Unknown`]) when no project is loaded or the name is not
170/// a registered global class. The `ScriptRefId` is the declaring file's `FileId`.
171fn resolve_class_name(db: &dyn Db, name: &str) -> Ty {
172 let Some(root) = db.source_root() else {
173 return Ty::Unknown;
174 };
175 match crate::queries::global_registry(db, root).resolve(name) {
176 Some(file) => Ty::ScriptRef(ScriptRefId(file.file_id(db).0)),
177 None => Ty::Unknown,
178 }
179}
180
181// ---- type-annotation resolution ----------------------------------------------------------
182
183/// Resolve a GDScript source type annotation (a `TypeRef` CST node) to a [`Ty`]. Handles
184/// `void`/`Variant`, builtins, engine classes, `Array`/`Array[T]`, `Dictionary`/
185/// `Dictionary[K, V]`, global enums, and `Class.Enum`; an unknown bare name is treated as a
186/// (cross-file) `class_name` and funneled through the [`resolve_external`] seam.
187#[must_use]
188pub fn resolve_type_ref(db: &dyn Db, api: &EngineApi, node: &GdNode) -> Ty {
189 // The leading dotted name comes from this node's *direct* `Ident`/`void` tokens; the type
190 // arguments (`[...]`) are *direct child* `TypeRef` nodes (the grammar nests them).
191 let names: Vec<String> = node
192 .children_with_tokens()
193 .filter_map(NodeOrToken::into_token)
194 .filter(|t| matches!(t.kind(), SyntaxKind::Ident | SyntaxKind::VoidKw))
195 .map(|t| t.text().to_owned())
196 .collect();
197 let args: Vec<GdNode> = node
198 .children()
199 .filter(|c| c.kind() == SyntaxKind::TypeRef)
200 .cloned()
201 .collect();
202 resolve_named(db, api, &names, &args)
203}
204
205/// Resolve a bare type *name* (no type arguments) — for callers that only have a string
206/// (completion detail, inlay display).
207#[must_use]
208pub fn resolve_type_name(db: &dyn Db, api: &EngineApi, name: &str) -> Ty {
209 resolve_named(db, api, std::slice::from_ref(&name.to_owned()), &[])
210}
211
212fn resolve_named(db: &dyn Db, api: &EngineApi, names: &[String], args: &[GdNode]) -> Ty {
213 let Some(head) = names.first() else {
214 return Ty::Variant;
215 };
216 if names.len() == 1 {
217 match head.as_str() {
218 "void" => return Ty::Void,
219 "Variant" => return Ty::Variant,
220 // Dedicated variants (see `resolve_tyref`) so annotations match lambda/signal values.
221 "Callable" => return Ty::Callable,
222 "Signal" => return Ty::Signal(None),
223 "Array" => return Ty::Array(Box::new(elem_arg(db, api, args, 0))),
224 "Dictionary" => {
225 return Ty::Dict(
226 Box::new(elem_arg(db, api, args, 0)),
227 Box::new(elem_arg(db, api, args, 1)),
228 );
229 }
230 _ => {}
231 }
232 if let Some(b) = api.builtin_by_name(head) {
233 return Ty::Builtin(b);
234 }
235 if let Some(c) = api.class_by_name(head) {
236 return Ty::Object(c);
237 }
238 if let Some(e) = api.global_enum(head) {
239 return Ty::Enum(EnumRef {
240 qualified: SmolStr::new(head),
241 bitfield: e.is_bitfield,
242 });
243 }
244 // Unknown bare name → most likely another script's `class_name` → the seam.
245 return resolve_external(db, &ExternalRef::ClassName(SmolStr::new(head)));
246 }
247 // Dotted: try `Class.Enum`; anything else (inner class, namespaced) is the seam.
248 if names.len() == 2
249 && let Some(c) = api.class_by_name(&names[0])
250 && let Some(e) = api.class(c).enums.iter().find(|e| e.name == names[1])
251 {
252 return Ty::Enum(EnumRef {
253 qualified: SmolStr::new(names.join(".")),
254 bitfield: e.is_bitfield,
255 });
256 }
257 resolve_external(db, &ExternalRef::ExtendsPath(SmolStr::new(names.join("."))))
258}
259
260/// Resolve the `i`-th type argument as a container element, collapsing a nested typed
261/// container to `Variant` (Phase 2 does not track nested element types — Playbook §2). A
262/// missing argument (bare `Array`/`Dictionary`) is `Variant`.
263fn elem_arg(db: &dyn Db, api: &EngineApi, args: &[GdNode], i: usize) -> Ty {
264 match args.get(i) {
265 Some(node) => match resolve_type_ref(db, api, node) {
266 Ty::Array(_) | Ty::Dict(..) => Ty::Variant,
267 other => other,
268 },
269 None => Ty::Variant,
270 }
271}
272
273/// Map a coarse engine-layer [`LayerTy`] (used by the hand-authored GDScript layer, which
274/// predates the loaded model's real ids) to a [`Ty`].
275#[must_use]
276pub fn layer_to_ty(api: &EngineApi, lt: LayerTy) -> Ty {
277 match lt {
278 LayerTy::Float => builtin(api, "float"),
279 LayerTy::Int => builtin(api, "int"),
280 LayerTy::Bool => builtin(api, "bool"),
281 LayerTy::Str => builtin(api, "String"),
282 LayerTy::Array => Ty::array_of_variant(),
283 LayerTy::Variant => Ty::Variant,
284 LayerTy::Unknown => Ty::Unknown,
285 LayerTy::Void => Ty::Void,
286 }
287}
288
289fn builtin(api: &EngineApi, name: &str) -> Ty {
290 api.builtin_by_name(name).map_or(Ty::Variant, Ty::Builtin)
291}
292
293// ---- base + class scope ------------------------------------------------------------------
294
295/// Resolve a file's (or inner class's) base type from its `extends`. A bare engine-class name
296/// resolves to `Object(id)`; a script-path / dotted / unknown base goes through the seam to
297/// `Unknown`. With no `extends`, a script implicitly extends `RefCounted`.
298#[must_use]
299pub fn resolve_base(db: &dyn Db, api: &EngineApi, tree: &ItemTree) -> Ty {
300 match &tree.extends {
301 None => api
302 .class_by_name("RefCounted")
303 .map_or(Ty::Unknown, Ty::Object),
304 Some(ExtendsRef::Name(n)) => api.class_by_name(n).map_or_else(
305 || resolve_external(db, &ExternalRef::ClassName(n.clone())),
306 Ty::Object,
307 ),
308 Some(ExtendsRef::Path(p) | ExtendsRef::ScriptPath(p)) => {
309 resolve_external(db, &ExternalRef::ExtendsPath(p.clone()))
310 }
311 }
312}
313
314/// What a class-level name resolves to within [`ClassScope`].
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ClassItem {
317 /// A declared member (index into [`ItemTree::members`]).
318 Member(usize),
319 /// A variant of an *anonymous* `enum { … }` (a class-level `int` constant).
320 EnumVariant,
321}
322
323/// The class-member tier of the binder (Playbook §3.2 step 2): this file's own members + the
324/// resolved base type. Anonymous-enum variants are flattened in as `int` constants.
325#[derive(Debug, Clone)]
326pub struct ClassScope<'a> {
327 /// The lowered item tree this scope describes.
328 pub tree: &'a ItemTree,
329 /// The resolved base type (`Object(id)` for an engine base, else `Unknown`).
330 pub base: Ty,
331 /// The static type of `self` in this class's bodies. Defaults to [`base`](Self::base), but
332 /// `analyze_file` overrides it with the script's *own* [`Ty::ScriptRef`] so that member access
333 /// on an **aliased** `self` (`var me := self; me.own_method()`) walks the file's own members
334 /// instead of only the engine base — otherwise a real own-method call would false-warn
335 /// `UNSAFE_METHOD_ACCESS`. (Direct `self.member` already uses the own-member fast path.)
336 pub self_ty: Ty,
337 /// Resolved types of this class's own fields (`var`/`const`), seeded by a first inference
338 /// pass over the field initializers so member references see the *inferred* type (e.g.
339 /// `var n := 0` → `int`), not just the annotation. Empty until populated.
340 pub member_types: FxHashMap<SmolStr, Ty>,
341 members: FxHashMap<SmolStr, ClassItem>,
342}
343
344impl<'a> ClassScope<'a> {
345 /// Build the scope for `tree` against the engine model.
346 #[must_use]
347 pub fn new(db: &dyn Db, api: &EngineApi, tree: &'a ItemTree) -> Self {
348 let mut members = FxHashMap::default();
349 for (i, m) in tree.members.iter().enumerate() {
350 match m {
351 Member::Enum(e) if e.name.is_none() => {
352 // Anonymous enum: its variants become bare class-level `int` constants.
353 for v in &e.variants {
354 members.insert(v.clone(), ClassItem::EnumVariant);
355 }
356 }
357 _ => {
358 if let Some(name) = m.name() {
359 members
360 .entry(SmolStr::new(name))
361 .or_insert(ClassItem::Member(i));
362 }
363 }
364 }
365 }
366 let base = resolve_base(db, api, tree);
367 Self {
368 tree,
369 self_ty: base.clone(),
370 base,
371 member_types: FxHashMap::default(),
372 members,
373 }
374 }
375
376 /// Resolve a name against this class's own members (not the base chain).
377 #[must_use]
378 pub fn lookup(&self, name: &str) -> Option<ClassItem> {
379 self.members.get(name).copied()
380 }
381
382 /// The member behind a [`ClassItem::Member`].
383 #[must_use]
384 pub fn member(&self, item: ClassItem) -> Option<&'a Member> {
385 match item {
386 ClassItem::Member(i) => self.tree.members.get(i),
387 ClassItem::EnumVariant => None,
388 }
389 }
390}
391
392// ---- global resolution -------------------------------------------------------------------
393
394/// What a bare *global* name resolves to (Playbook §3.2 step 4). The caller ([`crate::infer`])
395/// decides how to use it given the syntactic context (bare value vs. call vs. `.`-access).
396#[derive(Debug, Clone, PartialEq, Eq)]
397pub enum GlobalDef {
398 /// A pseudo-constant value (`PI` → `float`).
399 Const(Ty),
400 /// An engine singleton instance (`Input` → `Object(Input)`).
401 Singleton(ClassId),
402 /// A GDScript builtin function (`preload`/`range`/`len`/…).
403 Builtin,
404 /// A `@GlobalScope` utility function (`sin`, `print`, …).
405 Utility,
406 /// A builtin Variant type name used as a value / constructor (`Vector2`, `int`).
407 BuiltinType(BuiltinId),
408 /// An engine class name used as a value / constructor / type (`Node`, `Resource`).
409 ClassType(ClassId),
410 /// A global enum namespace (`Error`, `Key`) — a set of `int` constants.
411 GlobalEnum,
412}
413
414/// Resolve a bare global identifier. Order is deliberate: pseudo-constants and singletons take
415/// precedence over the same-named type (bare `Input` is the singleton instance, not the class).
416#[must_use]
417pub fn resolve_global(api: &EngineApi, name: &str) -> Option<GlobalDef> {
418 if let Some(gc) = api.global_const(name) {
419 return Some(GlobalDef::Const(layer_to_ty(api, gc.ty)));
420 }
421 if let Some(cid) = api.singleton(name) {
422 return Some(GlobalDef::Singleton(cid));
423 }
424 if api.gdscript_builtin(name).is_some() {
425 return Some(GlobalDef::Builtin);
426 }
427 if api.utility(name).is_some() {
428 return Some(GlobalDef::Utility);
429 }
430 if let Some(bid) = api.builtin_by_name(name) {
431 return Some(GlobalDef::BuiltinType(bid));
432 }
433 if let Some(cid) = api.class_by_name(name) {
434 return Some(GlobalDef::ClassType(cid));
435 }
436 if api.global_enum(name).is_some() {
437 return Some(GlobalDef::GlobalEnum);
438 }
439 None
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::item_tree::item_tree;
446 use gdscript_syntax::parse;
447
448 fn api() -> &'static EngineApi {
449 gdscript_api::bundled()
450 }
451
452 fn db() -> gdscript_db::RootDatabase {
453 gdscript_db::RootDatabase::default()
454 }
455
456 /// Resolve the first `TypeRef` node found in `decl` source.
457 fn ty_of_annotation(src: &str) -> Ty {
458 let parse = parse(src);
459 let root = parse.syntax_node();
460 let type_ref = gdscript_syntax::ast::descendants(&root)
461 .into_iter()
462 .find(|n| n.kind() == SyntaxKind::TypeRef)
463 .expect("a TypeRef node");
464 resolve_type_ref(&db(), api(), &type_ref)
465 }
466
467 #[test]
468 fn seam_is_unknown() {
469 assert_eq!(
470 resolve_external(&db(), &ExternalRef::ClassName(SmolStr::new("MyClass"))),
471 Ty::Unknown
472 );
473 }
474
475 #[test]
476 fn builtin_and_class_annotations() {
477 assert_eq!(
478 ty_of_annotation("var x: int\n"),
479 Ty::Builtin(api().builtin_by_name("int").unwrap())
480 );
481 assert_eq!(
482 ty_of_annotation("var n: Node\n"),
483 Ty::Object(api().class_by_name("Node").unwrap())
484 );
485 assert_eq!(ty_of_annotation("func f() -> void:\n\tpass\n"), Ty::Void);
486 }
487
488 #[test]
489 fn typed_container_annotations() {
490 let int = Ty::Builtin(api().builtin_by_name("int").unwrap());
491 assert_eq!(
492 ty_of_annotation("var a: Array[int]\n"),
493 Ty::Array(Box::new(int.clone()))
494 );
495 assert_eq!(ty_of_annotation("var a: Array\n"), Ty::array_of_variant());
496 assert_eq!(
497 ty_of_annotation("var d: Dictionary[String, int]\n"),
498 Ty::Dict(
499 Box::new(Ty::Builtin(api().builtin_by_name("String").unwrap())),
500 Box::new(int)
501 )
502 );
503 // Nested typed containers collapse to Variant (Playbook §2).
504 assert_eq!(
505 ty_of_annotation("var a: Array[Array[int]]\n"),
506 Ty::Array(Box::new(Ty::Variant))
507 );
508 }
509
510 #[test]
511 fn unknown_annotation_is_seam_not_error() {
512 // A user `class_name` we can't see (no false diagnostic territory).
513 assert_eq!(ty_of_annotation("var p: MyPlayer\n"), Ty::Unknown);
514 }
515
516 #[test]
517 fn base_resolution() {
518 let extends_node = item_tree(&parse("extends Node2D\n").syntax_node());
519 assert_eq!(
520 resolve_base(&db(), api(), &extends_node),
521 Ty::Object(api().class_by_name("Node2D").unwrap())
522 );
523 // No extends → implicit RefCounted.
524 let no_extends = item_tree(&parse("var x = 1\n").syntax_node());
525 assert_eq!(
526 resolve_base(&db(), api(), &no_extends),
527 Ty::Object(api().class_by_name("RefCounted").unwrap())
528 );
529 // Script-path base → seam.
530 let script_base = item_tree(&parse("extends \"res://b.gd\"\n").syntax_node());
531 assert_eq!(resolve_base(&db(), api(), &script_base), Ty::Unknown);
532 }
533
534 #[test]
535 fn class_scope_members_and_anon_enum() {
536 let tree = item_tree(
537 &parse(
538 "var hp := 10\nfunc attack():\n\tpass\nenum { FIRE, ICE }\nenum Named { A, B }\n",
539 )
540 .syntax_node(),
541 );
542 let scope = ClassScope::new(&db(), api(), &tree);
543 assert!(matches!(scope.lookup("hp"), Some(ClassItem::Member(_))));
544 assert!(matches!(scope.lookup("attack"), Some(ClassItem::Member(_))));
545 // Anonymous-enum variants flatten into the class scope as int consts.
546 assert_eq!(scope.lookup("FIRE"), Some(ClassItem::EnumVariant));
547 assert_eq!(scope.lookup("ICE"), Some(ClassItem::EnumVariant));
548 // A named enum binds its *name*, not its variants.
549 assert!(matches!(scope.lookup("Named"), Some(ClassItem::Member(_))));
550 assert_eq!(scope.lookup("A"), None);
551 }
552
553 #[test]
554 fn globals() {
555 assert!(matches!(
556 resolve_global(api(), "PI"),
557 Some(GlobalDef::Const(_))
558 ));
559 assert!(matches!(
560 resolve_global(api(), "Input"),
561 Some(GlobalDef::Singleton(_))
562 ));
563 assert!(matches!(
564 resolve_global(api(), "preload"),
565 Some(GlobalDef::Builtin)
566 ));
567 assert!(matches!(
568 resolve_global(api(), "Vector2"),
569 Some(GlobalDef::BuiltinType(_))
570 ));
571 assert!(matches!(
572 resolve_global(api(), "Node"),
573 Some(GlobalDef::ClassType(_))
574 ));
575 assert!(resolve_global(api(), "definitely_not_a_global").is_none());
576 }
577}