1use gdscript_api::{EngineApi, MemberRef, TyRef};
13use gdscript_base::{Diagnostic, DiagnosticSource, FileId, Severity, TextRange};
14use gdscript_db::Db;
15use gdscript_scene::{SceneModel, SceneNode};
16use gdscript_syntax::GdNode;
17use rustc_hash::{FxHashMap, FxHashSet};
18use smol_str::SmolStr;
19
20use std::sync::Arc;
21
22use crate::body::{self, BinOp, Body, Expr, ExprId, Literal, ParamBinding, Stmt, UnOp};
23use crate::cst::{self, AstPtr};
24use crate::flow::{self, FlowAnalysis, NarrowedTy, Place};
25use crate::item_tree::{ItemTree, Member, item_tree};
26use crate::resolve::{self, ClassItem, ClassScope, GlobalDef};
27use crate::ty::{self, Assign, EnumRef, ScriptRefId, Ty};
28use crate::warnings::{RawWarning, WarningCode};
29
30pub const INFERENCE_ON_VARIANT: &str = "INFERENCE_ON_VARIANT";
34pub const TYPE_MISMATCH: &str = "TYPE_MISMATCH";
36pub const NARROWING_CONVERSION: &str = "NARROWING_CONVERSION";
38pub const INTEGER_DIVISION: &str = "INTEGER_DIVISION";
40pub const UNSAFE_PROPERTY_ACCESS: &str = "UNSAFE_PROPERTY_ACCESS";
42pub const UNSAFE_METHOD_ACCESS: &str = "UNSAFE_METHOD_ACCESS";
44pub const UNSAFE_CALL_ARGUMENT: &str = "UNSAFE_CALL_ARGUMENT";
47pub const INVALID_NODE_PATH: &str = "INVALID_NODE_PATH";
51pub const SHADOWED_GLOBAL_IDENTIFIER: &str = "SHADOWED_GLOBAL_IDENTIFIER";
55pub const CYCLIC_INHERITANCE: &str = "CYCLIC_INHERITANCE";
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum BindingKind {
63 Var,
65 Param,
67 ForVar,
69 MatchBind,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Binding {
76 pub name: SmolStr,
78 pub name_range: TextRange,
80 pub ty: Ty,
83 pub init: Option<ExprId>,
85 pub annotated: bool,
87 pub inferred_colon_eq: bool,
89 pub is_const: bool,
92 pub kind: BindingKind,
94}
95
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct InferenceResult {
99 pub expr_ty: FxHashMap<ExprId, Ty>,
101 pub bindings: Vec<Binding>,
103 pub diagnostics: Vec<Diagnostic>,
106 pub raw_warnings: Vec<RawWarning>,
109}
110
111impl InferenceResult {
112 #[must_use]
114 pub fn type_of(&self, id: ExprId) -> Option<&Ty> {
115 self.expr_ty.get(&id)
116 }
117
118 #[must_use]
120 pub fn binding_at(&self, offset: u32) -> Option<&Binding> {
121 self.bindings
122 .iter()
123 .find(|b| b.name_range.start <= offset && offset < b.name_range.end)
124 }
125}
126
127fn collect_assign_lhs(body: &Body) -> FxHashSet<ExprId> {
133 body.exprs
134 .iter()
135 .filter_map(|e| match e {
136 Expr::Bin {
137 op: BinOp::Assign,
138 lhs,
139 ..
140 } => Some(*lhs),
141 _ => None,
142 })
143 .collect()
144}
145
146#[must_use]
147#[allow(
148 clippy::too_many_lines,
149 reason = "the per-body inference orchestration reads best whole"
150)]
151pub fn infer(
152 db: &dyn Db,
153 api: &EngineApi,
154 root: &GdNode,
155 class: &ClassScope,
156 body: &Body,
157 return_ty: Ty,
158 is_func_body: bool,
159) -> InferenceResult {
160 let self_ty = class.self_ty.clone();
161 let mut cx = Cx {
162 db,
163 api,
164 root,
165 body,
166 class,
167 self_ty,
168 return_ty,
169 expr_ty: FxHashMap::default(),
170 bindings: Vec::new(),
171 diagnostics: Vec::new(),
172 raw_warnings: Vec::new(),
173 locals: FxHashMap::default(),
174 used_locals: FxHashSet::default(),
175 narrowing: FxHashMap::default(),
176 flow: flow::analyze(body),
177 is_func_body,
178 assigned: flow::analyze_assigned(
179 body,
180 &body
181 .params
182 .iter()
183 .map(|p| p.name.clone())
184 .collect::<Vec<_>>(),
185 ),
186 cur_stmt: None,
187 needs_assignment: FxHashSet::default(),
188 assign_lhs: collect_assign_lhs(body),
189 };
190 let params = body.params.clone();
192 for p in ¶ms {
193 let ty = cx.param_ty(p);
194 cx.bindings.push(Binding {
195 name: p.name.clone(),
196 name_range: p.name_range,
197 ty: ty.clone(),
198 init: None,
199 annotated: p.type_ref.is_some(),
200 inferred_colon_eq: false,
201 is_const: false,
202 kind: BindingKind::Param,
203 });
204 cx.locals.insert(p.name.clone(), ty);
205 }
206 if let Some(tail) = body.tail {
207 cx.infer_expr(tail, &Expectation::None);
208 }
209 let block = body.block.clone();
210 cx.infer_block(&block);
211
212 if is_func_body {
216 let unused: Vec<(TextRange, WarningCode, String)> = cx
217 .bindings
218 .iter()
219 .filter_map(|b| {
220 if b.name.starts_with('_') || cx.used_locals.contains(&b.name) {
221 return None;
222 }
223 let (code, what) = match b.kind {
224 BindingKind::Param => (WarningCode::UnusedParameter, "parameter"),
225 BindingKind::Var if b.is_const => {
226 (WarningCode::UnusedLocalConstant, "local constant")
227 }
228 BindingKind::Var => (WarningCode::UnusedVariable, "local variable"),
229 BindingKind::ForVar | BindingKind::MatchBind => return None,
230 };
231 Some((
232 b.name_range,
233 code,
234 format!("The {what} \"{}\" is declared but never used.", b.name),
235 ))
236 })
237 .collect();
238 for (range, code, msg) in unused {
239 cx.warn(range, code, msg);
240 }
241 }
242
243 let unreachable = cx.flow.unreachable_ranges(body);
245 for range in unreachable {
246 cx.warn(
247 range,
248 WarningCode::UnreachableCode,
249 "Unreachable code (statement after a return, break, continue, or an exhaustive match)."
250 .to_owned(),
251 );
252 }
253
254 let unreachable_patterns = cx.flow.unreachable_pattern_ranges().to_vec();
256 for range in unreachable_patterns {
257 cx.warn(
258 range,
259 WarningCode::UnreachablePattern,
260 "Unreachable pattern: an earlier arm's wildcard (`_`) or `var` binding always matches."
261 .to_owned(),
262 );
263 }
264
265 InferenceResult {
266 expr_ty: cx.expr_ty,
267 bindings: cx.bindings,
268 diagnostics: cx.diagnostics,
269 raw_warnings: cx.raw_warnings,
270 }
271}
272
273#[must_use]
276pub fn infer_func(
277 db: &dyn Db,
278 api: &EngineApi,
279 root: &GdNode,
280 class: &ClassScope,
281 ptr: AstPtr,
282) -> InferenceResult {
283 let Some(node) = ptr.to_node(root) else {
284 return InferenceResult::default();
285 };
286 let body = body::body_of_func(&node);
287 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
290 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
291 infer(db, api, root, class, &body, return_ty, true)
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct Unit {
299 pub range: TextRange,
301 pub body: Body,
303 pub result: InferenceResult,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Default)]
310pub struct FileInference {
311 pub tree: Arc<ItemTree>,
313 pub units: Vec<Unit>,
315 pub diagnostics: Vec<Diagnostic>,
318 pub raw_warnings: Vec<RawWarning>,
321}
322
323impl FileInference {
324 #[must_use]
326 pub fn unit_at(&self, offset: u32) -> Option<&Unit> {
327 self.units
328 .iter()
329 .filter(|u| u.range.start <= offset && offset < u.range.end)
330 .min_by_key(|u| u.range.end - u.range.start)
331 }
332}
333
334#[must_use]
338#[allow(clippy::too_many_lines)] pub fn analyze_file(db: &dyn Db, api: &EngineApi, root: &GdNode, file_id: FileId) -> FileInference {
340 let tree = item_tree(root);
341 let mut units = Vec::new();
342 let mut diagnostics = Vec::new();
343 let mut raw_warnings: Vec<RawWarning> = Vec::new();
344
345 if tree.members.is_empty() && tree.class_name.is_none() && tree.extends.is_none() {
347 raw_warnings.push(RawWarning {
348 range: TextRange::new(0, 0),
349 code: WarningCode::EmptyFile,
350 message: "Empty script file.".to_owned(),
351 });
352 }
353 let mut member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
354 let self_ref = Ty::ScriptRef(ScriptRefId(file_id.0));
357 let res_path = db.file_text(file_id).and_then(|ft| ft.res_path(db));
359
360 if let Some(name) = tree.class_name.clone() {
365 let collides = collisions_contains(db, &name)
366 || resolve::resolve_global(api, &name).is_some()
367 || is_autoload_singleton(db, &name);
368 if collides && let Some(range) = class_name_decl_range(root) {
369 diagnostics.push(Diagnostic {
370 range,
371 severity: Severity::Warning,
372 code: SHADOWED_GLOBAL_IDENTIFIER.to_owned(),
373 message: format!(
374 "The global class \"{name}\" hides a built-in/native/global/autoload."
375 ),
376 source: DiagnosticSource::Type,
377 fixes: Vec::new(),
378 });
379 }
380 }
381
382 if extends_chain_is_cyclic(db, file_id)
390 && let Some(range) = extends_decl_range(root)
391 {
392 diagnostics.push(Diagnostic {
393 range,
394 severity: Severity::Warning,
395 code: CYCLIC_INHERITANCE.to_owned(),
396 message: "Cyclic class hierarchy: this class's `extends` chain returns to itself."
397 .to_owned(),
398 source: DiagnosticSource::Type,
399 fixes: Vec::new(),
400 });
401 }
402
403 raw_warnings.extend(member_level_warnings(
405 db,
406 api,
407 root,
408 &tree,
409 res_path.as_deref(),
410 ));
411
412 {
422 const MAX_ROUNDS: usize = 4;
426 let mut final_units: Vec<Unit> = Vec::new();
427 let mut final_diagnostics: Vec<Diagnostic> = Vec::new();
428 let mut final_raw_warnings: Vec<RawWarning> = Vec::new();
429 for _ in 0..MAX_ROUNDS {
430 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
431 class.self_ty = self_ref.clone();
432 class.member_types.clone_from(&member_types);
433 let mut next_member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
434 final_units = Vec::new();
435 final_diagnostics = Vec::new();
436 final_raw_warnings = Vec::new();
437 for m in &tree.members {
438 let (ptr, range) = match m {
439 Member::Var(v) => (v.ptr, v.range),
440 Member::Const(c) => (c.ptr, c.range),
441 _ => continue,
442 };
443 if let Some(unit) = unit_from_decl(db, api, root, &class, ptr, range) {
444 if let (Some(name), Some(b)) = (m.name(), unit.result.bindings.first()) {
445 next_member_types.insert(SmolStr::new(name), b.ty.clone());
446 }
447 final_diagnostics.extend(unit.result.diagnostics.iter().cloned());
448 final_raw_warnings.extend(unit.result.raw_warnings.iter().cloned());
449 final_units.push(unit);
450 }
451 }
452 if next_member_types == member_types {
453 break;
454 }
455 member_types = next_member_types;
456 }
457 diagnostics.extend(final_diagnostics);
458 raw_warnings.extend(final_raw_warnings);
459 units.extend(final_units);
460 }
461
462 {
464 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
465 class.member_types = member_types;
466 class.self_ty = self_ref.clone();
467 for m in &tree.members {
468 let Member::Func(f) = m else { continue };
469 let Some(node) = f.ptr.to_node(root) else {
470 continue;
471 };
472 let body = body::body_of_func(&node);
473 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
474 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
475 let result = infer(db, api, root, &class, &body, return_ty, true);
476 diagnostics.extend(result.diagnostics.iter().cloned());
477 raw_warnings.extend(result.raw_warnings.iter().cloned());
478 units.push(Unit {
479 range: f.range,
480 body,
481 result,
482 });
483 }
484 }
485
486 FileInference {
487 tree,
488 units,
489 diagnostics,
490 raw_warnings,
491 }
492}
493
494fn member_level_warnings(
498 db: &dyn Db,
499 api: &EngineApi,
500 root: &GdNode,
501 tree: &ItemTree,
502 res_path: Option<&str>,
503) -> Vec<RawWarning> {
504 let mut out = Vec::new();
505 let has_signal = tree.members.iter().any(|m| matches!(m, Member::Signal(_)));
506 let uses = has_signal.then(|| NameUses::collect(root));
508 let engine_base = match resolve::resolve_base(db, api, tree, res_path) {
510 Ty::Object(c) => Some(c),
511 _ => None,
512 };
513 for m in &tree.members {
514 match m {
515 Member::Var(v) if !v.has_init => {
517 if let Some(tref) = &v.type_ref
518 && matches!(resolve::resolve_type_name(db, api, tref), Ty::Enum(_))
519 {
520 out.push(RawWarning {
521 range: v.name_range,
522 code: WarningCode::EnumVariableWithoutDefault,
523 message: format!(
524 "The enum variable \"{}\" has no default value (it defaults to 0, which may not be a valid enum value).",
525 v.name
526 ),
527 });
528 }
529 }
530 Member::Signal(s) => {
534 if let Some(uses) = &uses
535 && !uses.is_referenced(&s.name)
536 {
537 out.push(RawWarning {
538 range: s.name_range,
539 code: WarningCode::UnusedSignal,
540 message: format!(
541 "The signal \"{}\" is never emitted or connected in this file.",
542 s.name
543 ),
544 });
545 }
546 }
547 Member::Func(f) => {
554 if let Some(base) = engine_base
555 && let Some(MemberRef::Method(vsig)) = api.lookup_member(base, &f.name)
556 && vsig.is_virtual
557 {
558 for (p, vp) in f.params.iter().zip(vsig.params.iter()) {
559 let Some(ann) = &p.type_ref else { continue };
560 let pty = resolve::resolve_type_name(db, api, ann);
561 let vty = ty::resolve_tyref(api, &vp.ty);
562 if types_definitely_clash(api, &pty, &vty) {
563 out.push(RawWarning {
564 range: f.name_range,
565 code: WarningCode::NativeMethodOverride,
566 message: format!(
567 "The override of the native virtual method \"{}\" has an incompatible type for parameter \"{}\".",
568 f.name, p.name
569 ),
570 });
571 break; }
573 }
574 }
575 }
576 _ => {}
577 }
578 }
579 out
580}
581
582fn types_definitely_clash(api: &EngineApi, a: &Ty, b: &Ty) -> bool {
587 if a.is_uninformative() || b.is_uninformative() {
588 return false;
589 }
590 if matches!(a, Ty::Enum(_)) || matches!(b, Ty::Enum(_)) {
595 return false;
596 }
597 matches!(ty::is_assignable(api, a, b), Assign::No)
598 && matches!(ty::is_assignable(api, b, a), Assign::No)
599}
600
601struct NameUses {
604 ident_counts: FxHashMap<SmolStr, u32>,
605 strings: FxHashSet<SmolStr>,
606}
607
608impl NameUses {
609 fn collect(root: &GdNode) -> Self {
610 let mut ident_counts: FxHashMap<SmolStr, u32> = FxHashMap::default();
611 let mut strings: FxHashSet<SmolStr> = FxHashSet::default();
612 for node in gdscript_syntax::ast::descendants(root) {
613 for el in node.children_with_tokens() {
614 let Some(tok) = el.into_token() else { continue };
615 match tok.kind() {
616 gdscript_syntax::SyntaxKind::Ident => {
617 *ident_counts.entry(SmolStr::new(tok.text())).or_insert(0) += 1;
618 }
619 gdscript_syntax::SyntaxKind::String => {
620 strings.insert(SmolStr::new(tok.text().trim_matches(['"', '\''])));
621 }
622 _ => {}
623 }
624 }
625 }
626 Self {
627 ident_counts,
628 strings,
629 }
630 }
631
632 fn is_referenced(&self, name: &str) -> bool {
635 self.ident_counts.get(name).copied().unwrap_or(0) > 1 || self.strings.contains(name)
636 }
637}
638
639fn collisions_contains(db: &dyn Db, name: &SmolStr) -> bool {
643 db.source_root()
644 .is_some_and(|root| crate::queries::class_name_collisions(db, root).contains(name))
645}
646
647fn is_autoload_singleton(db: &dyn Db, name: &str) -> bool {
650 db.project_config().is_some_and(|config| {
651 crate::queries::autoload_registry(db, config)
652 .resolve_path(name)
653 .is_some()
654 })
655}
656
657fn class_name_decl_range(root: &GdNode) -> Option<TextRange> {
662 use gdscript_syntax::SyntaxKind;
663 let decl = gdscript_syntax::ast::descendants(root)
664 .into_iter()
665 .find(|n| n.kind() == SyntaxKind::ClassNameDecl)?;
666 let name_node = decl.children().find(|c| c.kind() == SyntaxKind::Name)?;
667 let r = cst::text_range_of(name_node);
668 let text = name_node.text().to_string();
669 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
670 let len = u32::try_from(text.trim().len()).unwrap_or(0);
671 Some(TextRange::new(r.start + lead, r.start + lead + len))
672}
673
674fn extends_decl_range(root: &GdNode) -> Option<TextRange> {
681 use gdscript_syntax::SyntaxKind;
682 for child in root.children() {
683 match child.kind() {
684 SyntaxKind::ExtendsClause => return Some(cst::text_range_of(child)),
686 SyntaxKind::ClassNameDecl => {
688 if let Some(kw) = child.children().find(|c| c.kind() == SyntaxKind::ExtendsKw) {
689 let start = cst::text_range_of(kw).start;
690 let end = cst::text_range_of(child).end;
691 return Some(TextRange::new(start, end));
692 }
693 }
694 _ => {}
695 }
696 }
697 None
698}
699
700fn extends_chain_is_cyclic(db: &dyn Db, start: FileId) -> bool {
707 use std::collections::HashSet;
708 let mut visited: HashSet<FileId> = HashSet::new();
709 visited.insert(start);
710 let mut current = start;
711 for _ in 0..=64 {
712 let Some(file) = db.file_text(current) else {
713 return false;
714 };
715 let base = crate::queries::script_class(db, file).base().clone();
716 let Ty::ScriptRef(next) = base else {
717 return false; };
719 let next_id = FileId(next.0);
720 if !visited.insert(next_id) {
721 return true;
724 }
725 current = next_id;
726 }
727 false
728}
729
730fn unit_from_decl(
732 db: &dyn Db,
733 api: &EngineApi,
734 root: &GdNode,
735 class: &ClassScope,
736 ptr: AstPtr,
737 range: TextRange,
738) -> Option<Unit> {
739 let node = ptr.to_node(root)?;
740 let body = body::body_of_decl_stmt(&node);
741 let result = infer(db, api, root, class, &body, Ty::Variant, false);
742 Some(Unit {
743 range,
744 body,
745 result,
746 })
747}
748
749enum Expectation {
751 None,
753 Has(Ty),
755}
756
757struct Cx<'a> {
758 db: &'a dyn Db,
759 api: &'a EngineApi,
760 root: &'a GdNode,
761 body: &'a Body,
762 class: &'a ClassScope<'a>,
763 self_ty: Ty,
764 return_ty: Ty,
765 expr_ty: FxHashMap<ExprId, Ty>,
766 bindings: Vec<Binding>,
767 diagnostics: Vec<Diagnostic>,
768 raw_warnings: Vec<RawWarning>,
770 locals: FxHashMap<SmolStr, Ty>,
772 used_locals: FxHashSet<SmolStr>,
776 narrowing: FxHashMap<String, Ty>,
779 flow: FlowAnalysis,
782 is_func_body: bool,
786 assigned: flow::AssignedAnalysis,
789 cur_stmt: Option<body::StmtId>,
792 needs_assignment: FxHashSet<SmolStr>,
796 assign_lhs: FxHashSet<ExprId>,
799}
800
801impl Cx<'_> {
802 fn builtin(&self, name: &str) -> Ty {
805 self.api
806 .builtin_by_name(name)
807 .map_or(Ty::Variant, Ty::Builtin)
808 }
809 fn int_ty(&self) -> Ty {
810 self.builtin("int")
811 }
812 fn float_ty(&self) -> Ty {
813 self.builtin("float")
814 }
815 fn bool_ty(&self) -> Ty {
816 self.builtin("bool")
817 }
818 fn is_int(&self, ty: &Ty) -> bool {
819 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "int")
820 }
821 fn is_float(&self, ty: &Ty) -> bool {
822 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "float")
823 }
824 fn is_numeric(&self, ty: &Ty) -> bool {
825 self.is_int(ty) || self.is_float(ty)
826 }
827
828 fn emit(&mut self, range: TextRange, severity: Severity, code: &str, message: String) {
831 self.diagnostics.push(Diagnostic {
832 range,
833 severity,
834 code: code.to_owned(),
835 message,
836 source: DiagnosticSource::Type,
837 fixes: Vec::new(),
838 });
839 }
840
841 fn warn(&mut self, range: TextRange, code: WarningCode, message: String) {
845 self.raw_warnings.push(RawWarning {
846 range,
847 code,
848 message,
849 });
850 }
851
852 fn range_of(&self, id: ExprId) -> TextRange {
853 self.body.source_map.expr_range(id)
854 }
855
856 fn check_assign(&mut self, from: &Ty, to: &Ty, range: TextRange) {
859 match ty::is_assignable(self.api, from, to) {
860 Assign::Narrowing => self.warn(
861 range,
862 WarningCode::NarrowingConversion,
863 "Narrowing conversion (float is converted to int and loses precision).".to_owned(),
864 ),
865 Assign::No => {
866 let to_label = to.label(self.api).unwrap_or_else(|| "?".to_owned());
867 let from_label = from.label(self.api).unwrap_or_else(|| "?".to_owned());
868 self.emit(
869 range,
870 Severity::Error,
871 TYPE_MISMATCH,
872 format!(
873 "Cannot assign a value of type \"{from_label}\" to a target of type \"{to_label}\"."
874 ),
875 );
876 }
877 Assign::IntAsEnum => self.warn(
879 range,
880 WarningCode::IntAsEnumWithoutCast,
881 "Integer used when an enum value is expected. Cast the value to the enum type."
882 .to_owned(),
883 ),
884 Assign::Ok | Assign::OkUnsafe => {}
885 }
886 }
887
888 fn check_standalone(&mut self, e: ExprId) {
892 if self.expr_has_side_effect(e) {
893 return;
894 }
895 match self.body.expr(e) {
896 Expr::Ternary { .. } => self.warn(
897 self.range_of(e),
898 WarningCode::StandaloneTernary,
899 "Standalone ternary conditional: the return value is discarded.".to_owned(),
900 ),
901 Expr::Missing | Expr::Lambda { .. } | Expr::GetNode { .. } | Expr::Preload { .. } => {}
903 _ => self.warn(
904 self.range_of(e),
905 WarningCode::StandaloneExpression,
906 "Standalone expression (the line has no effect).".to_owned(),
907 ),
908 }
909 }
910
911 fn expr_has_side_effect(&self, e: ExprId) -> bool {
914 match self.body.expr(e) {
915 Expr::Call { .. }
916 | Expr::Await(_)
917 | Expr::Preload { .. }
918 | Expr::Bin {
919 op: BinOp::Assign, ..
920 } => true,
921 Expr::Bin { lhs, rhs, .. }
922 | Expr::In { lhs, rhs, .. }
923 | Expr::Index {
924 base: lhs,
925 index: rhs,
926 } => self.expr_has_side_effect(*lhs) || self.expr_has_side_effect(*rhs),
927 Expr::Unary { operand, .. }
928 | Expr::Paren(operand)
929 | Expr::Cast { operand, .. }
930 | Expr::Is { operand, .. } => self.expr_has_side_effect(*operand),
931 Expr::Field { receiver, .. } => self.expr_has_side_effect(*receiver),
932 Expr::Ternary {
933 cond,
934 then_branch,
935 else_branch,
936 } => {
937 self.expr_has_side_effect(*cond)
938 || self.expr_has_side_effect(*then_branch)
939 || self.expr_has_side_effect(*else_branch)
940 }
941 Expr::Array(items) => items.iter().any(|&i| self.expr_has_side_effect(i)),
942 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
943 self.expr_has_side_effect(*k) || v.is_some_and(|e| self.expr_has_side_effect(e))
944 }),
945 _ => false,
946 }
947 }
948
949 fn infer_block(&mut self, block: &[body::StmtId]) {
952 for &stmt in block {
953 self.infer_stmt(stmt);
954 }
955 }
956
957 fn infer_stmt(&mut self, id: body::StmtId) {
958 self.narrowing = self.facts_to_narrowing(id);
961 self.cur_stmt = Some(id); match self.body.stmt(id).clone() {
963 Stmt::Expr(e) => {
964 self.infer_expr(e, &Expectation::None);
965 self.check_standalone(e);
966 }
967 Stmt::Var(v) => self.infer_local_var(&v),
968 Stmt::Return(e) => {
969 if let Some(e) = e {
970 let expected = if self.return_ty.is_uninformative() {
971 Expectation::None
972 } else {
973 Expectation::Has(self.return_ty.clone())
974 };
975 let t = self.infer_expr(e, &expected);
976 if let Expectation::Has(ret) = expected {
977 self.check_assign(&t, &ret, self.range_of(e));
978 }
979 }
980 }
981 Stmt::If {
982 cond,
983 then_branch,
984 elifs,
985 else_branch,
986 } => {
987 let at_if = self.narrowing.clone();
991 self.infer_expr(cond, &Expectation::None);
992 self.infer_block(&then_branch);
993 for (econd, eblock) in elifs {
994 self.narrowing.clone_from(&at_if);
995 self.infer_expr(econd, &Expectation::None);
996 self.infer_block(&eblock);
997 }
998 if let Some(eb) = else_branch {
999 self.infer_block(&eb);
1000 }
1001 }
1002 Stmt::While { cond, body } => {
1003 self.infer_expr(cond, &Expectation::None);
1004 self.infer_block(&body);
1005 }
1006 Stmt::For(f) => {
1007 let iter_ty = self.infer_expr(f.iter, &Expectation::None);
1008 let var_ty = f.var_type.as_ref().map_or_else(
1009 || self.loop_var_ty(&iter_ty),
1010 |ptr| self.resolve_ptr_ty(*ptr),
1011 );
1012 self.bindings.push(Binding {
1013 name: f.var.clone(),
1014 name_range: f.var_range,
1015 ty: var_ty.clone(),
1016 init: None,
1017 annotated: f.var_type.is_some(),
1018 inferred_colon_eq: false,
1019 is_const: false,
1020 kind: BindingKind::ForVar,
1021 });
1022 self.locals.insert(f.var.clone(), var_ty);
1023 self.infer_block(&f.body);
1024 }
1025 Stmt::Match { scrutinee, arms } => {
1026 let at_match = self.narrowing.clone();
1027 self.infer_expr(scrutinee, &Expectation::None);
1028 for arm in arms {
1029 self.narrowing.clone_from(&at_match);
1032 for b in &arm.binds {
1033 self.bindings.push(Binding {
1037 name: b.name.clone(),
1038 name_range: b.range,
1039 ty: Ty::Variant,
1040 init: None,
1041 annotated: false,
1042 inferred_colon_eq: false,
1043 is_const: false,
1044 kind: BindingKind::MatchBind,
1045 });
1046 self.locals.insert(b.name.clone(), Ty::Variant);
1047 }
1048 if let Some(g) = arm.guard {
1049 self.infer_expr(g, &Expectation::None);
1050 }
1051 self.infer_block(&arm.body);
1052 }
1053 }
1054 Stmt::Break | Stmt::Continue | Stmt::Pass => {}
1055 Stmt::Assert(cond) => {
1056 if let Some(cond) = cond {
1057 self.infer_expr(cond, &Expectation::None);
1058 }
1059 }
1060 }
1061 }
1062
1063 fn infer_local_var(&mut self, v: &body::LocalVar) {
1064 let annotated = v.type_ref.map(|p| self.resolve_ptr_ty(p));
1065 let init_ty = v.init.map(|e| {
1066 let expected = annotated
1067 .as_ref()
1068 .map_or(Expectation::None, |t| Expectation::Has(t.clone()));
1069 self.infer_expr(e, &expected)
1070 });
1071 let range = v.init.map_or(v.name_range, |e| self.range_of(e));
1072
1073 let binding_ty = match (&annotated, &init_ty) {
1074 (Some(t), Some(init)) => {
1076 self.check_assign(init, t, range);
1077 t.clone()
1078 }
1079 (Some(t), None) => t.clone(),
1081 (None, Some(init)) if v.is_inferred => {
1083 if init.is_variant() {
1084 self.warn(
1085 range,
1086 WarningCode::InferenceOnVariant,
1087 inference_on_variant_msg(if v.is_const { "constant" } else { "variable" }),
1088 );
1089 Ty::Variant
1090 } else {
1091 init.clone()
1093 }
1094 }
1095 (None, Some(init)) => {
1097 if v.is_const {
1098 init.clone()
1099 } else {
1100 Ty::Variant
1101 }
1102 }
1103 (None, None) => Ty::Variant,
1104 };
1105 let shadows_param = self
1110 .bindings
1111 .iter()
1112 .any(|b| b.kind == BindingKind::Param && b.name == v.name);
1113 let shadows_member = match self.class.lookup(&v.name) {
1116 Some(ClassItem::EnumVariant) => true,
1117 Some(item) => matches!(
1118 self.class.member(item),
1119 Some(Member::Var(_) | Member::Const(_) | Member::Signal(_))
1120 ),
1121 None => false,
1122 };
1123 if self.is_func_body {
1124 let what = if v.is_const { "constant" } else { "variable" };
1125 if shadows_param || shadows_member {
1126 let outer = if shadows_param {
1127 "parameter"
1128 } else {
1129 "class member"
1130 };
1131 self.warn(
1132 v.name_range,
1133 WarningCode::ShadowedVariable,
1134 format!(
1135 "The local {what} \"{}\" shadows a {outer} of the same name.",
1136 v.name
1137 ),
1138 );
1139 } else if self.engine_base_has_value_member(&v.name) {
1140 self.warn(
1142 v.name_range,
1143 WarningCode::ShadowedVariableBaseClass,
1144 format!(
1145 "The local {what} \"{}\" shadows a member of a base class.",
1146 v.name
1147 ),
1148 );
1149 }
1150 if v.init.is_none() && matches!(annotated.as_ref(), Some(Ty::Enum(_))) {
1153 self.warn(
1154 v.name_range,
1155 WarningCode::EnumVariableWithoutDefault,
1156 format!(
1157 "The enum variable \"{}\" has no default value (it defaults to 0, which may not be a valid enum value).",
1158 v.name
1159 ),
1160 );
1161 }
1162 if v.type_ref.is_some() && v.init.is_none() {
1165 self.needs_assignment.insert(v.name.clone());
1166 }
1167 }
1168 self.bindings.push(Binding {
1169 name: v.name.clone(),
1170 name_range: v.name_range,
1171 ty: binding_ty.clone(),
1172 init: v.init,
1173 annotated: v.type_ref.is_some(),
1174 inferred_colon_eq: v.is_inferred,
1175 is_const: v.is_const,
1176 kind: BindingKind::Var,
1177 });
1178 self.locals.insert(v.name.clone(), binding_ty);
1180 }
1181
1182 fn infer_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1185 let ty = self.synth_expr(id, expected);
1186 self.expr_ty.insert(id, ty.clone());
1187 ty
1188 }
1189
1190 #[allow(clippy::too_many_lines)]
1191 fn synth_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1192 match self.body.expr(id).clone() {
1193 Expr::Missing => Ty::Error,
1194 Expr::Literal(lit) => self.literal_ty(lit),
1195 Expr::Name(name) => self.resolve_name(id, &name),
1196 Expr::SelfExpr => self.self_ty.clone(),
1197 Expr::Super => self.class.base.clone(),
1198 Expr::Paren(inner) => self.infer_expr(inner, expected),
1199 Expr::Bin { op, lhs, rhs } => self.infer_bin(id, op, lhs, rhs),
1200 Expr::Unary { op, operand } => {
1201 let t = self.infer_expr(operand, &Expectation::None);
1202 match op {
1203 UnOp::Not => self.bool_ty(),
1204 UnOp::BitNot => self.int_ty(),
1205 UnOp::Neg | UnOp::Pos => {
1206 if t.is_uninformative() || self.is_numeric(&t) {
1207 t
1208 } else {
1209 Ty::Variant
1210 }
1211 }
1212 }
1213 }
1214 Expr::Ternary {
1215 cond,
1216 then_branch,
1217 else_branch,
1218 } => {
1219 self.infer_expr(cond, &Expectation::None);
1220 let a = self.infer_expr(then_branch, expected);
1221 let b = self.infer_expr(else_branch, expected);
1222 if self.is_null(else_branch) {
1224 a
1225 } else if self.is_null(then_branch) {
1226 b
1227 } else {
1228 let r = self.join(&a, &b);
1229 if r.is_variant() && !a.is_uninformative() && !b.is_uninformative() {
1232 self.warn(
1233 self.range_of(id),
1234 WarningCode::IncompatibleTernary,
1235 "The values of the ternary conditional are not mutually compatible."
1236 .to_owned(),
1237 );
1238 }
1239 r
1240 }
1241 }
1242 Expr::Call { callee, args } => self.infer_call(callee, &args),
1243 Expr::Field {
1244 receiver,
1245 name,
1246 name_range,
1247 } => {
1248 self.infer_field(receiver, &name, name_range, false)
1249 }
1250 Expr::Index { base, index } => {
1251 let base_ty = self.infer_expr(base, &Expectation::None);
1252 self.infer_expr(index, &Expectation::None);
1253 self.index_ty(&base_ty)
1254 }
1255 Expr::Is { operand, .. } => {
1256 self.infer_expr(operand, &Expectation::None);
1257 self.bool_ty()
1258 }
1259 Expr::Cast { operand, ty } => {
1260 self.infer_expr(operand, &Expectation::None);
1261 ty.map_or(Ty::Variant, |p| self.resolve_ptr_ty(p))
1262 }
1263 Expr::In { lhs, rhs, .. } => {
1264 self.infer_expr(lhs, &Expectation::None);
1265 self.infer_expr(rhs, &Expectation::None);
1266 self.bool_ty()
1267 }
1268 Expr::Await(operand) => {
1269 let operand_ty = self.infer_expr(operand, &Expectation::None);
1270 if matches!(operand_ty, Ty::Signal(_)) {
1275 Ty::Unknown
1276 } else {
1277 operand_ty
1278 }
1279 }
1280 Expr::Array(elems) => {
1281 let pushed = match expected {
1285 Expectation::Has(Ty::Array(e)) => Some((**e).clone()),
1286 _ => None,
1287 };
1288 let elem_exp = pushed.clone().map_or(Expectation::None, Expectation::Has);
1289 for e in elems {
1290 self.infer_expr(e, &elem_exp);
1291 }
1292 pushed.map_or_else(Ty::array_of_variant, |e| Ty::Array(Box::new(e)))
1293 }
1294 Expr::Dict(entries) => {
1295 let pushed = match expected {
1296 Expectation::Has(Ty::Dict(k, v)) => Some(((**k).clone(), (**v).clone())),
1297 _ => None,
1298 };
1299 let (kx, vx) = pushed
1300 .clone()
1301 .map_or((Expectation::None, Expectation::None), |(k, v)| {
1302 (Expectation::Has(k), Expectation::Has(v))
1303 });
1304 for (k, v) in entries {
1305 self.infer_expr(k, &kx);
1306 if let Some(v) = v {
1307 self.infer_expr(v, &vx);
1308 }
1309 }
1310 pushed.map_or_else(Ty::dict_of_variant, |(k, v)| {
1311 Ty::Dict(Box::new(k), Box::new(v))
1312 })
1313 }
1314 Expr::Lambda { params, body } => {
1315 self.infer_lambda(¶ms, &body);
1316 Ty::Callable
1317 }
1318 Expr::Preload { arg, path } => {
1319 if let Some(arg) = arg {
1320 self.infer_expr(arg, &Expectation::None);
1321 }
1322 match path {
1327 Some(p) => {
1331 match resolve::anchor_res_path(self.self_res_path().as_deref(), &p) {
1332 Some(abs) => resolve::resolve_external(
1333 self.db,
1334 &resolve::ExternalRef::Preload(abs),
1335 ),
1336 None => Ty::Unknown,
1337 }
1338 }
1339 None => Ty::Unknown,
1340 }
1341 }
1342 Expr::GetNode { path, unique } => self.resolve_node_path(id, path.as_deref(), unique),
1345 }
1346 }
1347
1348 fn is_null(&self, id: ExprId) -> bool {
1350 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
1351 }
1352
1353 fn literal_ty(&self, lit: Literal) -> Ty {
1354 match lit {
1355 Literal::Int => self.int_ty(),
1356 Literal::Float | Literal::MathConst => self.float_ty(),
1357 Literal::Bool => self.bool_ty(),
1358 Literal::Str => self.builtin("String"),
1359 Literal::StringName => self.builtin("StringName"),
1360 Literal::NodePath => self.builtin("NodePath"),
1361 Literal::Null => Ty::Variant,
1363 }
1364 }
1365
1366 fn node_ty(&self) -> Ty {
1367 self.api
1368 .class_by_name("Node")
1369 .map_or(Ty::Unknown, Ty::Object)
1370 }
1371
1372 fn resolve_node_path(&mut self, id: ExprId, path: Option<&str>, unique: bool) -> Ty {
1380 use gdscript_scene::NodePathResolution as R;
1381 let fallback = self.node_ty();
1382 let Some(path) = path else {
1383 return fallback; };
1385 let Some(ctx) = self.owning_scene() else {
1386 return fallback; };
1388 let resolution = if unique {
1389 ctx.model.classify_unique(path)
1390 } else {
1391 ctx.model.classify_path_from(ctx.attach, path)
1392 };
1393 match resolution {
1394 R::Resolved(idx) => ctx
1395 .model
1396 .node(idx)
1397 .and_then(|n| self.scene_node_ty(&ctx.model, n, 0))
1398 .unwrap_or(fallback),
1399 R::Missing if !ctx.ambiguous => {
1400 let what = if unique { "unique name" } else { "node path" };
1401 let sigil = if unique { "%" } else { "$" };
1402 self.emit(
1403 self.range_of(id),
1404 Severity::Warning,
1405 INVALID_NODE_PATH,
1406 format!("no {what} `{sigil}{path}` in the owning scene"),
1407 );
1408 fallback
1409 }
1410 R::IntoInstance => {
1413 let walked = if unique {
1414 ctx.model.resolve_unique_into_instance(path)
1415 } else {
1416 ctx.model.resolve_into_instance(ctx.attach, path)
1417 };
1418 walked
1419 .and_then(|(inst, tail)| {
1420 let inst_node = ctx.model.node(inst)?;
1421 self.resolve_into_instance_ty(&ctx.model, inst_node, &tail, 0)
1422 })
1423 .unwrap_or(fallback)
1424 }
1425 _ => fallback,
1427 }
1428 }
1429
1430 fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
1434 let Ty::ScriptRef(sref) = &self.self_ty else {
1435 return None;
1436 };
1437 let ft = self.db.file_text(FileId(sref.0))?;
1438 crate::queries::scene_context(self.db, ft)
1439 }
1440
1441 fn self_res_path(&self) -> Option<SmolStr> {
1444 let Ty::ScriptRef(sref) = &self.self_ty else {
1445 return None;
1446 };
1447 self.db.file_text(FileId(sref.0))?.res_path(self.db)
1448 }
1449
1450 fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
1455 if let Some(script_ty) = self.node_script_ref(scene, node) {
1456 return Some(script_ty);
1457 }
1458 if let Some(decl) = node.decl_type.as_ref() {
1459 let ty = resolve::resolve_type_name(self.db, self.api, decl);
1460 if !ty.is_uninformative() {
1461 return Some(ty);
1462 }
1463 }
1464 self.instance_root_ty(scene, node, depth)
1465 }
1466
1467 fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
1472 if depth >= 16 {
1473 return None;
1474 }
1475 let (sub, sub_root) = self.instance_subscene(scene, node)?;
1476 let root_node = sub.node(sub_root)?;
1477 self.scene_node_ty(&sub, root_node, depth + 1)
1478 }
1479
1480 fn instance_subscene(
1485 &self,
1486 scene: &SceneModel,
1487 node: &SceneNode,
1488 ) -> Option<(Arc<SceneModel>, gdscript_scene::NodeIdx)> {
1489 let inst = node.instance.as_ref()?;
1490 let path = scene.ext_resources.get(inst)?.path.as_ref()?;
1491 let root = self.db.source_root()?;
1492 let file = crate::queries::res_path_registry(self.db, root)
1493 .get(path.as_str())
1494 .copied()?;
1495 let ft = self.db.file_text(file)?;
1496 let sub = crate::queries::scene_model(self.db, ft);
1497 let sub_root = sub.root?;
1498 Some((sub, sub_root))
1499 }
1500
1501 fn resolve_into_instance_ty(
1506 &self,
1507 scene: &SceneModel,
1508 instance_node: &SceneNode,
1509 tail: &str,
1510 depth: u32,
1511 ) -> Option<Ty> {
1512 if depth >= 16 {
1513 return None;
1514 }
1515 let (sub, sub_root) = self.instance_subscene(scene, instance_node)?;
1516 if let Some(idx) = sub.resolve_path_from(sub_root, tail) {
1517 let n = sub.node(idx)?;
1518 return self.scene_node_ty(&sub, n, depth + 1);
1519 }
1520 let (inner, inner_tail) = sub.resolve_into_instance(sub_root, tail)?;
1522 let inner_node = sub.node(inner)?;
1523 self.resolve_into_instance_ty(&sub, inner_node, &inner_tail, depth + 1)
1524 }
1525
1526 fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
1529 let path = scene
1530 .ext_resources
1531 .get(node.script.as_ref()?)?
1532 .path
1533 .as_ref()?;
1534 let root = self.db.source_root()?;
1535 let file = crate::queries::res_path_registry(self.db, root)
1536 .get(path.as_str())
1537 .copied()?;
1538 Some(Ty::ScriptRef(ScriptRefId(file.0)))
1539 }
1540
1541 fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
1542 if op == BinOp::Assign {
1543 return self.infer_assign(lhs, rhs);
1544 }
1545 if matches!(op, BinOp::And | BinOp::Or) {
1548 self.infer_expr(lhs, &Expectation::None);
1549 let saved = self.narrowing.clone();
1550 self.apply_condition_facts(lhs, op == BinOp::And);
1551 self.infer_expr(rhs, &Expectation::None);
1552 self.narrowing = saved;
1553 return self.bool_ty();
1554 }
1555 let lt = self.infer_expr(lhs, &Expectation::None);
1556 let rt = self.infer_expr(rhs, &Expectation::None);
1557 if op.is_boolean() {
1558 return self.bool_ty();
1559 }
1560 if op == BinOp::Div && self.is_int(<) && self.is_int(&rt) {
1562 self.warn(
1563 self.range_of(id),
1564 WarningCode::IntegerDivision,
1565 "Integer division. Decimal part will be discarded.".to_owned(),
1566 );
1567 return self.int_ty();
1568 }
1569 self.bin_result(op, <, &rt)
1570 }
1571
1572 fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
1573 let slot = self.infer_expr(lhs, &Expectation::None);
1574 let expected = if slot.is_uninformative() {
1575 Expectation::None
1576 } else {
1577 Expectation::Has(slot.clone())
1578 };
1579 let value = self.infer_expr(rhs, &expected);
1580 if !slot.is_uninformative() {
1581 self.check_assign(&value, &slot, self.range_of(rhs));
1582 }
1583 slot
1586 }
1587
1588 fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
1591 if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
1592 for o in self.api.builtin_operators(*b) {
1593 if o.op == sym
1594 && let Some(right) = &o.right
1595 && self.tyref_matches(right, rt)
1596 {
1597 return ty::resolve_tyref(self.api, &o.result);
1598 }
1599 }
1600 }
1601 if self.is_numeric(lt) && self.is_numeric(rt) {
1602 return if self.is_float(lt) || self.is_float(rt) {
1603 self.float_ty()
1604 } else {
1605 self.int_ty()
1606 };
1607 }
1608 if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
1611 return Ty::Unknown;
1612 }
1613 Ty::Variant
1614 }
1615
1616 fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
1617 let resolved = ty::resolve_tyref(self.api, tyref);
1618 resolved.is_variant() || &resolved == ty
1619 }
1620
1621 fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
1622 for &a in args {
1624 self.infer_expr(a, &Expectation::None);
1625 }
1626 let ret = match self.body.expr(callee).clone() {
1627 Expr::Field {
1628 receiver,
1629 name,
1630 name_range,
1631 } => {
1632 self.infer_field(receiver, &name, name_range, true)
1633 }
1634 Expr::Name(name) => {
1635 let ret = self.resolve_call_name(&name);
1636 self.expr_ty.insert(callee, Ty::Callable);
1637 ret
1638 }
1639 _ => {
1643 self.infer_expr(callee, &Expectation::None);
1644 Ty::Unknown
1645 }
1646 };
1647 self.check_call_args(callee, args);
1650 ret
1651 }
1652
1653 fn check_call_args(&mut self, callee: ExprId, args: &[ExprId]) {
1659 let Some(params) = self.call_param_tys(callee) else {
1660 return;
1661 };
1662 for (i, &arg) in args.iter().enumerate() {
1663 let Some(param_ty) = params.get(i) else {
1664 break; };
1666 if param_ty.is_uninformative() || param_ty.is_variant() {
1667 continue; }
1669 let arg_ty = self.expr_ty.get(&arg).cloned().unwrap_or(Ty::Unknown);
1671 if ty::is_assignable(self.api, &arg_ty, param_ty) == Assign::OkUnsafe {
1672 let pl = param_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1673 let al = arg_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
1674 self.warn(
1675 self.range_of(arg),
1676 WarningCode::UnsafeCallArgument,
1677 format!(
1678 "The argument {} requires a value of type \"{pl}\" but is passed \"{al}\", which is unsafe.",
1679 i + 1
1680 ),
1681 );
1682 }
1683 }
1684 }
1685
1686 fn call_param_tys(&self, callee: ExprId) -> Option<Vec<Ty>> {
1690 match self.body.expr(callee) {
1691 Expr::Name(name) => self.name_call_param_tys(name),
1692 Expr::Field { receiver, name, .. } => match self.expr_ty.get(receiver)? {
1693 Ty::Object(class) => match self.api.lookup_member(*class, name)? {
1694 MemberRef::Method(sig) => Some(
1695 sig.params
1696 .iter()
1697 .map(|p| ty::resolve_tyref(self.api, &p.ty))
1698 .collect(),
1699 ),
1700 _ => None,
1701 },
1702 _ => None,
1704 },
1705 _ => None,
1706 }
1707 }
1708
1709 fn name_call_param_tys(&self, name: &str) -> Option<Vec<Ty>> {
1713 if let Some(item) = self.class.lookup(name)
1714 && let Some(Member::Func(f)) = self.class.member(item)
1715 {
1716 return Some(
1717 f.params
1718 .iter()
1719 .map(|p| {
1720 p.type_ref.as_deref().map_or(Ty::Variant, |t| {
1721 resolve::resolve_type_name(self.db, self.api, t)
1722 })
1723 })
1724 .collect(),
1725 );
1726 }
1727 if let Ty::Object(base) = self.class.base
1728 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1729 {
1730 return Some(
1731 sig.params
1732 .iter()
1733 .map(|p| ty::resolve_tyref(self.api, &p.ty))
1734 .collect(),
1735 );
1736 }
1737 None
1738 }
1739
1740 fn resolve_call_name(&self, name: &str) -> Ty {
1742 if let Some(item) = self.class.lookup(name)
1743 && let Some(Member::Func(f)) = self.class.member(item)
1744 {
1745 return self.func_return_ty(f.return_type.as_deref());
1746 }
1747 if let Ty::Object(base) = self.class.base
1749 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
1750 {
1751 return ty::resolve_tyref(self.api, &sig.return_ty);
1752 }
1753 if let Some(u) = self.api.utility(name) {
1754 return ty::resolve_tyref(self.api, &u.return_ty);
1755 }
1756 if let Some(f) = self.api.gdscript_builtin(name) {
1757 return resolve::layer_to_ty(self.api, f.ret);
1758 }
1759 if let Some(b) = self.api.builtin_by_name(name) {
1763 return ty::resolve_tyref(self.api, &TyRef::Builtin(b));
1764 }
1765 Ty::Unknown
1768 }
1769
1770 fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
1771 annotation.map_or(Ty::Variant, |t| {
1772 resolve::resolve_type_name(self.db, self.api, t)
1773 })
1774 }
1775
1776 fn infer_field(
1780 &mut self,
1781 receiver: ExprId,
1782 name: &str,
1783 name_range: TextRange,
1784 as_method: bool,
1785 ) -> Ty {
1786 let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
1787 let recv_ty = self.infer_expr(receiver, &Expectation::None);
1788
1789 if is_self && let Some(item) = self.class.lookup(name) {
1791 return self.own_member_ty(item, as_method);
1792 }
1793
1794 match &recv_ty {
1795 t if t.is_uninformative() => recv_ty.clone(),
1800 Ty::Object(class) => {
1801 if name == "new" {
1802 recv_ty.clone()
1805 } else if let Some(m) = self.api.lookup_member(*class, name) {
1806 self.check_member_kind_misuse(&m, as_method, name, name_range);
1807 self.check_static_on_instance(receiver, &m, as_method, name_range);
1808 self.member_ref_ty(&m, as_method)
1809 } else if let Some(t) = self.class_enum_value(*class, name) {
1810 t
1812 } else {
1813 self.emit_unsafe(name, &recv_ty, name_range, as_method);
1815 Ty::Variant
1816 }
1817 }
1818 Ty::Builtin(_) | Ty::Array(_) | Ty::Dict(..) | Ty::Callable | Ty::Signal(_) => {
1819 self.builtin_member_ty(&recv_ty, name, name_range, as_method)
1820 }
1821 Ty::Enum(_) => self.int_ty(),
1823 Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
1825 _ => Ty::Variant,
1826 }
1827 }
1828
1829 fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
1834 if name == "new" {
1835 return Ty::ScriptRef(sref);
1836 }
1837 self.script_member_walk(sref, name, as_method, 0)
1838 .unwrap_or(Ty::Unknown)
1839 }
1840
1841 fn script_member_walk(
1845 &self,
1846 sref: ScriptRefId,
1847 name: &str,
1848 as_method: bool,
1849 depth: u32,
1850 ) -> Option<Ty> {
1851 if depth > 32 {
1852 return None;
1853 }
1854 let file = self.db.file_text(FileId(sref.0))?;
1855 let sc = crate::queries::script_class(self.db, file);
1856 if let Some(m) = sc.member(name) {
1857 return Some(match m {
1858 crate::queries::MemberSig::Method(ret) => {
1859 if as_method {
1860 ret.clone()
1861 } else {
1862 Ty::Callable
1863 }
1864 }
1865 crate::queries::MemberSig::Field(t) => t.clone(),
1866 crate::queries::MemberSig::Signal => Ty::Signal(None),
1867 });
1868 }
1869 match sc.base() {
1871 Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
1872 Ty::Object(class) => self
1873 .api
1874 .lookup_member(*class, name)
1875 .map(|m| self.member_ref_ty(&m, as_method)),
1876 _ => None,
1877 }
1878 }
1879
1880 fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
1885 match (sub, sup) {
1886 (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
1887 (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
1888 (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
1889 _ => false,
1890 }
1891 }
1892
1893 fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
1896 if depth > 32 {
1897 return false;
1898 }
1899 if sub == sup {
1900 return true;
1901 }
1902 let Some(file) = self.db.file_text(FileId(sub.0)) else {
1903 return false;
1904 };
1905 match crate::queries::script_class(self.db, file).base() {
1906 Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
1907 _ => false,
1908 }
1909 }
1910
1911 fn script_extends_engine(
1913 &self,
1914 sub: ScriptRefId,
1915 sup_native: gdscript_api::ClassId,
1916 depth: u32,
1917 ) -> bool {
1918 if depth > 32 {
1919 return false;
1920 }
1921 let Some(file) = self.db.file_text(FileId(sub.0)) else {
1922 return false;
1923 };
1924 match crate::queries::script_class(self.db, file).base() {
1925 Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
1926 Ty::Object(native) => self.api.is_subclass(*native, sup_native),
1927 _ => false,
1928 }
1929 }
1930
1931 fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
1932 let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
1933 let (code, message) = if as_method {
1934 (
1935 WarningCode::UnsafeMethodAccess,
1936 format!(
1937 "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1938 ),
1939 )
1940 } else {
1941 (
1942 WarningCode::UnsafePropertyAccess,
1943 format!(
1944 "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
1945 ),
1946 )
1947 };
1948 self.warn(range, code, message);
1949 }
1950
1951 fn engine_base_has_value_member(&self, name: &str) -> bool {
1957 let Ty::Object(base) = &self.class.base else {
1958 return false;
1959 };
1960 matches!(
1961 self.api.lookup_member(*base, name),
1962 Some(MemberRef::Property(_) | MemberRef::Const(_) | MemberRef::Signal(_))
1963 )
1964 }
1965
1966 fn check_member_kind_misuse(
1973 &mut self,
1974 m: &MemberRef,
1975 as_method: bool,
1976 name: &str,
1977 range: TextRange,
1978 ) {
1979 if !as_method {
1980 return;
1981 }
1982 let (code, kind, ty) = match m {
1983 MemberRef::Property(p) => (
1984 WarningCode::PropertyUsedAsFunction,
1985 "property",
1986 ty::resolve_tyref(self.api, &p.ty),
1987 ),
1988 MemberRef::Const(c) => (
1989 WarningCode::ConstantUsedAsFunction,
1990 "constant",
1991 ty::resolve_tyref(self.api, &c.ty),
1992 ),
1993 _ => return,
1994 };
1995 if ty.is_uninformative() || matches!(ty, Ty::Callable | Ty::Signal(_)) {
1997 return;
1998 }
1999 self.warn(
2000 range,
2001 code,
2002 format!("The {kind} \"{name}\" is being called as if it were a function."),
2003 );
2004 }
2005
2006 fn check_static_on_instance(
2011 &mut self,
2012 receiver: ExprId,
2013 m: &MemberRef,
2014 as_method: bool,
2015 range: TextRange,
2016 ) {
2017 if !as_method {
2018 return;
2019 }
2020 let MemberRef::Method(sig) = m else {
2021 return;
2022 };
2023 if !sig.is_static {
2024 return;
2025 }
2026 let Expr::Name(rname) = self.body.expr(receiver) else {
2027 return;
2028 };
2029 if !self.locals.contains_key(rname) {
2030 return;
2031 }
2032 if let Some(b) = self.bindings.iter().rev().find(|b| &b.name == rname)
2037 && let Some(init) = b.init
2038 && matches!(self.body.expr(init), Expr::Name(_))
2039 {
2040 return;
2041 }
2042 self.warn(
2043 range,
2044 WarningCode::StaticCalledOnInstance,
2045 "A static method is being called on an instance; call it on the type instead."
2046 .to_owned(),
2047 );
2048 }
2049
2050 fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
2051 match m {
2052 MemberRef::Method(sig) => {
2053 if as_method {
2054 ty::resolve_tyref(self.api, &sig.return_ty)
2055 } else {
2056 Ty::Callable
2057 }
2058 }
2059 MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
2060 || ty::resolve_tyref(self.api, &p.ty),
2061 |q| {
2062 Ty::Enum(EnumRef {
2063 qualified: SmolStr::new(q),
2064 bitfield: false,
2065 })
2066 },
2067 ),
2068 MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
2069 MemberRef::Signal(_) => Ty::Signal(None),
2070 MemberRef::Enum(_) => Ty::Variant,
2071 }
2072 }
2073
2074 fn builtin_member_ty(
2075 &mut self,
2076 recv: &Ty,
2077 name: &str,
2078 range: TextRange,
2079 as_method: bool,
2080 ) -> Ty {
2081 let Some(bid) = self.builtin_id_of(recv) else {
2082 return Ty::Variant;
2083 };
2084 if as_method {
2085 return if let Some(sig) = self.api.builtin_method(bid, name) {
2086 ty::resolve_tyref(self.api, &sig.return_ty)
2087 } else {
2088 self.emit_unsafe(name, recv, range, true);
2089 Ty::Variant
2090 };
2091 }
2092 if let Some(member) = self.api.builtin_member(bid, name) {
2093 return ty::resolve_tyref(self.api, &member.ty);
2094 }
2095 let data = self.api.builtin(bid);
2097 if let Some(c) = data.constants.iter().find(|c| c.name == name) {
2098 return ty::resolve_tyref(self.api, &c.ty);
2099 }
2100 if data
2101 .enums
2102 .iter()
2103 .any(|e| e.values.iter().any(|v| v.name == name))
2104 {
2105 return self.int_ty();
2106 }
2107 if self.api.builtin_method(bid, name).is_some() {
2108 return Ty::Callable;
2109 }
2110 self.emit_unsafe(name, recv, range, false);
2111 Ty::Variant
2112 }
2113
2114 fn class_enum_value(&self, class: gdscript_api::ClassId, name: &str) -> Option<Ty> {
2121 let mut cur = Some(class);
2122 while let Some(cid) = cur {
2123 let c = self.api.class(cid);
2124 if let Some(e) = c
2125 .enums
2126 .iter()
2127 .find(|e| e.values.iter().any(|v| v.name == name))
2128 {
2129 return Some(Ty::Enum(EnumRef {
2130 qualified: SmolStr::new(format!("{}.{}", c.name, e.name)),
2131 bitfield: e.is_bitfield,
2132 }));
2133 }
2134 cur = c.base;
2135 }
2136 None
2137 }
2138
2139 fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
2141 match ty {
2142 Ty::Builtin(b) => Some(*b),
2143 Ty::Array(_) => self.api.builtin_by_name("Array"),
2144 Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
2145 Ty::Callable => self.api.builtin_by_name("Callable"),
2146 Ty::Signal(_) => self.api.builtin_by_name("Signal"),
2147 _ => None,
2148 }
2149 }
2150
2151 fn index_ty(&self, base: &Ty) -> Ty {
2153 match base {
2154 Ty::Array(elem) => (**elem).clone(),
2155 Ty::Builtin(b) => self
2156 .api
2157 .builtin(*b)
2158 .indexing_return
2159 .as_ref()
2160 .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
2161 Ty::Unknown => Ty::Unknown,
2163 Ty::Error => Ty::Error,
2164 _ => Ty::Variant,
2165 }
2166 }
2167
2168 fn loop_var_ty(&self, iter: &Ty) -> Ty {
2170 match iter {
2171 Ty::Array(elem) => (**elem).clone(),
2172 Ty::Builtin(b) => {
2173 let data = self.api.builtin(*b);
2174 if data.name == "int" {
2175 self.int_ty()
2177 } else if let Some(r) = &data.indexing_return {
2178 ty::resolve_tyref(self.api, r)
2180 } else {
2181 Ty::Variant
2182 }
2183 }
2184 Ty::Unknown => Ty::Unknown,
2186 Ty::Error => Ty::Error,
2187 _ => Ty::Variant,
2188 }
2189 }
2190
2191 fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
2192 let saved_locals = self.locals.clone();
2196 let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
2197 for p in params {
2198 let ty = self.param_ty(p);
2199 self.bindings.push(Binding {
2200 name: p.name.clone(),
2201 name_range: p.name_range,
2202 ty: ty.clone(),
2203 init: None,
2204 annotated: p.type_ref.is_some(),
2205 inferred_colon_eq: false,
2206 is_const: false,
2207 kind: BindingKind::Param,
2208 });
2209 self.locals.insert(p.name.clone(), ty);
2210 }
2211 self.infer_block(body);
2212 self.return_ty = saved_ret;
2213 self.locals = saved_locals;
2214 }
2215
2216 fn param_ty(&mut self, p: &ParamBinding) -> Ty {
2217 if let Some(ptr) = p.type_ref {
2218 return self.resolve_ptr_ty(ptr);
2219 }
2220 p.default
2222 .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
2223 }
2224
2225 fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
2228 if self.locals.contains_key(name) {
2231 self.used_locals.insert(SmolStr::new(name));
2232 }
2233 if self.is_func_body
2237 && self.needs_assignment.contains(name)
2238 && !self.assign_lhs.contains(&id)
2239 && let Some(cur) = self.cur_stmt
2240 && self
2241 .assigned
2242 .assigned_before(cur)
2243 .is_some_and(|a| !a.contains(name))
2244 {
2245 self.warn(
2246 self.range_of(id),
2247 WarningCode::UnassignedVariable,
2248 format!("The variable \"{name}\" may be used before it is assigned a value."),
2249 );
2250 }
2251 if let Some(key) = self.narrow_key(id)
2253 && let Some(t) = self.narrowing.get(&key)
2254 {
2255 return t.clone();
2256 }
2257 if let Some(t) = self.locals.get(name) {
2258 return t.clone();
2259 }
2260 if let Some(item) = self.class.lookup(name) {
2261 return self.own_member_ty(item, false);
2262 }
2263 match self.class.base.clone() {
2267 Ty::Object(base) => {
2268 if let Some(m) = self.api.lookup_member(base, name) {
2269 return self.member_ref_ty(&m, false);
2270 }
2271 }
2272 Ty::ScriptRef(base) => {
2273 if let Some(t) = self.script_member_walk(base, name, false, 0) {
2274 return t;
2275 }
2276 }
2277 _ => {}
2278 }
2279 if let Some(g) = resolve::resolve_global(self.api, name) {
2280 return global_ty(&g);
2281 }
2282 let by_class = resolve::resolve_external(
2287 self.db,
2288 &resolve::ExternalRef::ClassName(SmolStr::new(name)),
2289 );
2290 if !by_class.is_unknown() {
2291 return by_class;
2292 }
2293 resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)))
2294 }
2295
2296 fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
2297 match item {
2298 ClassItem::EnumVariant => self.int_ty(),
2299 ClassItem::Member(_) => match self.class.member(item) {
2300 Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
2301 Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
2302 Some(Member::Func(f)) => {
2303 if as_method {
2304 self.func_return_ty(f.return_type.as_deref())
2305 } else {
2306 Ty::Callable
2307 }
2308 }
2309 Some(Member::Signal(_)) => Ty::Signal(None),
2310 Some(Member::Class(_)) => Ty::Unknown,
2311 Some(Member::Enum(_)) | None => Ty::Variant,
2312 },
2313 }
2314 }
2315
2316 fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
2319 if let Some(t) = self.class.member_types.get(name) {
2320 return t.clone();
2321 }
2322 self.resolve_decl_annotation(ptr)
2323 }
2324
2325 fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
2327 let Some(node) = ptr.to_node(self.root) else {
2328 return Ty::Variant;
2329 };
2330 cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
2331 .map_or(Ty::Variant, |t| {
2332 resolve::resolve_type_ref(self.db, self.api, &t)
2333 })
2334 }
2335
2336 fn facts_to_narrowing(&self, id: body::StmtId) -> FxHashMap<String, Ty> {
2348 let mut out = FxHashMap::default();
2349 if let Some(facts) = self.flow.facts_before(id) {
2350 for (place, nt) in facts.iter() {
2351 if let Some((key, ty)) = self.narrowing_entry(place, nt) {
2352 out.insert(key, ty);
2353 }
2354 }
2355 }
2356 out
2357 }
2358
2359 fn narrowing_entry(&self, place: &Place, nt: &NarrowedTy) -> Option<(String, Ty)> {
2363 let NarrowedTy::Is(ptr) = nt else {
2364 return None;
2365 };
2366 let narrowed = self.resolve_ptr_ty(*ptr);
2367 if narrowed.is_uninformative() {
2368 return None;
2369 }
2370 if let Place::Local(n) = place
2373 && let Some(cur) = self.locals.get(n)
2374 && !cur.is_uninformative()
2375 && !self.is_subtype(&narrowed, cur)
2376 {
2377 return None;
2378 }
2379 Some((place.dotted_key(), narrowed))
2380 }
2381
2382 fn apply_condition_facts(&mut self, cond: ExprId, truthy: bool) {
2385 for (place, nt) in flow::condition_facts(self.body, cond, truthy) {
2386 if let Some((key, ty)) = self.narrowing_entry(&place, &nt) {
2387 self.narrowing.insert(key, ty);
2388 }
2389 }
2390 }
2391
2392 fn narrow_key(&self, id: ExprId) -> Option<String> {
2395 match self.body.expr(id) {
2396 Expr::Name(n) => Some(n.to_string()),
2397 Expr::SelfExpr => Some("self".to_owned()),
2398 Expr::Paren(inner) => self.narrow_key(*inner),
2399 Expr::Field { receiver, name, .. } => {
2400 Some(format!("{}.{name}", self.narrow_key(*receiver)?))
2401 }
2402 _ => None,
2403 }
2404 }
2405
2406 fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
2407 ptr.to_node(self.root).map_or(Ty::Variant, |n| {
2408 resolve::resolve_type_ref(self.db, self.api, &n)
2409 })
2410 }
2411
2412 fn join(&self, a: &Ty, b: &Ty) -> Ty {
2423 if a == b {
2424 return a.clone();
2425 }
2426 if a.is_error() || b.is_error() {
2427 return Ty::Error;
2428 }
2429 if a.is_unknown() || b.is_unknown() {
2430 return Ty::Unknown;
2431 }
2432 if a.is_variant() || b.is_variant() {
2433 return Ty::Variant;
2434 }
2435 if ty::is_assignable(self.api, a, b) == Assign::Ok {
2436 return b.clone();
2437 }
2438 if ty::is_assignable(self.api, b, a) == Assign::Ok {
2439 return a.clone();
2440 }
2441 Ty::Variant
2442 }
2443}
2444
2445fn global_ty(g: &GlobalDef) -> Ty {
2447 match g {
2448 GlobalDef::Const(t) => t.clone(),
2449 GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
2450 GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
2451 GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
2453 GlobalDef::GlobalEnum => Ty::Variant,
2454 }
2455}
2456
2457fn inference_on_variant_msg(kind: &str) -> String {
2458 format!(
2459 "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
2460 )
2461}
2462
2463fn op_symbol(op: BinOp) -> Option<&'static str> {
2465 Some(match op {
2466 BinOp::Add => "+",
2467 BinOp::Sub => "-",
2468 BinOp::Mul => "*",
2469 BinOp::Div => "/",
2470 BinOp::Mod => "%",
2471 BinOp::Pow => "**",
2472 BinOp::BitAnd => "&",
2473 BinOp::BitOr => "|",
2474 BinOp::BitXor => "^",
2475 BinOp::Shl => "<<",
2476 BinOp::Shr => ">>",
2477 _ => return None,
2478 })
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483 use super::*;
2484 use crate::item_tree::item_tree;
2485 use gdscript_syntax::{SyntaxKind, parse};
2486
2487 struct Harness {
2488 result: InferenceResult,
2489 body: Body,
2490 }
2491
2492 fn infer_first_func(src: &str) -> Harness {
2494 let api = gdscript_api::bundled();
2495 let db = gdscript_db::RootDatabase::default();
2496 let root = parse(src).syntax_node();
2497 let tree = item_tree(&root);
2498 let class = ClassScope::new(&db, api, &tree, None);
2499 let func = gdscript_syntax::ast::descendants(&root)
2500 .into_iter()
2501 .find(|n| n.kind() == SyntaxKind::FuncDecl)
2502 .expect("a function");
2503 let body = body::body_of_func(&func);
2504 let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
2505 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
2506 let result = infer(&db, api, &root, &class, &body, return_ty, true);
2507 Harness { result, body }
2508 }
2509
2510 fn codes(h: &Harness) -> Vec<&str> {
2514 h.result
2515 .diagnostics
2516 .iter()
2517 .map(|d| d.code.as_str())
2518 .chain(h.result.raw_warnings.iter().map(|w| w.code.as_str()))
2519 .collect()
2520 }
2521
2522 fn file_codes(src: &str) -> Vec<String> {
2526 let api = gdscript_api::bundled();
2527 let db = gdscript_db::RootDatabase::default();
2528 let root = parse(src).syntax_node();
2529 let fi = analyze_file(&db, api, &root, FileId(0));
2530 fi.diagnostics
2531 .iter()
2532 .map(|d| d.code.clone())
2533 .chain(fi.raw_warnings.iter().map(|w| w.code.as_str().to_owned()))
2534 .collect()
2535 }
2536
2537 #[test]
2538 fn integer_division_warns() {
2539 let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
2540 assert!(codes(&h).contains(&INTEGER_DIVISION));
2541 }
2542
2543 #[test]
2544 fn float_div_does_not_warn() {
2545 let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
2546 assert!(!codes(&h).contains(&INTEGER_DIVISION));
2547 }
2548
2549 #[test]
2550 fn type_mismatch_on_hard_annotation() {
2551 let h = infer_first_func("func f():\n\tvar s: String = 5\n");
2552 assert!(codes(&h).contains(&TYPE_MISMATCH));
2553 }
2554
2555 #[test]
2556 fn narrowing_conversion_float_to_int() {
2557 let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
2558 assert!(codes(&h).contains(&NARROWING_CONVERSION));
2559 }
2560
2561 #[test]
2562 fn int_to_float_is_silent() {
2563 let h = infer_first_func("func f():\n\tvar x: float = 3\n\treturn x\n");
2564 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
2565 }
2566
2567 #[test]
2568 fn local_shadowing_a_param_warns_shadowed_variable() {
2569 let h = infer_first_func("func f(x):\n\tvar x = 1\n\treturn x\n");
2570 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
2571 }
2572
2573 #[test]
2574 fn local_shadowing_a_class_member_warns_shadowed_variable() {
2575 let h =
2577 infer_first_func("var health = 100\nfunc f():\n\tvar health = 1\n\treturn health\n");
2578 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
2579 }
2580
2581 #[test]
2582 fn non_shadowing_local_does_not_warn_shadowed_variable() {
2583 let h = infer_first_func("func f(x):\n\tvar y = 1\n\treturn x + y\n");
2584 assert!(!codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
2585 }
2586
2587 #[test]
2588 fn local_shadowing_a_base_member_warns_base_class() {
2589 let h =
2591 infer_first_func("extends Node2D\nfunc f():\n\tvar position = 1\n\treturn position\n");
2592 assert!(
2593 codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
2594 "{:?}",
2595 codes(&h)
2596 );
2597 }
2598
2599 #[test]
2600 fn shadowing_an_unresolved_base_is_silent() {
2601 let h = infer_first_func(
2603 "extends SomeUnknownThirdPartyClass\nfunc f():\n\tvar position = 1\n\treturn position\n",
2604 );
2605 assert!(
2606 !codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
2607 "{:?}",
2608 codes(&h)
2609 );
2610 }
2611
2612 #[test]
2613 fn typed_local_read_before_assignment_warns() {
2614 let h = infer_first_func("func f() -> int:\n\tvar x: int\n\treturn x\n");
2615 assert!(
2616 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2617 "{:?}",
2618 codes(&h)
2619 );
2620 }
2621
2622 #[test]
2623 fn typed_local_assigned_then_read_does_not_warn() {
2624 let h = infer_first_func("func f() -> int:\n\tvar x: int\n\tx = 5\n\treturn x\n");
2625 assert!(
2626 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2627 "{:?}",
2628 codes(&h)
2629 );
2630 }
2631
2632 #[test]
2633 fn typed_local_with_initializer_is_not_unassigned() {
2634 let h = infer_first_func("func f() -> int:\n\tvar x: int = 0\n\treturn x\n");
2635 assert!(
2636 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2637 "{:?}",
2638 codes(&h)
2639 );
2640 }
2641
2642 #[test]
2643 fn untyped_local_is_not_unassigned_checked() {
2644 let h = infer_first_func("func f():\n\tvar x\n\tvar y = x\n\treturn y\n");
2646 assert!(
2647 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2648 "{:?}",
2649 codes(&h)
2650 );
2651 }
2652
2653 #[test]
2654 fn typed_local_assigned_in_all_branches_then_read_does_not_warn() {
2655 let h = infer_first_func(
2657 "func f(c) -> int:\n\tvar x: int\n\tif c:\n\t\tx = 1\n\telse:\n\t\tx = 2\n\treturn x\n",
2658 );
2659 assert!(
2660 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2661 "{:?}",
2662 codes(&h)
2663 );
2664 }
2665
2666 #[test]
2667 fn typed_local_assigned_in_one_branch_then_read_warns() {
2668 let h =
2670 infer_first_func("func f(c) -> int:\n\tvar x: int\n\tif c:\n\t\tx = 1\n\treturn x\n");
2671 assert!(
2672 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
2673 "{:?}",
2674 codes(&h)
2675 );
2676 }
2677
2678 #[test]
2679 fn arm_after_wildcard_is_unreachable_pattern() {
2680 let h =
2681 infer_first_func("func f(x):\n\tmatch x:\n\t\t_:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n");
2682 assert!(
2683 codes(&h).contains(&"UNREACHABLE_PATTERN"),
2684 "{:?}",
2685 codes(&h)
2686 );
2687 }
2688
2689 #[test]
2690 fn arm_after_var_bind_is_unreachable_pattern() {
2691 let h = infer_first_func(
2692 "func f(x):\n\tmatch x:\n\t\tvar y:\n\t\t\treturn y\n\t\t1:\n\t\t\tpass\n",
2693 );
2694 assert!(
2695 codes(&h).contains(&"UNREACHABLE_PATTERN"),
2696 "{:?}",
2697 codes(&h)
2698 );
2699 }
2700
2701 #[test]
2702 fn arm_before_wildcard_is_not_unreachable() {
2703 let h =
2704 infer_first_func("func f(x):\n\tmatch x:\n\t\t1:\n\t\t\tpass\n\t\t_:\n\t\t\tpass\n");
2705 assert!(
2706 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
2707 "{:?}",
2708 codes(&h)
2709 );
2710 }
2711
2712 #[test]
2713 fn guarded_wildcard_is_not_a_catch_all() {
2714 let h = infer_first_func(
2716 "func f(x, c):\n\tmatch x:\n\t\t_ when c:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n",
2717 );
2718 assert!(
2719 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
2720 "{:?}",
2721 codes(&h)
2722 );
2723 }
2724
2725 #[test]
2726 fn multi_pattern_with_wildcard_is_conservatively_not_catch_all() {
2727 let h =
2729 infer_first_func("func f(x):\n\tmatch x:\n\t\t1, _:\n\t\t\tpass\n\t\t2:\n\t\t\tpass\n");
2730 assert!(
2731 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
2732 "{:?}",
2733 codes(&h)
2734 );
2735 }
2736
2737 #[test]
2738 fn enum_local_without_default_warns() {
2739 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode\n");
2740 assert!(
2741 codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
2742 "{:?}",
2743 codes(&h)
2744 );
2745 }
2746
2747 #[test]
2748 fn enum_member_without_default_warns() {
2749 let codes = file_codes("var err: Error\nfunc f():\n\tpass\n");
2750 assert!(
2751 codes.iter().any(|c| c == "ENUM_VARIABLE_WITHOUT_DEFAULT"),
2752 "{codes:?}"
2753 );
2754 }
2755
2756 #[test]
2757 fn native_virtual_override_with_clashing_param_type_warns() {
2758 let codes = file_codes("extends Node\nfunc _input(event: int):\n\tpass\n");
2760 assert!(
2761 codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
2762 "{codes:?}"
2763 );
2764 }
2765
2766 #[test]
2767 fn native_virtual_override_with_correct_param_type_does_not_warn() {
2768 let codes = file_codes("extends Node\nfunc _input(event: InputEvent):\n\tpass\n");
2769 assert!(
2770 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
2771 "{codes:?}"
2772 );
2773 }
2774
2775 #[test]
2776 fn native_virtual_override_with_untyped_param_does_not_warn() {
2777 let codes = file_codes("extends Node\nfunc _input(event):\n\tpass\n");
2778 assert!(
2779 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
2780 "{codes:?}"
2781 );
2782 }
2783
2784 #[test]
2785 fn a_non_virtual_method_is_not_a_native_override() {
2786 let codes = file_codes("extends Node\nfunc my_helper(x: int):\n\treturn x\n");
2787 assert!(
2788 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
2789 "{codes:?}"
2790 );
2791 }
2792
2793 #[test]
2794 fn dotted_enum_override_param_does_not_false_warn() {
2795 let codes = file_codes(
2798 "extends MultiplayerPeerExtension\nfunc _set_transfer_mode(p_mode: MultiplayerPeer.TransferMode):\n\tpass\n",
2799 );
2800 assert!(
2801 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
2802 "{codes:?}"
2803 );
2804 }
2805
2806 #[test]
2807 fn unused_signal_warns() {
2808 let codes = file_codes("signal my_event\nfunc f():\n\tpass\n");
2809 assert!(codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
2810 }
2811
2812 #[test]
2813 fn emitted_signal_is_not_unused() {
2814 let codes = file_codes("signal my_event\nfunc f():\n\tmy_event.emit()\n");
2815 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
2816 }
2817
2818 #[test]
2819 fn signal_connected_by_string_is_not_unused() {
2820 let codes = file_codes("signal my_event\nfunc f():\n\tconnect(\"my_event\", Callable())\n");
2821 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
2822 }
2823
2824 #[test]
2825 fn enum_local_with_default_does_not_warn() {
2826 let h = infer_first_func(
2827 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
2828 );
2829 assert!(
2830 !codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
2831 "{:?}",
2832 codes(&h)
2833 );
2834 }
2835
2836 #[test]
2837 fn static_method_on_instance_warns() {
2838 let h =
2840 infer_first_func("func f():\n\tvar j := JSON.new()\n\tj.stringify({})\n\treturn j\n");
2841 assert!(
2842 codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
2843 "{:?}",
2844 codes(&h)
2845 );
2846 }
2847
2848 #[test]
2849 fn static_method_on_the_type_does_not_warn() {
2850 let h = infer_first_func("func f():\n\tJSON.stringify({})\n");
2852 assert!(
2853 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
2854 "{:?}",
2855 codes(&h)
2856 );
2857 }
2858
2859 #[test]
2860 fn static_method_through_a_type_aliased_local_does_not_warn() {
2861 let h = infer_first_func("func f():\n\tvar t := JSON\n\tt.stringify({})\n");
2863 assert!(
2864 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
2865 "{:?}",
2866 codes(&h)
2867 );
2868 }
2869
2870 #[test]
2871 fn property_called_as_function_warns() {
2872 let h = infer_first_func("func f(n: Node):\n\tn.name()\n");
2874 assert!(
2875 codes(&h).contains(&"PROPERTY_USED_AS_FUNCTION"),
2876 "{:?}",
2877 codes(&h)
2878 );
2879 }
2880
2881 #[test]
2882 fn constant_called_as_function_warns() {
2883 let h = infer_first_func("func f(n: Node):\n\tn.NOTIFICATION_READY()\n");
2885 assert!(
2886 codes(&h).contains(&"CONSTANT_USED_AS_FUNCTION"),
2887 "{:?}",
2888 codes(&h)
2889 );
2890 }
2891
2892 #[test]
2893 fn calling_a_real_method_is_not_a_kind_misuse() {
2894 let h = infer_first_func("func f(n: Node):\n\tn.get_parent()\n");
2895 assert!(
2896 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
2897 "{:?}",
2898 codes(&h)
2899 );
2900 }
2901
2902 #[test]
2903 fn reading_a_property_as_a_value_is_not_a_kind_misuse() {
2904 let h = infer_first_func("func f(n: Node):\n\tvar s = n.name\n\treturn s\n");
2905 assert!(
2906 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
2907 "{:?}",
2908 codes(&h)
2909 );
2910 }
2911
2912 #[test]
2913 fn enum_member_into_its_own_enum_slot_is_not_int_as_enum() {
2914 let h = infer_first_func(
2918 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
2919 );
2920 assert!(
2921 !codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
2922 "{:?}",
2923 codes(&h)
2924 );
2925 }
2926
2927 #[test]
2928 fn bare_int_into_enum_slot_still_warns() {
2929 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode = 0\n\treturn m\n");
2931 assert!(
2932 codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
2933 "{:?}",
2934 codes(&h)
2935 );
2936 }
2937
2938 #[test]
2939 fn member_access_resolves_engine_property() {
2940 let h = infer_first_func(
2943 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
2944 );
2945 assert!(
2946 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
2947 "{:?}",
2948 h.result.diagnostics
2949 );
2950 }
2951
2952 #[test]
2953 fn unsafe_method_on_known_type() {
2954 let h = infer_first_func(
2955 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
2956 );
2957 assert!(
2958 codes(&h).contains(&UNSAFE_METHOD_ACCESS),
2959 "{:?}",
2960 h.result.diagnostics
2961 );
2962 }
2963
2964 #[test]
2965 fn is_narrowing_suppresses_unsafe() {
2966 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
2969 assert!(
2970 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
2971 "{:?}",
2972 h.result.diagnostics
2973 );
2974 }
2975
2976 #[test]
2977 fn is_narrowing_flags_real_missing_member() {
2978 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
2980 assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
2981 }
2982
2983 #[test]
2984 fn early_return_is_guard_narrows_past_the_guard() {
2985 let safe =
2988 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.get_parent()\n");
2989 assert!(
2990 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
2991 "real Node method must not warn after the guard: {:?}",
2992 codes(&safe)
2993 );
2994 let bogus =
2995 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.bogus_method()\n");
2996 assert!(
2997 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
2998 "missing method must warn after the guard: {:?}",
2999 codes(&bogus)
3000 );
3001 }
3002
3003 #[test]
3004 fn and_short_circuit_narrows_the_rhs() {
3005 let safe = infer_first_func("func f(x):\n\tif x is Node and x.get_parent():\n\t\tpass\n");
3008 assert!(
3009 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
3010 "real Node method in the and-rhs must not warn: {:?}",
3011 codes(&safe)
3012 );
3013 let bogus =
3014 infer_first_func("func f(x):\n\tif x is Node and x.bogus_method():\n\t\tpass\n");
3015 assert!(
3016 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
3017 "missing method in the and-rhs must warn: {:?}",
3018 codes(&bogus)
3019 );
3020 }
3021
3022 #[test]
3025 fn empty_file_warns() {
3026 assert!(file_codes("").iter().any(|c| c == "EMPTY_FILE"));
3027 assert!(
3028 file_codes("# just a comment\n")
3029 .iter()
3030 .any(|c| c == "EMPTY_FILE")
3031 );
3032 assert!(
3033 file_codes("extends Node\n")
3034 .iter()
3035 .all(|c| c != "EMPTY_FILE")
3036 );
3037 }
3038
3039 #[test]
3040 fn unused_variable_and_parameter() {
3041 let h = infer_first_func("func f(unused_p):\n\tvar unused_v = 1\n");
3042 assert!(codes(&h).contains(&"UNUSED_PARAMETER"), "{:?}", codes(&h));
3043 assert!(codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
3044 let used = infer_first_func("func f(p):\n\tvar v = p\n\treturn v\n");
3046 assert!(codes(&used).iter().all(|c| !c.starts_with("UNUSED")));
3047 let underscored = infer_first_func("func f(_ignored):\n\tpass\n");
3048 assert!(!codes(&underscored).contains(&"UNUSED_PARAMETER"));
3049 }
3050
3051 #[test]
3052 fn standalone_expression_and_ternary() {
3053 let expr = infer_first_func("func f(a, b):\n\ta + b\n");
3054 assert!(
3055 codes(&expr).contains(&"STANDALONE_EXPRESSION"),
3056 "{:?}",
3057 codes(&expr)
3058 );
3059 let tern = infer_first_func("func f(c):\n\t1 if c else 2\n");
3060 assert!(
3061 codes(&tern).contains(&"STANDALONE_TERNARY"),
3062 "{:?}",
3063 codes(&tern)
3064 );
3065 let call = infer_first_func("func f(n):\n\tn.queue_free()\n");
3067 assert!(codes(&call).iter().all(|c| !c.starts_with("STANDALONE")));
3068 }
3069
3070 #[test]
3071 fn unreachable_code_after_return() {
3072 let h = infer_first_func("func f():\n\treturn\n\tprint(\"dead\")\n");
3073 assert!(codes(&h).contains(&"UNREACHABLE_CODE"), "{:?}", codes(&h));
3074 }
3075
3076 #[test]
3077 fn incompatible_ternary_warns() {
3078 let h = infer_first_func("func f(c):\n\tvar x = \"s\" if c else 1\n\treturn x\n");
3080 assert!(
3081 codes(&h).contains(&"INCOMPATIBLE_TERNARY"),
3082 "{:?}",
3083 codes(&h)
3084 );
3085 }
3086
3087 #[test]
3088 fn variant_receiver_never_unsafe() {
3089 let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
3091 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
3092 }
3093
3094 #[test]
3095 fn unsafe_call_argument_on_variant_into_typed_param() {
3096 let h = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n: Node2D):\n\tpass\n");
3098 assert!(
3099 codes(&h).contains(&UNSAFE_CALL_ARGUMENT),
3100 "{:?}",
3101 h.result.diagnostics
3102 );
3103 }
3104
3105 #[test]
3106 fn unsafe_call_argument_silent_on_safe_and_untyped() {
3107 let upcast =
3109 infer_first_func("func f(n: Node2D):\n\ttake(n)\nfunc take(n: Node):\n\tpass\n");
3110 assert!(
3111 !codes(&upcast).contains(&UNSAFE_CALL_ARGUMENT),
3112 "upcast is safe: {:?}",
3113 upcast.result.diagnostics
3114 );
3115 let untyped = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n):\n\tpass\n");
3116 assert!(
3117 !codes(&untyped).contains(&UNSAFE_CALL_ARGUMENT),
3118 "untyped param accepts anything: {:?}",
3119 untyped.result.diagnostics
3120 );
3121 }
3122
3123 #[test]
3124 fn inference_on_variant() {
3125 let h = infer_first_func("func f(x):\n\tvar y := x\n");
3127 assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
3128 }
3129
3130 #[test]
3131 fn field_inferred_from_earlier_field_is_typed() {
3132 let codes = file_codes("var a := 1\nvar b := a + 1\n");
3136 assert!(
3137 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
3138 "field `b` from earlier field `a` should type as int, not Variant: {codes:?}"
3139 );
3140 }
3141
3142 #[test]
3143 fn field_forward_reference_is_seamed_not_warned() {
3144 let codes = file_codes("var b := a\nvar a := 1\n");
3148 assert!(
3149 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
3150 "forward field reference must not false-warn: {codes:?}"
3151 );
3152 }
3153
3154 #[test]
3155 fn standalone_inferred_field_unchanged() {
3156 let codes = file_codes("var n := 0\n");
3158 assert!(
3159 codes.is_empty(),
3160 "a literal-initialised field should produce no diagnostics: {codes:?}"
3161 );
3162 }
3163
3164 #[test]
3165 fn lambda_var_is_callable_not_variant() {
3166 let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
3167 assert!(
3168 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3169 "{:?}",
3170 h.result.diagnostics
3171 );
3172 }
3173
3174 #[test]
3175 fn multiline_lambda_then_paren_line_no_false_warning() {
3176 let src = "func f(state, i, loop):\n\tvar cb := func():\n\t\tif i >= state.size():\n\t\t\treturn\n\t(loop as SceneTree).process_frame.connect(cb, CONNECT_ONE_SHOT)\n";
3180 let h = infer_first_func(src);
3181 assert!(
3182 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3183 "{:?}",
3184 h.result.diagnostics
3185 );
3186 }
3187
3188 #[test]
3189 fn calling_a_callable_value_is_seam_not_variant() {
3190 let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
3194 let h = infer_first_func(src);
3195 assert!(
3196 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3197 "{:?}",
3198 h.result.diagnostics
3199 );
3200 }
3201
3202 #[test]
3203 fn ternary_with_seam_branch_does_not_collapse_to_variant() {
3204 let src =
3208 "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
3209 let h = infer_first_func(src);
3210 assert!(
3211 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3212 "seam branch should keep the ternary on the seam: {:?}",
3213 h.result.diagnostics
3214 );
3215 }
3216
3217 #[test]
3218 fn await_a_coroutine_call_recovers_its_return_type() {
3219 let src = "func g() -> int:\n\tvar x := await make()\n\treturn x\nfunc make() -> int:\n\treturn 5\n";
3222 let h = infer_first_func(src);
3223 assert!(
3224 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3225 "no false variant warning: {:?}",
3226 h.result.diagnostics
3227 );
3228 let api = gdscript_api::bundled();
3229 let x = &h.result.bindings[0];
3230 assert!(
3231 matches!(&x.ty, Ty::Builtin(b) if api.builtin(*b).name == "int"),
3232 "await make() should recover int, got {:?}",
3233 x.ty
3234 );
3235 }
3236
3237 #[test]
3238 fn await_a_signal_stays_the_seam() {
3239 let src = "func f():\n\tvar x := await get_tree().process_frame\n\treturn x\n";
3242 let h = infer_first_func(src);
3243 assert!(
3244 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3245 "awaiting a signal must not warn: {:?}",
3246 h.result.diagnostics
3247 );
3248 assert!(
3249 matches!(&h.result.bindings[0].ty, Ty::Unknown),
3250 "awaiting a signal stays the seam, got {:?}",
3251 h.result.bindings[0].ty
3252 );
3253 }
3254
3255 #[test]
3256 fn for_var_over_packed_string_array_is_string() {
3257 let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
3260 assert!(
3261 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3262 "{:?}",
3263 h.result.diagnostics
3264 );
3265 }
3266
3267 #[test]
3268 fn class_new_is_object_not_variant() {
3269 let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
3270 assert!(
3271 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3272 "{:?}",
3273 h.result.diagnostics
3274 );
3275 }
3276
3277 #[test]
3278 fn unknown_seam_never_warns() {
3279 let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
3281 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
3282 }
3283
3284 #[test]
3285 fn expr_types_are_memoized_for_hover() {
3286 let h = infer_first_func("func f():\n\tvar n := 42\n");
3287 let has_int = h
3289 .result
3290 .expr_ty
3291 .values()
3292 .any(|t| matches!(t, Ty::Builtin(_)));
3293 assert!(has_int);
3294 assert!(!h.body.exprs.is_empty());
3296 }
3297}