1use gdscript_api::{EngineApi, MemberRef, TyRef};
13use gdscript_base::{Diagnostic, DiagnosticSource, FileId, Severity, TextRange};
14use gdscript_db::Db;
15use gdscript_scene::{SceneModel, SceneNode};
16use gdscript_syntax::GdNode;
17use rustc_hash::{FxHashMap, FxHashSet};
18use smol_str::SmolStr;
19
20use std::sync::Arc;
21
22use crate::body::{self, BinOp, Body, Expr, ExprId, Literal, ParamBinding, Stmt, UnOp};
23use crate::cst::{self, AstPtr};
24use crate::flow::{self, FlowAnalysis, NarrowedTy, Place};
25use crate::item_tree::{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
146#[must_use]
151#[allow(
152 clippy::too_many_lines,
153 reason = "the per-body inference orchestration reads best whole"
154)]
155pub fn infer(
156 db: &dyn Db,
157 api: &EngineApi,
158 root: &GdNode,
159 class: &ClassScope,
160 body: &Body,
161 return_ty: Ty,
162 is_func_body: bool,
163) -> InferenceResult {
164 let self_ty = class.self_ty.clone();
165 let mut cx = Cx {
166 db,
167 api,
168 root,
169 body,
170 class,
171 self_ty,
172 return_ty,
173 expr_ty: FxHashMap::default(),
174 bindings: Vec::new(),
175 diagnostics: Vec::new(),
176 raw_warnings: Vec::new(),
177 locals: FxHashMap::default(),
178 used_locals: FxHashSet::default(),
179 narrowing: FxHashMap::default(),
180 flow: flow::analyze(body),
181 is_func_body,
182 assigned: flow::analyze_assigned(
183 body,
184 &body
185 .params
186 .iter()
187 .map(|p| p.name.clone())
188 .collect::<Vec<_>>(),
189 ),
190 cur_stmt: None,
191 needs_assignment: FxHashSet::default(),
192 assign_lhs: collect_assign_lhs(body),
193 };
194 let params = body.params.clone();
196 for p in ¶ms {
197 let ty = cx.param_ty(p);
198 cx.bindings.push(Binding {
199 name: p.name.clone(),
200 name_range: p.name_range,
201 ty: ty.clone(),
202 init: None,
203 annotated: p.type_ref.is_some(),
204 inferred_colon_eq: false,
205 is_const: false,
206 kind: BindingKind::Param,
207 });
208 cx.locals.insert(p.name.clone(), ty);
209 }
210 if let Some(tail) = body.tail {
211 cx.infer_expr(tail, &Expectation::None);
212 }
213 let block = body.block.clone();
214 cx.infer_block(&block);
215
216 if is_func_body {
220 let unused: Vec<(TextRange, WarningCode, String)> = cx
221 .bindings
222 .iter()
223 .filter_map(|b| {
224 if b.name.starts_with('_') || cx.used_locals.contains(&b.name) {
225 return None;
226 }
227 let (code, what) = match b.kind {
228 BindingKind::Param => (WarningCode::UnusedParameter, "parameter"),
229 BindingKind::Var if b.is_const => {
230 (WarningCode::UnusedLocalConstant, "local constant")
231 }
232 BindingKind::Var => (WarningCode::UnusedVariable, "local variable"),
233 BindingKind::ForVar | BindingKind::MatchBind => return None,
234 };
235 Some((
236 b.name_range,
237 code,
238 format!("The {what} \"{}\" is declared but never used.", b.name),
239 ))
240 })
241 .collect();
242 for (range, code, msg) in unused {
243 cx.warn(range, code, msg);
244 }
245 }
246
247 if is_func_body {
254 let global_shadows: Vec<(TextRange, String)> = cx
255 .bindings
256 .iter()
257 .filter_map(|b| {
258 let kind = shadowed_global_kind(db, api, &b.name)?;
259 let what = match b.kind {
260 BindingKind::Param => "parameter",
261 BindingKind::Var if b.is_const => "constant",
262 BindingKind::Var => "variable",
263 BindingKind::ForVar => "for loop variable",
264 BindingKind::MatchBind => "pattern bind",
265 };
266 Some((
267 b.name_range,
268 format!("The {what} \"{}\" has the same name as a {kind}.", b.name),
269 ))
270 })
271 .collect();
272 for (range, msg) in global_shadows {
273 cx.warn(range, WarningCode::ShadowedGlobalIdentifier, msg);
274 }
275 }
276
277 if is_func_body {
284 let decl_strictness: Vec<(TextRange, WarningCode, String)> = cx
285 .bindings
286 .iter()
287 .filter_map(|b| match b.kind {
288 BindingKind::Param if !b.annotated => Some((
289 b.name_range,
290 WarningCode::UntypedDeclaration,
291 format!("The parameter \"{}\" has no static type.", b.name),
292 )),
293 BindingKind::Var if !b.is_const && b.inferred_colon_eq => Some((
294 b.name_range,
295 WarningCode::InferredDeclaration,
296 format!(
297 "The variable \"{}\" uses inferred typing (`:=`); consider declaring its type explicitly.",
298 b.name
299 ),
300 )),
301 BindingKind::Var if !b.is_const && !b.annotated => Some((
302 b.name_range,
303 WarningCode::UntypedDeclaration,
304 format!("The variable \"{}\" has no static type.", b.name),
305 )),
306 _ => None,
307 })
308 .collect();
309 for (range, code, msg) in decl_strictness {
310 cx.warn(range, code, msg);
311 }
312 }
313
314 let confusable_bindings: Vec<TextRange> = cx
317 .bindings
318 .iter()
319 .filter(|b| is_confusable_identifier(&b.name))
320 .map(|b| b.name_range)
321 .collect();
322 for range in confusable_bindings {
323 cx.warn(
324 range,
325 WarningCode::ConfusableIdentifier,
326 "This identifier uses confusable characters (mixed scripts).".to_owned(),
327 );
328 }
329
330 let unreachable = cx.flow.unreachable_ranges(body);
332 for range in unreachable {
333 cx.warn(
334 range,
335 WarningCode::UnreachableCode,
336 "Unreachable code (statement after a return, break, continue, or an exhaustive match)."
337 .to_owned(),
338 );
339 }
340
341 let unreachable_patterns = cx.flow.unreachable_pattern_ranges().to_vec();
343 for range in unreachable_patterns {
344 cx.warn(
345 range,
346 WarningCode::UnreachablePattern,
347 "Unreachable pattern: an earlier arm's wildcard (`_`) or `var` binding always matches."
348 .to_owned(),
349 );
350 }
351
352 InferenceResult {
353 expr_ty: cx.expr_ty,
354 bindings: cx.bindings,
355 diagnostics: cx.diagnostics,
356 raw_warnings: cx.raw_warnings,
357 }
358}
359
360#[must_use]
363pub fn infer_func(
364 db: &dyn Db,
365 api: &EngineApi,
366 root: &GdNode,
367 class: &ClassScope,
368 ptr: AstPtr,
369) -> InferenceResult {
370 let Some(node) = ptr.to_node(root) else {
371 return InferenceResult::default();
372 };
373 let body = body::body_of_func(&node);
374 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
377 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
378 infer(db, api, root, class, &body, return_ty, true)
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
385pub struct Unit {
386 pub range: TextRange,
388 pub body: Body,
390 pub result: InferenceResult,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Default)]
397pub struct FileInference {
398 pub tree: Arc<ItemTree>,
400 pub units: Vec<Unit>,
402 pub diagnostics: Vec<Diagnostic>,
405 pub raw_warnings: Vec<RawWarning>,
408}
409
410impl FileInference {
411 #[must_use]
413 pub fn unit_at(&self, offset: u32) -> Option<&Unit> {
414 self.units
415 .iter()
416 .filter(|u| u.range.start <= offset && offset < u.range.end)
417 .min_by_key(|u| u.range.end - u.range.start)
418 }
419}
420
421#[must_use]
425#[allow(clippy::too_many_lines)] pub fn analyze_file(db: &dyn Db, api: &EngineApi, root: &GdNode, file_id: FileId) -> FileInference {
427 let tree = item_tree(root);
428 let mut units = Vec::new();
429 let mut diagnostics = Vec::new();
430 let mut raw_warnings: Vec<RawWarning> = Vec::new();
431
432 if tree.members.is_empty() && tree.class_name.is_none() && tree.extends.is_none() {
434 raw_warnings.push(RawWarning {
435 range: TextRange::new(0, 0),
436 code: WarningCode::EmptyFile,
437 message: "Empty script file.".to_owned(),
438 });
439 }
440 let mut member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
441 let self_ref = Ty::ScriptRef(ScriptRefId(file_id.0));
444 let res_path = db.file_text(file_id).and_then(|ft| ft.res_path(db));
446
447 if let Some(name) = tree.class_name.clone() {
452 let collides = collisions_contains(db, &name)
453 || resolve::resolve_global(api, &name).is_some()
454 || is_autoload_singleton(db, &name);
455 if collides && let Some(range) = class_name_decl_range(root) {
456 diagnostics.push(Diagnostic {
457 range,
458 severity: Severity::Warning,
459 code: SHADOWED_GLOBAL_IDENTIFIER.to_owned(),
460 message: format!(
461 "The global class \"{name}\" hides a built-in/native/global/autoload."
462 ),
463 source: DiagnosticSource::Type,
464 fixes: Vec::new(),
465 });
466 }
467 if is_confusable_identifier(&name)
469 && let Some(range) = class_name_decl_range(root)
470 {
471 raw_warnings.push(RawWarning {
472 range,
473 code: WarningCode::ConfusableIdentifier,
474 message: format!(
475 "The identifier \"{name}\" uses confusable characters (mixed scripts)."
476 ),
477 });
478 }
479 }
480
481 if extends_chain_is_cyclic(db, file_id)
489 && let Some(range) = extends_decl_range(root)
490 {
491 diagnostics.push(Diagnostic {
492 range,
493 severity: Severity::Warning,
494 code: CYCLIC_INHERITANCE.to_owned(),
495 message: "Cyclic class hierarchy: this class's `extends` chain returns to itself."
496 .to_owned(),
497 source: DiagnosticSource::Type,
498 fixes: Vec::new(),
499 });
500 }
501
502 raw_warnings.extend(member_level_warnings(
504 db,
505 api,
506 root,
507 &tree,
508 res_path.as_deref(),
509 ));
510
511 {
521 const MAX_ROUNDS: usize = 4;
525 let mut final_units: Vec<Unit> = Vec::new();
526 let mut final_diagnostics: Vec<Diagnostic> = Vec::new();
527 let mut final_raw_warnings: Vec<RawWarning> = Vec::new();
528 for _ in 0..MAX_ROUNDS {
529 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
530 class.self_ty = self_ref.clone();
531 class.member_types.clone_from(&member_types);
532 let mut next_member_types: FxHashMap<SmolStr, Ty> = FxHashMap::default();
533 final_units = Vec::new();
534 final_diagnostics = Vec::new();
535 final_raw_warnings = Vec::new();
536 for m in &tree.members {
537 let (ptr, range) = match m {
538 Member::Var(v) => (v.ptr, v.range),
539 Member::Const(c) => (c.ptr, c.range),
540 _ => continue,
541 };
542 if let Some(unit) = unit_from_decl(db, api, root, &class, ptr, range) {
543 if let (Some(name), Some(b)) = (m.name(), unit.result.bindings.first()) {
544 next_member_types.insert(SmolStr::new(name), b.ty.clone());
545 }
546 final_diagnostics.extend(unit.result.diagnostics.iter().cloned());
547 final_raw_warnings.extend(unit.result.raw_warnings.iter().cloned());
548 final_units.push(unit);
549 }
550 }
551 if next_member_types == member_types {
552 break;
553 }
554 member_types = next_member_types;
555 }
556 diagnostics.extend(final_diagnostics);
557 raw_warnings.extend(final_raw_warnings);
558 units.extend(final_units);
559 }
560
561 {
563 let mut class = ClassScope::new(db, api, &tree, res_path.as_deref());
564 class.member_types = member_types;
565 class.self_ty = self_ref.clone();
566 for m in &tree.members {
567 let Member::Func(f) = m else { continue };
568 let Some(node) = f.ptr.to_node(root) else {
569 continue;
570 };
571 let body = body::body_of_func(&node);
572 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
573 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
574 let result = infer(db, api, root, &class, &body, return_ty, true);
575 diagnostics.extend(result.diagnostics.iter().cloned());
576 raw_warnings.extend(result.raw_warnings.iter().cloned());
577 units.push(Unit {
578 range: f.range,
579 body,
580 result,
581 });
582 }
583 }
584
585 infer_inner_class_bodies(
590 db,
591 api,
592 root,
593 &tree,
594 file_id,
595 "",
596 res_path.as_deref(),
597 &mut units,
598 &mut diagnostics,
599 &mut raw_warnings,
600 0,
601 );
602
603 FileInference {
604 tree,
605 units,
606 diagnostics,
607 raw_warnings,
608 }
609}
610
611#[allow(
617 clippy::too_many_arguments,
618 reason = "threads the same analyze_file accumulators a free helper can't capture from a closure"
619)]
620fn infer_inner_class_bodies(
621 db: &dyn Db,
622 api: &EngineApi,
623 root: &GdNode,
624 tree: &ItemTree,
625 file_id: FileId,
626 path_prefix: &str,
627 res_path: Option<&str>,
628 units: &mut Vec<Unit>,
629 diagnostics: &mut Vec<Diagnostic>,
630 raw_warnings: &mut Vec<RawWarning>,
631 depth: u32,
632) {
633 if depth > 16 {
634 return;
635 }
636 for m in &tree.members {
637 let Member::Class(c) = m else { continue };
638 let inner_path = if path_prefix.is_empty() {
639 c.name.to_string()
640 } else {
641 format!("{path_prefix}.{}", c.name)
642 };
643 let mut class = ClassScope::new(db, api, &c.tree, res_path);
644 class.self_ty = Ty::InnerClass(crate::ty::InnerClassRef {
645 file: file_id.0,
646 path: SmolStr::new(&inner_path),
647 });
648 for im in &c.tree.members {
649 let Member::Func(f) = im else { continue };
650 let Some(node) = f.ptr.to_node(root) else {
651 continue;
652 };
653 let body = body::body_of_func(&node);
654 let return_ty = cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
655 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(db, api, &t));
656 let result = infer(db, api, root, &class, &body, return_ty, true);
657 diagnostics.extend(result.diagnostics.iter().cloned());
658 raw_warnings.extend(result.raw_warnings.iter().cloned());
659 units.push(Unit {
660 range: f.range,
661 body,
662 result,
663 });
664 }
665 infer_inner_class_bodies(
666 db,
667 api,
668 root,
669 &c.tree,
670 file_id,
671 &inner_path,
672 res_path,
673 units,
674 diagnostics,
675 raw_warnings,
676 depth + 1,
677 );
678 }
679}
680
681fn class_annotation_warnings(
685 db: &dyn Db,
686 api: &EngineApi,
687 root: &GdNode,
688 tree: &ItemTree,
689 res_path: Option<&str>,
690) -> Vec<RawWarning> {
691 let mut out = Vec::new();
692 if let Some(unload) = tree.annotations.iter().find(|a| a.name == "static_unload")
694 && !tree
695 .members
696 .iter()
697 .any(|m| matches!(m, Member::Var(v) if v.is_static))
698 {
699 out.push(RawWarning {
700 range: unload.range,
701 code: WarningCode::RedundantStaticUnload,
702 message: "`@static_unload` is redundant on a class with no static variables."
703 .to_owned(),
704 });
705 }
706 if !has_annotation(&tree.annotations, "tool")
709 && user_base_is_tool(db, api, tree, res_path)
710 && let Some(range) = extends_decl_range(root)
711 {
712 out.push(RawWarning {
713 range,
714 code: WarningCode::MissingTool,
715 message: "This class extends a `@tool` script but is not itself `@tool` (it will not run in the editor)."
716 .to_owned(),
717 });
718 }
719 out
720}
721
722#[allow(
727 clippy::too_many_lines,
728 reason = "a flat sequence of independent per-member warning checks; reads best as one walk"
729)]
730fn member_level_warnings(
731 db: &dyn Db,
732 api: &EngineApi,
733 root: &GdNode,
734 tree: &ItemTree,
735 res_path: Option<&str>,
736) -> Vec<RawWarning> {
737 let mut out = Vec::new();
738 let has_signal = tree.members.iter().any(|m| matches!(m, Member::Signal(_)));
739 let has_private_var = tree
741 .members
742 .iter()
743 .any(|m| matches!(m, Member::Var(v) if v.name.starts_with('_') && !v.is_exported));
744 let uses = (has_signal || has_private_var).then(|| NameUses::collect(root));
746 let engine_base = match resolve::resolve_base(db, api, tree, res_path) {
748 Ty::Object(c) => Some(c),
749 _ => None,
750 };
751
752 out.extend(class_annotation_warnings(db, api, root, tree, res_path));
753
754 for m in &tree.members {
755 if let Member::Var(v) = m
759 && v.name.starts_with('_')
760 && !v.is_exported
761 && let Some(uses) = &uses
762 && !uses.is_referenced(&v.name)
763 {
764 out.push(RawWarning {
765 range: v.name_range,
766 code: WarningCode::UnusedPrivateClassVariable,
767 message: format!(
768 "The class variable \"{}\" is never used in this file.",
769 v.name
770 ),
771 });
772 }
773 if let Member::Var(v) = m
775 && has_annotation(&v.annotations, "onready")
776 && v.is_exported
777 {
778 out.push(RawWarning {
779 range: v.name_range,
780 code: WarningCode::OnreadyWithExport,
781 message: format!(
782 "The member \"{}\" has both `@onready` and `@export`; they conflict.",
783 v.name
784 ),
785 });
786 }
787 if let Some((name, range, what)) = member_value_decl(m)
790 && let Some(kind) = shadowed_global_kind(db, api, name)
791 {
792 out.push(RawWarning {
793 range,
794 code: WarningCode::ShadowedGlobalIdentifier,
795 message: format!("The {what} \"{name}\" has the same name as a {kind}."),
796 });
797 }
798 if let Some((name, range)) = member_decl_name(m)
800 && is_confusable_identifier(name)
801 {
802 out.push(RawWarning {
803 range,
804 code: WarningCode::ConfusableIdentifier,
805 message: format!(
806 "The identifier \"{name}\" uses confusable characters (mixed scripts)."
807 ),
808 });
809 }
810 match m {
811 Member::Var(v) if !v.has_init => {
813 if let Some(tref) = &v.type_ref
814 && matches!(resolve::resolve_type_name(db, api, tref), Ty::Enum(_))
815 {
816 out.push(RawWarning {
817 range: v.name_range,
818 code: WarningCode::EnumVariableWithoutDefault,
819 message: format!(
820 "The enum variable \"{}\" has no default value (it defaults to 0, which may not be a valid enum value).",
821 v.name
822 ),
823 });
824 }
825 }
826 Member::Signal(s) => {
830 if let Some(uses) = &uses
831 && !uses.is_referenced(&s.name)
832 {
833 out.push(RawWarning {
834 range: s.name_range,
835 code: WarningCode::UnusedSignal,
836 message: format!(
837 "The signal \"{}\" is never emitted or connected in this file.",
838 s.name
839 ),
840 });
841 }
842 }
843 Member::Func(f) => {
850 if let Some(base) = engine_base
851 && let Some(MemberRef::Method(vsig)) = api.lookup_member(base, &f.name)
852 && vsig.is_virtual
853 {
854 for (p, vp) in f.params.iter().zip(vsig.params.iter()) {
855 let Some(ann) = &p.type_ref else { continue };
856 let pty = resolve::resolve_type_name(db, api, ann);
857 let vty = ty::resolve_tyref(api, &vp.ty);
858 if types_definitely_clash(api, &pty, &vty) {
859 out.push(RawWarning {
860 range: f.name_range,
861 code: WarningCode::NativeMethodOverride,
862 message: format!(
863 "The override of the native virtual method \"{}\" has an incompatible type for parameter \"{}\".",
864 f.name, p.name
865 ),
866 });
867 break; }
869 }
870 }
871 }
872 _ => {}
873 }
874 }
875 out
876}
877
878fn types_definitely_clash(api: &EngineApi, a: &Ty, b: &Ty) -> bool {
883 if a.is_uninformative() || b.is_uninformative() {
884 return false;
885 }
886 if matches!(a, Ty::Enum(_)) || matches!(b, Ty::Enum(_)) {
891 return false;
892 }
893 matches!(ty::is_assignable(api, a, b), Assign::No)
894 && matches!(ty::is_assignable(api, b, a), Assign::No)
895}
896
897struct NameUses {
900 ident_counts: FxHashMap<SmolStr, u32>,
901 strings: FxHashSet<SmolStr>,
902}
903
904impl NameUses {
905 fn collect(root: &GdNode) -> Self {
906 let mut ident_counts: FxHashMap<SmolStr, u32> = FxHashMap::default();
907 let mut strings: FxHashSet<SmolStr> = FxHashSet::default();
908 for node in gdscript_syntax::ast::descendants(root) {
909 for el in node.children_with_tokens() {
910 let Some(tok) = el.into_token() else { continue };
911 match tok.kind() {
912 gdscript_syntax::SyntaxKind::Ident => {
913 *ident_counts.entry(SmolStr::new(tok.text())).or_insert(0) += 1;
914 }
915 gdscript_syntax::SyntaxKind::String => {
916 strings.insert(SmolStr::new(tok.text().trim_matches(['"', '\''])));
917 }
918 _ => {}
919 }
920 }
921 }
922 Self {
923 ident_counts,
924 strings,
925 }
926 }
927
928 fn is_referenced(&self, name: &str) -> bool {
931 self.ident_counts.get(name).copied().unwrap_or(0) > 1 || self.strings.contains(name)
932 }
933}
934
935fn collisions_contains(db: &dyn Db, name: &SmolStr) -> bool {
939 db.source_root()
940 .is_some_and(|root| crate::queries::class_name_collisions(db, root).contains(name))
941}
942
943fn is_autoload_singleton(db: &dyn Db, name: &str) -> bool {
946 db.project_config().is_some_and(|config| {
947 crate::queries::autoload_registry(db, config)
948 .resolve_path(name)
949 .is_some()
950 })
951}
952
953fn user_base_is_tool(
957 db: &dyn Db,
958 api: &EngineApi,
959 tree: &ItemTree,
960 res_path: Option<&str>,
961) -> bool {
962 let Ty::ScriptRef(sref) = resolve::resolve_base(db, api, tree, res_path) else {
963 return false;
964 };
965 let Some(ft) = db.file_text(FileId(sref.0)) else {
966 return false;
967 };
968 has_annotation(&crate::queries::item_tree(db, ft).annotations, "tool")
969}
970
971fn is_registered_global_class(db: &dyn Db, name: &str) -> bool {
975 db.source_root().is_some_and(|root| {
976 crate::queries::global_registry(db, root)
977 .resolve(name)
978 .is_some()
979 })
980}
981
982fn shadowed_global_kind(db: &dyn Db, api: &EngineApi, name: &str) -> Option<&'static str> {
991 match resolve::resolve_global(api, name) {
992 Some(GlobalDef::Builtin | GlobalDef::Utility) => return Some("built-in function"),
993 Some(GlobalDef::BuiltinType(_)) => return Some("built-in type"),
994 Some(GlobalDef::ClassType(_)) => return Some("native class"),
995 Some(GlobalDef::Singleton(_)) => return Some("engine singleton"),
996 Some(GlobalDef::Const(_) | GlobalDef::GlobalEnum) | None => {}
998 }
999 if is_registered_global_class(db, name) {
1000 return Some("global class");
1001 }
1002 if is_autoload_singleton(db, name) {
1003 return Some("autoload");
1004 }
1005 None
1006}
1007
1008fn member_value_decl(m: &Member) -> Option<(&SmolStr, TextRange, &'static str)> {
1012 match m {
1013 Member::Var(v) => Some((&v.name, v.name_range, "variable")),
1014 Member::Const(c) => Some((&c.name, c.name_range, "constant")),
1015 Member::Signal(s) => Some((&s.name, s.name_range, "signal")),
1016 _ => None,
1017 }
1018}
1019
1020fn member_decl_name(m: &Member) -> Option<(&SmolStr, TextRange)> {
1024 match m {
1025 Member::Func(f) => Some((&f.name, f.name_range)),
1026 Member::Var(v) => Some((&v.name, v.name_range)),
1027 Member::Const(c) => Some((&c.name, c.name_range)),
1028 Member::Signal(s) => Some((&s.name, s.name_range)),
1029 Member::Class(c) => Some((&c.name, c.name_range)),
1030 Member::Enum(e) => e.name.as_ref().map(|n| (n, e.name_range)),
1031 }
1032}
1033
1034fn is_confusable_identifier(name: &str) -> bool {
1040 use unicode_security::RestrictionLevel as RL;
1041 use unicode_security::RestrictionLevelDetection;
1042 if name.is_ascii() {
1043 return false; }
1045 name.detect_restriction_level() >= RL::MinimallyRestrictive
1046}
1047
1048fn class_name_decl_range(root: &GdNode) -> Option<TextRange> {
1053 use gdscript_syntax::SyntaxKind;
1054 let decl = gdscript_syntax::ast::descendants(root)
1055 .into_iter()
1056 .find(|n| n.kind() == SyntaxKind::ClassNameDecl)?;
1057 let name_node = decl.children().find(|c| c.kind() == SyntaxKind::Name)?;
1058 let r = cst::text_range_of(name_node);
1059 let text = name_node.text().to_string();
1060 let lead = u32::try_from(text.len() - text.trim_start().len()).unwrap_or(0);
1061 let len = u32::try_from(text.trim().len()).unwrap_or(0);
1062 Some(TextRange::new(r.start + lead, r.start + lead + len))
1063}
1064
1065fn extends_decl_range(root: &GdNode) -> Option<TextRange> {
1072 use gdscript_syntax::SyntaxKind;
1073 for child in root.children() {
1074 match child.kind() {
1075 SyntaxKind::ExtendsClause => return Some(cst::text_range_of(child)),
1077 SyntaxKind::ClassNameDecl => {
1079 if let Some(kw) = child.children().find(|c| c.kind() == SyntaxKind::ExtendsKw) {
1080 let start = cst::text_range_of(kw).start;
1081 let end = cst::text_range_of(child).end;
1082 return Some(TextRange::new(start, end));
1083 }
1084 }
1085 _ => {}
1086 }
1087 }
1088 None
1089}
1090
1091fn extends_chain_is_cyclic(db: &dyn Db, start: FileId) -> bool {
1098 use std::collections::HashSet;
1099 let mut visited: HashSet<FileId> = HashSet::new();
1100 visited.insert(start);
1101 let mut current = start;
1102 for _ in 0..=64 {
1103 let Some(file) = db.file_text(current) else {
1104 return false;
1105 };
1106 let base = crate::queries::script_class(db, file).base().clone();
1107 let Ty::ScriptRef(next) = base else {
1108 return false; };
1110 let next_id = FileId(next.0);
1111 if !visited.insert(next_id) {
1112 return true;
1115 }
1116 current = next_id;
1117 }
1118 false
1119}
1120
1121fn unit_from_decl(
1123 db: &dyn Db,
1124 api: &EngineApi,
1125 root: &GdNode,
1126 class: &ClassScope,
1127 ptr: AstPtr,
1128 range: TextRange,
1129) -> Option<Unit> {
1130 let node = ptr.to_node(root)?;
1131 let body = body::body_of_decl_stmt(&node);
1132 let result = infer(db, api, root, class, &body, Ty::Variant, false);
1133 Some(Unit {
1134 range,
1135 body,
1136 result,
1137 })
1138}
1139
1140enum Expectation {
1142 None,
1144 Has(Ty),
1146}
1147
1148fn find_inner_class<'a>(tree: &'a ItemTree, path: &str) -> Option<&'a InnerClassItem> {
1151 let mut members: &'a [Member] = &tree.members;
1152 let mut found: Option<&'a InnerClassItem> = None;
1153 for seg in path.split('.') {
1154 found = members.iter().find_map(|m| match m {
1155 Member::Class(c) if c.name == seg => Some(c),
1156 _ => None,
1157 });
1158 members = &found?.tree.members;
1159 }
1160 found
1161}
1162
1163struct Cx<'a> {
1164 db: &'a dyn Db,
1165 api: &'a EngineApi,
1166 root: &'a GdNode,
1167 body: &'a Body,
1168 class: &'a ClassScope<'a>,
1169 self_ty: Ty,
1170 return_ty: Ty,
1171 expr_ty: FxHashMap<ExprId, Ty>,
1172 bindings: Vec<Binding>,
1173 diagnostics: Vec<Diagnostic>,
1174 raw_warnings: Vec<RawWarning>,
1176 locals: FxHashMap<SmolStr, Ty>,
1178 used_locals: FxHashSet<SmolStr>,
1183 narrowing: FxHashMap<String, Ty>,
1186 flow: FlowAnalysis,
1189 is_func_body: bool,
1193 assigned: flow::AssignedAnalysis,
1196 cur_stmt: Option<body::StmtId>,
1199 needs_assignment: FxHashSet<SmolStr>,
1203 assign_lhs: FxHashSet<ExprId>,
1206}
1207
1208impl Cx<'_> {
1209 fn builtin(&self, name: &str) -> Ty {
1212 self.api
1213 .builtin_by_name(name)
1214 .map_or(Ty::Variant, Ty::Builtin)
1215 }
1216 fn int_ty(&self) -> Ty {
1217 self.builtin("int")
1218 }
1219 fn float_ty(&self) -> Ty {
1220 self.builtin("float")
1221 }
1222 fn bool_ty(&self) -> Ty {
1223 self.builtin("bool")
1224 }
1225 fn is_int(&self, ty: &Ty) -> bool {
1226 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "int")
1227 }
1228 fn is_float(&self, ty: &Ty) -> bool {
1229 matches!(ty, Ty::Builtin(b) if self.api.builtin(*b).name == "float")
1230 }
1231 fn is_numeric(&self, ty: &Ty) -> bool {
1232 self.is_int(ty) || self.is_float(ty)
1233 }
1234
1235 fn emit(&mut self, range: TextRange, severity: Severity, code: &str, message: String) {
1238 self.diagnostics.push(Diagnostic {
1239 range,
1240 severity,
1241 code: code.to_owned(),
1242 message,
1243 source: DiagnosticSource::Type,
1244 fixes: Vec::new(),
1245 });
1246 }
1247
1248 fn warn(&mut self, range: TextRange, code: WarningCode, message: String) {
1252 self.raw_warnings.push(RawWarning {
1253 range,
1254 code,
1255 message,
1256 });
1257 }
1258
1259 fn range_of(&self, id: ExprId) -> TextRange {
1260 self.body.source_map.expr_range(id)
1261 }
1262
1263 fn check_assign(&mut self, from: &Ty, to: &Ty, range: TextRange) {
1266 match ty::is_assignable(self.api, from, to) {
1267 Assign::Narrowing => self.warn(
1268 range,
1269 WarningCode::NarrowingConversion,
1270 "Narrowing conversion (float is converted to int and loses precision).".to_owned(),
1271 ),
1272 Assign::No => {
1273 let to_label = to.label(self.api).unwrap_or_else(|| "?".to_owned());
1274 let from_label = from.label(self.api).unwrap_or_else(|| "?".to_owned());
1275 self.emit(
1276 range,
1277 Severity::Error,
1278 TYPE_MISMATCH,
1279 format!(
1280 "Cannot assign a value of type \"{from_label}\" to a target of type \"{to_label}\"."
1281 ),
1282 );
1283 }
1284 Assign::IntAsEnum => self.warn(
1286 range,
1287 WarningCode::IntAsEnumWithoutCast,
1288 "Integer used when an enum value is expected. Cast the value to the enum type."
1289 .to_owned(),
1290 ),
1291 Assign::Ok | Assign::OkUnsafe => {}
1292 }
1293 }
1294
1295 fn check_standalone(&mut self, e: ExprId) {
1299 if self.expr_has_side_effect(e) {
1300 return;
1301 }
1302 match self.body.expr(e) {
1303 Expr::Ternary { .. } => self.warn(
1304 self.range_of(e),
1305 WarningCode::StandaloneTernary,
1306 "Standalone ternary conditional: the return value is discarded.".to_owned(),
1307 ),
1308 Expr::Missing | Expr::Lambda { .. } | Expr::GetNode { .. } | Expr::Preload { .. } => {}
1310 _ => self.warn(
1311 self.range_of(e),
1312 WarningCode::StandaloneExpression,
1313 "Standalone expression (the line has no effect).".to_owned(),
1314 ),
1315 }
1316 }
1317
1318 fn expr_has_side_effect(&self, e: ExprId) -> bool {
1321 match self.body.expr(e) {
1322 Expr::Call { .. }
1323 | Expr::Await(_)
1324 | Expr::Preload { .. }
1325 | Expr::Bin {
1326 op: BinOp::Assign, ..
1327 } => true,
1328 Expr::Bin { lhs, rhs, .. }
1329 | Expr::In { lhs, rhs, .. }
1330 | Expr::Index {
1331 base: lhs,
1332 index: rhs,
1333 } => self.expr_has_side_effect(*lhs) || self.expr_has_side_effect(*rhs),
1334 Expr::Unary { operand, .. }
1335 | Expr::Paren(operand)
1336 | Expr::Cast { operand, .. }
1337 | Expr::Is { operand, .. } => self.expr_has_side_effect(*operand),
1338 Expr::Field { receiver, .. } => self.expr_has_side_effect(*receiver),
1339 Expr::Ternary {
1340 cond,
1341 then_branch,
1342 else_branch,
1343 } => {
1344 self.expr_has_side_effect(*cond)
1345 || self.expr_has_side_effect(*then_branch)
1346 || self.expr_has_side_effect(*else_branch)
1347 }
1348 Expr::Array(items) => items.iter().any(|&i| self.expr_has_side_effect(i)),
1349 Expr::Dict(entries) => entries.iter().any(|(k, v)| {
1350 self.expr_has_side_effect(*k) || v.is_some_and(|e| self.expr_has_side_effect(e))
1351 }),
1352 _ => false,
1353 }
1354 }
1355
1356 fn infer_block(&mut self, block: &[body::StmtId]) {
1359 for &stmt in block {
1360 self.infer_stmt(stmt);
1361 }
1362 }
1363
1364 fn infer_stmt(&mut self, id: body::StmtId) {
1365 self.narrowing = self.facts_to_narrowing(id);
1368 self.cur_stmt = Some(id); match self.body.stmt(id).clone() {
1370 Stmt::Expr(e) => {
1371 self.infer_expr(e, &Expectation::None);
1372 self.check_standalone(e);
1373 }
1374 Stmt::Var(v) => self.infer_local_var(&v),
1375 Stmt::Return(e) => {
1376 if let Some(e) = e {
1377 let expected = if self.return_ty.is_uninformative() {
1378 Expectation::None
1379 } else {
1380 Expectation::Has(self.return_ty.clone())
1381 };
1382 let t = self.infer_expr(e, &expected);
1383 if let Expectation::Has(ret) = expected {
1384 self.check_assign(&t, &ret, self.range_of(e));
1385 }
1386 }
1387 }
1388 Stmt::If {
1389 cond,
1390 then_branch,
1391 elifs,
1392 else_branch,
1393 } => {
1394 let at_if = self.narrowing.clone();
1398 self.infer_expr(cond, &Expectation::None);
1399 self.infer_block(&then_branch);
1400 for (econd, eblock) in elifs {
1401 self.narrowing.clone_from(&at_if);
1402 self.infer_expr(econd, &Expectation::None);
1403 self.infer_block(&eblock);
1404 }
1405 if let Some(eb) = else_branch {
1406 self.infer_block(&eb);
1407 }
1408 }
1409 Stmt::While { cond, body } => {
1410 self.infer_expr(cond, &Expectation::None);
1411 self.infer_block(&body);
1412 }
1413 Stmt::For(f) => {
1414 let iter_ty = self.infer_expr(f.iter, &Expectation::None);
1415 let var_ty = f.var_type.as_ref().map_or_else(
1416 || self.loop_var_ty(&iter_ty),
1417 |ptr| self.resolve_ptr_ty(*ptr),
1418 );
1419 self.bindings.push(Binding {
1420 name: f.var.clone(),
1421 name_range: f.var_range,
1422 ty: var_ty.clone(),
1423 init: None,
1424 annotated: f.var_type.is_some(),
1425 inferred_colon_eq: false,
1426 is_const: false,
1427 kind: BindingKind::ForVar,
1428 });
1429 self.locals.insert(f.var.clone(), var_ty);
1430 self.infer_block(&f.body);
1431 }
1432 Stmt::Match { scrutinee, arms } => {
1433 let at_match = self.narrowing.clone();
1434 self.infer_expr(scrutinee, &Expectation::None);
1435 for arm in arms {
1436 self.narrowing.clone_from(&at_match);
1439 for b in &arm.binds {
1440 self.bindings.push(Binding {
1444 name: b.name.clone(),
1445 name_range: b.range,
1446 ty: Ty::Variant,
1447 init: None,
1448 annotated: false,
1449 inferred_colon_eq: false,
1450 is_const: false,
1451 kind: BindingKind::MatchBind,
1452 });
1453 self.locals.insert(b.name.clone(), Ty::Variant);
1454 }
1455 if let Some(g) = arm.guard {
1456 self.infer_expr(g, &Expectation::None);
1457 }
1458 self.infer_block(&arm.body);
1459 }
1460 }
1461 Stmt::Break | Stmt::Continue | Stmt::Pass => {}
1462 Stmt::Assert(cond) => {
1463 if let Some(cond) = cond {
1464 self.infer_expr(cond, &Expectation::None);
1465 self.check_assert_constant(cond);
1466 }
1467 }
1468 }
1469 }
1470
1471 fn check_assert_constant(&mut self, cond: ExprId) {
1475 let Some(always) = self.const_bool_of(cond) else {
1476 return;
1477 };
1478 let (code, msg) = if always {
1479 (
1480 WarningCode::AssertAlwaysTrue,
1481 "The assert condition is always true, so this assert has no effect.",
1482 )
1483 } else {
1484 (
1485 WarningCode::AssertAlwaysFalse,
1486 "The assert condition is always false, so this assert will always fail.",
1487 )
1488 };
1489 self.warn(self.range_of(cond), code, msg.to_owned());
1490 }
1491
1492 fn const_bool_of(&self, expr: ExprId) -> Option<bool> {
1497 match self.body.expr(expr) {
1498 Expr::Literal(Literal::Bool(b)) => Some(*b),
1499 Expr::Literal(Literal::Null) => Some(false),
1500 _ => None,
1501 }
1502 }
1503
1504 fn infer_local_var(&mut self, v: &body::LocalVar) {
1505 let annotated = v.type_ref.map(|p| self.resolve_ptr_ty(p));
1506 let init_ty = v.init.map(|e| {
1507 let expected = annotated
1508 .as_ref()
1509 .map_or(Expectation::None, |t| Expectation::Has(t.clone()));
1510 self.infer_expr(e, &expected)
1511 });
1512 let range = v.init.map_or(v.name_range, |e| self.range_of(e));
1513
1514 let binding_ty = match (&annotated, &init_ty) {
1515 (Some(t), Some(init)) => {
1517 self.check_assign(init, t, range);
1518 t.clone()
1519 }
1520 (Some(t), None) => t.clone(),
1522 (None, Some(init)) if v.is_inferred => {
1524 if init.is_variant() {
1525 self.warn(
1526 range,
1527 WarningCode::InferenceOnVariant,
1528 inference_on_variant_msg(if v.is_const { "constant" } else { "variable" }),
1529 );
1530 Ty::Variant
1531 } else {
1532 init.clone()
1534 }
1535 }
1536 (None, Some(init)) => {
1538 if v.is_const {
1539 init.clone()
1540 } else {
1541 Ty::Variant
1542 }
1543 }
1544 (None, None) => Ty::Variant,
1545 };
1546 let shadows_param = self
1551 .bindings
1552 .iter()
1553 .any(|b| b.kind == BindingKind::Param && b.name == v.name);
1554 let shadows_member = match self.class.lookup(&v.name) {
1557 Some(ClassItem::EnumVariant) => true,
1558 Some(item) => matches!(
1559 self.class.member(item),
1560 Some(Member::Var(_) | Member::Const(_) | Member::Signal(_))
1561 ),
1562 None => false,
1563 };
1564 if self.is_func_body {
1565 let what = if v.is_const { "constant" } else { "variable" };
1566 if shadowed_global_kind(self.db, self.api, &v.name).is_none() {
1571 if shadows_param || shadows_member {
1572 let outer = if shadows_param {
1573 "parameter"
1574 } else {
1575 "class member"
1576 };
1577 self.warn(
1578 v.name_range,
1579 WarningCode::ShadowedVariable,
1580 format!(
1581 "The local {what} \"{}\" shadows a {outer} of the same name.",
1582 v.name
1583 ),
1584 );
1585 } else if self.engine_base_has_value_member(&v.name) {
1586 self.warn(
1588 v.name_range,
1589 WarningCode::ShadowedVariableBaseClass,
1590 format!(
1591 "The local {what} \"{}\" shadows a member of a base class.",
1592 v.name
1593 ),
1594 );
1595 }
1596 }
1597 if v.init.is_none() && matches!(annotated.as_ref(), Some(Ty::Enum(_))) {
1600 self.warn(
1601 v.name_range,
1602 WarningCode::EnumVariableWithoutDefault,
1603 format!(
1604 "The enum variable \"{}\" has no default value (it defaults to 0, which may not be a valid enum value).",
1605 v.name
1606 ),
1607 );
1608 }
1609 if v.type_ref.is_some() && v.init.is_none() {
1612 self.needs_assignment.insert(v.name.clone());
1613 }
1614 }
1615 self.bindings.push(Binding {
1616 name: v.name.clone(),
1617 name_range: v.name_range,
1618 ty: binding_ty.clone(),
1619 init: v.init,
1620 annotated: v.type_ref.is_some(),
1621 inferred_colon_eq: v.is_inferred,
1622 is_const: v.is_const,
1623 kind: BindingKind::Var,
1624 });
1625 self.locals.insert(v.name.clone(), binding_ty);
1627 }
1628
1629 fn infer_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1632 let ty = self.synth_expr(id, expected);
1633 self.expr_ty.insert(id, ty.clone());
1634 ty
1635 }
1636
1637 #[allow(clippy::too_many_lines)]
1638 fn synth_expr(&mut self, id: ExprId, expected: &Expectation) -> Ty {
1639 match self.body.expr(id).clone() {
1640 Expr::Missing => Ty::Error,
1641 Expr::Literal(lit) => self.literal_ty(lit),
1642 Expr::Name(name) => self.resolve_name(id, &name),
1643 Expr::SelfExpr => self.self_ty.clone(),
1644 Expr::Super => self.class.base.clone(),
1645 Expr::Paren(inner) => self.infer_expr(inner, expected),
1646 Expr::Bin { op, lhs, rhs } => self.infer_bin(id, op, lhs, rhs),
1647 Expr::Unary { op, operand } => {
1648 let t = self.infer_expr(operand, &Expectation::None);
1649 match op {
1650 UnOp::Not => self.bool_ty(),
1651 UnOp::BitNot => self.int_ty(),
1652 UnOp::Neg | UnOp::Pos => {
1653 if t.is_uninformative() || self.is_numeric(&t) {
1654 t
1655 } else {
1656 Ty::Variant
1657 }
1658 }
1659 }
1660 }
1661 Expr::Ternary {
1662 cond,
1663 then_branch,
1664 else_branch,
1665 } => {
1666 self.infer_expr(cond, &Expectation::None);
1667 let a = self.infer_expr(then_branch, expected);
1668 let b = self.infer_expr(else_branch, expected);
1669 if self.is_null(else_branch) {
1671 a
1672 } else if self.is_null(then_branch) {
1673 b
1674 } else {
1675 let r = self.join(&a, &b);
1676 if r.is_variant() && !a.is_uninformative() && !b.is_uninformative() {
1679 self.warn(
1680 self.range_of(id),
1681 WarningCode::IncompatibleTernary,
1682 "The values of the ternary conditional are not mutually compatible."
1683 .to_owned(),
1684 );
1685 }
1686 r
1687 }
1688 }
1689 Expr::Call { callee, args } => self.infer_call(callee, &args),
1690 Expr::Field {
1691 receiver,
1692 name,
1693 name_range,
1694 } => {
1695 self.infer_field(receiver, &name, name_range, false)
1696 }
1697 Expr::Index { base, index } => {
1698 let base_ty = self.infer_expr(base, &Expectation::None);
1699 self.infer_expr(index, &Expectation::None);
1700 self.index_ty(&base_ty)
1701 }
1702 Expr::Is { operand, .. } => {
1703 self.infer_expr(operand, &Expectation::None);
1704 self.bool_ty()
1705 }
1706 Expr::Cast { operand, ty } => {
1707 self.infer_expr(operand, &Expectation::None);
1708 ty.map_or(Ty::Variant, |p| self.resolve_ptr_ty(p))
1709 }
1710 Expr::In { lhs, rhs, .. } => {
1711 self.infer_expr(lhs, &Expectation::None);
1712 self.infer_expr(rhs, &Expectation::None);
1713 self.bool_ty()
1714 }
1715 Expr::Await(operand) => {
1716 let operand_ty = self.infer_expr(operand, &Expectation::None);
1717 if matches!(operand_ty, Ty::Signal(_)) {
1722 Ty::Unknown
1723 } else {
1724 operand_ty
1725 }
1726 }
1727 Expr::Array(elems) => {
1728 let pushed = match expected {
1732 Expectation::Has(Ty::Array(e)) => Some((**e).clone()),
1733 _ => None,
1734 };
1735 let elem_exp = pushed.clone().map_or(Expectation::None, Expectation::Has);
1736 for e in elems {
1737 self.infer_expr(e, &elem_exp);
1738 }
1739 pushed.map_or_else(Ty::array_of_variant, |e| Ty::Array(Box::new(e)))
1740 }
1741 Expr::Dict(entries) => {
1742 let pushed = match expected {
1743 Expectation::Has(Ty::Dict(k, v)) => Some(((**k).clone(), (**v).clone())),
1744 _ => None,
1745 };
1746 let (kx, vx) = pushed
1747 .clone()
1748 .map_or((Expectation::None, Expectation::None), |(k, v)| {
1749 (Expectation::Has(k), Expectation::Has(v))
1750 });
1751 for (k, v) in entries {
1752 self.infer_expr(k, &kx);
1753 if let Some(v) = v {
1754 self.infer_expr(v, &vx);
1755 }
1756 }
1757 pushed.map_or_else(Ty::dict_of_variant, |(k, v)| {
1758 Ty::Dict(Box::new(k), Box::new(v))
1759 })
1760 }
1761 Expr::Lambda { params, body } => {
1762 self.infer_lambda(¶ms, &body);
1763 Ty::Callable
1764 }
1765 Expr::Preload { arg, path } => {
1766 if let Some(arg) = arg {
1767 self.infer_expr(arg, &Expectation::None);
1768 }
1769 match path {
1774 Some(p) => {
1778 match resolve::anchor_res_path(self.self_res_path().as_deref(), &p) {
1779 Some(abs) => resolve::resolve_external(
1780 self.db,
1781 &resolve::ExternalRef::Preload(abs),
1782 ),
1783 None => Ty::Unknown,
1784 }
1785 }
1786 None => Ty::Unknown,
1787 }
1788 }
1789 Expr::GetNode { path, unique } => self.resolve_node_path(id, path.as_deref(), unique),
1792 }
1793 }
1794
1795 fn is_null(&self, id: ExprId) -> bool {
1797 matches!(self.body.expr(id), Expr::Literal(Literal::Null))
1798 }
1799
1800 fn literal_ty(&self, lit: Literal) -> Ty {
1801 match lit {
1802 Literal::Int => self.int_ty(),
1803 Literal::Float | Literal::MathConst => self.float_ty(),
1804 Literal::Bool(_) => self.bool_ty(),
1805 Literal::Str => self.builtin("String"),
1806 Literal::StringName => self.builtin("StringName"),
1807 Literal::NodePath => self.builtin("NodePath"),
1808 Literal::Null => Ty::Variant,
1810 }
1811 }
1812
1813 fn node_ty(&self) -> Ty {
1814 self.api
1815 .class_by_name("Node")
1816 .map_or(Ty::Unknown, Ty::Object)
1817 }
1818
1819 fn resolve_node_path(&mut self, id: ExprId, path: Option<&str>, unique: bool) -> Ty {
1827 use gdscript_scene::NodePathResolution as R;
1828 let fallback = self.node_ty();
1829 let Some(path) = path else {
1830 return fallback; };
1832 if !unique && let Some(ty) = self.resolve_root_autoload_path(path) {
1836 return ty;
1837 }
1838 let Some(ctx) = self.owning_scene() else {
1839 return fallback; };
1841 if ctx.ambiguous {
1846 return self.union_node_ty(path, unique).unwrap_or(fallback);
1847 }
1848 let resolution = if unique {
1849 ctx.model.classify_unique(path)
1850 } else {
1851 ctx.model.classify_path_from(ctx.attach, path)
1852 };
1853 match resolution {
1854 R::Resolved(idx) => ctx
1855 .model
1856 .node(idx)
1857 .and_then(|n| self.scene_node_ty(&ctx.model, n, 0))
1858 .unwrap_or(fallback),
1859 R::Missing => {
1860 let what = if unique { "unique name" } else { "node path" };
1861 let sigil = if unique { "%" } else { "$" };
1862 self.emit(
1863 self.range_of(id),
1864 Severity::Warning,
1865 INVALID_NODE_PATH,
1866 format!("no {what} `{sigil}{path}` in the owning scene"),
1867 );
1868 fallback
1869 }
1870 R::IntoInstance => {
1873 let walked = if unique {
1874 ctx.model.resolve_unique_into_instance(path)
1875 } else {
1876 ctx.model.resolve_into_instance(ctx.attach, path)
1877 };
1878 walked
1879 .and_then(|(inst, tail)| {
1880 let inst_node = ctx.model.node(inst)?;
1881 self.resolve_into_instance_ty(&ctx.model, inst_node, &tail, 0)
1882 })
1883 .unwrap_or(fallback)
1884 }
1885 R::Escaped => fallback,
1887 }
1888 }
1889
1890 fn union_node_ty(&self, path: &str, unique: bool) -> Option<Ty> {
1895 use gdscript_scene::NodePathResolution as R;
1896 let res_path = self.self_res_path()?;
1897 let root = self.db.source_root()?;
1898 let attaches = crate::queries::script_scene_attachments(self.db, root)
1899 .get(res_path.as_str())
1900 .cloned()?;
1901 let mut acc: Option<Ty> = None;
1902 for (scene_file, attach) in &attaches {
1903 let ft = self.db.file_text(*scene_file)?;
1904 let model = crate::queries::scene_model(self.db, ft);
1905 let resolution = if unique {
1906 model.classify_unique(path)
1907 } else {
1908 model.classify_path_from(*attach, path)
1909 };
1910 let R::Resolved(idx) = resolution else {
1911 return None; };
1913 let ty = model
1914 .node(idx)
1915 .and_then(|n| self.scene_node_ty(&model, n, 0))?;
1916 acc = Some(match acc {
1917 None => ty,
1918 Some(prev) => self.common_base(&prev, &ty),
1919 });
1920 }
1921 acc
1922 }
1923
1924 fn common_base(&self, a: &Ty, b: &Ty) -> Ty {
1929 if a == b {
1930 return a.clone();
1931 }
1932 if let (Ty::Object(ca), Ty::Object(cb)) = (a, b) {
1933 let mut cur = Some(*ca);
1934 while let Some(c) = cur {
1935 if self.api.is_subclass(*cb, c) {
1936 return Ty::Object(c);
1937 }
1938 cur = self.api.class(c).base;
1939 }
1940 }
1941 self.node_ty()
1942 }
1943
1944 fn resolve_root_autoload_path(&self, path: &str) -> Option<Ty> {
1949 let name = path.strip_prefix("/root/")?;
1950 if name.is_empty() || name.contains('/') {
1952 return None;
1953 }
1954 let ty = resolve::resolve_autoload_any(self.db, name);
1955 (!ty.is_uninformative()).then_some(ty)
1956 }
1957
1958 fn owning_scene(&self) -> Option<crate::queries::SceneContext> {
1962 let Ty::ScriptRef(sref) = &self.self_ty else {
1963 return None;
1964 };
1965 let ft = self.db.file_text(FileId(sref.0))?;
1966 crate::queries::scene_context(self.db, ft)
1967 }
1968
1969 fn self_res_path(&self) -> Option<SmolStr> {
1972 let Ty::ScriptRef(sref) = &self.self_ty else {
1973 return None;
1974 };
1975 self.db.file_text(FileId(sref.0))?.res_path(self.db)
1976 }
1977
1978 fn scene_node_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
1983 if let Some(script_ty) = self.node_script_ref(scene, node) {
1984 return Some(script_ty);
1985 }
1986 if let Some(decl) = node.decl_type.as_ref() {
1987 let ty = resolve::resolve_type_name(self.db, self.api, decl);
1988 if !ty.is_uninformative() {
1989 return Some(ty);
1990 }
1991 }
1992 self.instance_root_ty(scene, node, depth)
1993 .or_else(|| self.override_child_ty(scene, node, depth))
1994 }
1995
1996 fn override_child_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
2004 if depth >= 16 {
2005 return None;
2006 }
2007 let mut segs_rev: Vec<String> = vec![node.name.to_string()];
2008 let mut parent_idx = node.parent_idx?;
2009 let mut guard = 0u32;
2010 loop {
2011 let parent = scene.node(parent_idx)?;
2012 if parent.instance.is_some() {
2013 segs_rev.reverse();
2014 let rel = segs_rev.join("/");
2015 return self.resolve_into_instance_ty(scene, parent, &rel, depth + 1);
2016 }
2017 segs_rev.push(parent.name.to_string());
2018 parent_idx = parent.parent_idx?;
2019 guard += 1;
2020 if guard > 4096 {
2021 return None;
2022 }
2023 }
2024 }
2025
2026 fn instance_root_ty(&self, scene: &SceneModel, node: &SceneNode, depth: u32) -> Option<Ty> {
2031 if depth >= 16 {
2032 return None;
2033 }
2034 let (sub, sub_root) = self.instance_subscene(scene, node)?;
2035 let root_node = sub.node(sub_root)?;
2036 self.scene_node_ty(&sub, root_node, depth + 1)
2037 }
2038
2039 fn instance_subscene(
2044 &self,
2045 scene: &SceneModel,
2046 node: &SceneNode,
2047 ) -> Option<(Arc<SceneModel>, gdscript_scene::NodeIdx)> {
2048 let inst = node.instance.as_ref()?;
2049 let path = scene.ext_resources.get(inst)?.path.as_ref()?;
2050 let root = self.db.source_root()?;
2051 let file = crate::queries::res_path_registry(self.db, root)
2052 .get(path.as_str())
2053 .copied()?;
2054 let ft = self.db.file_text(file)?;
2055 let sub = crate::queries::scene_model(self.db, ft);
2056 let sub_root = sub.root?;
2057 Some((sub, sub_root))
2058 }
2059
2060 fn resolve_into_instance_ty(
2065 &self,
2066 scene: &SceneModel,
2067 instance_node: &SceneNode,
2068 tail: &str,
2069 depth: u32,
2070 ) -> Option<Ty> {
2071 if depth >= 16 {
2072 return None;
2073 }
2074 let (sub, sub_root) = self.instance_subscene(scene, instance_node)?;
2075 if let Some(idx) = sub.resolve_path_from(sub_root, tail) {
2076 let n = sub.node(idx)?;
2077 return self.scene_node_ty(&sub, n, depth + 1);
2078 }
2079 let (inner, inner_tail) = sub.resolve_into_instance(sub_root, tail)?;
2081 let inner_node = sub.node(inner)?;
2082 self.resolve_into_instance_ty(&sub, inner_node, &inner_tail, depth + 1)
2083 }
2084
2085 fn node_script_ref(&self, scene: &SceneModel, node: &SceneNode) -> Option<Ty> {
2088 let path = scene
2089 .ext_resources
2090 .get(node.script.as_ref()?)?
2091 .path
2092 .as_ref()?;
2093 let root = self.db.source_root()?;
2094 let file = crate::queries::res_path_registry(self.db, root)
2095 .get(path.as_str())
2096 .copied()?;
2097 Some(Ty::ScriptRef(ScriptRefId(file.0)))
2098 }
2099
2100 fn infer_bin(&mut self, id: ExprId, op: BinOp, lhs: ExprId, rhs: ExprId) -> Ty {
2101 if op == BinOp::Assign {
2102 return self.infer_assign(lhs, rhs);
2103 }
2104 if matches!(op, BinOp::And | BinOp::Or) {
2107 self.infer_expr(lhs, &Expectation::None);
2108 let saved = self.narrowing.clone();
2109 self.apply_condition_facts(lhs, op == BinOp::And);
2110 self.infer_expr(rhs, &Expectation::None);
2111 self.narrowing = saved;
2112 return self.bool_ty();
2113 }
2114 let lt = self.infer_expr(lhs, &Expectation::None);
2115 let rt = self.infer_expr(rhs, &Expectation::None);
2116 if op.is_boolean() {
2117 return self.bool_ty();
2118 }
2119 if op == BinOp::Div && self.is_int(<) && self.is_int(&rt) {
2121 self.warn(
2122 self.range_of(id),
2123 WarningCode::IntegerDivision,
2124 "Integer division. Decimal part will be discarded.".to_owned(),
2125 );
2126 return self.int_ty();
2127 }
2128 self.bin_result(op, <, &rt)
2129 }
2130
2131 fn infer_assign(&mut self, lhs: ExprId, rhs: ExprId) -> Ty {
2132 let slot = self.infer_expr(lhs, &Expectation::None);
2133 let expected = if slot.is_uninformative() {
2134 Expectation::None
2135 } else {
2136 Expectation::Has(slot.clone())
2137 };
2138 let value = self.infer_expr(rhs, &expected);
2139 if !slot.is_uninformative() {
2140 self.check_assign(&value, &slot, self.range_of(rhs));
2141 }
2142 slot
2145 }
2146
2147 fn bin_result(&self, op: BinOp, lt: &Ty, rt: &Ty) -> Ty {
2150 if let (Ty::Builtin(b), Some(sym)) = (lt, op_symbol(op)) {
2151 for o in self.api.builtin_operators(*b) {
2152 if o.op == sym
2153 && let Some(right) = &o.right
2154 && self.tyref_matches(right, rt)
2155 {
2156 return ty::resolve_tyref(self.api, &o.result);
2157 }
2158 }
2159 }
2160 if self.is_numeric(lt) && self.is_numeric(rt) {
2161 return if self.is_float(lt) || self.is_float(rt) {
2162 self.float_ty()
2163 } else {
2164 self.int_ty()
2165 };
2166 }
2167 if lt.is_unknown() || rt.is_unknown() || lt.is_error() || rt.is_error() {
2170 return Ty::Unknown;
2171 }
2172 Ty::Variant
2173 }
2174
2175 fn tyref_matches(&self, tyref: &TyRef, ty: &Ty) -> bool {
2176 let resolved = ty::resolve_tyref(self.api, tyref);
2177 resolved.is_variant() || &resolved == ty
2178 }
2179
2180 fn infer_call(&mut self, callee: ExprId, args: &[ExprId]) -> Ty {
2181 for &a in args {
2183 self.infer_expr(a, &Expectation::None);
2184 }
2185 let ret = match self.body.expr(callee).clone() {
2186 Expr::Field {
2187 receiver,
2188 name,
2189 name_range,
2190 } => {
2191 self.infer_field(receiver, &name, name_range, true)
2192 }
2193 Expr::Name(name) => {
2194 let ret = self.resolve_call_name(&name);
2195 self.expr_ty.insert(callee, Ty::Callable);
2196 ret
2197 }
2198 _ => {
2202 self.infer_expr(callee, &Expectation::None);
2203 Ty::Unknown
2204 }
2205 };
2206 self.check_call_args(callee, args);
2209 ret
2210 }
2211
2212 fn check_call_args(&mut self, callee: ExprId, args: &[ExprId]) {
2218 let Some(params) = self.call_param_tys(callee) else {
2219 return;
2220 };
2221 for (i, &arg) in args.iter().enumerate() {
2222 let Some(param_ty) = params.get(i) else {
2223 break; };
2225 if param_ty.is_uninformative() || param_ty.is_variant() {
2226 continue; }
2228 let arg_ty = self.expr_ty.get(&arg).cloned().unwrap_or(Ty::Unknown);
2230 if ty::is_assignable(self.api, &arg_ty, param_ty) == Assign::OkUnsafe {
2231 let pl = param_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
2232 let al = arg_ty.label(self.api).unwrap_or_else(|| "?".to_owned());
2233 self.warn(
2234 self.range_of(arg),
2235 WarningCode::UnsafeCallArgument,
2236 format!(
2237 "The argument {} requires a value of type \"{pl}\" but is passed \"{al}\", which is unsafe.",
2238 i + 1
2239 ),
2240 );
2241 }
2242 }
2243 }
2244
2245 fn call_param_tys(&self, callee: ExprId) -> Option<Vec<Ty>> {
2249 match self.body.expr(callee) {
2250 Expr::Name(name) => self.name_call_param_tys(name),
2251 Expr::Field { receiver, name, .. } => match self.expr_ty.get(receiver)? {
2252 Ty::Object(class) => match self.api.lookup_member(*class, name)? {
2253 MemberRef::Method(sig) => Some(
2254 sig.params
2255 .iter()
2256 .map(|p| ty::resolve_tyref(self.api, &p.ty))
2257 .collect(),
2258 ),
2259 _ => None,
2260 },
2261 _ => None,
2263 },
2264 _ => None,
2265 }
2266 }
2267
2268 fn name_call_param_tys(&self, name: &str) -> Option<Vec<Ty>> {
2272 if let Some(item) = self.class.lookup(name)
2273 && let Some(Member::Func(f)) = self.class.member(item)
2274 {
2275 return Some(
2276 f.params
2277 .iter()
2278 .map(|p| {
2279 p.type_ref.as_deref().map_or(Ty::Variant, |t| {
2280 resolve::resolve_type_name(self.db, self.api, t)
2281 })
2282 })
2283 .collect(),
2284 );
2285 }
2286 if let Ty::Object(base) = self.class.base
2287 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
2288 {
2289 return Some(
2290 sig.params
2291 .iter()
2292 .map(|p| ty::resolve_tyref(self.api, &p.ty))
2293 .collect(),
2294 );
2295 }
2296 None
2297 }
2298
2299 fn resolve_call_name(&self, name: &str) -> Ty {
2301 if let Some(item) = self.class.lookup(name)
2302 && let Some(Member::Func(f)) = self.class.member(item)
2303 {
2304 return self.func_return_ty(f.return_type.as_deref());
2305 }
2306 if let Ty::Object(base) = self.class.base
2308 && let Some(MemberRef::Method(sig)) = self.api.lookup_member(base, name)
2309 {
2310 return ty::resolve_tyref(self.api, &sig.return_ty);
2311 }
2312 if let Some(u) = self.api.utility(name) {
2313 return ty::resolve_tyref(self.api, &u.return_ty);
2314 }
2315 if let Some(f) = self.api.gdscript_builtin(name) {
2316 return resolve::layer_to_ty(self.api, f.ret);
2317 }
2318 if let Some(b) = self.api.builtin_by_name(name) {
2322 return ty::resolve_tyref(self.api, &TyRef::Builtin(b));
2323 }
2324 Ty::Unknown
2327 }
2328
2329 fn func_return_ty(&self, annotation: Option<&str>) -> Ty {
2330 annotation.map_or(Ty::Variant, |t| {
2331 resolve::resolve_type_name(self.db, self.api, t)
2332 })
2333 }
2334
2335 fn infer_field(
2339 &mut self,
2340 receiver: ExprId,
2341 name: &str,
2342 name_range: TextRange,
2343 as_method: bool,
2344 ) -> Ty {
2345 let is_self = matches!(self.body.expr(receiver), Expr::SelfExpr);
2346 let recv_ty = self.infer_expr(receiver, &Expectation::None);
2347
2348 if is_self && let Some(item) = self.class.lookup(name) {
2350 return self.own_member_ty(item, as_method);
2351 }
2352
2353 match &recv_ty {
2354 t if t.is_uninformative() => recv_ty.clone(),
2359 Ty::Object(class) => {
2360 if name == "new" {
2361 recv_ty.clone()
2364 } else if let Some(m) = self.api.lookup_member(*class, name) {
2365 self.check_member_kind_misuse(&m, as_method, name, name_range);
2366 self.check_static_on_instance(receiver, &m, as_method, name_range);
2367 self.member_ref_ty(&m, as_method)
2368 } else if let Some(t) = self.class_enum_value(*class, name) {
2369 t
2371 } else {
2372 self.emit_unsafe(name, &recv_ty, name_range, as_method);
2374 Ty::Variant
2375 }
2376 }
2377 Ty::Builtin(_) | Ty::Array(_) | Ty::Dict(..) | Ty::Callable | Ty::Signal(_) => {
2378 self.builtin_member_ty(&recv_ty, name, name_range, as_method)
2379 }
2380 Ty::Enum(er) => Ty::Enum(er.clone()),
2384 Ty::ScriptRef(sref) => self.script_member_ty(*sref, name, as_method),
2386 Ty::InnerClass(iref) => self.inner_class_member_ty(iref, name, as_method),
2388 _ => Ty::Variant,
2389 }
2390 }
2391
2392 fn inner_class_member_ty(
2398 &self,
2399 iref: &crate::ty::InnerClassRef,
2400 name: &str,
2401 as_method: bool,
2402 ) -> Ty {
2403 if name == "new" && as_method {
2404 return Ty::InnerClass(iref.clone());
2405 }
2406 self.inner_member_walk(iref, name, as_method, 0)
2407 .unwrap_or(Ty::Unknown)
2408 }
2409
2410 fn inner_member_walk(
2413 &self,
2414 iref: &crate::ty::InnerClassRef,
2415 name: &str,
2416 as_method: bool,
2417 depth: u32,
2418 ) -> Option<Ty> {
2419 if depth > 32 {
2420 return None;
2421 }
2422 let ft = self.db.file_text(FileId(iref.file))?;
2423 let tree = crate::queries::item_tree(self.db, ft);
2424 let inner = find_inner_class(&tree, &iref.path)?;
2425 if let Some(m) = inner.tree.member(name) {
2426 return self.inner_member_item_ty(m, as_method, iref);
2427 }
2428 let res_path = self.self_res_path();
2430 match resolve::resolve_base(self.db, self.api, &inner.tree, res_path.as_deref()) {
2431 Ty::Object(class) => self
2432 .api
2433 .lookup_member(class, name)
2434 .map(|m| self.member_ref_ty(&m, as_method)),
2435 Ty::ScriptRef(base) => self.script_member_walk(base, name, as_method, depth + 1),
2436 Ty::InnerClass(base) => self.inner_member_walk(&base, name, as_method, depth + 1),
2437 _ => None,
2438 }
2439 }
2440
2441 fn inner_member_item_ty(
2444 &self,
2445 m: &Member,
2446 as_method: bool,
2447 iref: &crate::ty::InnerClassRef,
2448 ) -> Option<Ty> {
2449 Some(match m {
2450 Member::Func(f) => {
2451 if as_method {
2452 f.return_type.as_deref().map_or(Ty::Variant, |t| {
2453 resolve::resolve_type_name(self.db, self.api, t)
2454 })
2455 } else {
2456 Ty::Callable
2457 }
2458 }
2459 Member::Var(v) => resolve::resolve_type_name(self.db, self.api, v.type_ref.as_deref()?),
2460 Member::Const(c) => {
2461 resolve::resolve_type_name(self.db, self.api, c.type_ref.as_deref()?)
2462 }
2463 Member::Signal(_) => Ty::Signal(None),
2464 Member::Enum(e) => Ty::Enum(EnumRef {
2465 qualified: e.name.clone()?,
2466 bitfield: false,
2467 }),
2468 Member::Class(c) => Ty::InnerClass(crate::ty::InnerClassRef {
2470 file: iref.file,
2471 path: SmolStr::new(format!("{}.{}", iref.path, c.name)),
2472 }),
2473 })
2474 }
2475
2476 fn script_member_ty(&self, sref: ScriptRefId, name: &str, as_method: bool) -> Ty {
2481 if name == "new" {
2482 return Ty::ScriptRef(sref);
2483 }
2484 self.script_member_walk(sref, name, as_method, 0)
2485 .unwrap_or(Ty::Unknown)
2486 }
2487
2488 fn script_member_walk(
2492 &self,
2493 sref: ScriptRefId,
2494 name: &str,
2495 as_method: bool,
2496 depth: u32,
2497 ) -> Option<Ty> {
2498 if depth > 32 {
2499 return None;
2500 }
2501 let file = self.db.file_text(FileId(sref.0))?;
2502 let sc = crate::queries::script_class(self.db, file);
2503 if let Some(m) = sc.member(name) {
2504 return Some(match m {
2505 crate::queries::MemberSig::Method(ret) => {
2506 if as_method {
2507 ret.clone()
2508 } else {
2509 Ty::Callable
2510 }
2511 }
2512 crate::queries::MemberSig::Field(t) => t.clone(),
2513 crate::queries::MemberSig::Signal => Ty::Signal(None),
2514 });
2515 }
2516 match sc.base() {
2518 Ty::ScriptRef(base) => self.script_member_walk(*base, name, as_method, depth + 1),
2519 Ty::Object(class) => self
2520 .api
2521 .lookup_member(*class, name)
2522 .map(|m| self.member_ref_ty(&m, as_method)),
2523 _ => None,
2524 }
2525 }
2526
2527 fn is_subtype(&self, sub: &Ty, sup: &Ty) -> bool {
2532 match (sub, sup) {
2533 (Ty::Object(a), Ty::Object(b)) => self.api.is_subclass(*a, *b),
2534 (Ty::ScriptRef(a), Ty::ScriptRef(b)) => self.script_is_subtype(*a, *b, 0),
2535 (Ty::ScriptRef(a), Ty::Object(b)) => self.script_extends_engine(*a, *b, 0),
2536 _ => false,
2537 }
2538 }
2539
2540 fn script_is_subtype(&self, sub: ScriptRefId, sup: ScriptRefId, depth: u32) -> bool {
2543 if depth > 32 {
2544 return false;
2545 }
2546 if sub == sup {
2547 return true;
2548 }
2549 let Some(file) = self.db.file_text(FileId(sub.0)) else {
2550 return false;
2551 };
2552 match crate::queries::script_class(self.db, file).base() {
2553 Ty::ScriptRef(base) => self.script_is_subtype(*base, sup, depth + 1),
2554 _ => false,
2555 }
2556 }
2557
2558 fn script_extends_engine(
2560 &self,
2561 sub: ScriptRefId,
2562 sup_native: gdscript_api::ClassId,
2563 depth: u32,
2564 ) -> bool {
2565 if depth > 32 {
2566 return false;
2567 }
2568 let Some(file) = self.db.file_text(FileId(sub.0)) else {
2569 return false;
2570 };
2571 match crate::queries::script_class(self.db, file).base() {
2572 Ty::ScriptRef(base) => self.script_extends_engine(*base, sup_native, depth + 1),
2573 Ty::Object(native) => self.api.is_subclass(*native, sup_native),
2574 _ => false,
2575 }
2576 }
2577
2578 fn emit_unsafe(&mut self, name: &str, recv: &Ty, range: TextRange, as_method: bool) {
2579 let recv_label = recv.label(self.api).unwrap_or_else(|| "?".to_owned());
2580 let (code, message) = if as_method {
2581 (
2582 WarningCode::UnsafeMethodAccess,
2583 format!(
2584 "The method \"{name}()\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
2585 ),
2586 )
2587 } else {
2588 (
2589 WarningCode::UnsafePropertyAccess,
2590 format!(
2591 "The property \"{name}\" is not present on the inferred type \"{recv_label}\" (but may be present on a subtype)."
2592 ),
2593 )
2594 };
2595 self.warn(range, code, message);
2596 }
2597
2598 fn engine_base_has_value_member(&self, name: &str) -> bool {
2604 let Ty::Object(base) = &self.class.base else {
2605 return false;
2606 };
2607 matches!(
2608 self.api.lookup_member(*base, name),
2609 Some(MemberRef::Property(_) | MemberRef::Const(_) | MemberRef::Signal(_))
2610 )
2611 }
2612
2613 fn check_member_kind_misuse(
2620 &mut self,
2621 m: &MemberRef,
2622 as_method: bool,
2623 name: &str,
2624 range: TextRange,
2625 ) {
2626 if !as_method {
2627 return;
2628 }
2629 let (code, kind, ty) = match m {
2630 MemberRef::Property(p) => (
2631 WarningCode::PropertyUsedAsFunction,
2632 "property",
2633 ty::resolve_tyref(self.api, &p.ty),
2634 ),
2635 MemberRef::Const(c) => (
2636 WarningCode::ConstantUsedAsFunction,
2637 "constant",
2638 ty::resolve_tyref(self.api, &c.ty),
2639 ),
2640 _ => return,
2641 };
2642 if ty.is_uninformative() || matches!(ty, Ty::Callable | Ty::Signal(_)) {
2644 return;
2645 }
2646 self.warn(
2647 range,
2648 code,
2649 format!("The {kind} \"{name}\" is being called as if it were a function."),
2650 );
2651 }
2652
2653 fn check_static_on_instance(
2658 &mut self,
2659 receiver: ExprId,
2660 m: &MemberRef,
2661 as_method: bool,
2662 range: TextRange,
2663 ) {
2664 if !as_method {
2665 return;
2666 }
2667 let MemberRef::Method(sig) = m else {
2668 return;
2669 };
2670 if !sig.is_static {
2671 return;
2672 }
2673 let Expr::Name(rname) = self.body.expr(receiver) else {
2674 return;
2675 };
2676 if !self.locals.contains_key(rname) {
2677 return;
2678 }
2679 if let Some(b) = self.bindings.iter().rev().find(|b| &b.name == rname)
2684 && let Some(init) = b.init
2685 && matches!(self.body.expr(init), Expr::Name(_))
2686 {
2687 return;
2688 }
2689 self.warn(
2690 range,
2691 WarningCode::StaticCalledOnInstance,
2692 "A static method is being called on an instance; call it on the type instead."
2693 .to_owned(),
2694 );
2695 }
2696
2697 fn member_ref_ty(&self, m: &MemberRef, as_method: bool) -> Ty {
2698 match m {
2699 MemberRef::Method(sig) => {
2700 if as_method {
2701 ty::resolve_tyref(self.api, &sig.return_ty)
2702 } else {
2703 Ty::Callable
2704 }
2705 }
2706 MemberRef::Property(p) => p.enum_of.as_ref().map_or_else(
2707 || ty::resolve_tyref(self.api, &p.ty),
2708 |q| {
2709 Ty::Enum(EnumRef {
2710 qualified: SmolStr::new(q),
2711 bitfield: false,
2712 })
2713 },
2714 ),
2715 MemberRef::Const(c) => ty::resolve_tyref(self.api, &c.ty),
2716 MemberRef::Signal(_) => Ty::Signal(None),
2717 MemberRef::Enum(_) => Ty::Variant,
2718 }
2719 }
2720
2721 fn builtin_member_ty(
2722 &mut self,
2723 recv: &Ty,
2724 name: &str,
2725 range: TextRange,
2726 as_method: bool,
2727 ) -> Ty {
2728 let Some(bid) = self.builtin_id_of(recv) else {
2729 return Ty::Variant;
2730 };
2731 if as_method {
2732 return if let Some(sig) = self.api.builtin_method(bid, name) {
2733 ty::resolve_tyref(self.api, &sig.return_ty)
2734 } else {
2735 self.emit_unsafe(name, recv, range, true);
2736 Ty::Variant
2737 };
2738 }
2739 if let Some(member) = self.api.builtin_member(bid, name) {
2740 return ty::resolve_tyref(self.api, &member.ty);
2741 }
2742 let data = self.api.builtin(bid);
2744 if let Some(c) = data.constants.iter().find(|c| c.name == name) {
2745 return ty::resolve_tyref(self.api, &c.ty);
2746 }
2747 if data
2748 .enums
2749 .iter()
2750 .any(|e| e.values.iter().any(|v| v.name == name))
2751 {
2752 return self.int_ty();
2753 }
2754 if self.api.builtin_method(bid, name).is_some() {
2755 return Ty::Callable;
2756 }
2757 self.emit_unsafe(name, recv, range, false);
2758 Ty::Variant
2759 }
2760
2761 fn class_enum_value(&self, class: gdscript_api::ClassId, name: &str) -> Option<Ty> {
2768 let mut cur = Some(class);
2769 while let Some(cid) = cur {
2770 let c = self.api.class(cid);
2771 if let Some(e) = c
2772 .enums
2773 .iter()
2774 .find(|e| e.values.iter().any(|v| v.name == name))
2775 {
2776 return Some(Ty::Enum(EnumRef {
2777 qualified: SmolStr::new(format!("{}.{}", c.name, e.name)),
2778 bitfield: e.is_bitfield,
2779 }));
2780 }
2781 cur = c.base;
2782 }
2783 None
2784 }
2785
2786 fn builtin_id_of(&self, ty: &Ty) -> Option<gdscript_api::BuiltinId> {
2788 match ty {
2789 Ty::Builtin(b) => Some(*b),
2790 Ty::Array(_) => self.api.builtin_by_name("Array"),
2791 Ty::Dict(..) => self.api.builtin_by_name("Dictionary"),
2792 Ty::Callable => self.api.builtin_by_name("Callable"),
2793 Ty::Signal(_) => self.api.builtin_by_name("Signal"),
2794 _ => None,
2795 }
2796 }
2797
2798 fn index_ty(&self, base: &Ty) -> Ty {
2800 match base {
2801 Ty::Array(elem) => (**elem).clone(),
2802 Ty::Builtin(b) => self
2803 .api
2804 .builtin(*b)
2805 .indexing_return
2806 .as_ref()
2807 .map_or(Ty::Variant, |r| ty::resolve_tyref(self.api, r)),
2808 Ty::Unknown => Ty::Unknown,
2810 Ty::Error => Ty::Error,
2811 _ => Ty::Variant,
2812 }
2813 }
2814
2815 fn loop_var_ty(&self, iter: &Ty) -> Ty {
2817 match iter {
2818 Ty::Array(elem) => (**elem).clone(),
2819 Ty::Builtin(b) => {
2820 let data = self.api.builtin(*b);
2821 if data.name == "int" {
2822 self.int_ty()
2824 } else if let Some(r) = &data.indexing_return {
2825 ty::resolve_tyref(self.api, r)
2827 } else {
2828 Ty::Variant
2829 }
2830 }
2831 Ty::Unknown => Ty::Unknown,
2833 Ty::Error => Ty::Error,
2834 _ => Ty::Variant,
2835 }
2836 }
2837
2838 fn infer_lambda(&mut self, params: &[ParamBinding], body: &[body::StmtId]) {
2839 let saved_locals = self.locals.clone();
2843 let saved_ret = std::mem::replace(&mut self.return_ty, Ty::Variant);
2844 for p in params {
2845 let ty = self.param_ty(p);
2846 self.bindings.push(Binding {
2847 name: p.name.clone(),
2848 name_range: p.name_range,
2849 ty: ty.clone(),
2850 init: None,
2851 annotated: p.type_ref.is_some(),
2852 inferred_colon_eq: false,
2853 is_const: false,
2854 kind: BindingKind::Param,
2855 });
2856 self.locals.insert(p.name.clone(), ty);
2857 }
2858 self.infer_block(body);
2859 self.return_ty = saved_ret;
2860 self.locals = saved_locals;
2861 }
2862
2863 fn param_ty(&mut self, p: &ParamBinding) -> Ty {
2864 if let Some(ptr) = p.type_ref {
2865 return self.resolve_ptr_ty(ptr);
2866 }
2867 p.default
2869 .map_or(Ty::Variant, |e| self.infer_expr(e, &Expectation::None))
2870 }
2871
2872 fn resolve_name(&mut self, id: ExprId, name: &str) -> Ty {
2881 if self.locals.contains_key(name) && !self.assign_lhs.contains(&id) {
2887 self.used_locals.insert(SmolStr::new(name));
2888 }
2889 if self.is_func_body
2893 && self.needs_assignment.contains(name)
2894 && !self.assign_lhs.contains(&id)
2895 && let Some(cur) = self.cur_stmt
2896 && self
2897 .assigned
2898 .assigned_before(cur)
2899 .is_some_and(|a| !a.contains(name))
2900 {
2901 self.warn(
2902 self.range_of(id),
2903 WarningCode::UnassignedVariable,
2904 format!("The variable \"{name}\" may be used before it is assigned a value."),
2905 );
2906 }
2907 if let Some(key) = self.narrow_key(id)
2909 && let Some(t) = self.narrowing.get(&key)
2910 {
2911 return t.clone();
2912 }
2913 if let Some(t) = self.locals.get(name) {
2914 return t.clone();
2915 }
2916 if let Some(item) = self.class.lookup(name) {
2917 return self.own_member_ty(item, false);
2918 }
2919 match self.class.base.clone() {
2923 Ty::Object(base) => {
2924 if let Some(m) = self.api.lookup_member(base, name) {
2925 return self.member_ref_ty(&m, false);
2926 }
2927 }
2928 Ty::ScriptRef(base) => {
2929 if let Some(t) = self.script_member_walk(base, name, false, 0) {
2930 return t;
2931 }
2932 }
2933 _ => {}
2934 }
2935 if let Some(g) = resolve::resolve_global(self.api, name) {
2936 return global_ty(&g);
2937 }
2938 let by_class = resolve::resolve_external(
2943 self.db,
2944 &resolve::ExternalRef::ClassName(SmolStr::new(name)),
2945 );
2946 if !by_class.is_unknown() {
2947 return by_class;
2948 }
2949 resolve::resolve_external(self.db, &resolve::ExternalRef::Autoload(SmolStr::new(name)))
2950 }
2951
2952 fn own_member_ty(&self, item: ClassItem, as_method: bool) -> Ty {
2953 match item {
2954 ClassItem::EnumVariant => self.int_ty(),
2955 ClassItem::Member(_) => match self.class.member(item) {
2956 Some(Member::Var(v)) => self.field_ty(&v.name, v.ptr),
2957 Some(Member::Const(c)) => self.field_ty(&c.name, c.ptr),
2958 Some(Member::Func(f)) => {
2959 if as_method {
2960 self.func_return_ty(f.return_type.as_deref())
2961 } else {
2962 Ty::Callable
2963 }
2964 }
2965 Some(Member::Signal(_)) => Ty::Signal(None),
2966 Some(Member::Class(c)) => match &self.self_ty {
2971 Ty::ScriptRef(sref) => Ty::InnerClass(crate::ty::InnerClassRef {
2972 file: sref.0,
2973 path: c.name.clone(),
2974 }),
2975 _ => Ty::Unknown,
2976 },
2977 Some(Member::Enum(e)) => e.name.as_ref().map_or(Ty::Variant, |n| {
2982 Ty::Enum(EnumRef {
2983 qualified: n.clone(),
2984 bitfield: false,
2985 })
2986 }),
2987 None => Ty::Variant,
2988 },
2989 }
2990 }
2991
2992 fn field_ty(&self, name: &str, ptr: AstPtr) -> Ty {
2995 if let Some(t) = self.class.member_types.get(name) {
2996 return t.clone();
2997 }
2998 self.resolve_decl_annotation(ptr)
2999 }
3000
3001 fn resolve_decl_annotation(&self, ptr: AstPtr) -> Ty {
3003 let Some(node) = ptr.to_node(self.root) else {
3004 return Ty::Variant;
3005 };
3006 cst::first_child(&node, |k| k == gdscript_syntax::SyntaxKind::TypeRef)
3007 .map_or(Ty::Variant, |t| {
3008 resolve::resolve_type_ref(self.db, self.api, &t)
3009 })
3010 }
3011
3012 fn facts_to_narrowing(&self, id: body::StmtId) -> FxHashMap<String, Ty> {
3024 let mut out = FxHashMap::default();
3025 if let Some(facts) = self.flow.facts_before(id) {
3026 for (place, nt) in facts.iter() {
3027 if let Some((key, ty)) = self.narrowing_entry(place, nt) {
3028 out.insert(key, ty);
3029 }
3030 }
3031 }
3032 out
3033 }
3034
3035 fn narrowing_entry(&self, place: &Place, nt: &NarrowedTy) -> Option<(String, Ty)> {
3039 let NarrowedTy::Is(ptr) = nt else {
3040 return None;
3041 };
3042 let narrowed = self.resolve_ptr_ty(*ptr);
3043 if narrowed.is_uninformative() {
3044 return None;
3045 }
3046 if let Place::Local(n) = place
3049 && let Some(cur) = self.locals.get(n)
3050 && !cur.is_uninformative()
3051 && !self.is_subtype(&narrowed, cur)
3052 {
3053 return None;
3054 }
3055 Some((place.dotted_key(), narrowed))
3056 }
3057
3058 fn apply_condition_facts(&mut self, cond: ExprId, truthy: bool) {
3061 for (place, nt) in flow::condition_facts(self.body, cond, truthy) {
3062 if let Some((key, ty)) = self.narrowing_entry(&place, &nt) {
3063 self.narrowing.insert(key, ty);
3064 }
3065 }
3066 }
3067
3068 fn narrow_key(&self, id: ExprId) -> Option<String> {
3071 match self.body.expr(id) {
3072 Expr::Name(n) => Some(n.to_string()),
3073 Expr::SelfExpr => Some("self".to_owned()),
3074 Expr::Paren(inner) => self.narrow_key(*inner),
3075 Expr::Field { receiver, name, .. } => {
3076 Some(format!("{}.{name}", self.narrow_key(*receiver)?))
3077 }
3078 _ => None,
3079 }
3080 }
3081
3082 fn resolve_ptr_ty(&self, ptr: AstPtr) -> Ty {
3083 ptr.to_node(self.root).map_or(Ty::Variant, |n| {
3084 resolve::resolve_type_ref(self.db, self.api, &n)
3085 })
3086 }
3087
3088 fn join(&self, a: &Ty, b: &Ty) -> Ty {
3099 if a == b {
3100 return a.clone();
3101 }
3102 if a.is_error() || b.is_error() {
3103 return Ty::Error;
3104 }
3105 if a.is_unknown() || b.is_unknown() {
3106 return Ty::Unknown;
3107 }
3108 if a.is_variant() || b.is_variant() {
3109 return Ty::Variant;
3110 }
3111 if ty::is_assignable(self.api, a, b) == Assign::Ok {
3112 return b.clone();
3113 }
3114 if ty::is_assignable(self.api, b, a) == Assign::Ok {
3115 return a.clone();
3116 }
3117 Ty::Variant
3118 }
3119}
3120
3121fn global_ty(g: &GlobalDef) -> Ty {
3123 match g {
3124 GlobalDef::Const(t) => t.clone(),
3125 GlobalDef::Singleton(c) | GlobalDef::ClassType(c) => Ty::Object(*c),
3126 GlobalDef::BuiltinType(b) => Ty::Builtin(*b),
3127 GlobalDef::Builtin | GlobalDef::Utility => Ty::Callable,
3129 GlobalDef::GlobalEnum => Ty::Variant,
3130 }
3131}
3132
3133fn inference_on_variant_msg(kind: &str) -> String {
3134 format!(
3135 "The {kind} type is being inferred from a Variant value, so it will be typed as Variant."
3136 )
3137}
3138
3139fn op_symbol(op: BinOp) -> Option<&'static str> {
3141 Some(match op {
3142 BinOp::Add => "+",
3143 BinOp::Sub => "-",
3144 BinOp::Mul => "*",
3145 BinOp::Div => "/",
3146 BinOp::Mod => "%",
3147 BinOp::Pow => "**",
3148 BinOp::BitAnd => "&",
3149 BinOp::BitOr => "|",
3150 BinOp::BitXor => "^",
3151 BinOp::Shl => "<<",
3152 BinOp::Shr => ">>",
3153 _ => return None,
3154 })
3155}
3156
3157#[cfg(test)]
3158mod tests {
3159 use super::*;
3160 use crate::item_tree::item_tree;
3161 use gdscript_syntax::{SyntaxKind, parse};
3162
3163 struct Harness {
3164 result: InferenceResult,
3165 body: Body,
3166 }
3167
3168 fn infer_first_func(src: &str) -> Harness {
3170 let api = gdscript_api::bundled();
3171 let db = gdscript_db::RootDatabase::default();
3172 let root = parse(src).syntax_node();
3173 let tree = item_tree(&root);
3174 let class = ClassScope::new(&db, api, &tree, None);
3175 let func = gdscript_syntax::ast::descendants(&root)
3176 .into_iter()
3177 .find(|n| n.kind() == SyntaxKind::FuncDecl)
3178 .expect("a function");
3179 let body = body::body_of_func(&func);
3180 let return_ty = cst::first_child(&func, |k| k == SyntaxKind::TypeRef)
3181 .map_or(Ty::Variant, |t| resolve::resolve_type_ref(&db, api, &t));
3182 let result = infer(&db, api, &root, &class, &body, return_ty, true);
3183 Harness { result, body }
3184 }
3185
3186 const DECLARATION_STRICTNESS: &[&str] = &["UNTYPED_DECLARATION", "INFERRED_DECLARATION"];
3194
3195 fn codes(h: &Harness) -> Vec<&str> {
3196 h.result
3197 .diagnostics
3198 .iter()
3199 .map(|d| d.code.as_str())
3200 .chain(h.result.raw_warnings.iter().map(|w| w.code.as_str()))
3201 .filter(|c| !DECLARATION_STRICTNESS.contains(c))
3202 .collect()
3203 }
3204
3205 fn file_codes(src: &str) -> Vec<String> {
3209 let api = gdscript_api::bundled();
3210 let db = gdscript_db::RootDatabase::default();
3211 let root = parse(src).syntax_node();
3212 let fi = analyze_file(&db, api, &root, FileId(0));
3213 fi.diagnostics
3214 .iter()
3215 .map(|d| d.code.clone())
3216 .chain(fi.raw_warnings.iter().map(|w| w.code.as_str().to_owned()))
3217 .filter(|c| !DECLARATION_STRICTNESS.contains(&c.as_str()))
3218 .collect()
3219 }
3220
3221 #[test]
3222 fn integer_division_warns() {
3223 let h = infer_first_func("func f():\n\tvar x = 5 / 2\n");
3224 assert!(codes(&h).contains(&INTEGER_DIVISION));
3225 }
3226
3227 #[test]
3228 fn float_div_does_not_warn() {
3229 let h = infer_first_func("func f():\n\tvar x = 5.0 / 2\n");
3230 assert!(!codes(&h).contains(&INTEGER_DIVISION));
3231 }
3232
3233 #[test]
3234 fn type_mismatch_on_hard_annotation() {
3235 let h = infer_first_func("func f():\n\tvar s: String = 5\n");
3236 assert!(codes(&h).contains(&TYPE_MISMATCH));
3237 }
3238
3239 #[test]
3240 fn vector_scalar_compound_assign_is_not_a_mismatch() {
3241 let h = infer_first_func(
3243 "func f() -> Vector2:\n\tvar v := Vector2()\n\tv *= 0.5\n\treturn v\n",
3244 );
3245 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
3246 }
3247
3248 #[test]
3249 fn array_literal_to_packed_array_is_allowed() {
3250 let h = infer_first_func("func f():\n\tvar p: PackedStringArray = [\"a\", \"b\"]\n");
3251 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
3252 }
3253
3254 #[test]
3255 fn vector2i_to_vector2_is_allowed() {
3256 let h = infer_first_func("func f():\n\tvar v: Vector2 = Vector2i(1, 2)\n");
3257 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
3258 }
3259
3260 #[test]
3261 fn local_enum_member_access_types_as_the_enum_not_variant() {
3262 let h = infer_first_func(
3265 "enum State { IDLE, RUN }\nfunc f():\n\tvar x := State.IDLE\n\treturn x\n",
3266 );
3267 assert!(
3268 !codes(&h).contains(&INFERENCE_ON_VARIANT),
3269 "{:?}",
3270 codes(&h)
3271 );
3272 }
3273
3274 #[test]
3275 fn lua_style_dict_key_is_not_an_assignment() {
3276 let h = infer_first_func(
3279 "var pos: Vector2\nfunc f():\n\tvar d = { pos = \"x\" }\n\treturn d\n",
3280 );
3281 assert!(!codes(&h).contains(&TYPE_MISMATCH), "{:?}", codes(&h));
3282 }
3283
3284 #[test]
3285 fn narrowing_conversion_float_to_int() {
3286 let h = infer_first_func("func f():\n\tvar n: int = 1.5\n");
3287 assert!(codes(&h).contains(&NARROWING_CONVERSION));
3288 }
3289
3290 #[test]
3291 fn int_to_float_is_silent() {
3292 let h = infer_first_func("func f():\n\tvar x: float = 3\n\treturn x\n");
3293 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
3294 }
3295
3296 #[test]
3297 fn local_shadowing_a_param_warns_shadowed_variable() {
3298 let h = infer_first_func("func f(x):\n\tvar x = 1\n\treturn x\n");
3299 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
3300 }
3301
3302 #[test]
3303 fn local_shadowing_a_class_member_warns_shadowed_variable() {
3304 let h =
3306 infer_first_func("var health = 100\nfunc f():\n\tvar health = 1\n\treturn health\n");
3307 assert!(codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
3308 }
3309
3310 #[test]
3311 fn non_shadowing_local_does_not_warn_shadowed_variable() {
3312 let h = infer_first_func("func f(x):\n\tvar y = 1\n\treturn x + y\n");
3313 assert!(!codes(&h).contains(&"SHADOWED_VARIABLE"), "{:?}", codes(&h));
3314 }
3315
3316 #[test]
3317 fn local_shadowing_a_base_member_warns_base_class() {
3318 let h =
3320 infer_first_func("extends Node2D\nfunc f():\n\tvar position = 1\n\treturn position\n");
3321 assert!(
3322 codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
3323 "{:?}",
3324 codes(&h)
3325 );
3326 }
3327
3328 #[test]
3329 fn shadowing_an_unresolved_base_is_silent() {
3330 let h = infer_first_func(
3332 "extends SomeUnknownThirdPartyClass\nfunc f():\n\tvar position = 1\n\treturn position\n",
3333 );
3334 assert!(
3335 !codes(&h).contains(&"SHADOWED_VARIABLE_BASE_CLASS"),
3336 "{:?}",
3337 codes(&h)
3338 );
3339 }
3340
3341 #[test]
3342 fn local_named_after_a_native_class_warns_shadowed_global() {
3343 let h = infer_first_func("func f():\n\tvar Node = 1\n\treturn Node\n");
3344 assert!(
3345 codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
3346 "{:?}",
3347 codes(&h)
3348 );
3349 }
3350
3351 #[test]
3352 fn param_named_after_a_builtin_type_warns_shadowed_global() {
3353 let h = infer_first_func("func f(Vector2):\n\treturn Vector2\n");
3354 assert!(
3355 codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
3356 "{:?}",
3357 codes(&h)
3358 );
3359 }
3360
3361 #[test]
3362 fn member_named_after_a_native_class_warns_shadowed_global() {
3363 let cs = file_codes("var Timer = null\n");
3364 assert!(
3365 cs.iter().any(|c| c == "SHADOWED_GLOBAL_IDENTIFIER"),
3366 "{cs:?}"
3367 );
3368 }
3369
3370 #[test]
3371 fn ordinary_local_does_not_warn_shadowed_global() {
3372 let h = infer_first_func("func f():\n\tvar count = 1\n\treturn count\n");
3374 assert!(
3375 !codes(&h).contains(&SHADOWED_GLOBAL_IDENTIFIER),
3376 "{:?}",
3377 codes(&h)
3378 );
3379 }
3380
3381 #[test]
3382 fn global_shadow_takes_precedence_over_variable_shadow() {
3383 let cs = file_codes("var Color = null\nfunc f():\n\tvar Color = 1\n\treturn Color\n");
3386 assert!(
3387 cs.iter().any(|c| c == "SHADOWED_GLOBAL_IDENTIFIER"),
3388 "{cs:?}"
3389 );
3390 assert!(
3391 !cs.iter().any(|c| c == "SHADOWED_VARIABLE"),
3392 "the local's variable-shadow must be suppressed in favor of the global one: {cs:?}"
3393 );
3394 }
3395
3396 #[test]
3397 fn assert_true_warns_always_true() {
3398 let h = infer_first_func("func f():\n\tassert(true)\n");
3399 assert!(codes(&h).contains(&"ASSERT_ALWAYS_TRUE"), "{:?}", codes(&h));
3400 }
3401
3402 #[test]
3403 fn assert_false_warns_always_false() {
3404 let h = infer_first_func("func f():\n\tassert(false, \"nope\")\n");
3405 assert!(
3406 codes(&h).contains(&"ASSERT_ALWAYS_FALSE"),
3407 "{:?}",
3408 codes(&h)
3409 );
3410 }
3411
3412 #[test]
3413 fn assert_null_warns_always_false() {
3414 let h = infer_first_func("func f():\n\tassert(null)\n");
3415 assert!(
3416 codes(&h).contains(&"ASSERT_ALWAYS_FALSE"),
3417 "{:?}",
3418 codes(&h)
3419 );
3420 }
3421
3422 #[test]
3423 fn assert_on_a_variable_is_silent() {
3424 let h = infer_first_func("func f(x):\n\tassert(x)\n");
3426 assert!(
3427 !codes(&h).iter().any(|c| c.starts_with("ASSERT_ALWAYS")),
3428 "{:?}",
3429 codes(&h)
3430 );
3431 }
3432
3433 #[test]
3434 fn untyped_and_inferred_declarations_warn() {
3435 let h = infer_first_func("func f(p):\n\tvar a = 1\n\tvar b := 2\n\tvar c: int = 3\n");
3438 let raw: Vec<&str> = h
3439 .result
3440 .raw_warnings
3441 .iter()
3442 .map(|w| w.code.as_str())
3443 .collect();
3444 let untyped = raw.iter().filter(|c| **c == "UNTYPED_DECLARATION").count();
3446 assert_eq!(untyped, 2, "only `p` and `a` are untyped: {raw:?}");
3447 let inferred = raw.iter().filter(|c| **c == "INFERRED_DECLARATION").count();
3448 assert_eq!(inferred, 1, "only `b` uses `:=`: {raw:?}");
3449 }
3450
3451 #[test]
3452 fn confusable_identifier_warns_on_a_mixed_script_local() {
3453 let h = infer_first_func("func f():\n\tvar p\u{0430}ypal = 1\n\treturn p\u{0430}ypal\n");
3455 assert!(
3456 codes(&h).contains(&"CONFUSABLE_IDENTIFIER"),
3457 "{:?}",
3458 codes(&h)
3459 );
3460 }
3461
3462 #[test]
3463 fn an_ordinary_ascii_identifier_is_not_confusable() {
3464 let h = infer_first_func("func f():\n\tvar paypal = 1\n\treturn paypal\n");
3465 assert!(
3466 !codes(&h).contains(&"CONFUSABLE_IDENTIFIER"),
3467 "{:?}",
3468 codes(&h)
3469 );
3470 }
3471
3472 #[test]
3473 fn a_member_with_a_confusable_name_warns() {
3474 let cs = file_codes("var b\u{0430}lance = 0\n");
3476 assert!(cs.iter().any(|c| c == "CONFUSABLE_IDENTIFIER"), "{cs:?}");
3477 }
3478
3479 #[test]
3480 fn an_assigned_but_never_read_local_is_unused() {
3481 let h = infer_first_func("func f():\n\tvar x = 1\n\tx = 2\n");
3483 assert!(codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
3484 }
3485
3486 #[test]
3487 fn a_read_local_is_not_unused() {
3488 let h = infer_first_func("func f() -> int:\n\tvar x = 1\n\tx = 2\n\treturn x\n");
3489 assert!(!codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
3490 }
3491
3492 #[test]
3493 fn unused_private_class_variable_warns() {
3494 let cs = file_codes("var _cache = 0\nfunc f():\n\tpass\n");
3495 assert!(
3496 cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
3497 "{cs:?}"
3498 );
3499 }
3500
3501 #[test]
3502 fn a_read_private_class_variable_is_silent() {
3503 let cs = file_codes("var _cache = 0\nfunc f() -> int:\n\treturn _cache\n");
3504 assert!(
3505 !cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
3506 "{cs:?}"
3507 );
3508 }
3509
3510 #[test]
3511 fn an_exported_private_var_is_not_unused_private() {
3512 let cs = file_codes("@export var _hidden = 0\n");
3514 assert!(
3515 !cs.iter().any(|c| c == "UNUSED_PRIVATE_CLASS_VARIABLE"),
3516 "{cs:?}"
3517 );
3518 }
3519
3520 #[test]
3521 fn onready_with_export_warns() {
3522 let cs = file_codes("@onready @export var n = null\n");
3523 assert!(cs.iter().any(|c| c == "ONREADY_WITH_EXPORT"), "{cs:?}");
3524 }
3525
3526 #[test]
3527 fn redundant_static_unload_warns_without_a_static_var() {
3528 let cs = file_codes("@static_unload\nclass_name Foo\nvar x = 1\n");
3529 assert!(cs.iter().any(|c| c == "REDUNDANT_STATIC_UNLOAD"), "{cs:?}");
3530 }
3531
3532 #[test]
3533 fn static_unload_with_a_static_var_is_silent() {
3534 let cs = file_codes("@static_unload\nstatic var pool = []\n");
3535 assert!(!cs.iter().any(|c| c == "REDUNDANT_STATIC_UNLOAD"), "{cs:?}");
3536 }
3537
3538 #[test]
3539 fn typed_local_read_before_assignment_warns() {
3540 let h = infer_first_func("func f() -> int:\n\tvar x: int\n\treturn x\n");
3541 assert!(
3542 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3543 "{:?}",
3544 codes(&h)
3545 );
3546 }
3547
3548 #[test]
3549 fn typed_local_assigned_then_read_does_not_warn() {
3550 let h = infer_first_func("func f() -> int:\n\tvar x: int\n\tx = 5\n\treturn x\n");
3551 assert!(
3552 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3553 "{:?}",
3554 codes(&h)
3555 );
3556 }
3557
3558 #[test]
3559 fn typed_local_with_initializer_is_not_unassigned() {
3560 let h = infer_first_func("func f() -> int:\n\tvar x: int = 0\n\treturn x\n");
3561 assert!(
3562 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3563 "{:?}",
3564 codes(&h)
3565 );
3566 }
3567
3568 #[test]
3569 fn untyped_local_is_not_unassigned_checked() {
3570 let h = infer_first_func("func f():\n\tvar x\n\tvar y = x\n\treturn y\n");
3572 assert!(
3573 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3574 "{:?}",
3575 codes(&h)
3576 );
3577 }
3578
3579 #[test]
3580 fn typed_local_assigned_in_all_branches_then_read_does_not_warn() {
3581 let h = infer_first_func(
3583 "func f(c) -> int:\n\tvar x: int\n\tif c:\n\t\tx = 1\n\telse:\n\t\tx = 2\n\treturn x\n",
3584 );
3585 assert!(
3586 !codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3587 "{:?}",
3588 codes(&h)
3589 );
3590 }
3591
3592 #[test]
3593 fn typed_local_assigned_in_one_branch_then_read_warns() {
3594 let h =
3596 infer_first_func("func f(c) -> int:\n\tvar x: int\n\tif c:\n\t\tx = 1\n\treturn x\n");
3597 assert!(
3598 codes(&h).contains(&"UNASSIGNED_VARIABLE"),
3599 "{:?}",
3600 codes(&h)
3601 );
3602 }
3603
3604 #[test]
3605 fn arm_after_wildcard_is_unreachable_pattern() {
3606 let h =
3607 infer_first_func("func f(x):\n\tmatch x:\n\t\t_:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n");
3608 assert!(
3609 codes(&h).contains(&"UNREACHABLE_PATTERN"),
3610 "{:?}",
3611 codes(&h)
3612 );
3613 }
3614
3615 #[test]
3616 fn arm_after_var_bind_is_unreachable_pattern() {
3617 let h = infer_first_func(
3618 "func f(x):\n\tmatch x:\n\t\tvar y:\n\t\t\treturn y\n\t\t1:\n\t\t\tpass\n",
3619 );
3620 assert!(
3621 codes(&h).contains(&"UNREACHABLE_PATTERN"),
3622 "{:?}",
3623 codes(&h)
3624 );
3625 }
3626
3627 #[test]
3628 fn arm_before_wildcard_is_not_unreachable() {
3629 let h =
3630 infer_first_func("func f(x):\n\tmatch x:\n\t\t1:\n\t\t\tpass\n\t\t_:\n\t\t\tpass\n");
3631 assert!(
3632 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
3633 "{:?}",
3634 codes(&h)
3635 );
3636 }
3637
3638 #[test]
3639 fn guarded_wildcard_is_not_a_catch_all() {
3640 let h = infer_first_func(
3642 "func f(x, c):\n\tmatch x:\n\t\t_ when c:\n\t\t\tpass\n\t\t1:\n\t\t\tpass\n",
3643 );
3644 assert!(
3645 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
3646 "{:?}",
3647 codes(&h)
3648 );
3649 }
3650
3651 #[test]
3652 fn multi_pattern_with_wildcard_is_conservatively_not_catch_all() {
3653 let h =
3655 infer_first_func("func f(x):\n\tmatch x:\n\t\t1, _:\n\t\t\tpass\n\t\t2:\n\t\t\tpass\n");
3656 assert!(
3657 !codes(&h).contains(&"UNREACHABLE_PATTERN"),
3658 "{:?}",
3659 codes(&h)
3660 );
3661 }
3662
3663 #[test]
3664 fn enum_local_without_default_warns() {
3665 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode\n");
3666 assert!(
3667 codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
3668 "{:?}",
3669 codes(&h)
3670 );
3671 }
3672
3673 #[test]
3674 fn enum_member_without_default_warns() {
3675 let codes = file_codes("var err: Error\nfunc f():\n\tpass\n");
3676 assert!(
3677 codes.iter().any(|c| c == "ENUM_VARIABLE_WITHOUT_DEFAULT"),
3678 "{codes:?}"
3679 );
3680 }
3681
3682 #[test]
3683 fn native_virtual_override_with_clashing_param_type_warns() {
3684 let codes = file_codes("extends Node\nfunc _input(event: int):\n\tpass\n");
3686 assert!(
3687 codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
3688 "{codes:?}"
3689 );
3690 }
3691
3692 #[test]
3693 fn native_virtual_override_with_correct_param_type_does_not_warn() {
3694 let codes = file_codes("extends Node\nfunc _input(event: InputEvent):\n\tpass\n");
3695 assert!(
3696 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
3697 "{codes:?}"
3698 );
3699 }
3700
3701 #[test]
3702 fn native_virtual_override_with_untyped_param_does_not_warn() {
3703 let codes = file_codes("extends Node\nfunc _input(event):\n\tpass\n");
3704 assert!(
3705 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
3706 "{codes:?}"
3707 );
3708 }
3709
3710 #[test]
3711 fn a_non_virtual_method_is_not_a_native_override() {
3712 let codes = file_codes("extends Node\nfunc my_helper(x: int):\n\treturn x\n");
3713 assert!(
3714 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
3715 "{codes:?}"
3716 );
3717 }
3718
3719 #[test]
3720 fn dotted_enum_override_param_does_not_false_warn() {
3721 let codes = file_codes(
3724 "extends MultiplayerPeerExtension\nfunc _set_transfer_mode(p_mode: MultiplayerPeer.TransferMode):\n\tpass\n",
3725 );
3726 assert!(
3727 !codes.iter().any(|c| c == "NATIVE_METHOD_OVERRIDE"),
3728 "{codes:?}"
3729 );
3730 }
3731
3732 #[test]
3733 fn unused_signal_warns() {
3734 let codes = file_codes("signal my_event\nfunc f():\n\tpass\n");
3735 assert!(codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
3736 }
3737
3738 #[test]
3739 fn emitted_signal_is_not_unused() {
3740 let codes = file_codes("signal my_event\nfunc f():\n\tmy_event.emit()\n");
3741 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
3742 }
3743
3744 #[test]
3745 fn signal_connected_by_string_is_not_unused() {
3746 let codes = file_codes("signal my_event\nfunc f():\n\tconnect(\"my_event\", Callable())\n");
3747 assert!(!codes.iter().any(|c| c == "UNUSED_SIGNAL"), "{codes:?}");
3748 }
3749
3750 #[test]
3751 fn enum_local_with_default_does_not_warn() {
3752 let h = infer_first_func(
3753 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
3754 );
3755 assert!(
3756 !codes(&h).contains(&"ENUM_VARIABLE_WITHOUT_DEFAULT"),
3757 "{:?}",
3758 codes(&h)
3759 );
3760 }
3761
3762 #[test]
3763 fn static_method_on_instance_warns() {
3764 let h =
3766 infer_first_func("func f():\n\tvar j := JSON.new()\n\tj.stringify({})\n\treturn j\n");
3767 assert!(
3768 codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
3769 "{:?}",
3770 codes(&h)
3771 );
3772 }
3773
3774 #[test]
3775 fn static_method_on_the_type_does_not_warn() {
3776 let h = infer_first_func("func f():\n\tJSON.stringify({})\n");
3778 assert!(
3779 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
3780 "{:?}",
3781 codes(&h)
3782 );
3783 }
3784
3785 #[test]
3786 fn static_method_through_a_type_aliased_local_does_not_warn() {
3787 let h = infer_first_func("func f():\n\tvar t := JSON\n\tt.stringify({})\n");
3789 assert!(
3790 !codes(&h).contains(&"STATIC_CALLED_ON_INSTANCE"),
3791 "{:?}",
3792 codes(&h)
3793 );
3794 }
3795
3796 #[test]
3797 fn property_called_as_function_warns() {
3798 let h = infer_first_func("func f(n: Node):\n\tn.name()\n");
3800 assert!(
3801 codes(&h).contains(&"PROPERTY_USED_AS_FUNCTION"),
3802 "{:?}",
3803 codes(&h)
3804 );
3805 }
3806
3807 #[test]
3808 fn constant_called_as_function_warns() {
3809 let h = infer_first_func("func f(n: Node):\n\tn.NOTIFICATION_READY()\n");
3811 assert!(
3812 codes(&h).contains(&"CONSTANT_USED_AS_FUNCTION"),
3813 "{:?}",
3814 codes(&h)
3815 );
3816 }
3817
3818 #[test]
3819 fn calling_a_real_method_is_not_a_kind_misuse() {
3820 let h = infer_first_func("func f(n: Node):\n\tn.get_parent()\n");
3821 assert!(
3822 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
3823 "{:?}",
3824 codes(&h)
3825 );
3826 }
3827
3828 #[test]
3829 fn reading_a_property_as_a_value_is_not_a_kind_misuse() {
3830 let h = infer_first_func("func f(n: Node):\n\tvar s = n.name\n\treturn s\n");
3831 assert!(
3832 codes(&h).iter().all(|c| !c.ends_with("_USED_AS_FUNCTION")),
3833 "{:?}",
3834 codes(&h)
3835 );
3836 }
3837
3838 #[test]
3839 fn enum_member_into_its_own_enum_slot_is_not_int_as_enum() {
3840 let h = infer_first_func(
3844 "func f():\n\tvar m: Tween.TweenProcessMode = Tween.TWEEN_PROCESS_IDLE\n\treturn m\n",
3845 );
3846 assert!(
3847 !codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
3848 "{:?}",
3849 codes(&h)
3850 );
3851 }
3852
3853 #[test]
3854 fn bare_int_into_enum_slot_still_warns() {
3855 let h = infer_first_func("func f():\n\tvar m: Tween.TweenProcessMode = 0\n\treturn m\n");
3857 assert!(
3858 codes(&h).contains(&"INT_AS_ENUM_WITHOUT_CAST"),
3859 "{:?}",
3860 codes(&h)
3861 );
3862 }
3863
3864 #[test]
3865 fn member_access_resolves_engine_property() {
3866 let h = infer_first_func(
3869 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.get_parent()\n",
3870 );
3871 assert!(
3872 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
3873 "{:?}",
3874 h.result.diagnostics
3875 );
3876 }
3877
3878 #[test]
3879 fn unsafe_method_on_known_type() {
3880 let h = infer_first_func(
3881 "extends Node\nfunc f():\n\tvar n := get_node(\"x\")\n\tn.totally_bogus_method()\n",
3882 );
3883 assert!(
3884 codes(&h).contains(&UNSAFE_METHOD_ACCESS),
3885 "{:?}",
3886 h.result.diagnostics
3887 );
3888 }
3889
3890 #[test]
3891 fn is_narrowing_suppresses_unsafe() {
3892 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.queue_free()\n");
3895 assert!(
3896 codes(&h).iter().all(|c| !c.starts_with("UNSAFE")),
3897 "{:?}",
3898 h.result.diagnostics
3899 );
3900 }
3901
3902 #[test]
3903 fn is_narrowing_flags_real_missing_member() {
3904 let h = infer_first_func("func f(x):\n\tif x is Node:\n\t\tx.bogus_method()\n");
3906 assert!(codes(&h).contains(&UNSAFE_METHOD_ACCESS));
3907 }
3908
3909 #[test]
3910 fn early_return_is_guard_narrows_past_the_guard() {
3911 let safe =
3914 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.get_parent()\n");
3915 assert!(
3916 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
3917 "real Node method must not warn after the guard: {:?}",
3918 codes(&safe)
3919 );
3920 let bogus =
3921 infer_first_func("func f(x):\n\tif not (x is Node):\n\t\treturn\n\tx.bogus_method()\n");
3922 assert!(
3923 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
3924 "missing method must warn after the guard: {:?}",
3925 codes(&bogus)
3926 );
3927 }
3928
3929 #[test]
3930 fn and_short_circuit_narrows_the_rhs() {
3931 let safe = infer_first_func("func f(x):\n\tif x is Node and x.get_parent():\n\t\tpass\n");
3934 assert!(
3935 codes(&safe).iter().all(|c| !c.starts_with("UNSAFE")),
3936 "real Node method in the and-rhs must not warn: {:?}",
3937 codes(&safe)
3938 );
3939 let bogus =
3940 infer_first_func("func f(x):\n\tif x is Node and x.bogus_method():\n\t\tpass\n");
3941 assert!(
3942 codes(&bogus).contains(&UNSAFE_METHOD_ACCESS),
3943 "missing method in the and-rhs must warn: {:?}",
3944 codes(&bogus)
3945 );
3946 }
3947
3948 #[test]
3951 fn empty_file_warns() {
3952 assert!(file_codes("").iter().any(|c| c == "EMPTY_FILE"));
3953 assert!(
3954 file_codes("# just a comment\n")
3955 .iter()
3956 .any(|c| c == "EMPTY_FILE")
3957 );
3958 assert!(
3959 file_codes("extends Node\n")
3960 .iter()
3961 .all(|c| c != "EMPTY_FILE")
3962 );
3963 }
3964
3965 #[test]
3966 fn unused_variable_and_parameter() {
3967 let h = infer_first_func("func f(unused_p):\n\tvar unused_v = 1\n");
3968 assert!(codes(&h).contains(&"UNUSED_PARAMETER"), "{:?}", codes(&h));
3969 assert!(codes(&h).contains(&"UNUSED_VARIABLE"), "{:?}", codes(&h));
3970 let used = infer_first_func("func f(p):\n\tvar v = p\n\treturn v\n");
3972 assert!(codes(&used).iter().all(|c| !c.starts_with("UNUSED")));
3973 let underscored = infer_first_func("func f(_ignored):\n\tpass\n");
3974 assert!(!codes(&underscored).contains(&"UNUSED_PARAMETER"));
3975 }
3976
3977 #[test]
3978 fn standalone_expression_and_ternary() {
3979 let expr = infer_first_func("func f(a, b):\n\ta + b\n");
3980 assert!(
3981 codes(&expr).contains(&"STANDALONE_EXPRESSION"),
3982 "{:?}",
3983 codes(&expr)
3984 );
3985 let tern = infer_first_func("func f(c):\n\t1 if c else 2\n");
3986 assert!(
3987 codes(&tern).contains(&"STANDALONE_TERNARY"),
3988 "{:?}",
3989 codes(&tern)
3990 );
3991 let call = infer_first_func("func f(n):\n\tn.queue_free()\n");
3993 assert!(codes(&call).iter().all(|c| !c.starts_with("STANDALONE")));
3994 }
3995
3996 #[test]
3997 fn unreachable_code_after_return() {
3998 let h = infer_first_func("func f():\n\treturn\n\tprint(\"dead\")\n");
3999 assert!(codes(&h).contains(&"UNREACHABLE_CODE"), "{:?}", codes(&h));
4000 }
4001
4002 #[test]
4003 fn incompatible_ternary_warns() {
4004 let h = infer_first_func("func f(c):\n\tvar x = \"s\" if c else 1\n\treturn x\n");
4006 assert!(
4007 codes(&h).contains(&"INCOMPATIBLE_TERNARY"),
4008 "{:?}",
4009 codes(&h)
4010 );
4011 }
4012
4013 #[test]
4014 fn variant_receiver_never_unsafe() {
4015 let h = infer_first_func("func f(x):\n\tx.anything_at_all()\n");
4017 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
4018 }
4019
4020 #[test]
4021 fn unsafe_call_argument_on_variant_into_typed_param() {
4022 let h = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n: Node2D):\n\tpass\n");
4024 assert!(
4025 codes(&h).contains(&UNSAFE_CALL_ARGUMENT),
4026 "{:?}",
4027 h.result.diagnostics
4028 );
4029 }
4030
4031 #[test]
4032 fn unsafe_call_argument_silent_on_safe_and_untyped() {
4033 let upcast =
4035 infer_first_func("func f(n: Node2D):\n\ttake(n)\nfunc take(n: Node):\n\tpass\n");
4036 assert!(
4037 !codes(&upcast).contains(&UNSAFE_CALL_ARGUMENT),
4038 "upcast is safe: {:?}",
4039 upcast.result.diagnostics
4040 );
4041 let untyped = infer_first_func("func f(p):\n\ttake(p)\nfunc take(n):\n\tpass\n");
4042 assert!(
4043 !codes(&untyped).contains(&UNSAFE_CALL_ARGUMENT),
4044 "untyped param accepts anything: {:?}",
4045 untyped.result.diagnostics
4046 );
4047 }
4048
4049 #[test]
4050 fn inference_on_variant() {
4051 let h = infer_first_func("func f(x):\n\tvar y := x\n");
4053 assert!(codes(&h).contains(&INFERENCE_ON_VARIANT));
4054 }
4055
4056 #[test]
4057 fn field_inferred_from_earlier_field_is_typed() {
4058 let codes = file_codes("var a := 1\nvar b := a + 1\n");
4062 assert!(
4063 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
4064 "field `b` from earlier field `a` should type as int, not Variant: {codes:?}"
4065 );
4066 }
4067
4068 #[test]
4069 fn field_forward_reference_is_seamed_not_warned() {
4070 let codes = file_codes("var b := a\nvar a := 1\n");
4074 assert!(
4075 !codes.iter().any(|c| c == INFERENCE_ON_VARIANT),
4076 "forward field reference must not false-warn: {codes:?}"
4077 );
4078 }
4079
4080 #[test]
4081 fn standalone_inferred_field_unchanged() {
4082 let codes = file_codes("var n := 0\n");
4084 assert!(
4085 codes.is_empty(),
4086 "a literal-initialised field should produce no diagnostics: {codes:?}"
4087 );
4088 }
4089
4090 #[test]
4091 fn lambda_var_is_callable_not_variant() {
4092 let h = infer_first_func("func f():\n\tvar cb := func():\n\t\tpass\n");
4093 assert!(
4094 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4095 "{:?}",
4096 h.result.diagnostics
4097 );
4098 }
4099
4100 #[test]
4101 fn multiline_lambda_then_paren_line_no_false_warning() {
4102 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";
4106 let h = infer_first_func(src);
4107 assert!(
4108 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4109 "{:?}",
4110 h.result.diagnostics
4111 );
4112 }
4113
4114 #[test]
4115 fn calling_a_callable_value_is_seam_not_variant() {
4116 let src = "func f(cb: Callable):\n\tvar x := (cb)()\n\treturn x\n";
4120 let h = infer_first_func(src);
4121 assert!(
4122 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4123 "{:?}",
4124 h.result.diagnostics
4125 );
4126 }
4127
4128 #[test]
4129 fn ternary_with_seam_branch_does_not_collapse_to_variant() {
4130 let src =
4134 "func f(c: bool):\n\tvar x := 5 if c else await get_tree().process_frame\n\treturn x\n";
4135 let h = infer_first_func(src);
4136 assert!(
4137 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4138 "seam branch should keep the ternary on the seam: {:?}",
4139 h.result.diagnostics
4140 );
4141 }
4142
4143 #[test]
4144 fn await_a_coroutine_call_recovers_its_return_type() {
4145 let src = "func g() -> int:\n\tvar x := await make()\n\treturn x\nfunc make() -> int:\n\treturn 5\n";
4148 let h = infer_first_func(src);
4149 assert!(
4150 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4151 "no false variant warning: {:?}",
4152 h.result.diagnostics
4153 );
4154 let api = gdscript_api::bundled();
4155 let x = &h.result.bindings[0];
4156 assert!(
4157 matches!(&x.ty, Ty::Builtin(b) if api.builtin(*b).name == "int"),
4158 "await make() should recover int, got {:?}",
4159 x.ty
4160 );
4161 }
4162
4163 #[test]
4164 fn await_a_signal_stays_the_seam() {
4165 let src = "func f():\n\tvar x := await get_tree().process_frame\n\treturn x\n";
4168 let h = infer_first_func(src);
4169 assert!(
4170 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4171 "awaiting a signal must not warn: {:?}",
4172 h.result.diagnostics
4173 );
4174 assert!(
4175 matches!(&h.result.bindings[0].ty, Ty::Unknown),
4176 "awaiting a signal stays the seam, got {:?}",
4177 h.result.bindings[0].ty
4178 );
4179 }
4180
4181 #[test]
4182 fn for_var_over_packed_string_array_is_string() {
4183 let h = infer_first_func("func f():\n\tfor s in \"a,b\".split(\",\"):\n\t\tvar x := s\n");
4186 assert!(
4187 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4188 "{:?}",
4189 h.result.diagnostics
4190 );
4191 }
4192
4193 #[test]
4194 fn class_new_is_object_not_variant() {
4195 let h = infer_first_func("func f():\n\tvar s := GDScript.new()\n");
4196 assert!(
4197 !codes(&h).contains(&INFERENCE_ON_VARIANT),
4198 "{:?}",
4199 h.result.diagnostics
4200 );
4201 }
4202
4203 #[test]
4204 fn unknown_seam_never_warns() {
4205 let h = infer_first_func("func f():\n\tvar s := preload(\"res://x.gd\")\n\ts.whatever()\n");
4207 assert!(codes(&h).is_empty(), "{:?}", codes(&h));
4208 }
4209
4210 #[test]
4211 fn expr_types_are_memoized_for_hover() {
4212 let h = infer_first_func("func f():\n\tvar n := 42\n");
4213 let has_int = h
4215 .result
4216 .expr_ty
4217 .values()
4218 .any(|t| matches!(t, Ty::Builtin(_)));
4219 assert!(has_int);
4220 assert!(!h.body.exprs.is_empty());
4222 }
4223}