1use gdscript_api::{EngineApi, MemberRef, MethodSig, 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::{InnerClassItem, ItemTree, Member, has_annotation, 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
146fn collect_rebound_names(body: &Body) -> FxHashSet<SmolStr> {
155 let mut out = FxHashSet::default();
156 for e in &body.exprs {
157 let Expr::Bin {
158 op: BinOp::Assign,
159 lhs,
160 ..
161 } = e
162 else {
163 continue;
164 };
165 let mut cur = *lhs;
166 loop {
167 match body.expr(cur) {
168 Expr::Name(n) => {
169 out.insert(n.clone());
170 break;
171 }
172 Expr::Paren(inner) => cur = *inner,
173 Expr::Index { base, .. } => cur = *base,
174 Expr::Cast { operand, .. } => cur = *operand,
176 _ => break,
177 }
178 }
179 }
180 out
181}
182
183fn collect_escaping_names(body: &Body) -> FxHashSet<SmolStr> {
192 fn mark(safe: &mut FxHashSet<ExprId>, body: &Body, mut id: ExprId) {
193 loop {
194 safe.insert(id);
195 match body.expr(id) {
196 Expr::Paren(inner) => id = *inner,
197 _ => return,
198 }
199 }
200 }
201 let mut safe: FxHashSet<ExprId> = FxHashSet::default();
202 for e in &body.exprs {
203 match e {
204 Expr::Index { base, .. } => mark(&mut safe, body, *base),
205 Expr::Field { receiver, .. } => mark(&mut safe, body, *receiver),
206 Expr::Call { callee, .. } => mark(&mut safe, body, *callee),
207 Expr::Bin {
208 op: BinOp::Assign,
209 lhs,
210 ..
211 } => mark(&mut safe, body, *lhs),
212 _ => {}
213 }
214 }
215 for s in &body.stmts {
216 if let Stmt::Return(Some(e)) = s {
217 mark(&mut safe, body, *e);
218 }
219 }
220 let mut out = FxHashSet::default();
221 for (i, e) in body.exprs.iter().enumerate() {
222 if let Expr::Name(n) = e
223 && !safe.contains(&ExprId(u32::try_from(i).unwrap_or(u32::MAX)))
224 {
225 out.insert(n.clone());
226 }
227 }
228 out
229}
230
231#[must_use]
236#[allow(
237 clippy::too_many_lines,
238 clippy::too_many_arguments,
239 reason = "the per-body inference orchestration reads best whole; the params are the fixed analysis inputs (db/api/tree/scope/body + result shape), not a config to bundle"
240)]
241pub fn infer(
242 db: &dyn Db,
243 api: &EngineApi,
244 root: &GdNode,
245 class: &ClassScope,
246 body: &Body,
247 return_ty: Ty,
248 is_func_body: bool,
249 owner: Option<&str>,
250) -> InferenceResult {
251 let self_ty = class.self_ty.clone();
252 let mut cx = Cx {
253 db,
254 api,
255 root,
256 body,
257 class,
258 self_ty,
259 return_ty,
260 expr_ty: FxHashMap::default(),
261 bindings: Vec::new(),
262 diagnostics: Vec::new(),
263 raw_warnings: Vec::new(),
264 locals: FxHashMap::default(),
265 used_locals: FxHashSet::default(),
266 narrowing: FxHashMap::default(),
267 flow: flow::analyze(body),
268 is_func_body,
269 owner,
270 assigned: flow::analyze_assigned(
271 body,
272 &body
273 .params
274 .iter()
275 .map(|p| p.name.clone())
276 .collect::<Vec<_>>(),
277 ),
278 cur_stmt: None,
279 needs_assignment: FxHashSet::default(),
280 assign_lhs: collect_assign_lhs(body),
281 rebound: collect_rebound_names(body),
282 escaping: collect_escaping_names(body),
283 };
284 let params = body.params.clone();
286 for p in ¶ms {
287 let ty = cx.param_ty(p);
288 cx.bindings.push(Binding {
289 name: p.name.clone(),
290 name_range: p.name_range,
291 ty: ty.clone(),
292 init: None,
293 annotated: p.type_ref.is_some(),
294 inferred_colon_eq: false,
295 is_const: false,
296 kind: BindingKind::Param,
297 });
298 cx.locals.insert(p.name.clone(), ty);
299 }
300 if let Some(tail) = body.tail {
301 cx.infer_expr(tail, &Expectation::None);
302 }
303 let block = body.block.clone();
304 cx.infer_block(&block);
305
306 if is_func_body {
310 let fn_name = owner.unwrap_or("<anonymous>");
313 let unused: Vec<(TextRange, WarningCode, String)> = cx
314 .bindings
315 .iter()
316 .filter_map(|b| {
317 if b.name.starts_with('_') || cx.used_locals.contains(&b.name) {
318 return None;
319 }
320 let (code, msg) = match b.kind {
321 BindingKind::Param => (
322 WarningCode::UnusedParameter,
323 format!(
324 "The parameter \"{n}\" is never used in the function \"{fn_name}()\". If this is intended, prefix it with an underscore: \"_{n}\".",
325 n = b.name
326 ),
327 ),
328 BindingKind::Var if b.is_const => (
329 WarningCode::UnusedLocalConstant,
330 format!(
331 "The local constant \"{n}\" is declared but never used in the block. If this is intended, prefix it with an underscore: \"_{n}\".",
332 n = b.name
333 ),
334 ),
335 BindingKind::Var => (
336 WarningCode::UnusedVariable,
337 format!(
338 "The local variable \"{n}\" is declared but never used in the block. If this is intended, prefix it with an underscore: \"_{n}\".",
339 n = b.name
340 ),
341 ),
342 BindingKind::ForVar | BindingKind::MatchBind => return None,
343 };
344 Some((b.name_range, code, msg))
345 })
346 .collect();
347 for (range, code, msg) in unused {
348 cx.warn(range, code, msg);
349 }
350 }
351
352 if is_func_body {
359 let global_shadows: Vec<(TextRange, String)> = cx
360 .bindings
361 .iter()
362 .filter_map(|b| {
363 let kind = shadowed_global_kind(db, api, &b.name)?;
364 let what = match b.kind {
365 BindingKind::Param => "parameter",
366 BindingKind::Var if b.is_const => "constant",
367 BindingKind::Var => "variable",
368 BindingKind::ForVar => "for loop variable",
369 BindingKind::MatchBind => "pattern bind",
370 };
371 Some((
372 b.name_range,
373 format!("The {what} \"{}\" has the same name as a {kind}.", b.name),
374 ))
375 })
376 .collect();
377 for (range, msg) in global_shadows {
378 cx.warn(range, WarningCode::ShadowedGlobalIdentifier, msg);
379 }
380 }
381
382 if is_func_body {
389 let decl_strictness: Vec<(TextRange, WarningCode, String)> = cx
390 .bindings
391 .iter()
392 .filter_map(|b| match b.kind {
393 BindingKind::Param if !b.annotated => Some((
394 b.name_range,
395 WarningCode::UntypedDeclaration,
396 format!("Parameter \"{}\" has no static type.", b.name),
397 )),
398 BindingKind::Var if !b.is_const && b.inferred_colon_eq => Some((
399 b.name_range,
400 WarningCode::InferredDeclaration,
401 format!(
402 "Variable \"{}\" has an implicitly inferred static type.",
403 b.name
404 ),
405 )),
406 BindingKind::Var if !b.is_const && !b.annotated => Some((
407 b.name_range,
408 WarningCode::UntypedDeclaration,
409 format!("Variable \"{}\" has no static type.", b.name),
410 )),
411 _ => None,
412 })
413 .collect();
414 for (range, code, msg) in decl_strictness {
415 cx.warn(range, code, msg);
416 }
417 }
418
419 let confusable_bindings: Vec<(TextRange, SmolStr)> = cx
422 .bindings
423 .iter()
424 .filter(|b| is_confusable_identifier(&b.name))
425 .map(|b| (b.name_range, b.name.clone()))
426 .collect();
427 for (range, name) in confusable_bindings {
428 cx.warn(
429 range,
430 WarningCode::ConfusableIdentifier,
431 format!(
432 "The identifier \"{name}\" has misleading characters and might be confused with something else."
433 ),
434 );
435 }
436
437 let unreachable = cx.flow.unreachable_ranges(body);
442 for (range, cause) in unreachable {
443 let msg = match cause {
444 flow::UnreachableCause::AfterReturn => format!(
445 "Unreachable code (statement after return) in function \"{}()\".",
446 cx.owner.unwrap_or("<anonymous>")
447 ),
448 flow::UnreachableCause::Other => {
449 "Unreachable code (statement after a break, continue, or a diverging branch)."
450 .to_owned()
451 }
452 };
453 cx.warn(range, WarningCode::UnreachableCode, msg);
454 }
455
456 let unreachable_patterns = cx.flow.unreachable_pattern_ranges().to_vec();
458 for range in unreachable_patterns {
459 cx.warn(
460 range,
461 WarningCode::UnreachablePattern,
462 "Unreachable pattern (pattern after wildcard or bind).".to_owned(),
463 );
464 }
465
466 InferenceResult {
467 expr_ty: cx.expr_ty,
468 bindings: cx.bindings,
469 diagnostics: cx.diagnostics,
470 raw_warnings: cx.raw_warnings,
471 }
472}
473
474#[must_use]
477pub fn infer_func(
478 db: &dyn Db,
479 api: &EngineApi,
480 root: &GdNode,
481 class: &ClassScope,
482 ptr: AstPtr,
483) -> InferenceResult {
484 let Some(node) = ptr.to_node(root) else {
485 return InferenceResult::default();
486 };
487 let body = body::body_of_func(&node);
488 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
491 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
492 let owner = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::Name)
493 .map(|n| n.text().to_string());
494 infer(
495 db,
496 api,
497 root,
498 class,
499 &body,
500 return_ty,
501 true,
502 owner.as_deref(),
503 )
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
510pub struct Unit {
511 pub range: TextRange,
513 pub body: Body,
515 pub result: InferenceResult,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Default)]
522pub struct FileInference {
523 pub tree: Arc<ItemTree>,
525 pub units: Vec<Unit>,
527 pub diagnostics: Vec<Diagnostic>,
530 pub raw_warnings: Vec<RawWarning>,
533}
534
535impl FileInference {
536 #[must_use]
538 pub fn unit_at(&self, offset: u32) -> Option<&Unit> {
539 self.units
540 .iter()
541 .filter(|u| u.range.start <= offset && offset < u.range.end)
542 .min_by_key(|u| u.range.end - u.range.start)
543 }
544}
545
546#[must_use]
550#[allow(clippy::too_many_lines)] pub fn analyze_file(db: &dyn Db, api: &EngineApi, root: &GdNode, file_id: FileId) -> FileInference {
552 let tree = item_tree(root);
553 let mut units = Vec::new();
554 let mut diagnostics = Vec::new();
555 let mut raw_warnings: Vec<RawWarning> = Vec::new();
556
557 if tree.members.is_empty() && tree.class_name.is_none() && tree.extends.is_none() {
559 raw_warnings.push(RawWarning {
560 range: TextRange::new(0, 0),
561 code: WarningCode::EmptyFile,
562 message: "Empty script file.".to_owned(),
563 });
564 }
565 let mut member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
566 let self_ref = Ty::ScriptRef(ScriptRefId(file_id.0));
569 let res_path = db.file_text(file_id).and_then(|ft| ft.res_path(db));
571
572 if let Some(name) = tree.class_name.clone() {
577 let hides = if resolve::resolve_global(api, &name).is_some() {
583 match resolve::resolve_global(api, &name) {
584 Some(GlobalDef::BuiltinType(_)) => Some("a built-in type"),
585 Some(GlobalDef::Builtin | GlobalDef::Utility) => Some("a built-in function"),
586 Some(_) => Some("a native class"),
587 None => None,
588 }
589 } else if is_autoload_singleton(db, &name) {
590 Some("an autoload singleton")
591 } else if collisions_contains(db, &name) {
592 Some("a global script class")
593 } else {
594 None
595 };
596 if let Some(kind) = hides
597 && let Some(range) = class_name_decl_range(root)
598 {
599 diagnostics.push(Diagnostic {
600 range,
601 severity: Severity::Warning,
602 code: SHADOWED_GLOBAL_IDENTIFIER.to_owned(),
603 message: format!("Class \"{name}\" hides {kind}."),
604 source: DiagnosticSource::Type,
605 fixes: Vec::new(),
606 tags: Vec::new(),
607 });
608 }
609 if is_confusable_identifier(&name)
611 && let Some(range) = class_name_decl_range(root)
612 {
613 raw_warnings.push(RawWarning {
614 range,
615 code: WarningCode::ConfusableIdentifier,
616 message: format!(
617 "The identifier \"{name}\" has misleading characters and might be confused with something else."
618 ),
619 });
620 }
621 }
622
623 if extends_chain_is_cyclic(db, file_id)
631 && let Some(range) = extends_decl_range(root)
632 {
633 diagnostics.push(Diagnostic {
634 range,
635 severity: Severity::Warning,
636 code: CYCLIC_INHERITANCE.to_owned(),
637 message: "Cyclic class hierarchy: this class's `extends` chain returns to itself."
638 .to_owned(),
639 source: DiagnosticSource::Type,
640 tags: Vec::new(),
641 fixes: Vec::new(),
642 });
643 }
644
645 raw_warnings.extend(member_level_warnings(
647 db,
648 api,
649 root,
650 &tree,
651 res_path.as_deref(),
652 ));
653
654 {
664 const MAX_ROUNDS: usize = 4;
668 let mut final_units: Vec<Unit> = Vec::new();
669 let mut final_diagnostics: Vec<Diagnostic> = Vec::new();
670 let mut final_raw_warnings: Vec<RawWarning> = Vec::new();
671 for _ in 0..MAX_ROUNDS {
672 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
673 class.self_ty = self_ref.clone();
674 class.member_types.clone_from(&member_types);
675 let mut next_member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
676 final_units = Vec::new();
677 final_diagnostics = Vec::new();
678 final_raw_warnings = Vec::new();
679 for m in &tree.members {
680 let (ptr, range) = match m {
681 Member::Var(v) => (v.ptr, v.range),
682 Member::Const(c) => (c.ptr, c.range),
683 _ => continue,
684 };
685 if let Some(unit) = unit_from_decl(db, api, root, &class, ptr, range) {
686 if let (Some(name), Some(b)) = (m.name(), unit.result.bindings.first()) {
687 next_member_types.insert(SmolStr::new(name), b.ty.clone());
688 }
689 final_diagnostics.extend(unit.result.diagnostics.iter().cloned());
690 final_raw_warnings.extend(unit.result.raw_warnings.iter().cloned());
691 final_units.push(unit);
692 }
693 }
694 if next_member_types == member_types {
695 break;
696 }
697 member_types = next_member_types;
698 }
699 diagnostics.extend(final_diagnostics);
700 raw_warnings.extend(final_raw_warnings);
701 units.extend(final_units);
702 }
703
704 {
706 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
707 class.member_types = member_types;
708 class.self_ty = self_ref.clone();
709 for m in &tree.members {
710 let Member::Func(f) = m else { continue };
711 let Some(node) = f.ptr.to_node(root) else {
712 continue;
713 };
714 let body = body::body_of_func(&node);
715 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
716 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
717 let result = infer(db, api, root, &class, &body, return_ty, true, Some(&f.name));
718 diagnostics.extend(result.diagnostics.iter().cloned());
719 raw_warnings.extend(result.raw_warnings.iter().cloned());
720 units.push(Unit {
721 range: f.range,
722 body,
723 result,
724 });
725 }
726 }
727
728 infer_inner_class_bodies(
733 db,
734 api,
735 root,
736 &tree,
737 file_id,
738 "",
739 res_path.as_deref(),
740 &mut units,
741 &mut diagnostics,
742 &mut raw_warnings,
743 0,
744 );
745
746 FileInference {
747 tree,
748 units,
749 diagnostics,
750 raw_warnings,
751 }
752}
753
754#[allow(
760 clippy::too_many_arguments,
761 reason = "threads the same analyze_file accumulators a free helper can't capture from a closure"
762)]
763fn infer_inner_class_bodies(
764 db: &dyn Db,
765 api: &EngineApi,
766 root: &GdNode,
767 tree: &ItemTree,
768 file_id: FileId,
769 path_prefix: &str,
770 res_path: Option<&str>,
771 units: &mut Vec<Unit>,
772 diagnostics: &mut Vec<Diagnostic>,
773 raw_warnings: &mut Vec<RawWarning>,
774 depth: u32,
775) {
776 if depth > 16 {
777 return;
778 }
779 for m in &tree.members {
780 let Member::Class(c) = m else { continue };
781 let inner_path = if path_prefix.is_empty() {
782 c.name.to_string()
783 } else {
784 format!("{path_prefix}.{}", c.name)
785 };
786 let mut class = ClassScope::new(db, api, &c.tree, res_path);
787 class.self_ty = Ty::InnerClass(crate::ty::InnerClassRef {
788 file: file_id.0,
789 path: SmolStr::new(&inner_path),
790 });
791 for im in &c.tree.members {
792 let Member::Func(f) = im else { continue };
793 let Some(node) = f.ptr.to_node(root) else {
794 continue;
795 };
796 let body = body::body_of_func(&node);
797 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
798 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
799 let result = infer(db, api, root, &class, &body, return_ty, true, Some(&f.name));
800 diagnostics.extend(result.diagnostics.iter().cloned());
801 raw_warnings.extend(result.raw_warnings.iter().cloned());
802 units.push(Unit {
803 range: f.range,
804 body,
805 result,
806 });
807 }
808 infer_inner_class_bodies(
809 db,
810 api,
811 root,
812 &c.tree,
813 file_id,
814 &inner_path,
815 res_path,
816 units,
817 diagnostics,
818 raw_warnings,
819 depth + 1,
820 );
821 }
822}
823
824fn class_annotation_warnings(
828 db: &dyn Db,
829 api: &EngineApi,
830 root: &GdNode,
831 tree: &ItemTree,
832 res_path: Option<&str>,
833) -> Vec<RawWarning> {
834 let mut out = Vec::new();
835 if let Some(unload) = tree.annotations.iter().find(|a| a.name == "static_unload")
837 && !tree
838 .members
839 .iter()
840 .any(|m| matches!(m, Member::Var(v) if v.is_static))
841 {
842 out.push(RawWarning {
843 range: unload.range,
844 code: WarningCode::RedundantStaticUnload,
845 message: "`@static_unload` is redundant on a class with no static variables."
846 .to_owned(),
847 });
848 }
849 if !has_annotation(&tree.annotations, "tool")
852 && user_base_is_tool(db, api, tree, res_path)
853 && let Some(range) = extends_decl_range(root)
854 {
855 out.push(RawWarning {
856 range,
857 code: WarningCode::MissingTool,
858 message: "This class extends a `@tool` script but is not itself `@tool` (it will not run in the editor)."
859 .to_owned(),
860 });
861 }
862 out
863}
864
865#[allow(
870 clippy::too_many_lines,
871 reason = "a flat sequence of independent per-member warning checks; reads best as one walk"
872)]
873fn member_level_warnings(
874 db: &dyn Db,
875 api: &EngineApi,
876 root: &GdNode,
877 tree: &ItemTree,
878 res_path: Option<&str>,
879) -> Vec<RawWarning> {
880 let mut out = Vec::new();
881 let has_signal = tree.members.iter().any(|m| matches!(m, Member::Signal(_)));
882 let has_private_var = tree
884 .members
885 .iter()
886 .any(|m| matches!(m, Member::Var(v) if v.name.starts_with('_') && !v.is_exported));
887 let uses = (has_signal || has_private_var).then(|| NameUses::collect(root));
889 let engine_base = match resolve::resolve_base(db, api, tree, res_path) {
891 Ty::Object(c) => Some(c),
892 _ => None,
893 };
894
895 out.extend(class_annotation_warnings(db, api, root, tree, res_path));
896
897 for m in &tree.members {
898 if let Member::Var(v) = m
902 && v.name.starts_with('_')
903 && !v.is_exported
904 && let Some(uses) = &uses
905 && !uses.is_referenced(&v.name)
906 {
907 out.push(RawWarning {
908 range: v.name_range,
909 code: WarningCode::UnusedPrivateClassVariable,
910 message: format!(
911 "The class variable \"{}\" is declared but never used in the class.",
912 v.name
913 ),
914 });
915 }
916 if let Member::Var(v) = m
918 && has_annotation(&v.annotations, "onready")
919 && v.is_exported
920 {
921 out.push(RawWarning {
922 range: v.name_range,
923 code: WarningCode::OnreadyWithExport,
924 message: "\"@onready\" will set the default value after \"@export\" takes effect and will override it.".to_owned(),
925 });
926 }
927 if let Some((name, range, what)) = member_value_decl(m)
930 && let Some(kind) = shadowed_global_kind(db, api, name)
931 {
932 out.push(RawWarning {
933 range,
934 code: WarningCode::ShadowedGlobalIdentifier,
935 message: format!("The {what} \"{name}\" has the same name as a {kind}."),
936 });
937 }
938 if let Some((name, range)) = member_decl_name(m)
940 && is_confusable_identifier(name)
941 {
942 out.push(RawWarning {
943 range,
944 code: WarningCode::ConfusableIdentifier,
945 message: format!(
946 "The identifier \"{name}\" has misleading characters and might be confused with something else."
947 ),
948 });
949 }
950 match m {
951 Member::Var(v) if !v.has_init => {
956 if let Some(tref) = &v.type_ref
957 && let Ty::Enum(eref) = resolve::resolve_type_name(db, api, tref)
958 && enum_lacks_zero(api, &eref)
959 {
960 out.push(RawWarning {
961 range: v.name_range,
962 code: WarningCode::EnumVariableWithoutDefault,
963 message: format!(
964 "The variable \"{}\" has an enum type and does not set an explicit default value. The default will be set to \"0\".",
965 v.name
966 ),
967 });
968 }
969 }
970 Member::Signal(s) => {
974 if let Some(uses) = &uses
975 && !uses.is_referenced(&s.name)
976 {
977 out.push(RawWarning {
978 range: s.name_range,
979 code: WarningCode::UnusedSignal,
980 message: format!(
981 "The signal \"{}\" is declared but never explicitly used in the class.",
982 s.name
983 ),
984 });
985 }
986 }
987 Member::Func(f) => {
994 if let Some(base) = engine_base
995 && let Some(MemberRef::Method(vsig)) = api.lookup_member(base, &f.name)
996 && vsig.is_virtual
997 {
998 for (p, vp) in f.params.iter().zip(vsig.params.iter()) {
999 let Some(ann) = &p.type_ref else { continue };
1000 let pty = resolve::resolve_type_name(db, api, ann);
1001 let vty = ty::resolve_tyref(api, &vp.ty);
1002 if types_definitely_clash(api, &pty, &vty) {
1003 out.push(RawWarning {
1004 range: f.name_range,
1005 code: WarningCode::NativeMethodOverride,
1006 message: format!(
1007 "The override of the native virtual method \"{}\" has an incompatible type for parameter \"{}\".",
1008 f.name, p.name
1009 ),
1010 });
1011 break; }
1013 }
1014 }
1015 }
1016 _ => {}
1017 }
1018 }
1019 out
1020}
1021
1022fn types_definitely_clash(api: &EngineApi, a: &Ty, b: &Ty) -> bool {
1027 if a.is_uninformative() || b.is_uninformative() {
1028 return false;
1029 }
1030 if matches!(a, Ty::Enum(_)) || matches!(b, Ty::Enum(_)) {
1035 return false;
1036 }
1037 matches!(ty::is_assignable(api, a, b), Assign::No)
1038 && matches!(ty::is_assignable(api, b, a), Assign::No)
1039}
1040
1041struct NameUses {
1044 ident_counts: FxHashMap<SmolStr, u32>,
1045 strings: FxHashSet<SmolStr>,
1046}
1047
1048impl NameUses {
1049 fn collect(root: &GdNode) -> Self {
1050 let mut ident_counts: FxHashMap<SmolStr, u32> = FxHashMap::default();
1051 let mut strings: FxHashSet<SmolStr> = FxHashSet::default();
1052 for node in gdscript_syntax::ast::descendants(root) {
1053 for el in node.children_with_tokens() {
1054 let Some(tok) = el.into_token() else { continue };
1055 match tok.kind() {
1056 gdscript_syntax::SyntaxKind::Ident => {
1057 *ident_counts.entry(SmolStr::new(tok.text())).or_insert(0) += 1;
1058 }
1059 gdscript_syntax::SyntaxKind::String => {
1060 strings.insert(SmolStr::new(tok.text().trim_matches(['"', '\''])));
1061 }
1062 _ => {}
1063 }
1064 }
1065 }
1066 Self {
1067 ident_counts,
1068 strings,
1069 }
1070 }
1071
1072 fn is_referenced(&self, name: &str) -> bool {
1075 self.ident_counts.get(name).copied().unwrap_or(0) > 1 || self.strings.contains(name)
1076 }
1077}
1078
1079fn collisions_contains(db: &dyn Db, name: &SmolStr) -> bool {
1083 db.source_root()
1084 .is_some_and(|root| crate::queries::class_name_collisions(db, root).contains(name))
1085}
1086
1087fn is_autoload_singleton(db: &dyn Db, name: &str) -> bool {
1090 db.project_config().is_some_and(|config| {
1091 crate::queries::autoload_registry(db, config)
1092 .resolve_path(name)
1093 .is_some()
1094 })
1095}
1096
1097fn user_base_is_tool(
1101 db: &dyn Db,
1102 api: &EngineApi,
1103 tree: &ItemTree,
1104 res_path: Option<&str>,
1105) -> bool {
1106 let Ty::ScriptRef(sref) = resolve::resolve_base(db, api, tree, res_path) else {
1107 return false;
1108 };
1109 let Some(ft) = db.file_text(FileId(sref.0)) else {
1110 return false;
1111 };
1112 has_annotation(&crate::queries::item_tree(db, ft).annotations, "tool")
1113}
1114
1115fn is_registered_global_class(db: &dyn Db, name: &str) -> bool {
1119 db.source_root().is_some_and(|root| {
1120 crate::queries::global_registry(db, root)
1121 .resolve(name)
1122 .is_some()
1123 })
1124}
1125
1126fn enum_lacks_zero(api: &EngineApi, eref: &ty::EnumRef) -> bool {
1140 let info = match eref.qualified.split_once('.') {
1141 None => api.global_enum(&eref.qualified),
1142 Some((class, en)) => match resolve::resolve_global(api, class) {
1143 Some(GlobalDef::ClassType(cid)) => match api.lookup_member(cid, en) {
1144 Some(MemberRef::Enum(e)) => Some(e),
1145 _ => None,
1146 },
1147 _ => None,
1148 },
1149 };
1150 info.is_some_and(|e| !e.values.iter().any(|v| v.value == 0))
1151}
1152
1153fn member_name_range(m: &Member) -> Option<TextRange> {
1157 match m {
1158 Member::Var(v) => Some(v.name_range),
1159 Member::Const(c) => Some(c.name_range),
1160 Member::Signal(s) => Some(s.name_range),
1161 Member::Func(f) => Some(f.name_range),
1162 _ => None,
1163 }
1164}
1165
1166fn shadowed_global_kind(db: &dyn Db, api: &EngineApi, name: &str) -> Option<&'static str> {
1167 match resolve::resolve_global(api, name) {
1168 Some(GlobalDef::Builtin | GlobalDef::Utility) => return Some("built-in function"),
1169 Some(GlobalDef::BuiltinType(_)) => return Some("built-in type"),
1170 Some(GlobalDef::ClassType(_) | GlobalDef::Singleton(_)) => return Some("native class"),
1172 Some(GlobalDef::Const(_) | GlobalDef::GlobalEnum) | None => {}
1174 }
1175 if is_registered_global_class(db, name) {
1176 return Some("global class");
1177 }
1178 if is_autoload_singleton(db, name) {
1179 return Some("autoload");
1180 }
1181 None
1182}
1183
1184fn member_value_decl(m: &Member) -> Option<(&SmolStr, TextRange, &'static str)> {
1188 match m {
1189 Member::Var(v) => Some((&v.name, v.name_range, "variable")),
1190 Member::Const(c) => Some((&c.name, c.name_range, "constant")),
1191 Member::Signal(s) => Some((&s.name, s.name_range, "signal")),
1192 _ => None,
1193 }
1194}
1195
1196fn member_decl_name(m: &Member) -> Option<(&SmolStr, TextRange)> {
1200 match m {
1201 Member::Func(f) => Some((&f.name, f.name_range)),
1202 Member::Var(v) => Some((&v.name, v.name_range)),
1203 Member::Const(c) => Some((&c.name, c.name_range)),
1204 Member::Signal(s) => Some((&s.name, s.name_range)),
1205 Member::Class(c) => Some((&c.name, c.name_range)),
1206 Member::Enum(e) => e.name.as_ref().map(|n| (n, e.name_range)),
1207 }
1208}
1209
1210fn is_confusable_identifier(name: &str) -> bool {
1216 use unicode_security::RestrictionLevel as RL;
1217 use unicode_security::RestrictionLevelDetection;
1218 if name.is_ascii() {
1219 return false; }
1221 name.detect_restriction_level() >= RL::MinimallyRestrictive
1222}
1223
1224fn class_name_decl_range(root: &GdNode) -> Option<TextRange> {
1229 use gdscript_syntax::SyntaxKind;
1230 let decl = gdscript_syntax::ast::descendants(root)
1231 .into_iter()
1232 .find(|n| n.kind() == SyntaxKind::ClassNameDecl)?;
1233 let name_node = decl.children().find(|c| c.kind() == SyntaxKind::Name)?;
1234 let r = cst::text_range_of(name_node);
1235 let text = name_node.text().to_string();
1236 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
1237 let len = u32::try_from(text.trim().len()).unwrap_or(0);
1238 Some(TextRange::new(r.start + lead, r.start + lead + len))
1239}
1240
1241fn extends_decl_range(root: &GdNode) -> Option<TextRange> {
1248 use gdscript_syntax::SyntaxKind;
1249 for child in root.children() {
1250 match child.kind() {
1251 SyntaxKind::ExtendsClause => return Some(cst::text_range_of(child)),
1253 SyntaxKind::ClassNameDecl => {
1255 if let Some(kw) = child.children().find(|c| c.kind() == SyntaxKind::ExtendsKw) {
1256 let start = cst::text_range_of(kw).start;
1257 let end = cst::text_range_of(child).end;
1258 return Some(TextRange::new(start, end));
1259 }
1260 }
1261 _ => {}
1262 }
1263 }
1264 None
1265}
1266
1267fn extends_chain_is_cyclic(db: &dyn Db, start: FileId) -> bool {
1274 use std::collections::HashSet;
1275 let mut visited: HashSet<FileId> = HashSet::new();
1276 visited.insert(start);
1277 let mut current = start;
1278 for _ in 0..=64 {
1279 let Some(file) = db.file_text(current) else {
1280 return false;
1281 };
1282 let base = crate::queries::script_class(db, file).base().clone();
1283 let Ty::ScriptRef(next) = base else {
1284 return false; };
1286 let next_id = FileId(next.0);
1287 if !visited.insert(next_id) {
1288 return true;
1291 }
1292 current = next_id;
1293 }
1294 false
1295}
1296
1297fn unit_from_decl(
1299 db: &dyn Db,
1300 api: &EngineApi,
1301 root: &GdNode,
1302 class: &ClassScope,
1303 ptr: AstPtr,
1304 range: TextRange,
1305) -> Option<Unit> {
1306 let node = ptr.to_node(root)?;
1307 let body = body::body_of_decl_stmt(&node);
1308 let result = infer(db, api, root, class, &body, Ty::Variant, false, None);
1309 Some(Unit {
1310 range,
1311 body,
1312 result,
1313 })
1314}
1315
1316#[derive(Clone, Copy)]
1320enum AssignCtx {
1321 Assign,
1322 Return,
1323}
1324
1325struct CallSignature {
1328 name: SmolStr,
1329 params: Vec<Ty>,
1331 required: usize,
1333 is_vararg: bool,
1335}
1336
1337fn method_call_signature(api: &EngineApi, sig: &MethodSig) -> CallSignature {
1339 CallSignature {
1340 name: SmolStr::new(&sig.name),
1341 params: sig
1342 .params
1343 .iter()
1344 .map(|p| ty::resolve_tyref(api, &p.ty))
1345 .collect(),
1346 required: sig.params.iter().filter(|p| p.default.is_none()).count(),
1347 is_vararg: sig.is_vararg,
1348 }
1349}
1350
1351enum Expectation {
1353 None,
1355 Has(Ty),
1357}
1358
1359fn find_inner_class<'a>(tree: &'a ItemTree, path: &str) -> Option<&'a InnerClassItem> {
1362 let mut members: &'a [Member] = &tree.members;
1363 let mut found: Option<&'a InnerClassItem> = None;
1364 for seg in path.split('.') {
1365 found = members.iter().find_map(|m| match m {
1366 Member::Class(c) if c.name == seg => Some(c),
1367 _ => None,
1368 });
1369 members = &found?.tree.members;
1370 }
1371 found
1372}
1373
1374struct Cx<'a> {
1375 db: &'a dyn Db,
1376 api: &'a EngineApi,
1377 root: &'a GdNode,
1378 body: &'a Body,
1379 class: &'a ClassScope<'a>,
1380 self_ty: Ty,
1381 return_ty: Ty,
1382 expr_ty: FxHashMap<ExprId, Ty>,
1383 bindings: Vec<Binding>,
1384 diagnostics: Vec<Diagnostic>,
1385 raw_warnings: Vec<RawWarning>,
1387 locals: FxHashMap<SmolStr, Ty>,
1389 used_locals: FxHashSet<SmolStr>,
1394 narrowing: FxHashMap<String, Ty>,
1397 flow: FlowAnalysis,
1400 is_func_body: bool,
1404 owner: Option<&'a str>,
1409 assigned: flow::AssignedAnalysis,
1412 cur_stmt: Option<body::StmtId>,
1415 needs_assignment: FxHashSet<SmolStr>,
1420 assign_lhs: FxHashSet<ExprId>,
1423 rebound: FxHashSet<SmolStr>,
1427 escaping: FxHashSet<SmolStr>,
1430}
1431
1432impl Cx<'_> {
1433 fn builtin(&self, name: &str) -> Ty {
1436 self.api
1437 .builtin_by_name(name)
1438 .map_or(Ty::Variant, Ty::Builtin)
1439 }
1440 fn int_ty(&self) -> Ty {
1441 self.builtin("int")
1442 }
1443 fn float_ty(&self) -> Ty {
1444 self.builtin("float")
1445 }
1446 fn bool_ty(&self) -> Ty {
1447 self.builtin("bool")
1448 }
1449 fn is_int(&self, ty: &Ty) -> bool {
1450 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "int")
1451 }
1452 fn is_float(&self, ty: &Ty) -> bool {
1453 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "float")
1454 }
1455 fn is_numeric(&self, ty: &Ty) -> bool {
1456 self.is_int(ty) || self.is_float(ty)
1457 }
1458
1459 fn emit(&mut self, range: TextRange, severity: Severity, code: &str, message: String) {
1462 self.diagnostics.push(Diagnostic {
1463 range,
1464 severity,
1465 code: code.to_owned(),
1466 message,
1467 source: DiagnosticSource::Type,
1468 fixes: Vec::new(),
1469 tags: Vec::new(),
1470 });
1471 }
1472
1473 fn warn(&mut self, range: TextRange, code: WarningCode, message: String) {
1477 self.raw_warnings.push(RawWarning {
1478 range,
1479 code,
1480 message,
1481 });
1482 }
1483
1484 fn range_of(&self, id: ExprId) -> TextRange {
1485 self.body.source_map.expr_range(id)
1486 }
1487
1488 fn check_assign(&mut self, from: &Ty, to: &Ty, range: TextRange, ctx: AssignCtx) {
1495 match ty::is_assignable(self.api, from, to) {
1496 Assign::Narrowing => self.warn(
1497 range,
1498 WarningCode::NarrowingConversion,
1499 "Narrowing conversion (float is converted to int and loses precision).".to_owned(),
1500 ),
1501 Assign::No => {
1502 let to_label = to.label(self.api).unwrap_or_else(|| "?".to_owned());
1503 let from_label = from.label(self.api).unwrap_or_else(|| "?".to_owned());
1504 let verb = match ctx {
1505 AssignCtx::Assign => "assign",
1506 AssignCtx::Return => "return",
1507 };
1508 self.emit(
1509 range,
1510 Severity::Error,
1511 TYPE_MISMATCH,
1512 format!(
1513 "Cannot {verb} a value of type \"{from_label}\" as \"{to_label}\"."
1514 ),
1515 );
1516 }
1517 Assign::IntAsEnum => self.warn(
1519 range,
1520 WarningCode::IntAsEnumWithoutCast,
1521 "Integer used when an enum value is expected. If this is intended, cast the integer to the enum type using the \"as\" keyword."
1522 .to_owned(),
1523 ),
1524 Assign::Ok | Assign::OkUnsafe => {}
1525 }
1526 }
1527
1528 fn check_standalone(&mut self, e: ExprId) {
1532 if self.expr_has_side_effect(e) {
1533 return;
1534 }
1535 match self.body.expr(e) {
1536 Expr::Ternary { .. } => self.warn(
1537 self.range_of(e),
1538 WarningCode::StandaloneTernary,
1539 "Standalone ternary operator (the return value is being discarded).".to_owned(),
1540 ),
1541 Expr::Missing | Expr::Lambda { .. } | Expr::GetNode { .. } | Expr::Preload { .. } => {}
1543 _ => self.warn(
1544 self.range_of(e),
1545 WarningCode::StandaloneExpression,
1546 "Standalone expression (the line may have no effect).".to_owned(),
1547 ),
1548 }
1549 }
1550
1551 fn expr_has_side_effect(&self, e: ExprId) -> bool {
1554 match self.body.expr(e) {
1555 Expr::Call { .. }
1556 | Expr::Await(_)
1557 | Expr::Preload { .. }
1558 | Expr::Bin {
1559 op: BinOp::Assign, ..
1560 } => true,
1561 Expr::Bin { lhs, rhs, .. }
1562 | Expr::In { lhs, rhs, .. }
1563 | Expr::Index {
1564 base: lhs,
1565 index: rhs,
1566 } => self.expr_has_side_effect(*lhs) || self.expr_has_side_effect(*rhs),
1567 Expr::Unary { operand, .. }
1568 | Expr::Paren(operand)
1569 | Expr::Cast { operand, .. }
1570 | Expr::Is { operand, .. } => self.expr_has_side_effect(*operand),
1571 Expr::Field { receiver, .. } => self.expr_has_side_effect(*receiver),
1572 Expr::Ternary {
1573 cond,
1574 then_branch,
1575 else_branch,
1576 } => {
1577 self.expr_has_side_effect(*cond)
1578 || self.expr_has_side_effect(*then_branch)
1579 || self.expr_has_side_effect(*else_branch)
1580 }
1581 Expr::Array(items) => items.iter().any(|&i| self.expr_has_side_effect(i)),
1582 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
1583 self.expr_has_side_effect(*k) || v.is_some_and(|e| self.expr_has_side_effect(e))
1584 }),
1585 _ => false,
1586 }
1587 }
1588
1589 fn infer_block(&mut self, block: &[body::StmtId]) {
1592 for &stmt in block {
1593 self.infer_stmt(stmt);
1594 }
1595 }
1596
1597 fn infer_stmt(&mut self, id: body::StmtId) {
1598 self.narrowing = self.facts_to_narrowing(id);
1601 self.cur_stmt = Some(id); match self.body.stmt(id).clone() {
1603 Stmt::Expr(e) => {
1604 self.infer_expr(e, &Expectation::None);
1605 self.check_standalone(e);
1606 }
1607 Stmt::Var(v) => self.infer_local_var(&v),
1608 Stmt::Return(e) => {
1609 if let Some(e) = e {
1610 let expected = if self.return_ty.is_uninformative() {
1611 Expectation::None
1612 } else {
1613 Expectation::Has(self.return_ty.clone())
1614 };
1615 let t = self.infer_expr(e, &expected);
1616 if let Expectation::Has(ret) = expected {
1617 self.check_assign(&t, &ret, self.range_of(e), AssignCtx::Return);
1618 }
1619 }
1620 }
1621 Stmt::If {
1622 cond,
1623 then_branch,
1624 elifs,
1625 else_branch,
1626 } => {
1627 let at_if = self.narrowing.clone();
1631 self.infer_expr(cond, &Expectation::None);
1632 self.infer_block(&then_branch);
1633 for (econd, eblock) in elifs {
1634 self.narrowing.clone_from(&at_if);
1635 self.infer_expr(econd, &Expectation::None);
1636 self.infer_block(&eblock);
1637 }
1638 if let Some(eb) = else_branch {
1639 self.infer_block(&eb);
1640 }
1641 }
1642 Stmt::While { cond, body } => {
1643 self.infer_expr(cond, &Expectation::None);
1644 self.infer_block(&body);
1645 }
1646 Stmt::For(f) => {
1647 let iter_ty = self.infer_expr(f.iter, &Expectation::None);
1648 let var_ty = f.var_type.as_ref().map_or_else(
1649 || self.loop_var_ty(&iter_ty),
1650 |ptr| self.resolve_ptr_ty(*ptr),
1651 );
1652 self.bindings.push(Binding {
1653 name: f.var.clone(),
1654 name_range: f.var_range,
1655 ty: var_ty.clone(),
1656 init: None,
1657 annotated: f.var_type.is_some(),
1658 inferred_colon_eq: false,
1659 is_const: false,
1660 kind: BindingKind::ForVar,
1661 });
1662 self.locals.insert(f.var.clone(), var_ty);
1663 self.infer_block(&f.body);
1664 }
1665 Stmt::Match { scrutinee, arms } => {
1666 let at_match = self.narrowing.clone();
1667 self.infer_expr(scrutinee, &Expectation::None);
1668 for arm in arms {
1669 self.narrowing.clone_from(&at_match);
1672 for b in &arm.binds {
1673 self.bindings.push(Binding {
1677 name: b.name.clone(),
1678 name_range: b.range,
1679 ty: Ty::Variant,
1680 init: None,
1681 annotated: false,
1682 inferred_colon_eq: false,
1683 is_const: false,
1684 kind: BindingKind::MatchBind,
1685 });
1686 self.locals.insert(b.name.clone(), Ty::Variant);
1687 }
1688 if let Some(g) = arm.guard {
1689 self.infer_expr(g, &Expectation::None);
1690 }
1691 self.infer_block(&arm.body);
1692 }
1693 }
1694 Stmt::Break | Stmt::Continue | Stmt::Pass => {}
1695 Stmt::Assert(cond) => {
1696 if let Some(cond) = cond {
1697 self.infer_expr(cond, &Expectation::None);
1698 self.check_assert_constant(cond);
1699 }
1700 }
1701 }
1702 }
1703
1704 fn check_assert_constant(&mut self, cond: ExprId) {
1708 let Some(always) = self.const_bool_of(cond) else {
1709 return;
1710 };
1711 let (code, msg) = if always {
1712 (
1713 WarningCode::AssertAlwaysTrue,
1714 "The assert condition is always true, so this assert has no effect.",
1715 )
1716 } else {
1717 (
1718 WarningCode::AssertAlwaysFalse,
1719 "The assert condition is always false, so this assert will always fail.",
1720 )
1721 };
1722 self.warn(self.range_of(cond), code, msg.to_owned());
1723 }
1724
1725 fn const_bool_of(&self, expr: ExprId) -> Option<bool> {
1730 match self.body.expr(expr) {
1731 Expr::Literal(Literal::Bool(b)) => Some(*b),
1732 Expr::Literal(Literal::Null) => Some(false),
1733 _ => None,
1734 }
1735 }
1736
1737 fn infer_local_var(&mut self, v: &body::LocalVar) {
1738 let annotated = v.type_ref.map(|p| self.resolve_ptr_ty(p));
1739 self.locals.insert(v.name.clone(), Ty::Unknown);
1747 let init_ty = v.init.map(|e| {
1748 let expected = annotated
1749 .as_ref()
1750 .map_or(Expectation::None, |t| Expectation::Has(t.clone()));
1751 self.infer_expr(e, &expected)
1752 });
1753 let range = v.init.map_or(v.name_range, |e| self.range_of(e));
1754 let binding_ty = self.local_binding_ty(v, annotated.as_ref(), init_ty.as_ref(), range);
1755 let shadows_param = self
1760 .bindings
1761 .iter()
1762 .any(|b| b.kind == BindingKind::Param && b.name == v.name);
1763 let shadows_member = match self.class.lookup(&v.name) {
1766 Some(ClassItem::EnumVariant) => true,
1767 Some(item) => matches!(
1768 self.class.member(item),
1769 Some(Member::Var(_) | Member::Const(_) | Member::Signal(_))
1770 ),
1771 None => false,
1772 };
1773 if self.is_func_body {
1774 let what = if v.is_const { "constant" } else { "variable" };
1775 if shadowed_global_kind(self.db, self.api, &v.name).is_none() {
1780 if shadows_param {
1781 self.warn(
1786 v.name_range,
1787 WarningCode::ShadowedVariable,
1788 format!(
1789 "There is already a parameter named \"{}\" declared in this scope.",
1790 v.name
1791 ),
1792 );
1793 } else if shadows_member {
1794 let msg = match self
1798 .class
1799 .lookup(&v.name)
1800 .and_then(|item| self.class.member(item))
1801 .and_then(member_name_range)
1802 {
1803 Some(r) => format!(
1804 "The local {what} \"{}\" is shadowing an already-declared variable at line {} in the current class.",
1805 v.name,
1806 self.line_of(r)
1807 ),
1808 None => format!(
1809 "The local {what} \"{}\" is shadowing an already-declared variable in the current class.",
1810 v.name
1811 ),
1812 };
1813 self.warn(v.name_range, WarningCode::ShadowedVariable, msg);
1814 } else if let Some((kind, base)) = self.engine_base_value_member_kind(&v.name) {
1815 self.warn(
1818 v.name_range,
1819 WarningCode::ShadowedVariableBaseClass,
1820 format!(
1821 "The local {what} \"{}\" is shadowing an already-declared {kind} in the base class \"{base}\".",
1822 v.name
1823 ),
1824 );
1825 }
1826 }
1827 let enum_typed = matches!(annotated.as_ref(), Some(Ty::Enum(_)));
1833 if v.init.is_none() && (v.type_ref.is_none() || enum_typed) {
1834 self.needs_assignment.insert(v.name.clone());
1835 }
1836 }
1837 self.bindings.push(Binding {
1838 name: v.name.clone(),
1839 name_range: v.name_range,
1840 ty: binding_ty.clone(),
1841 init: v.init,
1842 annotated: v.type_ref.is_some(),
1843 inferred_colon_eq: v.is_inferred,
1844 is_const: v.is_const,
1845 kind: BindingKind::Var,
1846 });
1847 self.locals.insert(v.name.clone(), binding_ty);
1849 }
1850
1851 fn infer_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1854 let ty = self.synth_expr(id, expected);
1855 self.expr_ty.insert(id, ty.clone());
1856 ty
1857 }
1858
1859 #[allow(clippy::too_many_lines)]
1860 fn synth_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1861 match self.body.expr(id).clone() {
1862 Expr::Missing => Ty::Error,
1863 Expr::Literal(lit) => self.literal_ty(lit),
1864 Expr::Name(name) => self.resolve_name(id, &name),
1865 Expr::SelfExpr => self.self_ty.clone(),
1866 Expr::Super => self.class.base.clone(),
1867 Expr::Paren(inner) => self.infer_expr(inner, expected),
1868 Expr::Bin { op, lhs, rhs } => self.infer_bin(id, op, lhs, rhs),
1869 Expr::Unary { op, operand } => {
1870 let t = self.infer_expr(operand, &Expectation::None);
1871 match op {
1872 UnOp::Not => self.bool_ty(),
1873 UnOp::BitNot => self.int_ty(),
1874 UnOp::Neg | UnOp::Pos => {
1875 if t.is_uninformative() || self.is_numeric(&t) {
1876 t
1877 } else {
1878 Ty::Variant
1879 }
1880 }
1881 }
1882 }
1883 Expr::Ternary {
1884 cond,
1885 then_branch,
1886 else_branch,
1887 } => {
1888 self.infer_expr(cond, &Expectation::None);
1889 let a = self.infer_expr(then_branch, expected);
1890 let b = self.infer_expr(else_branch, expected);
1891 if self.is_null(else_branch) {
1893 a
1894 } else if self.is_null(then_branch) {
1895 b
1896 } else {
1897 let r = self.join(&a, &b);
1898 if r.is_variant() && !a.is_uninformative() && !b.is_uninformative() {
1901 self.warn(
1902 self.range_of(id),
1903 WarningCode::IncompatibleTernary,
1904 "The values of the ternary conditional are not mutually compatible."
1905 .to_owned(),
1906 );
1907 }
1908 r
1909 }
1910 }
1911 Expr::Call { callee, args } => self.infer_call(callee, &args),
1912 Expr::Field {
1913 receiver,
1914 name,
1915 name_range,
1916 } => {
1917 self.infer_field(receiver, &name, name_range, false)
1918 }
1919 Expr::Index { base, index } => {
1920 let base_ty = self.infer_expr(base, &Expectation::None);
1921 self.infer_expr(index, &Expectation::None);
1922 self.index_ty(&base_ty, index)
1923 }
1924 Expr::Is { operand, .. } => {
1925 self.infer_expr(operand, &Expectation::None);
1926 self.bool_ty()
1927 }
1928 Expr::Cast { operand, ty } => {
1929 self.infer_expr(operand, &Expectation::None);
1930 ty.map_or(Ty::Variant, |p| self.resolve_ptr_ty(p))
1931 }
1932 Expr::In { lhs, rhs, .. } => {
1933 self.infer_expr(lhs, &Expectation::None);
1934 self.infer_expr(rhs, &Expectation::None);
1935 self.bool_ty()
1936 }
1937 Expr::Await(operand) => {
1938 let operand_ty = self.infer_expr(operand, &Expectation::None);
1939 if matches!(operand_ty, Ty::Signal(_)) {
1944 Ty::Unknown
1945 } else {
1946 operand_ty
1947 }
1948 }
1949 Expr::Array(elems) => {
1950 let pushed = match expected {
1954 Expectation::Has(Ty::Array(e)) => Some((**e).clone()),
1955 _ => None,
1956 };
1957 let elem_exp = pushed.clone().map_or(Expectation::None, Expectation::Has);
1958 for e in elems {
1959 self.infer_expr(e, &elem_exp);
1960 }
1961 pushed.map_or_else(Ty::array_of_variant, |e| Ty::Array(Box::new(e)))
1962 }
1963 Expr::Dict(entries) => {
1964 let pushed = match expected {
1965 Expectation::Has(Ty::Dict(k, v)) => Some(((**k).clone(), (**v).clone())),
1966 _ => None,
1967 };
1968 let (kx, vx) = pushed
1969 .clone()
1970 .map_or((Expectation::None, Expectation::None), |(k, v)| {
1971 (Expectation::Has(k), Expectation::Has(v))
1972 });
1973 for (k, v) in entries {
1974 self.infer_expr(k, &kx);
1975 if let Some(v) = v {
1976 self.infer_expr(v, &vx);
1977 }
1978 }
1979 pushed.map_or_else(Ty::dict_of_variant, |(k, v)| {
1980 Ty::Dict(Box::new(k), Box::new(v))
1981 })
1982 }
1983 Expr::Lambda { params, body } => {
1984 self.infer_lambda(¶ms, &body);
1985 Ty::Callable
1986 }
1987 Expr::Preload { arg, path } => {
1988 if let Some(arg) = arg {
1989 self.infer_expr(arg, &Expectation::None);
1990 }
1991 match path {
1996 Some(p) => {
2000 match resolve::anchor_res_path(self.self_res_path().as_deref(), &p) {
2001 Some(abs) => resolve::resolve_external(
2002 self.db,
2003 &resolve::ExternalRef::Preload(abs),
2004 ),
2005 None => Ty::Unknown,
2006 }
2007 }
2008 None => Ty::Unknown,
2009 }
2010 }
2011 Expr::GetNode { path, unique } => self.resolve_node_path(id, path.as_deref(), unique),
2014 }
2015 }
2016
2017 fn is_null(&self, id: ExprId) -> bool {
2019 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
2020 }
2021
2022 fn literal_ty(&self, lit: Literal) -> Ty {
2023 match lit {
2024 Literal::Int(_) => self.int_ty(),
2025 Literal::Float | Literal::MathConst => self.float_ty(),
2026 Literal::Bool(_) => self.bool_ty(),
2027 Literal::Str => self.builtin("String"),
2028 Literal::StringName => self.builtin("StringName"),
2029 Literal::NodePath => self.builtin("NodePath"),
2030 Literal::Null => Ty::Variant,
2032 }
2033 }
2034
2035 fn node_ty(&self) -> Ty {
2036 self.api
2037 .class_by_name("Node")
2038 .map_or(Ty::Unknown, Ty::Object)
2039 }
2040
2041 fn resolve_node_path(&mut self, id: ExprId, path: Option<&str>, unique: bool) -> Ty {
2049 use gdscript_scene::NodePathResolution as R;
2050 let fallback = self.node_ty();
2051 let Some(path) = path else {
2052 return fallback; };
2054 if !unique && let Some(ty) = self.resolve_root_autoload_path(path) {
2058 return ty;
2059 }
2060 let Some(ctx) = self.owning_scene() else {
2061 return fallback; };
2063 if ctx.ambiguous {
2068 return self.union_node_ty(path, unique).unwrap_or(fallback);
2069 }
2070 let resolution = if unique {
2071 ctx.model.classify_unique(path)
2072 } else {
2073 ctx.model.classify_path_from(ctx.attach, path)
2074 };
2075 match resolution {
2076 R::Resolved(idx) => ctx
2077 .model
2078 .node(idx)
2079 .and_then(|n| self.scene_node_ty(&ctx.model, n, 0))
2080 .unwrap_or(fallback),
2081 R::Missing => {
2082 let what = if unique { "unique name" } else { "node path" };
2083 let sigil = if unique { "%" } else { "$" };
2084 self.emit(
2085 self.range_of(id),
2086 Severity::Warning,
2087 INVALID_NODE_PATH,
2088 format!("no {what} `{sigil}{path}` in the owning scene"),
2089 );
2090 fallback
2091 }
2092 R::IntoInstance => {
2095 let walked = if unique {
2096 ctx.model.resolve_unique_into_instance(path)
2097 } else {
2098 ctx.model.resolve_into_instance(ctx.attach, path)
2099 };
2100 walked
2101 .and_then(|(inst, tail)| {
2102 let inst_node = ctx.model.node(inst)?;
2103 self.resolve_into_instance_ty(&ctx.model, inst_node, &tail, 0)
2104 })
2105 .unwrap_or(fallback)
2106 }
2107 R::Escaped => fallback,
2109 }
2110 }
2111
2112 fn union_node_ty(&self, path: &str, unique: bool) -> Option<Ty> {
2117 use gdscript_scene::NodePathResolution as R;
2118 let res_path = self.self_res_path()?;
2119 let root = self.db.source_root()?;
2120 let attaches = crate::queries::script_scene_attachments(self.db, root)
2121 .get(res_path.as_str())
2122 .cloned()?;
2123 let mut acc: Option<Ty> = None;
2124 for (scene_file, attach) in &attaches {
2125 let ft = self.db.file_text(*scene_file)?;
2126 let model = crate::queries::scene_model(self.db, ft);
2127 let resolution = if unique {
2128 model.classify_unique(path)
2129 } else {
2130 model.classify_path_from(*attach, path)
2131 };
2132 let R::Resolved(idx) = resolution else {
2133 return None; };
2135 let ty = model
2136 .node(idx)
2137 .and_then(|n| self.scene_node_ty(&model, n, 0))?;
2138 acc = Some(match acc {
2139 None => ty,
2140 Some(prev) => self.common_base(&prev, &ty),
2141 });
2142 }
2143 acc
2144 }
2145
2146 fn common_base(&self, a: &Ty, b: &Ty) -> Ty {
2151 if a == b {
2152 return a.clone();
2153 }
2154 if let (Ty::Object(ca), Ty::Object(cb)) = (a, b) {
2155 let mut cur = Some(*ca);
2156 while let Some(c) = cur {
2157 if self.api.is_subclass(*cb, c) {
2158 return Ty::Object(c);
2159 }
2160 cur = self.api.class(c).base;
2161 }
2162 }
2163 self.node_ty()
2164 }
2165
2166 fn resolve_root_autoload_path(&self, path: &str) -> Option<Ty> {
2171 let name = path.strip_prefix("/root/")?;
2172 if name.is_empty() || name.contains('/') {
2174 return None;
2175 }
2176 let ty = resolve::resolve_autoload_any(self.db, name);
2177 (!ty.is_uninformative()).then_some(ty)
2178 }
2179
2180 fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
2184 let Ty::ScriptRef(sref) = &self.self_ty else {
2185 return None;
2186 };
2187 let ft = self.db.file_text(FileId(sref.0))?;
2188 crate::queries::scene_context(self.db, ft)
2189 }
2190
2191 fn self_res_path(&self) -> Option<SmolStr> {
2194 let Ty::ScriptRef(sref) = &self.self_ty else {
2195 return None;
2196 };
2197 self.db.file_text(FileId(sref.0))?.res_path(self.db)
2198 }
2199
2200 fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
2205 if let Some(script_ty) = self.node_script_ref(scene, node) {
2206 return Some(script_ty);
2207 }
2208 if let Some(decl) = node.decl_type.as_ref() {
2209 let ty = resolve::resolve_type_name(self.db, self.api, decl);
2210 if !ty.is_uninformative() {
2211 return Some(ty);
2212 }
2213 }
2214 self.instance_root_ty(scene, node, depth)
2215 .or_else(|| self.override_child_ty(scene, node, depth))
2216 }
2217
2218 fn override_child_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
2226 if depth >= 16 {
2227 return None;
2228 }
2229 let mut segs_rev: Vec<String> = vec![node.name.to_string()];
2230 let mut parent_idx = node.parent_idx?;
2231 let mut guard = 0u32;
2232 loop {
2233 let parent = scene.node(parent_idx)?;
2234 if parent.instance.is_some() {
2235 segs_rev.reverse();
2236 let rel = segs_rev.join("/");
2237 return self.resolve_into_instance_ty(scene, parent, &rel, depth + 1);
2238 }
2239 segs_rev.push(parent.name.to_string());
2240 parent_idx = parent.parent_idx?;
2241 guard += 1;
2242 if guard > 4096 {
2243 return None;
2244 }
2245 }
2246 }
2247
2248 fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
2253 if depth >= 16 {
2254 return None;
2255 }
2256 let (sub, sub_root) = self.instance_subscene(scene, node)?;
2257 let root_node = sub.node(sub_root)?;
2258 self.scene_node_ty(&sub, root_node, depth + 1)
2259 }
2260
2261 fn instance_subscene(
2266 &self,
2267 scene: &SceneModel,
2268 node: &SceneNode,
2269 ) -> Option<(Arc<SceneModel>, gdscript_scene::NodeIdx)> {
2270 let inst = node.instance.as_ref()?;
2271 let path = scene.ext_resources.get(inst)?.path.as_ref()?;
2272 let root = self.db.source_root()?;
2273 let file = crate::queries::res_path_registry(self.db, root)
2274 .get(path.as_str())
2275 .copied()?;
2276 let ft = self.db.file_text(file)?;
2277 let sub = crate::queries::scene_model(self.db, ft);
2278 let sub_root = sub.root?;
2279 Some((sub, sub_root))
2280 }
2281
2282 fn resolve_into_instance_ty(
2287 &self,
2288 scene: &SceneModel,
2289 instance_node: &SceneNode,
2290 tail: &str,
2291 depth: u32,
2292 ) -> Option<Ty> {
2293 if depth >= 16 {
2294 return None;
2295 }
2296 let (sub, sub_root) = self.instance_subscene(scene, instance_node)?;
2297 if let Some(idx) = sub.resolve_path_from(sub_root, tail) {
2298 let n = sub.node(idx)?;
2299 return self.scene_node_ty(&sub, n, depth + 1);
2300 }
2301 let (inner, inner_tail) = sub.resolve_into_instance(sub_root, tail)?;
2303 let inner_node = sub.node(inner)?;
2304 self.resolve_into_instance_ty(&sub, inner_node, &inner_tail, depth + 1)
2305 }
2306
2307 fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
2310 let path = scene
2311 .ext_resources
2312 .get(node.script.as_ref()?)?
2313 .path
2314 .as_ref()?;
2315 let root = self.db.source_root()?;
2316 let file = crate::queries::res_path_registry(self.db, root)
2317 .get(path.as_str())
2318 .copied()?;
2319 Some(Ty::ScriptRef(ScriptRefId(file.0)))
2320 }
2321
2322 fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
2323 if op == BinOp::Assign {
2324 return self.infer_assign(lhs, rhs);
2325 }
2326 if matches!(op, BinOp::And | BinOp::Or) {
2329 self.infer_expr(lhs, &Expectation::None);
2330 let saved = self.narrowing.clone();
2331 self.apply_condition_facts(lhs, op == BinOp::And);
2332 self.infer_expr(rhs, &Expectation::None);
2333 self.narrowing = saved;
2334 return self.bool_ty();
2335 }
2336 let lt = self.infer_expr(lhs, &Expectation::None);
2337 let rt = self.infer_expr(rhs, &Expectation::None);
2338 if op.is_boolean() {
2339 return self.bool_ty();
2340 }
2341 if op == BinOp::Div && self.is_int(<) && self.is_int(&rt) {
2343 self.warn(
2344 self.range_of(id),
2345 WarningCode::IntegerDivision,
2346 "Integer division. Decimal part will be discarded.".to_owned(),
2347 );
2348 return self.int_ty();
2349 }
2350 self.bin_result(op, <, &rt)
2351 }
2352
2353 fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
2354 let slot = self.infer_expr(lhs, &Expectation::None);
2355 let expected = if slot.is_uninformative() {
2356 Expectation::None
2357 } else {
2358 Expectation::Has(slot.clone())
2359 };
2360 let value = self.infer_expr(rhs, &expected);
2361 if !slot.is_uninformative() {
2362 self.check_assign(&value, &slot, self.range_of(rhs), AssignCtx::Assign);
2363 }
2364 slot
2367 }
2368
2369 fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
2372 if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
2373 for o in self.api.builtin_operators(*b) {
2374 if o.op == sym
2375 && let Some(right) = &o.right
2376 && self.tyref_matches(right, rt)
2377 {
2378 return ty::resolve_tyref(self.api, &o.result);
2379 }
2380 }
2381 }
2382 if self.is_numeric(lt) && self.is_numeric(rt) {
2383 return if self.is_float(lt) || self.is_float(rt) {
2384 self.float_ty()
2385 } else {
2386 self.int_ty()
2387 };
2388 }
2389 if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
2392 return Ty::Unknown;
2393 }
2394 Ty::Variant
2395 }
2396
2397 fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
2398 let resolved = ty::resolve_tyref(self.api, tyref);
2399 resolved.is_variant() || &resolved == ty
2400 }
2401
2402 fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
2403 for &a in args {
2405 self.infer_expr(a, &Expectation::None);
2406 }
2407 let ret = match self.body.expr(callee).clone() {
2408 Expr::Field {
2409 receiver,
2410 name,
2411 name_range,
2412 } => {
2413 self.infer_field(receiver, &name, name_range, true)
2414 }
2415 Expr::Name(name) => {
2416 let ret = if self.locals.contains_key(name.as_str()) {
2426 self.resolve_name(callee, &name);
2427 Ty::Unknown
2428 } else if let Some(t) = self.resolve_call_name(&name) {
2429 t
2430 } else {
2431 self.maybe_warn_undefined_call(callee, &name);
2435 Ty::Unknown
2436 };
2437 self.expr_ty.insert(callee, Ty::Callable);
2438 ret
2439 }
2440 _ => {
2444 self.infer_expr(callee, &Expectation::None);
2445 Ty::Unknown
2446 }
2447 };
2448 self.check_call_args(callee, args);
2451 ret
2452 }
2453
2454 fn check_call_args(&mut self, callee: ExprId, args: &[ExprId]) {
2461 let Some(sig) = self.call_signature(callee) else {
2462 return;
2463 };
2464 if args.len() < sig.required {
2465 self.warn(
2466 self.range_of(callee),
2467 WarningCode::TooFewArguments,
2468 format!(
2469 "Too few arguments for \"{}()\" call. Expected at least {} but received {}.",
2470 sig.name,
2471 sig.required,
2472 args.len()
2473 ),
2474 );
2475 } else if !sig.is_vararg && args.len() > sig.params.len() {
2476 self.warn(
2477 self.range_of(callee),
2478 WarningCode::TooManyArguments,
2479 format!(
2480 "Too many arguments for \"{}()\" call. Expected at most {} but received {}.",
2481 sig.name,
2482 sig.params.len(),
2483 args.len()
2484 ),
2485 );
2486 }
2487 for (i, &arg) in args.iter().enumerate() {
2488 let Some(param_ty) = sig.params.get(i) else {
2489 break; };
2491 if param_ty.is_uninformative() || param_ty.is_variant() {
2492 continue; }
2494 let arg_ty = self.expr_ty.get(&arg).cloned().unwrap_or(Ty::Unknown);
2496 let pl = param_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
2497 let al = arg_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
2498 match ty::is_assignable(self.api, &arg_ty, param_ty) {
2499 Assign::No => self.emit(
2503 self.range_of(arg),
2504 Severity::Error,
2505 TYPE_MISMATCH,
2506 format!(
2507 "Invalid argument for \"{}()\" function: argument {} should be \"{pl}\" but is \"{al}\".",
2508 sig.name,
2509 i + 1
2510 ),
2511 ),
2512 Assign::OkUnsafe => self.warn(
2513 self.range_of(arg),
2514 WarningCode::UnsafeCallArgument,
2515 format!(
2516 "The argument {} of the function \"{}()\" requires the subtype \"{pl}\" but the supertype \"{al}\" was provided.",
2517 i + 1,
2518 sig.name
2519 ),
2520 ),
2521 Assign::Ok | Assign::Narrowing | Assign::IntAsEnum => {}
2524 }
2525 }
2526 }
2527
2528 fn call_signature(&self, callee: ExprId) -> Option<CallSignature> {
2533 match self.body.expr(callee) {
2534 Expr::Name(name) => self.name_call_signature(name),
2535 Expr::Field { receiver, name, .. } => match self.expr_ty.get(receiver)? {
2536 Ty::Object(class) => match self.api.lookup_member(*class, name)? {
2537 MemberRef::Method(sig) => Some(method_call_signature(self.api, sig)),
2538 _ => None,
2539 },
2540 recv => {
2544 let bid = self.builtin_id_of(recv)?;
2545 let sig = self.api.builtin_method(bid, name)?;
2546 Some(method_call_signature(self.api, sig))
2547 }
2548 },
2549 _ => None,
2550 }
2551 }
2552
2553 fn name_call_signature(&self, name: &str) -> Option<CallSignature> {
2557 if self.locals.contains_key(name) {
2561 return None;
2562 }
2563 if let Some(item) = self.class.lookup(name)
2564 && let Some(Member::Func(f)) = self.class.member(item)
2565 {
2566 return Some(CallSignature {
2567 name: f.name.clone(),
2568 params: f
2569 .params
2570 .iter()
2571 .map(|p| {
2572 p.type_ref.as_deref().map_or(Ty::Variant, |t| {
2573 resolve::resolve_type_name(self.db, self.api, t)
2574 })
2575 })
2576 .collect(),
2577 required: f.params.iter().filter(|p| !p.has_default).count(),
2578 is_vararg: f.is_vararg,
2581 });
2582 }
2583 if let Ty::Object(base) = self.class.base
2584 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
2585 {
2586 return Some(method_call_signature(self.api, sig));
2587 }
2588 if let Some(u) = self.api.utility(name) {
2591 return Some(CallSignature {
2592 name: SmolStr::new(&u.name),
2593 params: u
2594 .params
2595 .iter()
2596 .map(|p| ty::resolve_tyref(self.api, &p.ty))
2597 .collect(),
2598 required: u.params.iter().filter(|p| p.default.is_none()).count(),
2599 is_vararg: u.is_vararg,
2600 });
2601 }
2602 if let Some(f) = self.api.gdscript_builtin(name) {
2607 let fixed = usize::from(f.max_args.unwrap_or(f.min_args));
2608 return Some(CallSignature {
2609 name: SmolStr::new(f.name),
2610 params: vec![Ty::Variant; fixed],
2611 required: usize::from(f.min_args),
2612 is_vararg: f.max_args.is_none(),
2613 });
2614 }
2615 None
2616 }
2617
2618 fn resolve_call_name(&self, name: &str) -> Option<Ty> {
2625 if let Some(item) = self.class.lookup(name)
2626 && let Some(Member::Func(f)) = self.class.member(item)
2627 {
2628 return Some(self.func_call_return_ty(f));
2629 }
2630 if let Ty::Object(base) = self.class.base
2632 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
2633 {
2634 return Some(ty::resolve_tyref(self.api, &sig.return_ty));
2635 }
2636 if let Some(u) = self.api.utility(name) {
2637 return Some(ty::resolve_tyref(self.api, &u.return_ty));
2638 }
2639 if let Some(f) = self.api.gdscript_builtin(name) {
2640 return Some(resolve::layer_to_ty(self.api, f.ret));
2641 }
2642 if let Some(b) = self.api.builtin_by_name(name) {
2646 return Some(ty::resolve_tyref(self.api, &TyRef::Builtin(b)));
2647 }
2648 None
2651 }
2652
2653 fn undefined_symbols_provable(&self) -> bool {
2665 matches!(self.self_ty, Ty::ScriptRef(_))
2666 && matches!(self.class.base, Ty::Object(_))
2667 && self.db.source_root().is_some_and(|r| r.complete(self.db))
2668 && crate::queries::project_engine_version(self.db)
2669 .is_none_or(|v| v <= crate::warnings::bundled_version())
2670 }
2671
2672 fn name_token_range(&self, id: ExprId, name: &str) -> TextRange {
2677 let r = self.range_of(id);
2678 let len = u32::try_from(name.len()).unwrap_or(0);
2679 TextRange::new(r.end.saturating_sub(len).max(r.start), r.end)
2680 }
2681
2682 fn maybe_warn_undefined_call(&mut self, callee: ExprId, name: &str) {
2690 if !self.undefined_symbols_provable() || self.class.lookup(name).is_some() {
2691 return;
2692 }
2693 if let Ty::Object(base) = self.class.base
2694 && self.api.lookup_member(base, name).is_some()
2695 {
2696 return;
2697 }
2698 if resolve::resolve_global(self.api, name).is_some()
2699 || is_registered_global_class(self.db, name)
2700 || is_autoload_singleton(self.db, name)
2701 {
2702 return;
2703 }
2704 self.warn(
2705 self.name_token_range(callee, name),
2706 WarningCode::UndefinedFunction,
2707 format!("Function \"{name}()\" not found in base self."),
2710 );
2711 }
2712
2713 fn maybe_warn_undefined_identifier(&mut self, id: ExprId, name: &str) {
2718 if !self.undefined_symbols_provable() {
2719 return;
2720 }
2721 if is_autoload_singleton(self.db, name) {
2725 return;
2726 }
2727 self.warn(
2728 self.name_token_range(id, name),
2729 WarningCode::UndefinedIdentifier,
2730 format!("Identifier \"{name}\" not declared in the current scope."),
2732 );
2733 }
2734
2735 fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
2736 annotation.map_or(Ty::Variant, |t| {
2737 resolve::resolve_type_name(self.db, self.api, t)
2738 })
2739 }
2740
2741 fn func_call_return_ty(&self, f: &crate::item_tree::FuncItem) -> Ty {
2745 if let Some(names) = &f.tuple_return {
2746 return resolve::resolve_tuple_return(self.db, self.api, names);
2747 }
2748 self.func_return_ty(f.return_type.as_deref())
2749 }
2750
2751 fn infer_field(
2755 &mut self,
2756 receiver: ExprId,
2757 name: &str,
2758 name_range: TextRange,
2759 as_method: bool,
2760 ) -> Ty {
2761 let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
2762 let recv_ty = self.infer_expr(receiver, &Expectation::None);
2763
2764 if is_self && let Some(item) = self.class.lookup(name) {
2766 return self.own_member_ty(item, as_method);
2767 }
2768
2769 match &recv_ty {
2770 t if t.is_uninformative() => recv_ty.clone(),
2775 Ty::Object(class) => {
2776 if name == "new" {
2777 recv_ty.clone()
2780 } else if let Some(m) = self.api.lookup_member(*class, name) {
2781 self.check_member_kind_misuse(&m, as_method, name, name_range);
2785 self.check_static_on_instance(receiver, &m, as_method, name_range);
2786 self.member_ref_ty(&m, as_method)
2787 } else {
2788 self.emit_unsafe(name, &recv_ty, name_range, as_method);
2790 Ty::Variant
2791 }
2792 }
2793 Ty::Builtin(_)
2795 | Ty::Array(_)
2796 | Ty::Tuple(_)
2797 | Ty::Dict(..)
2798 | Ty::Callable
2799 | Ty::Signal(_) => self.builtin_member_ty(&recv_ty, name, name_range, as_method),
2800 Ty::Enum(er) => Ty::Enum(er.clone()),
2804 Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
2806 Ty::InnerClass(iref) => self.inner_class_member_ty(iref, name, as_method),
2808 _ => Ty::Variant,
2809 }
2810 }
2811
2812 fn inner_class_member_ty(
2818 &self,
2819 iref: &crate::ty::InnerClassRef,
2820 name: &str,
2821 as_method: bool,
2822 ) -> Ty {
2823 if name == "new" && as_method {
2824 return Ty::InnerClass(iref.clone());
2825 }
2826 self.inner_member_walk(iref, name, as_method, 0)
2827 .unwrap_or(Ty::Unknown)
2828 }
2829
2830 fn inner_member_walk(
2833 &self,
2834 iref: &crate::ty::InnerClassRef,
2835 name: &str,
2836 as_method: bool,
2837 depth: u32,
2838 ) -> Option<Ty> {
2839 if depth > 32 {
2840 return None;
2841 }
2842 let ft = self.db.file_text(FileId(iref.file))?;
2843 let tree = crate::queries::item_tree(self.db, ft);
2844 let inner = find_inner_class(&tree, &iref.path)?;
2845 if let Some(m) = inner.tree.member(name) {
2846 return self.inner_member_item_ty(m, as_method, iref);
2847 }
2848 let res_path = self.self_res_path();
2850 match resolve::resolve_base(self.db, self.api, &inner.tree, res_path.as_deref()) {
2851 Ty::Object(class) => self
2852 .api
2853 .lookup_member(class, name)
2854 .map(|m| self.member_ref_ty(&m, as_method)),
2855 Ty::ScriptRef(base) => self.script_member_walk(base, name, as_method, depth + 1),
2856 Ty::InnerClass(base) => self.inner_member_walk(&base, name, as_method, depth + 1),
2857 _ => None,
2858 }
2859 }
2860
2861 fn inner_member_item_ty(
2864 &self,
2865 m: &Member,
2866 as_method: bool,
2867 iref: &crate::ty::InnerClassRef,
2868 ) -> Option<Ty> {
2869 Some(match m {
2870 Member::Func(f) => {
2871 if as_method {
2872 f.return_type.as_deref().map_or(Ty::Variant, |t| {
2873 resolve::resolve_type_name(self.db, self.api, t)
2874 })
2875 } else {
2876 Ty::Callable
2877 }
2878 }
2879 Member::Var(v) => resolve::resolve_type_name(self.db, self.api, v.type_ref.as_deref()?),
2880 Member::Const(c) => {
2881 resolve::resolve_type_name(self.db, self.api, c.type_ref.as_deref()?)
2882 }
2883 Member::Signal(_) => Ty::Signal(None),
2884 Member::Enum(e) => Ty::Enum(EnumRef {
2885 qualified: e.name.clone()?,
2886 bitfield: false,
2887 }),
2888 Member::Class(c) => Ty::InnerClass(crate::ty::InnerClassRef {
2890 file: iref.file,
2891 path: SmolStr::new(format!("{}.{}", iref.path, c.name)),
2892 }),
2893 })
2894 }
2895
2896 fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
2901 if name == "new" {
2902 return Ty::ScriptRef(sref);
2903 }
2904 self.script_member_walk(sref, name, as_method, 0)
2905 .unwrap_or(Ty::Unknown)
2906 }
2907
2908 fn script_member_walk(
2912 &self,
2913 sref: ScriptRefId,
2914 name: &str,
2915 as_method: bool,
2916 depth: u32,
2917 ) -> Option<Ty> {
2918 if depth > 32 {
2919 return None;
2920 }
2921 let file = self.db.file_text(FileId(sref.0))?;
2922 let sc = crate::queries::script_class(self.db, file);
2923 if let Some(m) = sc.member(name) {
2924 return Some(match m {
2925 crate::queries::MemberSig::Method(ret) => {
2926 if as_method {
2927 ret.clone()
2928 } else {
2929 Ty::Callable
2930 }
2931 }
2932 crate::queries::MemberSig::Field(t) => t.clone(),
2933 crate::queries::MemberSig::Signal => Ty::Signal(None),
2934 });
2935 }
2936 match sc.base() {
2938 Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
2939 Ty::Object(class) => self
2940 .api
2941 .lookup_member(*class, name)
2942 .map(|m| self.member_ref_ty(&m, as_method)),
2943 _ => None,
2944 }
2945 }
2946
2947 fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
2952 match (sub, sup) {
2953 (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
2954 (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
2955 (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
2956 _ => false,
2957 }
2958 }
2959
2960 fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
2963 if depth > 32 {
2964 return false;
2965 }
2966 if sub == sup {
2967 return true;
2968 }
2969 let Some(file) = self.db.file_text(FileId(sub.0)) else {
2970 return false;
2971 };
2972 match crate::queries::script_class(self.db, file).base() {
2973 Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
2974 _ => false,
2975 }
2976 }
2977
2978 fn script_extends_engine(
2980 &self,
2981 sub: ScriptRefId,
2982 sup_native: gdscript_api::ClassId,
2983 depth: u32,
2984 ) -> bool {
2985 if depth > 32 {
2986 return false;
2987 }
2988 let Some(file) = self.db.file_text(FileId(sub.0)) else {
2989 return false;
2990 };
2991 match crate::queries::script_class(self.db, file).base() {
2992 Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
2993 Ty::Object(native) => self.api.is_subclass(*native, sup_native),
2994 _ => false,
2995 }
2996 }
2997
2998 fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
2999 let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
3000 let (code, message) = if as_method {
3001 (
3002 WarningCode::UnsafeMethodAccess,
3003 format!(
3004 "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
3005 ),
3006 )
3007 } else {
3008 (
3009 WarningCode::UnsafePropertyAccess,
3010 format!(
3011 "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
3012 ),
3013 )
3014 };
3015 self.warn(range, code, message);
3016 }
3017
3018 fn engine_base_value_member_kind(&self, name: &str) -> Option<(&'static str, String)> {
3028 let Ty::Object(base) = &self.class.base else {
3029 return None;
3030 };
3031 let kind = match self.api.lookup_member(*base, name)? {
3032 MemberRef::Property(_) => "property",
3033 MemberRef::Const(_) => "constant",
3034 MemberRef::Signal(_) => "signal",
3035 _ => return None,
3036 };
3037 let label = Ty::Object(*base)
3038 .label(self.api)
3039 .unwrap_or_else(|| "?".to_owned());
3040 Some((kind, label))
3041 }
3042
3043 #[allow(
3047 clippy::naive_bytecount,
3048 reason = "a cold diagnostic-message path over one chunk prefix; not worth a bytecount dependency"
3049 )]
3050 fn line_of(&self, range: TextRange) -> u32 {
3051 let mut line: u32 = 1;
3052 let mut seen: usize = 0;
3053 let target = range.start as usize;
3054 self.root.text().for_each_chunk(|chunk| {
3055 let remaining = target.saturating_sub(seen);
3056 if remaining > 0 {
3057 let take = remaining.min(chunk.len());
3058 line += u32::try_from(
3059 chunk.as_bytes()[..take]
3060 .iter()
3061 .filter(|&&b| b == b'\n')
3062 .count(),
3063 )
3064 .unwrap_or(0);
3065 }
3066 seen += chunk.len();
3067 });
3068 line
3069 }
3070
3071 fn check_member_kind_misuse(
3078 &mut self,
3079 m: &MemberRef,
3080 as_method: bool,
3081 name: &str,
3082 range: TextRange,
3083 ) {
3084 if !as_method {
3085 return;
3086 }
3087 let (code, ty) = match m {
3088 MemberRef::Property(p) => (
3089 WarningCode::PropertyUsedAsFunction,
3090 ty::resolve_tyref(self.api, &p.ty),
3091 ),
3092 MemberRef::Const(c) => (
3093 WarningCode::ConstantUsedAsFunction,
3094 ty::resolve_tyref(self.api, &c.ty),
3095 ),
3096 _ => return,
3097 };
3098 if ty.is_uninformative() || matches!(ty, Ty::Callable | Ty::Signal(_)) {
3100 return;
3101 }
3102 let ty_label = ty.label(self.api).unwrap_or_else(|| "?".to_owned());
3106 self.warn(
3107 range,
3108 code,
3109 format!("Name \"{name}\" called as a function but is a \"{ty_label}\"."),
3110 );
3111 }
3112
3113 fn check_static_on_instance(
3118 &mut self,
3119 receiver: ExprId,
3120 m: &MemberRef,
3121 as_method: bool,
3122 range: TextRange,
3123 ) {
3124 if !as_method {
3125 return;
3126 }
3127 let MemberRef::Method(sig) = m else {
3128 return;
3129 };
3130 if !sig.is_static {
3131 return;
3132 }
3133 let Expr::Name(rname) = self.body.expr(receiver) else {
3134 return;
3135 };
3136 if !self.locals.contains_key(rname) {
3137 return;
3138 }
3139 if let Some(b) = self.bindings.iter().rev().find(|b| &b.name == rname)
3144 && let Some(init) = b.init
3145 && matches!(self.body.expr(init), Expr::Name(_))
3146 {
3147 return;
3148 }
3149 let type_label = self
3151 .locals
3152 .get(rname)
3153 .and_then(|t| t.label(self.api))
3154 .unwrap_or_else(|| "?".to_owned());
3155 self.warn(
3156 range,
3157 WarningCode::StaticCalledOnInstance,
3158 format!(
3159 "The function \"{name}()\" is a static function but was called from an instance. Instead, it should be directly called from the type: \"{type_label}.{name}()\".",
3160 name = sig.name
3161 ),
3162 );
3163 }
3164
3165 fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
3166 match m {
3167 MemberRef::Method(sig) => {
3168 if as_method {
3169 ty::resolve_tyref(self.api, &sig.return_ty)
3170 } else {
3171 Ty::Callable
3172 }
3173 }
3174 MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
3180 || ty::resolve_tyref(self.api, &p.ty),
3181 |q| resolve::resolve_type_name(self.db, self.api, q),
3182 ),
3183 MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
3184 MemberRef::Signal(_) => Ty::Signal(None),
3185 MemberRef::Enum(_) => Ty::Variant,
3186 MemberRef::EnumValue { class, decl, .. } => Ty::Enum(EnumRef {
3191 qualified: SmolStr::new(format!("{}.{}", class, decl.name)),
3192 bitfield: decl.is_bitfield,
3193 }),
3194 }
3195 }
3196
3197 fn local_binding_ty(
3199 &mut self,
3200 v: &body::LocalVar,
3201 annotated: Option<&Ty>,
3202 init_ty: Option<&Ty>,
3203 range: TextRange,
3204 ) -> Ty {
3205 match (annotated, init_ty) {
3206 (Some(t), Some(init)) => {
3208 self.check_assign(init, t, range, AssignCtx::Assign);
3209 t.clone()
3210 }
3211 (Some(t), None) => t.clone(),
3213 (None, Some(init)) if v.is_inferred => {
3215 if init.is_variant() {
3216 self.warn(
3217 range,
3218 WarningCode::InferenceOnVariant,
3219 inference_on_variant_msg(if v.is_const { "constant" } else { "variable" }),
3220 );
3221 Ty::Variant
3222 } else {
3223 self.tuple_alias_guard(&v.name, init)
3226 }
3227 }
3228 (None, Some(init)) => {
3235 if v.is_const {
3236 init.clone()
3237 } else if !init.is_uninformative() && !self.rebound.contains(&v.name) {
3238 self.tuple_alias_guard(&v.name, init)
3239 } else {
3240 Ty::Variant
3241 }
3242 }
3243 (None, None) => Ty::Variant,
3244 }
3245 }
3246
3247 fn tuple_alias_guard(&self, name: &SmolStr, ty: &Ty) -> Ty {
3253 if matches!(ty, Ty::Tuple(_))
3254 && (self.escaping.contains(name) || self.rebound.contains(name))
3255 {
3256 return ty.widen_tuple();
3257 }
3258 ty.clone()
3259 }
3260
3261 fn builtin_member_ty(
3262 &mut self,
3263 recv: &Ty,
3264 name: &str,
3265 range: TextRange,
3266 as_method: bool,
3267 ) -> Ty {
3268 let Some(bid) = self.builtin_id_of(recv) else {
3269 return Ty::Variant;
3270 };
3271 if as_method {
3272 return if let Some(sig) = self.api.builtin_method(bid, name) {
3273 ty::resolve_tyref(self.api, &sig.return_ty)
3274 } else {
3275 self.emit_builtin_miss(name, recv, bid, range, true);
3276 Ty::Variant
3277 };
3278 }
3279 let data = self.api.builtin(bid);
3288 if let Ty::Dict(_, v) = recv {
3289 return (**v).clone();
3290 }
3291 if data.is_keyed {
3292 return Ty::Variant;
3293 }
3294 if let Some(member) = self.api.builtin_member(bid, name) {
3295 return ty::resolve_tyref(self.api, &member.ty);
3296 }
3297 if let Some(c) = data.constants.iter().find(|c| c.name == name) {
3299 return ty::resolve_tyref(self.api, &c.ty);
3300 }
3301 if data
3302 .enums
3303 .iter()
3304 .any(|e| e.values.iter().any(|v| v.name == name))
3305 {
3306 return self.int_ty();
3307 }
3308 if self.api.builtin_method(bid, name).is_some() {
3309 return Ty::Callable;
3310 }
3311 self.emit_builtin_miss(name, recv, bid, range, false);
3312 Ty::Variant
3313 }
3314
3315 fn emit_builtin_miss(
3329 &mut self,
3330 name: &str,
3331 recv: &Ty,
3332 bid: gdscript_api::BuiltinId,
3333 range: TextRange,
3334 as_method: bool,
3335 ) {
3336 if crate::queries::project_engine_version(self.db)
3337 .is_some_and(|v| v > crate::warnings::bundled_version())
3338 {
3339 self.emit_unsafe(name, recv, range, as_method);
3340 return;
3341 }
3342 if self.api.builtin(bid).name == "Nil" {
3343 return;
3344 }
3345 let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
3346 let (code, message) = if as_method {
3347 (
3348 WarningCode::UndefinedMethod,
3349 format!("Function \"{name}()\" not found in base {recv_label}."),
3351 )
3352 } else {
3353 (
3354 WarningCode::UndefinedProperty,
3355 format!("Cannot find member \"{name}\" in base \"{recv_label}\"."),
3357 )
3358 };
3359 self.warn(range, code, message);
3360 }
3361
3362 fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
3364 match ty {
3365 Ty::Builtin(b) => Some(*b),
3366 Ty::Array(_) | Ty::Tuple(_) => self.api.builtin_by_name("Array"),
3368 Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
3369 Ty::Callable => self.api.builtin_by_name("Callable"),
3370 Ty::Signal(_) => self.api.builtin_by_name("Signal"),
3371 _ => None,
3372 }
3373 }
3374
3375 fn index_ty(&self, base: &Ty, index: ExprId) -> Ty {
3379 match base {
3380 Ty::Array(elem) => (**elem).clone(),
3381 Ty::Tuple(elems) => match self.body.expr(index) {
3385 Expr::Literal(body::Literal::Int(Some(i))) => usize::try_from(*i)
3386 .ok()
3387 .and_then(|i| elems.get(i).cloned())
3388 .unwrap_or(Ty::Variant),
3389 _ => Ty::Variant,
3390 },
3391 Ty::Builtin(b) => self
3392 .api
3393 .builtin(*b)
3394 .indexing_return
3395 .as_ref()
3396 .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
3397 Ty::Unknown => Ty::Unknown,
3399 Ty::Error => Ty::Error,
3400 _ => Ty::Variant,
3401 }
3402 }
3403
3404 fn loop_var_ty(&self, iter: &Ty) -> Ty {
3408 match iter {
3409 Ty::Array(elem) => (**elem).clone(),
3410 Ty::Builtin(b) => {
3411 let data = self.api.builtin(*b);
3412 if data.name == "int" {
3413 self.int_ty()
3415 } else if let Some(r) = &data.indexing_return {
3416 ty::resolve_tyref(self.api, r)
3418 } else {
3419 Ty::Variant
3420 }
3421 }
3422 Ty::Unknown => Ty::Unknown,
3424 Ty::Error => Ty::Error,
3425 _ => Ty::Variant,
3426 }
3427 }
3428
3429 fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
3430 let saved_locals = self.locals.clone();
3434 let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
3435 for p in params {
3436 let ty = self.param_ty(p);
3437 self.bindings.push(Binding {
3438 name: p.name.clone(),
3439 name_range: p.name_range,
3440 ty: ty.clone(),
3441 init: None,
3442 annotated: p.type_ref.is_some(),
3443 inferred_colon_eq: false,
3444 is_const: false,
3445 kind: BindingKind::Param,
3446 });
3447 self.locals.insert(p.name.clone(), ty);
3448 }
3449 self.infer_block(body);
3450 self.return_ty = saved_ret;
3451 self.locals = saved_locals;
3452 }
3453
3454 fn param_ty(&mut self, p: &ParamBinding) -> Ty {
3455 if let Some(ptr) = p.type_ref {
3456 return self.resolve_ptr_ty(ptr);
3457 }
3458 p.default
3460 .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
3461 }
3462
3463 fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
3472 if self.locals.contains_key(name) && !self.assign_lhs.contains(&id) {
3478 self.used_locals.insert(SmolStr::new(name));
3479 }
3480 if self.is_func_body
3484 && self.needs_assignment.contains(name)
3485 && !self.assign_lhs.contains(&id)
3486 && let Some(cur) = self.cur_stmt
3487 && self
3488 .assigned
3489 .assigned_before(cur)
3490 .is_some_and(|a| !a.contains(name))
3491 {
3492 self.warn(
3493 self.range_of(id),
3494 WarningCode::UnassignedVariable,
3495 format!("The variable \"{name}\" is used before being assigned a value."),
3496 );
3497 }
3498 if let Some(key) = self.narrow_key(id)
3500 && let Some(t) = self.narrowing.get(&key)
3501 {
3502 return t.clone();
3503 }
3504 if let Some(t) = self.locals.get(name) {
3505 return t.clone();
3506 }
3507 if let Some(item) = self.class.lookup(name) {
3508 return self.own_member_ty(item, false);
3509 }
3510 match self.class.base.clone() {
3514 Ty::Object(base) => {
3515 if let Some(m) = self.api.lookup_member(base, name) {
3516 return self.member_ref_ty(&m, false);
3517 }
3518 }
3519 Ty::ScriptRef(base) => {
3520 if let Some(t) = self.script_member_walk(base, name, false, 0) {
3521 return t;
3522 }
3523 }
3524 _ => {}
3525 }
3526 if let Some(g) = resolve::resolve_global(self.api, name) {
3527 return global_ty(&g);
3528 }
3529 let by_class = resolve::resolve_external(
3534 self.db,
3535 &resolve::ExternalRef::ClassName(SmolStr::new(name)),
3536 );
3537 if !by_class.is_unknown() {
3538 return by_class;
3539 }
3540 let by_autoload =
3541 resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)));
3542 if by_autoload.is_unknown() {
3543 self.maybe_warn_undefined_identifier(id, name);
3546 }
3547 by_autoload
3548 }
3549
3550 fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
3551 match item {
3552 ClassItem::EnumVariant => self.int_ty(),
3553 ClassItem::Member(_) => match self.class.member(item) {
3554 Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
3555 Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
3556 Some(Member::Func(f)) => {
3557 if as_method {
3558 self.func_call_return_ty(f)
3559 } else {
3560 Ty::Callable
3561 }
3562 }
3563 Some(Member::Signal(_)) => Ty::Signal(None),
3564 Some(Member::Class(c)) => match &self.self_ty {
3569 Ty::ScriptRef(sref) => Ty::InnerClass(crate::ty::InnerClassRef {
3570 file: sref.0,
3571 path: c.name.clone(),
3572 }),
3573 _ => Ty::Unknown,
3574 },
3575 Some(Member::Enum(e)) => e.name.as_ref().map_or(Ty::Variant, |n| {
3580 Ty::Enum(EnumRef {
3581 qualified: n.clone(),
3582 bitfield: false,
3583 })
3584 }),
3585 None => Ty::Variant,
3586 },
3587 }
3588 }
3589
3590 fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
3593 if let Some(t) = self.class.member_types.get(name) {
3594 return t.clone();
3595 }
3596 self.resolve_decl_annotation(ptr)
3597 }
3598
3599 fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
3601 let Some(node) = ptr.to_node(self.root) else {
3602 return Ty::Variant;
3603 };
3604 cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
3605 .map_or(Ty::Variant, |t| {
3606 resolve::resolve_type_ref(self.db, self.api, &t)
3607 })
3608 }
3609
3610 fn facts_to_narrowing(&self, id: body::StmtId) -> FxHashMap<String, Ty> {
3622 let mut out = FxHashMap::default();
3623 if let Some(facts) = self.flow.facts_before(id) {
3624 for (place, nt) in facts.iter() {
3625 if let Some((key, ty)) = self.narrowing_entry(place, nt) {
3626 out.insert(key, ty);
3627 }
3628 }
3629 }
3630 out
3631 }
3632
3633 fn narrowing_entry(&self, place: &Place, nt: &NarrowedTy) -> Option<(String, Ty)> {
3637 let NarrowedTy::Is(ptr) = nt else {
3638 return None;
3639 };
3640 let narrowed = self.resolve_ptr_ty(*ptr);
3641 if narrowed.is_uninformative() {
3642 return None;
3643 }
3644 if let Place::Local(n) = place
3647 && let Some(cur) = self.locals.get(n)
3648 && !cur.is_uninformative()
3649 && !self.is_subtype(&narrowed, cur)
3650 {
3651 return None;
3652 }
3653 Some((place.dotted_key(), narrowed))
3654 }
3655
3656 fn apply_condition_facts(&mut self, cond: ExprId, truthy: bool) {
3659 for (place, nt) in flow::condition_facts(self.body, cond, truthy) {
3660 if let Some((key, ty)) = self.narrowing_entry(&place, &nt) {
3661 self.narrowing.insert(key, ty);
3662 }
3663 }
3664 }
3665
3666 fn narrow_key(&self, id: ExprId) -> Option<String> {
3669 match self.body.expr(id) {
3670 Expr::Name(n) => Some(n.to_string()),
3671 Expr::SelfExpr => Some("self".to_owned()),
3672 Expr::Paren(inner) => self.narrow_key(*inner),
3673 Expr::Field { receiver, name, .. } => {
3674 Some(format!("{}.{name}", self.narrow_key(*receiver)?))
3675 }
3676 _ => None,
3677 }
3678 }
3679
3680 fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
3681 ptr.to_node(self.root).map_or(Ty::Variant, |n| {
3682 resolve::resolve_type_ref(self.db, self.api, &n)
3683 })
3684 }
3685
3686 fn join(&self, a: &Ty, b: &Ty) -> Ty {
3697 if a == b {
3698 return a.clone();
3699 }
3700 if a.is_error() || b.is_error() {
3701 return Ty::Error;
3702 }
3703 if a.is_unknown() || b.is_unknown() {
3704 return Ty::Unknown;
3705 }
3706 if a.is_variant() || b.is_variant() {
3707 return Ty::Variant;
3708 }
3709 if ty::is_assignable(self.api, a, b) == Assign::Ok {
3710 return b.clone();
3711 }
3712 if ty::is_assignable(self.api, b, a) == Assign::Ok {
3713 return a.clone();
3714 }
3715 Ty::Variant
3716 }
3717}
3718
3719fn global_ty(g: &GlobalDef) -> Ty {
3721 match g {
3722 GlobalDef::Const(t) => t.clone(),
3723 GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
3724 GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
3725 GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
3727 GlobalDef::GlobalEnum => Ty::Variant,
3728 }
3729}
3730
3731fn inference_on_variant_msg(kind: &str) -> String {
3732 format!(
3733 "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
3734 )
3735}
3736
3737fn op_symbol(op: BinOp) -> Option<&'static str> {
3739 Some(match op {
3740 BinOp::Add => "+",
3741 BinOp::Sub => "-",
3742 BinOp::Mul => "*",
3743 BinOp::Div => "/",
3744 BinOp::Mod => "%",
3745 BinOp::Pow => "**",
3746 BinOp::BitAnd => "&",
3747 BinOp::BitOr => "|",
3748 BinOp::BitXor => "^",
3749 BinOp::Shl => "<<",
3750 BinOp::Shr => ">>",
3751 _ => return None,
3752 })
3753}
3754
3755#[cfg(test)]
3756mod tests {
3757 use super::*;
3758 use crate::item_tree::item_tree;
3759 use gdscript_syntax::{SyntaxKind, parse};
3760
3761 struct Harness {
3762 result: InferenceResult,
3763 body: Body,
3764 }
3765
3766 fn infer_first_func(src: &str) -> Harness {
3768 let api = gdscript_api::bundled();
3769 let db = gdscript_db::RootDatabase::default();
3770 let root = parse(src).syntax_node();
3771 let tree = item_tree(&root);
3772 let class = ClassScope::new(&db, api, &tree, None);
3773 let func = gdscript_syntax::ast::descendants(&root)
3774 .into_iter()
3775 .find(|n| n.kind() == SyntaxKind::FuncDecl)
3776 .expect("a function");
3777 let body = body::body_of_func(&func);
3778 let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
3779 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
3780 let owner =
3781 cst::first_child(&func, |k| k == SyntaxKind::Name).map(|n| n.text().to_string());
3782 let result = infer(
3783 &db,
3784 api,
3785 &root,
3786 &class,
3787 &body,
3788 return_ty,
3789 true,
3790 owner.as_deref(),
3791 );
3792 Harness { result, body }
3793 }
3794
3795 const DECLARATION_STRICTNESS: &[&str] = &["UNTYPED_DECLARATION", "INFERRED_DECLARATION"];
3803
3804 fn codes(h: &Harness) -> Vec<&str> {
3805 h.result
3806 .diagnostics
3807 .iter()
3808 .map(|d| d.code.as_str())
3809 .chain(h.result.raw_warnings.iter().map(|w| w.code.as_str()))
3810 .filter(|c| !DECLARATION_STRICTNESS.contains(c))
3811 .collect()
3812 }
3813
3814 fn project_codes(
3818 files: &[(&str, &str)],
3819 project_godot: Option<&str>,
3820 complete: bool,
3821 ) -> Vec<String> {
3822 let api = gdscript_api::bundled();
3823 let mut db = gdscript_db::RootDatabase::default();
3824 for (i, (path, text)) in files.iter().enumerate() {
3825 let id = FileId(u32::try_from(i).unwrap());
3826 db.set_file_text(id, text, salsa::Durability::LOW);
3827 db.set_file_path(id, path);
3828 }
3829 if let Some(cfg) = project_godot {
3830 db.set_project_config(cfg);
3831 }
3832 db.sync_source_root();
3833 db.set_workspace_complete(complete);
3834 let root = parse(files[0].1).syntax_node();
3835 let fi = analyze_file(&db, api, &root, FileId(0));
3836 fi.diagnostics
3837 .iter()
3838 .map(|d| d.code.clone())
3839 .chain(fi.raw_warnings.iter().map(|w| w.code.as_str().to_owned()))
3840 .filter(|c| !DECLARATION_STRICTNESS.contains(&c.as_str()))
3841 .collect()
3842 }
3843
3844 fn file_codes(src: &str) -> Vec<String> {
3848 let api = gdscript_api::bundled();
3849 let db = gdscript_db::RootDatabase::default();
3850 let root = parse(src).syntax_node();
3851 let fi = analyze_file(&db, api, &root, FileId(0));
3852 fi.diagnostics
3853 .iter()
3854 .map(|d| d.code.clone())
3855 .chain(fi.raw_warnings.iter().map(|w| w.code.as_str().to_owned()))
3856 .filter(|c| !DECLARATION_STRICTNESS.contains(&c.as_str()))
3857 .collect()
3858 }
3859
3860 #[test]
3863 fn bare_global_enum_value_types_as_its_enum_not_int() {
3864 let h =
3868 infer_first_func("func f():\n\tvar b: MouseButton = MOUSE_BUTTON_LEFT\n\treturn b\n");
3869 assert!(
3870 !codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
3871 "{:?}",
3872 h.result.diagnostics
3873 );
3874 }
3875
3876 #[test]
3877 fn gdscript_builtin_surface_is_complete_no_undefined_function() {
3878 let src = "func f():\n\tprint_debug(\"x\")\n\tprint_stack()\n\tvar st = get_stack()\n\tvar o = ord(\"a\")\n\tvar c = convert(1, TYPE_STRING)\n\tvar t = type_exists(\"Node\")\n\tvar d = inst_to_dict(self)\n\tvar i = dict_to_inst(d)\n\treturn [st, o, c, t, d, i]\n";
3882 let c = project_codes(&[("res://main.gd", src)], None, true);
3883 assert!(
3884 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
3885 "every @GDScript builtin must resolve: {c:?}"
3886 );
3887 }
3888
3889 #[test]
3890 fn scene_autoload_singleton_is_never_undefined() {
3891 let c = project_codes(
3894 &[("res://main.gd", "func f():\n\treturn Music\n")],
3895 Some("[autoload]\nMusic=\"*res://music.tscn\"\n"),
3896 true,
3897 );
3898 assert!(
3899 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
3900 "a registered scene autoload is defined: {c:?}"
3901 );
3902 }
3903
3904 #[test]
3905 fn calling_an_unassigned_local_callable_still_flags_unassigned() {
3906 let h2 = infer_first_func("func f():\n\tvar cb\n\tcb()\n\tcb = Callable()\n");
3911 assert!(
3912 codes(&h2).contains(&"UNASSIGNED_VARIABLE"),
3913 "a bare call reads the local before assignment: {:?}",
3914 codes(&h2)
3915 );
3916 }
3917
3918 #[test]
3919 fn over_indent_recovered_statements_are_still_analyzed() {
3920 let src =
3923 "func f():\n\tvar a = 1\n\t\tvar bad = 5 / 2\n\tvar b = 2\n\treturn [a, b, bad]\n";
3924 let c = file_codes(src);
3925 assert!(
3926 c.iter().any(|x| x == INTEGER_DIVISION),
3927 "recovered statements must be analyzed: {c:?}"
3928 );
3929 }
3930
3931 const HOOK_DECL: &str =
3934 "## @return-tuple(Variant, Callable)\nstatic func useState(v):\n\treturn [v, Callable()]\n";
3935
3936 #[test]
3937 fn return_tuple_tag_projects_a_constant_index() {
3938 let src = format!(
3942 "{HOOK_DECL}func f():\n\tvar s := useState(0)\n\ts[1].call(1)\n\ts[1].casll(1)\n\tuseState(0)[1].casll(2)\n"
3943 );
3944 let c = file_codes(&src);
3945 assert_eq!(
3946 c.iter().filter(|x| *x == "UNDEFINED_METHOD").count(),
3947 2,
3948 "exactly the two typo'd setter methods flag: {c:?}"
3949 );
3950 }
3951
3952 #[test]
3953 fn return_tuple_untyped_local_projects_via_initializer_narrowing() {
3954 let src =
3959 format!("{HOOK_DECL}func f():\n\tvar s = useState(0)\n\ts[1].casll(1)\n\treturn s\n");
3960 let c = file_codes(&src);
3961 assert!(
3962 c.iter().any(|x| x == "UNDEFINED_METHOD"),
3963 "initializer narrowing projects the tuple through the untyped local: {c:?}"
3964 );
3965 }
3966
3967 #[test]
3968 fn initializer_narrowing_is_invalidated_by_rebinds_and_index_stores() {
3969 let rebound = format!(
3974 "{HOOK_DECL}func f():\n\tvar s = useState(0)\n\ts = 5\n\ts[1].casll(1)\n\treturn s\n"
3975 );
3976 let c = file_codes(&rebound);
3977 assert!(
3978 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
3979 "a rebound local stays Variant: {c:?}"
3980 );
3981 let stored = format!(
3982 "{HOOK_DECL}func f():\n\tvar s = useState(0)\n\ts[1] = 5\n\ts[1].casll(1)\n\treturn s\n"
3983 );
3984 let c2 = file_codes(&stored);
3985 assert!(
3986 !c2.iter().any(|x| x.starts_with("UNDEFINED_")),
3987 "an index-stored local stays Variant: {c2:?}"
3988 );
3989 let lambda = format!(
3990 "{HOOK_DECL}func f():\n\tvar s = useState(0)\n\tvar g := func():\n\t\ts = 1\n\ts[1].casll(1)\n\treturn [s, g]\n"
3991 );
3992 let c3 = file_codes(&lambda);
3993 assert!(
3994 !c3.iter().any(|x| x.starts_with("UNDEFINED_")),
3995 "a lambda rebind conservatively invalidates: {c3:?}"
3996 );
3997 let field_store = "func f():\n\tvar v = Vector2.ONE\n\tv.x = 1.0\n\tv.zzz()\n\treturn v\n";
3998 let c4 = file_codes(field_store);
3999 assert!(
4000 c4.iter().any(|x| x == "UNDEFINED_METHOD"),
4001 "a field store keeps the narrowing (the binding type is untouched): {c4:?}"
4002 );
4003 }
4004
4005 #[test]
4006 fn initializer_narrowing_skips_null_and_seam_initializers() {
4007 let c = file_codes("func f():\n\tvar x = null\n\tx.foo()\n");
4010 assert!(
4011 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4012 "null initializers never narrow: {c:?}"
4013 );
4014 let c2 = file_codes("func f():\n\tvar x = Missing.thing()\n\tx.foo()\n");
4015 assert!(
4016 !c2.iter().any(|x| x.starts_with("UNDEFINED_")),
4017 "seam initializers never narrow: {c2:?}"
4018 );
4019 }
4020
4021 #[test]
4022 fn return_tuple_dynamic_or_oob_index_degrades_to_variant() {
4023 let src = format!(
4026 "{HOOK_DECL}func f(i: int):\n\tvar s := useState(0)\n\ts[i].casll(1)\n\ts[7].casll(1)\n"
4027 );
4028 let c = file_codes(&src);
4029 assert!(
4030 !c.iter().any(|x| x == "UNSAFE_METHOD_ACCESS"),
4031 "no confident projection without a constant in-bounds index: {c:?}"
4032 );
4033 }
4034
4035 #[test]
4036 fn return_tuple_widens_to_array_for_assignment_and_methods() {
4037 let src = format!(
4040 "{HOOK_DECL}func f():\n\tvar s := useState(0)\n\tvar a: Array = s\n\treturn [a, s.size()]\n"
4041 );
4042 let c = file_codes(&src);
4043 assert!(
4044 !c.iter()
4045 .any(|x| x == TYPE_MISMATCH || x == "UNSAFE_METHOD_ACCESS"),
4046 "a tuple behaves as its runtime Array: {c:?}"
4047 );
4048 }
4049
4050 #[test]
4051 fn return_tuple_resolves_cross_file_via_the_member_table() {
4052 let files = [
4055 (
4056 "res://main.gd",
4057 "func f():\n\tvar s := Hooks.useState(0)\n\ts[1].casll(1)\n",
4058 ),
4059 (
4060 "res://hooks.gd",
4061 "class_name Hooks\n## @return-tuple(Variant, Callable)\nstatic func useState(v):\n\treturn [v, Callable()]\n",
4062 ),
4063 ];
4064 let c = project_codes(&files, None, true);
4065 assert!(
4066 c.iter().any(|x| x == "UNDEFINED_METHOD"),
4067 "the typo'd setter method must flag cross-file: {c:?}"
4068 );
4069 }
4070
4071 #[test]
4072 fn return_tuple_tag_is_ignored_when_malformed() {
4073 let src = "## @return-tuple(Variant)\nstatic func one(v):\n\treturn [v]\nfunc f():\n\tvar s = one(0)\n\ts[0].casll(1)\n";
4075 let c = file_codes(src);
4076 assert!(
4077 !c.iter().any(|x| x == "UNSAFE_METHOD_ACCESS"),
4078 "a malformed tag must not project: {c:?}"
4079 );
4080 }
4081
4082 #[test]
4085 fn undefined_function_fires_with_a_complete_workspace() {
4086 let c = project_codes(
4087 &[("res://main.gd", "func f():\n\tusseState(0)\n")],
4088 None,
4089 true,
4090 );
4091 assert!(
4092 c.iter().any(|x| x == "UNDEFINED_FUNCTION"),
4093 "a provable typo call must fire: {c:?}"
4094 );
4095 }
4096
4097 #[test]
4098 fn undefined_function_is_silent_without_the_completeness_claim() {
4099 let c = project_codes(
4102 &[("res://main.gd", "func f():\n\tusseState(0)\n")],
4103 None,
4104 false,
4105 );
4106 assert!(
4107 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4108 "incomplete workspace must stay silent: {c:?}"
4109 );
4110 }
4111
4112 #[test]
4113 fn undefined_function_skips_cross_file_class_names_and_autoloads() {
4114 let files = [
4117 (
4118 "res://main.gd",
4119 "func f():\n\tvar e = Enemy.new()\n\tMusic.play()\n\treturn e\n",
4120 ),
4121 ("res://enemy.gd", "class_name Enemy\nfunc hit(): pass\n"),
4122 ("res://music.gd", "func play(): pass\n"),
4123 ];
4124 let c = project_codes(
4125 &files,
4126 Some("[autoload]\nMusic=\"*res://music.gd\"\n"),
4127 true,
4128 );
4129 assert!(
4130 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4131 "cross-file globals + autoloads must resolve: {c:?}"
4132 );
4133 }
4134
4135 #[test]
4136 fn undefined_function_skips_locals_engine_methods_utilities_and_members() {
4137 let src = "extends Node\nvar handler: Callable\nfunc f():\n\tvar cb = func(): return 1\n\tcb()\n\tget_child(0)\n\tprint(\"x\")\n\thandler()\n";
4140 let c = project_codes(&[("res://main.gd", src)], None, true);
4141 assert!(
4142 !c.iter().any(|x| x == "UNDEFINED_FUNCTION"),
4143 "locals / engine methods / utilities / members must never flag: {c:?}"
4144 );
4145 }
4146
4147 #[test]
4148 fn undefined_function_is_silent_under_a_user_script_base() {
4149 let files = [
4153 (
4154 "res://main.gd",
4155 "extends Base\nfunc f():\n\tbase_method()\n",
4156 ),
4157 (
4158 "res://base.gd",
4159 "class_name Base\nfunc base_method(): pass\n",
4160 ),
4161 ];
4162 let c = project_codes(&files, None, true);
4163 assert!(
4164 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4165 "a ScriptRef base must gate emission off: {c:?}"
4166 );
4167 }
4168
4169 #[test]
4170 fn undefined_identifier_fires_and_respects_the_gate() {
4171 let fire = project_codes(
4172 &[("res://main.gd", "func f():\n\treturn nonexistent_thing\n")],
4173 None,
4174 true,
4175 );
4176 assert!(
4177 fire.iter().any(|x| x == "UNDEFINED_IDENTIFIER"),
4178 "a provable undeclared read must fire: {fire:?}"
4179 );
4180 let silent = project_codes(
4181 &[("res://main.gd", "func f():\n\treturn nonexistent_thing\n")],
4182 None,
4183 false,
4184 );
4185 assert!(
4186 !silent.iter().any(|x| x.starts_with("UNDEFINED_")),
4187 "incomplete workspace must stay silent: {silent:?}"
4188 );
4189 }
4190
4191 #[test]
4192 fn builtin_member_miss_is_undefined_method_or_property() {
4193 let c = file_codes("func f():\n\tvar c: Callable = Callable()\n\tc.casll()\n");
4198 assert!(c.iter().any(|x| x == "UNDEFINED_METHOD"), "{c:?}");
4199 let c2 = file_codes("func f():\n\tvar v := Vector2.ONE\n\treturn v.zzz\n");
4200 assert!(c2.iter().any(|x| x == "UNDEFINED_PROPERTY"), "{c2:?}");
4201 let c3 = file_codes("func f():\n\tvar i: int = 5\n\ti.to_float()\n");
4203 assert!(c3.iter().any(|x| x == "UNDEFINED_METHOD"), "{c3:?}");
4204 let ok = file_codes("func f():\n\tvar s: String = \"a\"\n\treturn s.to_upper().length()\n");
4206 assert!(!ok.iter().any(|x| x.starts_with("UNDEFINED_")), "{ok:?}");
4207 let obj = file_codes("func f():\n\tvar n: Node = Node.new()\n\tn.frobnicate()\n");
4209 assert!(!obj.iter().any(|x| x.starts_with("UNDEFINED_")), "{obj:?}");
4210 assert!(obj.iter().any(|x| x == "UNSAFE_METHOD_ACCESS"), "{obj:?}");
4211 }
4212
4213 #[test]
4214 fn dictionary_property_access_is_keyed_sugar_never_undefined() {
4215 let c = file_codes(
4219 "func f():\n\tvar d: Dictionary = {}\n\tvar x = d.some_key\n\td.other_key = 5\n\treturn x\n",
4220 );
4221 assert!(
4222 !c.iter()
4223 .any(|x| x.starts_with("UNDEFINED_") || x.starts_with("UNSAFE_")),
4224 "keyed access is not a member miss: {c:?}"
4225 );
4226 let m = file_codes("func f():\n\tvar d: Dictionary = {}\n\td.casll()\n");
4227 assert!(
4228 m.iter().any(|x| x == "UNDEFINED_METHOD"),
4229 "a Dictionary METHOD miss is still a Godot compile error: {m:?}"
4230 );
4231 }
4232
4233 #[test]
4234 fn dictionary_key_wins_over_method_name_in_property_sugar() {
4235 let c = file_codes(
4240 "func f():\n\tvar d: Dictionary = {\"size\": 3}\n\tvar n: int = d.size\n\td.size = 5\n\treturn n\n",
4241 );
4242 assert!(
4243 !c.iter()
4244 .any(|x| x == "TYPE_MISMATCH" || x.starts_with("UNDEFINED_")),
4245 "the key wins over the method name in property sugar: {c:?}"
4246 );
4247 }
4248
4249 #[test]
4250 fn dictionary_method_call_on_a_key_is_a_real_runtime_error() {
4251 let c = file_codes("func f():\n\tvar d = {\"greet\": 1}\n\td.greet()\n");
4256 assert!(
4257 c.iter().any(|x| x == "UNDEFINED_METHOD"),
4258 "method-call sugar does not dispatch to keys — a runtime crash: {c:?}"
4259 );
4260 }
4261
4262 #[test]
4263 fn tuple_narrowing_widens_when_the_local_escapes_into_an_alias() {
4264 let aliased = format!(
4269 "{HOOK_DECL}func f():\n\tvar s = useState(0)\n\tvar t = s\n\tt[1] = 5\n\ts[1].casll(1)\n\treturn [s, t]\n"
4270 );
4271 let c = file_codes(&aliased);
4272 assert!(
4273 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4274 "an escaped tuple must not keep positional projections: {c:?}"
4275 );
4276 let arg = format!(
4277 "{HOOK_DECL}func g(a):\n\ta[1] = 5\nfunc f():\n\tvar s := useState(0)\n\tg(s)\n\ts[1].casll(1)\n"
4278 );
4279 let c2 = file_codes(&arg);
4280 assert!(
4281 !c2.iter().any(|x| x.starts_with("UNDEFINED_")),
4282 "a tuple passed as an argument (by reference) must widen: {c2:?}"
4283 );
4284 let cast_store = format!(
4285 "{HOOK_DECL}func f():\n\tvar s = useState(0)\n\t(s as Array)[1] = \"x\"\n\ts[1].casll(1)\n\treturn s\n"
4286 );
4287 let c3 = file_codes(&cast_store);
4288 assert!(
4289 !c3.iter().any(|x| x.starts_with("UNDEFINED_")),
4290 "a cast-wrapped index store still invalidates: {c3:?}"
4291 );
4292 let clean = format!("{HOOK_DECL}func f():\n\tvar s = useState(0)\n\ts[1].casll(1)\n");
4294 let c4 = file_codes(&clean);
4295 assert!(
4296 c4.iter().any(|x| x == "UNDEFINED_METHOD"),
4297 "index-read-only locals keep the projection: {c4:?}"
4298 );
4299 }
4300
4301 #[test]
4302 fn builtin_member_miss_falls_back_to_unsafe_on_a_newer_project_engine() {
4303 let files = [(
4306 "res://main.gd",
4307 "func f():\n\tvar c: Callable = Callable()\n\tc.casll()\n",
4308 )];
4309 let cfg = "[application]\nconfig/features=PackedStringArray(\"4.9\")\n";
4310 let c = project_codes(&files, Some(cfg), false);
4311 assert!(!c.iter().any(|x| x == "UNDEFINED_METHOD"), "{c:?}");
4312 assert!(c.iter().any(|x| x == "UNSAFE_METHOD_ACCESS"), "{c:?}");
4313 }
4314
4315 #[test]
4316 fn undefined_identifier_skips_lambda_captures_and_inner_class_bodies() {
4317 let src = "static func outer_helper(): pass\nclass Inner:\n\tfunc g():\n\t\touter_helper()\nfunc f():\n\tvar captured = 1\n\tvar l = func(): return captured\n\treturn l\n";
4320 let c = project_codes(&[("res://main.gd", src)], None, true);
4321 assert!(
4322 !c.iter().any(|x| x.starts_with("UNDEFINED_")),
4323 "captures + inner-class bodies must never flag: {c:?}"
4324 );
4325 }
4326
4327 #[test]
4328 fn integer_division_warns() {
4329 let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
4330 assert!(codes(&h).contains(&INTEGER_DIVISION));
4331 }
4332
4333 #[test]
4334 fn float_div_does_not_warn() {
4335 let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
4336 assert!(!codes(&h).contains(&INTEGER_DIVISION));
4337 }
4338
4339 #[test]
4340 fn type_mismatch_on_hard_annotation() {
4341 let h = infer_first_func("func f():\n\tvar s: String = 5\n");
4342 assert!(codes(&h).contains(&TYPE_MISMATCH));
4343 }
4344
4345 #[test]
4346 fn vector_scalar_compound_assign_is_not_a_mismatch() {
4347 let h = infer_first_func(
4349 "func f() -> Vector2:\n\tvar v := Vector2()\n\tv *= 0.5\n\treturn v\n",
4350 );
4351 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
4352 }
4353
4354 #[test]
4355 fn array_literal_to_packed_array_is_allowed() {
4356 let h = infer_first_func("func f():\n\tvar p: PackedStringArray = [\"a\", \"b\"]\n");
4357 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
4358 }
4359
4360 #[test]
4361 fn vector2i_to_vector2_is_allowed() {
4362 let h = infer_first_func("func f():\n\tvar v: Vector2 = Vector2i(1, 2)\n");
4363 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
4364 }
4365
4366 #[test]
4367 fn local_enum_member_access_types_as_the_enum_not_variant() {
4368 let h = infer_first_func(
4371 "enum State { IDLE, RUN }\nfunc f():\n\tvar x := State.IDLE\n\treturn x\n",
4372 );
4373 assert!(
4374 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4375 "{:?}",
4376 codes(&h)
4377 );
4378 }
4379
4380 #[test]
4381 fn lua_style_dict_key_is_not_an_assignment() {
4382 let h = infer_first_func(
4385 "var pos: Vector2\nfunc f():\n\tvar d = { pos = \"x\" }\n\treturn d\n",
4386 );
4387 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
4388 }
4389
4390 #[test]
4391 fn narrowing_conversion_float_to_int() {
4392 let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
4393 assert!(codes(&h).contains(&NARROWING_CONVERSION));
4394 }
4395
4396 #[test]
4397 fn int_to_float_is_silent() {
4398 let h = infer_first_func("func f():\n\tvar x: float = 3\n\treturn x\n");
4399 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
4400 }
4401
4402 #[test]
4403 fn local_shadowing_a_param_warns_shadowed_variable() {
4404 let h = infer_first_func("func f(x):\n\tvar x = 1\n\treturn x\n");
4405 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
4406 }
4407
4408 #[test]
4409 fn local_shadowing_a_class_member_warns_shadowed_variable() {
4410 let h =
4412 infer_first_func("var health = 100\nfunc f():\n\tvar health = 1\n\treturn health\n");
4413 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
4414 }
4415
4416 #[test]
4417 fn non_shadowing_local_does_not_warn_shadowed_variable() {
4418 let h = infer_first_func("func f(x):\n\tvar y = 1\n\treturn x + y\n");
4419 assert!(!codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
4420 }
4421
4422 #[test]
4423 fn local_shadowing_a_base_member_warns_base_class() {
4424 let h =
4426 infer_first_func("extends Node2D\nfunc f():\n\tvar position = 1\n\treturn position\n");
4427 assert!(
4428 codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
4429 "{:?}",
4430 codes(&h)
4431 );
4432 }
4433
4434 #[test]
4435 fn shadowing_an_unresolved_base_is_silent() {
4436 let h = infer_first_func(
4438 "extends SomeUnknownThirdPartyClass\nfunc f():\n\tvar position = 1\n\treturn position\n",
4439 );
4440 assert!(
4441 !codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
4442 "{:?}",
4443 codes(&h)
4444 );
4445 }
4446
4447 #[test]
4448 fn local_named_after_a_native_class_warns_shadowed_global() {
4449 let h = infer_first_func("func f():\n\tvar Node = 1\n\treturn Node\n");
4450 assert!(
4451 codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
4452 "{:?}",
4453 codes(&h)
4454 );
4455 }
4456
4457 #[test]
4458 fn param_named_after_a_builtin_type_warns_shadowed_global() {
4459 let h = infer_first_func("func f(Vector2):\n\treturn Vector2\n");
4460 assert!(
4461 codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
4462 "{:?}",
4463 codes(&h)
4464 );
4465 }
4466
4467 #[test]
4468 fn member_named_after_a_native_class_warns_shadowed_global() {
4469 let cs = file_codes("var Timer = null\n");
4470 assert!(
4471 cs.iter().any(|c| c == "SHADOWED_GLOBAL_IDENTIFIER"),
4472 "{cs:?}"
4473 );
4474 }
4475
4476 #[test]
4477 fn ordinary_local_does_not_warn_shadowed_global() {
4478 let h = infer_first_func("func f():\n\tvar count = 1\n\treturn count\n");
4480 assert!(
4481 !codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
4482 "{:?}",
4483 codes(&h)
4484 );
4485 }
4486
4487 #[test]
4488 fn global_shadow_takes_precedence_over_variable_shadow() {
4489 let cs = file_codes("var Color = null\nfunc f():\n\tvar Color = 1\n\treturn Color\n");
4492 assert!(
4493 cs.iter().any(|c| c == "SHADOWED_GLOBAL_IDENTIFIER"),
4494 "{cs:?}"
4495 );
4496 assert!(
4497 !cs.iter().any(|c| c == "SHADOWED_VARIABLE"),
4498 "the local's variable-shadow must be suppressed in favor of the global one: {cs:?}"
4499 );
4500 }
4501
4502 #[test]
4503 fn assert_true_warns_always_true() {
4504 let h = infer_first_func("func f():\n\tassert(true)\n");
4505 assert!(codes(&h).contains(&"ASSERT_ALWAYS_TRUE"), "{:?}", codes(&h));
4506 }
4507
4508 #[test]
4509 fn assert_false_warns_always_false() {
4510 let h = infer_first_func("func f():\n\tassert(false, \"nope\")\n");
4511 assert!(
4512 codes(&h).contains(&"ASSERT_ALWAYS_FALSE"),
4513 "{:?}",
4514 codes(&h)
4515 );
4516 }
4517
4518 #[test]
4519 fn assert_null_warns_always_false() {
4520 let h = infer_first_func("func f():\n\tassert(null)\n");
4521 assert!(
4522 codes(&h).contains(&"ASSERT_ALWAYS_FALSE"),
4523 "{:?}",
4524 codes(&h)
4525 );
4526 }
4527
4528 #[test]
4529 fn assert_on_a_variable_is_silent() {
4530 let h = infer_first_func("func f(x):\n\tassert(x)\n");
4532 assert!(
4533 !codes(&h).iter().any(|c| c.starts_with("ASSERT_ALWAYS")),
4534 "{:?}",
4535 codes(&h)
4536 );
4537 }
4538
4539 #[test]
4540 fn untyped_and_inferred_declarations_warn() {
4541 let h = infer_first_func("func f(p):\n\tvar a = 1\n\tvar b := 2\n\tvar c: int = 3\n");
4544 let raw: Vec<&str> = h
4545 .result
4546 .raw_warnings
4547 .iter()
4548 .map(|w| w.code.as_str())
4549 .collect();
4550 let untyped = raw.iter().filter(|c| **c == "UNTYPED_DECLARATION").count();
4552 assert_eq!(untyped, 2, "only `p` and `a` are untyped: {raw:?}");
4553 let inferred = raw.iter().filter(|c| **c == "INFERRED_DECLARATION").count();
4554 assert_eq!(inferred, 1, "only `b` uses `:=`: {raw:?}");
4555 }
4556
4557 #[test]
4558 fn confusable_identifier_warns_on_a_mixed_script_local() {
4559 let h = infer_first_func("func f():\n\tvar p\u{0430}ypal = 1\n\treturn p\u{0430}ypal\n");
4561 assert!(
4562 codes(&h).contains(&"CONFUSABLE_IDENTIFIER"),
4563 "{:?}",
4564 codes(&h)
4565 );
4566 }
4567
4568 #[test]
4569 fn an_ordinary_ascii_identifier_is_not_confusable() {
4570 let h = infer_first_func("func f():\n\tvar paypal = 1\n\treturn paypal\n");
4571 assert!(
4572 !codes(&h).contains(&"CONFUSABLE_IDENTIFIER"),
4573 "{:?}",
4574 codes(&h)
4575 );
4576 }
4577
4578 #[test]
4579 fn a_member_with_a_confusable_name_warns() {
4580 let cs = file_codes("var b\u{0430}lance = 0\n");
4582 assert!(cs.iter().any(|c| c == "CONFUSABLE_IDENTIFIER"), "{cs:?}");
4583 }
4584
4585 #[test]
4586 fn an_assigned_but_never_read_local_is_unused() {
4587 let h = infer_first_func("func f():\n\tvar x = 1\n\tx = 2\n");
4589 assert!(codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
4590 }
4591
4592 #[test]
4593 fn a_read_local_is_not_unused() {
4594 let h = infer_first_func("func f() -> int:\n\tvar x = 1\n\tx = 2\n\treturn x\n");
4595 assert!(!codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
4596 }
4597
4598 #[test]
4599 fn unused_private_class_variable_warns() {
4600 let cs = file_codes("var _cache = 0\nfunc f():\n\tpass\n");
4601 assert!(
4602 cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
4603 "{cs:?}"
4604 );
4605 }
4606
4607 #[test]
4608 fn a_read_private_class_variable_is_silent() {
4609 let cs = file_codes("var _cache = 0\nfunc f() -> int:\n\treturn _cache\n");
4610 assert!(
4611 !cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
4612 "{cs:?}"
4613 );
4614 }
4615
4616 #[test]
4617 fn an_exported_private_var_is_not_unused_private() {
4618 let cs = file_codes("@export var _hidden = 0\n");
4620 assert!(
4621 !cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
4622 "{cs:?}"
4623 );
4624 }
4625
4626 #[test]
4627 fn onready_with_export_warns() {
4628 let cs = file_codes("@onready @export var n = null\n");
4629 assert!(cs.iter().any(|c| c == "ONREADY_WITH_EXPORT"), "{cs:?}");
4630 }
4631
4632 #[test]
4633 fn redundant_static_unload_warns_without_a_static_var() {
4634 let cs = file_codes("@static_unload\nclass_name Foo\nvar x = 1\n");
4635 assert!(cs.iter().any(|c| c == "REDUNDANT_STATIC_UNLOAD"), "{cs:?}");
4636 }
4637
4638 #[test]
4639 fn static_unload_with_a_static_var_is_silent() {
4640 let cs = file_codes("@static_unload\nstatic var pool = []\n");
4641 assert!(!cs.iter().any(|c| c == "REDUNDANT_STATIC_UNLOAD"), "{cs:?}");
4642 }
4643
4644 #[test]
4645 fn typed_local_read_before_assignment_does_not_warn() {
4646 let h = infer_first_func("func f() -> int:\n\tvar x: int\n\treturn x\n");
4649 assert!(
4650 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4651 "{:?}",
4652 codes(&h)
4653 );
4654 }
4655
4656 #[test]
4657 fn untyped_local_assigned_then_read_does_not_warn() {
4658 let h = infer_first_func("func f():\n\tvar x\n\tx = 5\n\treturn x\n");
4659 assert!(
4660 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4661 "{:?}",
4662 codes(&h)
4663 );
4664 }
4665
4666 #[test]
4667 fn typed_local_with_initializer_is_not_unassigned() {
4668 let h = infer_first_func("func f() -> int:\n\tvar x: int = 0\n\treturn x\n");
4669 assert!(
4670 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4671 "{:?}",
4672 codes(&h)
4673 );
4674 }
4675
4676 #[test]
4677 fn untyped_local_read_before_assignment_warns() {
4678 let h = infer_first_func("func f():\n\tvar x\n\tvar y = x\n\treturn y\n");
4681 assert!(
4682 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4683 "{:?}",
4684 codes(&h)
4685 );
4686 assert!(
4687 h.result
4688 .raw_warnings
4689 .iter()
4690 .any(|w| w.message == "The variable \"x\" is used before being assigned a value."),
4691 "Godot-verbatim wording expected: {:?}",
4692 h.result.raw_warnings
4693 );
4694 }
4695
4696 #[test]
4697 fn untyped_local_assigned_in_all_branches_then_read_does_not_warn() {
4698 let h = infer_first_func(
4700 "func f(c):\n\tvar x\n\tif c:\n\t\tx = 1\n\telse:\n\t\tx = 2\n\treturn x\n",
4701 );
4702 assert!(
4703 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4704 "{:?}",
4705 codes(&h)
4706 );
4707 }
4708
4709 #[test]
4710 fn untyped_local_assigned_in_one_branch_then_read_warns() {
4711 let h = infer_first_func("func f(c):\n\tvar x\n\tif c:\n\t\tx = 1\n\treturn x\n");
4713 assert!(
4714 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4715 "{:?}",
4716 codes(&h)
4717 );
4718 }
4719
4720 #[test]
4721 fn arm_after_wildcard_is_unreachable_pattern() {
4722 let h =
4723 infer_first_func("func f(x):\n\tmatch x:\n\t\t_:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n");
4724 assert!(
4725 codes(&h).contains(&"UNREACHABLE_PATTERN"),
4726 "{:?}",
4727 codes(&h)
4728 );
4729 }
4730
4731 #[test]
4732 fn arm_after_var_bind_is_unreachable_pattern() {
4733 let h = infer_first_func(
4734 "func f(x):\n\tmatch x:\n\t\tvar y:\n\t\t\treturn y\n\t\t1:\n\t\t\tpass\n",
4735 );
4736 assert!(
4737 codes(&h).contains(&"UNREACHABLE_PATTERN"),
4738 "{:?}",
4739 codes(&h)
4740 );
4741 }
4742
4743 #[test]
4744 fn arm_before_wildcard_is_not_unreachable() {
4745 let h =
4746 infer_first_func("func f(x):\n\tmatch x:\n\t\t1:\n\t\t\tpass\n\t\t_:\n\t\t\tpass\n");
4747 assert!(
4748 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
4749 "{:?}",
4750 codes(&h)
4751 );
4752 }
4753
4754 #[test]
4755 fn guarded_wildcard_is_not_a_catch_all() {
4756 let h = infer_first_func(
4758 "func f(x, c):\n\tmatch x:\n\t\t_ when c:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n",
4759 );
4760 assert!(
4761 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
4762 "{:?}",
4763 codes(&h)
4764 );
4765 }
4766
4767 #[test]
4768 fn multi_pattern_with_wildcard_is_conservatively_not_catch_all() {
4769 let h =
4771 infer_first_func("func f(x):\n\tmatch x:\n\t\t1, _:\n\t\t\tpass\n\t\t2:\n\t\t\tpass\n");
4772 assert!(
4773 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
4774 "{:?}",
4775 codes(&h)
4776 );
4777 }
4778
4779 #[test]
4780 fn enum_local_without_default_is_unassigned_tracked_not_warned() {
4781 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode\n\tprint(m)\n");
4784 assert!(
4785 !codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
4786 "{:?}",
4787 codes(&h)
4788 );
4789 assert!(
4790 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
4791 "{:?}",
4792 codes(&h)
4793 );
4794 }
4795
4796 #[test]
4797 fn enum_member_without_default_warns_only_without_a_zero_value() {
4798 let codes = file_codes("var m: MouseButtonMask\nfunc f():\n\tpass\n");
4801 assert!(
4802 codes.iter().any(|c| c == "ENUM_VARIABLE_WITHOUT_DEFAULT"),
4803 "{codes:?}"
4804 );
4805 let with_zero = file_codes("var err: Error\nfunc f():\n\tpass\n");
4806 assert!(
4807 !with_zero
4808 .iter()
4809 .any(|c| c == "ENUM_VARIABLE_WITHOUT_DEFAULT"),
4810 "{with_zero:?}"
4811 );
4812 }
4813
4814 #[test]
4815 fn user_vararg_function_absorbs_surplus_arguments() {
4816 let c = file_codes(
4819 "func v(a: int, ...rest) -> void:\n\tprint(a, rest)\n\nfunc t() -> void:\n\tv(1, 2, 3, 4)\n",
4820 );
4821 assert!(!c.iter().any(|x| x == "TOO_MANY_ARGUMENTS"), "{c:?}");
4822 let few = file_codes(
4824 "func v(a: int, ...rest) -> void:\n\tprint(a, rest)\n\nfunc t() -> void:\n\tv()\n",
4825 );
4826 assert!(few.iter().any(|x| x == "TOO_FEW_ARGUMENTS"), "{few:?}");
4827 }
4828
4829 #[test]
4830 fn gdscript_builtin_arity_uses_min_max() {
4831 let few = file_codes("func t() -> void:\n\tfor i in range():\n\t\tprint(i)\n");
4833 assert!(few.iter().any(|x| x == "TOO_FEW_ARGUMENTS"), "{few:?}");
4834 let many = file_codes("func t() -> void:\n\tfor i in range(1, 2, 3, 4):\n\t\tprint(i)\n");
4835 assert!(many.iter().any(|x| x == "TOO_MANY_ARGUMENTS"), "{many:?}");
4836 }
4837
4838 #[test]
4839 fn builtin_receiver_method_arity_checks() {
4840 let c = file_codes(
4842 "func t() -> void:\n\tvar s: String = \"abc\"\n\tprint(s.substr(1, 2, 3))\n",
4843 );
4844 assert!(c.iter().any(|x| x == "TOO_MANY_ARGUMENTS"), "{c:?}");
4845 }
4846
4847 #[test]
4848 fn shadowing_local_callable_is_not_arity_checked() {
4849 let c = file_codes(
4852 "func work(a: int) -> void:\n\tprint(a)\n\nfunc t() -> void:\n\tvar work = func(): pass\n\twork()\n",
4853 );
4854 assert!(!c.iter().any(|x| x == "TOO_FEW_ARGUMENTS"), "{c:?}");
4855 }
4856
4857 #[test]
4858 fn constructors_are_not_arity_checked() {
4859 let c = file_codes("func t() -> void:\n\tprint(Vector2(1.0, 2.0, 3.0, 4.0))\n");
4862 assert!(!c.iter().any(|x| x == "TOO_MANY_ARGUMENTS"), "{c:?}");
4863 }
4864
4865 #[test]
4866 fn unresolved_callee_is_not_arity_checked() {
4867 let c = file_codes("func t() -> void:\n\tsome_cross_file_fn(1, 2, 3)\n");
4869 assert!(
4870 !c.iter()
4871 .any(|x| x == "TOO_FEW_ARGUMENTS" || x == "TOO_MANY_ARGUMENTS"),
4872 "{c:?}"
4873 );
4874 }
4875
4876 #[test]
4877 fn native_virtual_override_with_clashing_param_type_warns() {
4878 let codes = file_codes("extends Node\nfunc _input(event: int):\n\tpass\n");
4880 assert!(
4881 codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
4882 "{codes:?}"
4883 );
4884 }
4885
4886 #[test]
4887 fn native_virtual_override_with_correct_param_type_does_not_warn() {
4888 let codes = file_codes("extends Node\nfunc _input(event: InputEvent):\n\tpass\n");
4889 assert!(
4890 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
4891 "{codes:?}"
4892 );
4893 }
4894
4895 #[test]
4896 fn native_virtual_override_with_untyped_param_does_not_warn() {
4897 let codes = file_codes("extends Node\nfunc _input(event):\n\tpass\n");
4898 assert!(
4899 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
4900 "{codes:?}"
4901 );
4902 }
4903
4904 #[test]
4905 fn a_non_virtual_method_is_not_a_native_override() {
4906 let codes = file_codes("extends Node\nfunc my_helper(x: int):\n\treturn x\n");
4907 assert!(
4908 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
4909 "{codes:?}"
4910 );
4911 }
4912
4913 #[test]
4914 fn dotted_enum_override_param_does_not_false_warn() {
4915 let codes = file_codes(
4918 "extends MultiplayerPeerExtension\nfunc _set_transfer_mode(p_mode: MultiplayerPeer.TransferMode):\n\tpass\n",
4919 );
4920 assert!(
4921 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
4922 "{codes:?}"
4923 );
4924 }
4925
4926 #[test]
4927 fn unused_signal_warns() {
4928 let codes = file_codes("signal my_event\nfunc f():\n\tpass\n");
4929 assert!(codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
4930 }
4931
4932 #[test]
4933 fn emitted_signal_is_not_unused() {
4934 let codes = file_codes("signal my_event\nfunc f():\n\tmy_event.emit()\n");
4935 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
4936 }
4937
4938 #[test]
4939 fn signal_connected_by_string_is_not_unused() {
4940 let codes = file_codes("signal my_event\nfunc f():\n\tconnect(\"my_event\", Callable())\n");
4941 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
4942 }
4943
4944 #[test]
4945 fn enum_local_with_default_does_not_warn() {
4946 let h = infer_first_func(
4947 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
4948 );
4949 assert!(
4950 !codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
4951 "{:?}",
4952 codes(&h)
4953 );
4954 }
4955
4956 #[test]
4957 fn static_method_on_instance_warns() {
4958 let h =
4960 infer_first_func("func f():\n\tvar j := JSON.new()\n\tj.stringify({})\n\treturn j\n");
4961 assert!(
4962 codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
4963 "{:?}",
4964 codes(&h)
4965 );
4966 }
4967
4968 #[test]
4969 fn static_method_on_the_type_does_not_warn() {
4970 let h = infer_first_func("func f():\n\tJSON.stringify({})\n");
4972 assert!(
4973 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
4974 "{:?}",
4975 codes(&h)
4976 );
4977 }
4978
4979 #[test]
4980 fn static_method_through_a_type_aliased_local_does_not_warn() {
4981 let h = infer_first_func("func f():\n\tvar t := JSON\n\tt.stringify({})\n");
4983 assert!(
4984 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
4985 "{:?}",
4986 codes(&h)
4987 );
4988 }
4989
4990 #[test]
4991 fn property_called_as_function_warns() {
4992 let h = infer_first_func("func f(n: Node):\n\tn.name()\n");
4994 assert!(
4995 codes(&h).contains(&"PROPERTY_USED_AS_FUNCTION"),
4996 "{:?}",
4997 codes(&h)
4998 );
4999 }
5000
5001 #[test]
5002 fn constant_called_as_function_warns() {
5003 let h = infer_first_func("func f(n: Node):\n\tn.NOTIFICATION_READY()\n");
5005 assert!(
5006 codes(&h).contains(&"CONSTANT_USED_AS_FUNCTION"),
5007 "{:?}",
5008 codes(&h)
5009 );
5010 }
5011
5012 #[test]
5013 fn calling_a_real_method_is_not_a_kind_misuse() {
5014 let h = infer_first_func("func f(n: Node):\n\tn.get_parent()\n");
5015 assert!(
5016 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
5017 "{:?}",
5018 codes(&h)
5019 );
5020 }
5021
5022 #[test]
5023 fn reading_a_property_as_a_value_is_not_a_kind_misuse() {
5024 let h = infer_first_func("func f(n: Node):\n\tvar s = n.name\n\treturn s\n");
5025 assert!(
5026 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
5027 "{:?}",
5028 codes(&h)
5029 );
5030 }
5031
5032 #[test]
5033 fn enum_member_into_its_own_enum_slot_is_not_int_as_enum() {
5034 let h = infer_first_func(
5038 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
5039 );
5040 assert!(
5041 !codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
5042 "{:?}",
5043 codes(&h)
5044 );
5045 }
5046
5047 #[test]
5048 fn bare_int_into_enum_slot_still_warns() {
5049 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode = 0\n\treturn m\n");
5051 assert!(
5052 codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
5053 "{:?}",
5054 codes(&h)
5055 );
5056 }
5057
5058 #[test]
5059 fn member_access_resolves_engine_property() {
5060 let h = infer_first_func(
5063 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
5064 );
5065 assert!(
5066 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
5067 "{:?}",
5068 h.result.diagnostics
5069 );
5070 }
5071
5072 #[test]
5073 fn unsafe_method_on_known_type() {
5074 let h = infer_first_func(
5075 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
5076 );
5077 assert!(
5078 codes(&h).contains(&UNSAFE_METHOD_ACCESS),
5079 "{:?}",
5080 h.result.diagnostics
5081 );
5082 }
5083
5084 #[test]
5085 fn is_narrowing_suppresses_unsafe() {
5086 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
5089 assert!(
5090 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
5091 "{:?}",
5092 h.result.diagnostics
5093 );
5094 }
5095
5096 #[test]
5097 fn is_narrowing_flags_real_missing_member() {
5098 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
5100 assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
5101 }
5102
5103 #[test]
5104 fn early_return_is_guard_narrows_past_the_guard() {
5105 let safe =
5108 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.get_parent()\n");
5109 assert!(
5110 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
5111 "real Node method must not warn after the guard: {:?}",
5112 codes(&safe)
5113 );
5114 let bogus =
5115 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.bogus_method()\n");
5116 assert!(
5117 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
5118 "missing method must warn after the guard: {:?}",
5119 codes(&bogus)
5120 );
5121 }
5122
5123 #[test]
5124 fn and_short_circuit_narrows_the_rhs() {
5125 let safe = infer_first_func("func f(x):\n\tif x is Node and x.get_parent():\n\t\tpass\n");
5128 assert!(
5129 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
5130 "real Node method in the and-rhs must not warn: {:?}",
5131 codes(&safe)
5132 );
5133 let bogus =
5134 infer_first_func("func f(x):\n\tif x is Node and x.bogus_method():\n\t\tpass\n");
5135 assert!(
5136 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
5137 "missing method in the and-rhs must warn: {:?}",
5138 codes(&bogus)
5139 );
5140 }
5141
5142 #[test]
5145 fn empty_file_warns() {
5146 assert!(file_codes("").iter().any(|c| c == "EMPTY_FILE"));
5147 assert!(
5148 file_codes("# just a comment\n")
5149 .iter()
5150 .any(|c| c == "EMPTY_FILE")
5151 );
5152 assert!(
5153 file_codes("extends Node\n")
5154 .iter()
5155 .all(|c| c != "EMPTY_FILE")
5156 );
5157 }
5158
5159 #[test]
5160 fn unused_variable_and_parameter() {
5161 let h = infer_first_func("func f(unused_p):\n\tvar unused_v = 1\n");
5162 assert!(codes(&h).contains(&"UNUSED_PARAMETER"), "{:?}", codes(&h));
5163 assert!(codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
5164 let used = infer_first_func("func f(p):\n\tvar v = p\n\treturn v\n");
5166 assert!(codes(&used).iter().all(|c| !c.starts_with("UNUSED")));
5167 let underscored = infer_first_func("func f(_ignored):\n\tpass\n");
5168 assert!(!codes(&underscored).contains(&"UNUSED_PARAMETER"));
5169 }
5170
5171 #[test]
5172 fn standalone_expression_and_ternary() {
5173 let expr = infer_first_func("func f(a, b):\n\ta + b\n");
5174 assert!(
5175 codes(&expr).contains(&"STANDALONE_EXPRESSION"),
5176 "{:?}",
5177 codes(&expr)
5178 );
5179 let tern = infer_first_func("func f(c):\n\t1 if c else 2\n");
5180 assert!(
5181 codes(&tern).contains(&"STANDALONE_TERNARY"),
5182 "{:?}",
5183 codes(&tern)
5184 );
5185 let call = infer_first_func("func f(n):\n\tn.queue_free()\n");
5187 assert!(codes(&call).iter().all(|c| !c.starts_with("STANDALONE")));
5188 }
5189
5190 #[test]
5191 fn unreachable_code_after_return() {
5192 let h = infer_first_func("func f():\n\treturn\n\tprint(\"dead\")\n");
5193 assert!(codes(&h).contains(&"UNREACHABLE_CODE"), "{:?}", codes(&h));
5194 }
5195
5196 #[test]
5197 fn incompatible_ternary_warns() {
5198 let h = infer_first_func("func f(c):\n\tvar x = \"s\" if c else 1\n\treturn x\n");
5200 assert!(
5201 codes(&h).contains(&"INCOMPATIBLE_TERNARY"),
5202 "{:?}",
5203 codes(&h)
5204 );
5205 }
5206
5207 #[test]
5208 fn variant_receiver_never_unsafe() {
5209 let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
5211 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
5212 }
5213
5214 #[test]
5215 fn unsafe_call_argument_on_variant_into_typed_param() {
5216 let h = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n: Node2D):\n\tpass\n");
5218 assert!(
5219 codes(&h).contains(&UNSAFE_CALL_ARGUMENT),
5220 "{:?}",
5221 h.result.diagnostics
5222 );
5223 }
5224
5225 #[test]
5226 fn unsafe_call_argument_silent_on_safe_and_untyped() {
5227 let upcast =
5229 infer_first_func("func f(n: Node2D):\n\ttake(n)\nfunc take(n: Node):\n\tpass\n");
5230 assert!(
5231 !codes(&upcast).contains(&UNSAFE_CALL_ARGUMENT),
5232 "upcast is safe: {:?}",
5233 upcast.result.diagnostics
5234 );
5235 let untyped = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n):\n\tpass\n");
5236 assert!(
5237 !codes(&untyped).contains(&UNSAFE_CALL_ARGUMENT),
5238 "untyped param accepts anything: {:?}",
5239 untyped.result.diagnostics
5240 );
5241 }
5242
5243 #[test]
5244 fn inference_on_variant() {
5245 let h = infer_first_func("func f(x):\n\tvar y := x\n");
5247 assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
5248 }
5249
5250 #[test]
5251 fn field_inferred_from_earlier_field_is_typed() {
5252 let codes = file_codes("var a := 1\nvar b := a + 1\n");
5256 assert!(
5257 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
5258 "field `b` from earlier field `a` should type as int, not Variant: {codes:?}"
5259 );
5260 }
5261
5262 #[test]
5263 fn field_forward_reference_is_seamed_not_warned() {
5264 let codes = file_codes("var b := a\nvar a := 1\n");
5268 assert!(
5269 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
5270 "forward field reference must not false-warn: {codes:?}"
5271 );
5272 }
5273
5274 #[test]
5275 fn standalone_inferred_field_unchanged() {
5276 let codes = file_codes("var n := 0\n");
5278 assert!(
5279 codes.is_empty(),
5280 "a literal-initialised field should produce no diagnostics: {codes:?}"
5281 );
5282 }
5283
5284 #[test]
5285 fn lambda_var_is_callable_not_variant() {
5286 let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
5287 assert!(
5288 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5289 "{:?}",
5290 h.result.diagnostics
5291 );
5292 }
5293
5294 #[test]
5295 fn multiline_lambda_then_paren_line_no_false_warning() {
5296 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";
5300 let h = infer_first_func(src);
5301 assert!(
5302 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5303 "{:?}",
5304 h.result.diagnostics
5305 );
5306 }
5307
5308 #[test]
5309 fn calling_a_callable_value_is_seam_not_variant() {
5310 let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
5314 let h = infer_first_func(src);
5315 assert!(
5316 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5317 "{:?}",
5318 h.result.diagnostics
5319 );
5320 }
5321
5322 #[test]
5323 fn calling_a_local_callable_param_is_a_use() {
5324 let src = "func f(cb: Callable):\n\tcb(0)\n";
5328 let h = infer_first_func(src);
5329 assert!(
5330 !codes(&h).contains(&"UNUSED_PARAMETER"),
5331 "calling a param is a use: {:?}",
5332 h.result.diagnostics
5333 );
5334 }
5335
5336 #[test]
5337 fn calling_a_local_lambda_var_is_a_use() {
5338 let src = "func g():\n\tvar lam = func(x): return x + 1\n\tlam(5)\n";
5342 let h = infer_first_func(src);
5343 assert!(
5344 !codes(&h).contains(&"UNUSED_VARIABLE"),
5345 "calling a local is a use: {:?}",
5346 h.result.diagnostics
5347 );
5348 }
5349
5350 #[test]
5351 fn calling_a_local_callable_result_stays_the_seam() {
5352 let src = "func f(cb: Callable):\n\tvar x := cb(0)\n\treturn x\n";
5355 let h = infer_first_func(src);
5356 assert!(
5357 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5358 "{:?}",
5359 h.result.diagnostics
5360 );
5361 }
5362
5363 #[test]
5364 fn local_callable_shadows_same_named_method_in_a_call() {
5365 let src = "func helper(x: int) -> int:\n\treturn x\nfunc f():\n\tvar helper = func(): return 1\n\thelper(\"not an int\")\n";
5370 let c = file_codes(src);
5371 assert!(
5372 !c.iter().any(|x| x == "UNUSED_VARIABLE"),
5373 "the shadowing local is used by the call: {c:?}"
5374 );
5375 assert!(
5376 !c.iter()
5377 .any(|x| x == "UNSAFE_CALL_ARGUMENT" || x == TYPE_MISMATCH),
5378 "the shadowed method's signature must not check the local's call: {c:?}"
5379 );
5380 }
5381
5382 #[test]
5383 fn ternary_with_seam_branch_does_not_collapse_to_variant() {
5384 let src =
5388 "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
5389 let h = infer_first_func(src);
5390 assert!(
5391 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5392 "seam branch should keep the ternary on the seam: {:?}",
5393 h.result.diagnostics
5394 );
5395 }
5396
5397 #[test]
5398 fn await_a_coroutine_call_recovers_its_return_type() {
5399 let src = "func g() -> int:\n\tvar x := await make()\n\treturn x\nfunc make() -> int:\n\treturn 5\n";
5402 let h = infer_first_func(src);
5403 assert!(
5404 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5405 "no false variant warning: {:?}",
5406 h.result.diagnostics
5407 );
5408 let api = gdscript_api::bundled();
5409 let x = &h.result.bindings[0];
5410 assert!(
5411 matches!(&x.ty, Ty::Builtin(b) if api.builtin(*b).name == "int"),
5412 "await make() should recover int, got {:?}",
5413 x.ty
5414 );
5415 }
5416
5417 #[test]
5418 fn await_a_signal_stays_the_seam() {
5419 let src = "func f():\n\tvar x := await get_tree().process_frame\n\treturn x\n";
5422 let h = infer_first_func(src);
5423 assert!(
5424 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5425 "awaiting a signal must not warn: {:?}",
5426 h.result.diagnostics
5427 );
5428 assert!(
5429 matches!(&h.result.bindings[0].ty, Ty::Unknown),
5430 "awaiting a signal stays the seam, got {:?}",
5431 h.result.bindings[0].ty
5432 );
5433 }
5434
5435 #[test]
5436 fn for_var_over_packed_string_array_is_string() {
5437 let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
5440 assert!(
5441 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5442 "{:?}",
5443 h.result.diagnostics
5444 );
5445 }
5446
5447 #[test]
5448 fn class_new_is_object_not_variant() {
5449 let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
5450 assert!(
5451 !codes(&h).contains(&INFERENCE_ON_VARIANT),
5452 "{:?}",
5453 h.result.diagnostics
5454 );
5455 }
5456
5457 #[test]
5458 fn unknown_seam_never_warns() {
5459 let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
5461 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
5462 }
5463
5464 #[test]
5465 fn expr_types_are_memoized_for_hover() {
5466 let h = infer_first_func("func f():\n\tvar n := 42\n");
5467 let has_int = h
5469 .result
5470 .expr_ty
5471 .values()
5472 .any(|t| matches!(t, Ty::Builtin(_)));
5473 assert!(has_int);
5474 assert!(!h.body.exprs.is_empty());
5476 }
5477}