1use cstree::util::NodeOrToken;
15use gdscript_base::{Diagnostic, DiagnosticSource, Severity, TextRange};
16use gdscript_syntax::{GdNode, SyntaxKind};
17use rustc_hash::FxHashMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum WarningCode {
25 UnassignedVariable,
28 UnassignedVariableOpAssign,
30 UnusedVariable,
32 UnusedLocalConstant,
34 UnusedPrivateClassVariable,
36 UnusedParameter,
38 UnusedSignal,
40 ShadowedVariable,
43 ShadowedVariableBaseClass,
45 ShadowedGlobalIdentifier,
47 UnreachableCode,
50 UnreachablePattern,
52 StandaloneExpression,
54 StandaloneTernary,
56 IncompatibleTernary,
58 UnsafeVoidReturn,
61 StaticCalledOnInstance,
63 MissingTool,
66 RedundantStaticUnload,
68 RedundantAwait,
70 AssertAlwaysTrue,
73 AssertAlwaysFalse,
75 IntegerDivision,
78 NarrowingConversion,
80 IntAsEnumWithoutCast,
82 IntAsEnumWithoutMatch,
84 EnumVariableWithoutDefault,
86 EmptyFile,
89 DeprecatedKeyword,
91 ConfusableIdentifier,
94 ConfusableLocalDeclaration,
96 ConfusableLocalUsage,
98 ConfusableCaptureReassignment,
100 ConfusableTemporaryModification,
102 PropertyUsedAsFunction,
105 ConstantUsedAsFunction,
107 FunctionUsedAsProperty,
109 UntypedDeclaration,
112 InferredDeclaration,
114 UnsafePropertyAccess,
116 UnsafeMethodAccess,
118 UnsafeCast,
120 UnsafeCallArgument,
122 ReturnValueDiscarded,
124 MissingAwait,
126 InferenceOnVariant,
129 NativeMethodOverride,
131 GetNodeDefaultWithoutOnready,
133 OnreadyWithExport,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum WarnLevel {
140 Ignore,
142 Warn,
144 Error,
146}
147
148impl WarnLevel {
149 #[must_use]
151 pub fn from_int(n: u32) -> Option<Self> {
152 match n {
153 0 => Some(Self::Ignore),
154 1 => Some(Self::Warn),
155 2 => Some(Self::Error),
156 _ => None,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum Since {
165 V4_3,
167 Master,
169}
170
171impl Since {
172 #[must_use]
174 pub fn min_version(self) -> (u32, u32) {
175 match self {
176 Self::V4_3 => (4, 3),
177 Self::Master => bundled_version(),
178 }
179 }
180}
181
182impl WarningCode {
183 pub const ALL: &'static [WarningCode] = &[
186 Self::UnassignedVariable,
187 Self::UnassignedVariableOpAssign,
188 Self::UnusedVariable,
189 Self::UnusedLocalConstant,
190 Self::UnusedPrivateClassVariable,
191 Self::UnusedParameter,
192 Self::UnusedSignal,
193 Self::ShadowedVariable,
194 Self::ShadowedVariableBaseClass,
195 Self::ShadowedGlobalIdentifier,
196 Self::UnreachableCode,
197 Self::UnreachablePattern,
198 Self::StandaloneExpression,
199 Self::StandaloneTernary,
200 Self::IncompatibleTernary,
201 Self::UnsafeVoidReturn,
202 Self::StaticCalledOnInstance,
203 Self::MissingTool,
204 Self::RedundantStaticUnload,
205 Self::RedundantAwait,
206 Self::AssertAlwaysTrue,
207 Self::AssertAlwaysFalse,
208 Self::IntegerDivision,
209 Self::NarrowingConversion,
210 Self::IntAsEnumWithoutCast,
211 Self::IntAsEnumWithoutMatch,
212 Self::EnumVariableWithoutDefault,
213 Self::EmptyFile,
214 Self::DeprecatedKeyword,
215 Self::ConfusableIdentifier,
216 Self::ConfusableLocalDeclaration,
217 Self::ConfusableLocalUsage,
218 Self::ConfusableCaptureReassignment,
219 Self::ConfusableTemporaryModification,
220 Self::PropertyUsedAsFunction,
221 Self::ConstantUsedAsFunction,
222 Self::FunctionUsedAsProperty,
223 Self::UntypedDeclaration,
224 Self::InferredDeclaration,
225 Self::UnsafePropertyAccess,
226 Self::UnsafeMethodAccess,
227 Self::UnsafeCast,
228 Self::UnsafeCallArgument,
229 Self::ReturnValueDiscarded,
230 Self::MissingAwait,
231 Self::InferenceOnVariant,
232 Self::NativeMethodOverride,
233 Self::GetNodeDefaultWithoutOnready,
234 Self::OnreadyWithExport,
235 ];
236
237 #[must_use]
240 pub fn as_str(self) -> &'static str {
241 match self {
242 Self::UnassignedVariable => "UNASSIGNED_VARIABLE",
243 Self::UnassignedVariableOpAssign => "UNASSIGNED_VARIABLE_OP_ASSIGN",
244 Self::UnusedVariable => "UNUSED_VARIABLE",
245 Self::UnusedLocalConstant => "UNUSED_LOCAL_CONSTANT",
246 Self::UnusedPrivateClassVariable => "UNUSED_PRIVATE_CLASS_VARIABLE",
247 Self::UnusedParameter => "UNUSED_PARAMETER",
248 Self::UnusedSignal => "UNUSED_SIGNAL",
249 Self::ShadowedVariable => "SHADOWED_VARIABLE",
250 Self::ShadowedVariableBaseClass => "SHADOWED_VARIABLE_BASE_CLASS",
251 Self::ShadowedGlobalIdentifier => "SHADOWED_GLOBAL_IDENTIFIER",
252 Self::UnreachableCode => "UNREACHABLE_CODE",
253 Self::UnreachablePattern => "UNREACHABLE_PATTERN",
254 Self::StandaloneExpression => "STANDALONE_EXPRESSION",
255 Self::StandaloneTernary => "STANDALONE_TERNARY",
256 Self::IncompatibleTernary => "INCOMPATIBLE_TERNARY",
257 Self::UnsafeVoidReturn => "UNSAFE_VOID_RETURN",
258 Self::StaticCalledOnInstance => "STATIC_CALLED_ON_INSTANCE",
259 Self::MissingTool => "MISSING_TOOL",
260 Self::RedundantStaticUnload => "REDUNDANT_STATIC_UNLOAD",
261 Self::RedundantAwait => "REDUNDANT_AWAIT",
262 Self::AssertAlwaysTrue => "ASSERT_ALWAYS_TRUE",
263 Self::AssertAlwaysFalse => "ASSERT_ALWAYS_FALSE",
264 Self::IntegerDivision => "INTEGER_DIVISION",
265 Self::NarrowingConversion => "NARROWING_CONVERSION",
266 Self::IntAsEnumWithoutCast => "INT_AS_ENUM_WITHOUT_CAST",
267 Self::IntAsEnumWithoutMatch => "INT_AS_ENUM_WITHOUT_MATCH",
268 Self::EnumVariableWithoutDefault => "ENUM_VARIABLE_WITHOUT_DEFAULT",
269 Self::EmptyFile => "EMPTY_FILE",
270 Self::DeprecatedKeyword => "DEPRECATED_KEYWORD",
271 Self::ConfusableIdentifier => "CONFUSABLE_IDENTIFIER",
272 Self::ConfusableLocalDeclaration => "CONFUSABLE_LOCAL_DECLARATION",
273 Self::ConfusableLocalUsage => "CONFUSABLE_LOCAL_USAGE",
274 Self::ConfusableCaptureReassignment => "CONFUSABLE_CAPTURE_REASSIGNMENT",
275 Self::ConfusableTemporaryModification => "CONFUSABLE_TEMPORARY_MODIFICATION",
276 Self::PropertyUsedAsFunction => "PROPERTY_USED_AS_FUNCTION",
277 Self::ConstantUsedAsFunction => "CONSTANT_USED_AS_FUNCTION",
278 Self::FunctionUsedAsProperty => "FUNCTION_USED_AS_PROPERTY",
279 Self::UntypedDeclaration => "UNTYPED_DECLARATION",
280 Self::InferredDeclaration => "INFERRED_DECLARATION",
281 Self::UnsafePropertyAccess => "UNSAFE_PROPERTY_ACCESS",
282 Self::UnsafeMethodAccess => "UNSAFE_METHOD_ACCESS",
283 Self::UnsafeCast => "UNSAFE_CAST",
284 Self::UnsafeCallArgument => "UNSAFE_CALL_ARGUMENT",
285 Self::ReturnValueDiscarded => "RETURN_VALUE_DISCARDED",
286 Self::MissingAwait => "MISSING_AWAIT",
287 Self::InferenceOnVariant => "INFERENCE_ON_VARIANT",
288 Self::NativeMethodOverride => "NATIVE_METHOD_OVERRIDE",
289 Self::GetNodeDefaultWithoutOnready => "GET_NODE_DEFAULT_WITHOUT_ONREADY",
290 Self::OnreadyWithExport => "ONREADY_WITH_EXPORT",
291 }
292 }
293
294 #[must_use]
296 pub fn setting_name(self) -> String {
297 self.as_str().to_ascii_lowercase()
298 }
299
300 #[must_use]
303 pub fn description(self) -> &'static str {
304 match self {
305 Self::UnassignedVariable => "A typed local is read before it is assigned a value.",
306 Self::UnassignedVariableOpAssign => {
307 "A compound assignment (`+=`, …) is applied to a still-unassigned local."
308 }
309 Self::UnusedVariable => "A local variable is declared but never read.",
310 Self::UnusedLocalConstant => "A local constant is declared but never read.",
311 Self::UnusedPrivateClassVariable => {
312 "A `_`-prefixed class member is never read within the class."
313 }
314 Self::UnusedParameter => "A function parameter is never used (prefix it with `_`).",
315 Self::UnusedSignal => "A signal is never emitted or connected in the file.",
316 Self::ShadowedVariable => "A local shadows an outer local or parameter.",
317 Self::ShadowedVariableBaseClass => "A member shadows a member of a base class.",
318 Self::ShadowedGlobalIdentifier => {
319 "A `class_name`, member, or local shadows a global identifier."
320 }
321 Self::UnreachableCode => {
322 "A statement follows an unconditional `return`/`break`/`continue` (or an exhaustive `match`)."
323 }
324 Self::UnreachablePattern => {
325 "A `match` pattern can never match (it follows a wildcard)."
326 }
327 Self::StandaloneExpression => "An expression statement has no effect.",
328 Self::StandaloneTernary => {
329 "A ternary conditional is used as a statement; its value is discarded."
330 }
331 Self::IncompatibleTernary => {
332 "The two values of a ternary conditional have no common type."
333 }
334 Self::UnsafeVoidReturn => "A `Variant` value is returned from a `-> void` function.",
335 Self::StaticCalledOnInstance => "A static method is called through an instance.",
336 Self::MissingTool => "A class extends a `@tool` class but is not itself `@tool`.",
337 Self::RedundantStaticUnload => {
338 "`@static_unload` is used on a class with no static variables."
339 }
340 Self::RedundantAwait => "`await` is applied to a non-coroutine, non-signal value.",
341 Self::AssertAlwaysTrue => "An `assert(...)` condition is always true.",
342 Self::AssertAlwaysFalse => "An `assert(...)` condition is always false.",
343 Self::IntegerDivision => "Integer division discards the fractional part.",
344 Self::NarrowingConversion => "A `float` is stored into an `int`, losing precision.",
345 Self::IntAsEnumWithoutCast => "An integer is assigned to an enum value without a cast.",
346 Self::IntAsEnumWithoutMatch => "An integer is compared to an enum value in a `match`.",
347 Self::EnumVariableWithoutDefault => {
348 "An enum-typed variable has no explicit default value."
349 }
350 Self::EmptyFile => "The script file has no members, `class_name`, or `extends`.",
351 Self::DeprecatedKeyword => "A deprecated keyword (e.g. `yield`) is used.",
352 Self::ConfusableIdentifier => {
353 "An identifier mixes scripts / uses confusable characters."
354 }
355 Self::ConfusableLocalDeclaration => "A local is declared after a same-name outer use.",
356 Self::ConfusableLocalUsage => {
357 "A local shadowing a member is used before its declaration."
358 }
359 Self::ConfusableCaptureReassignment => {
360 "A captured variable is reassigned inside a lambda."
361 }
362 Self::ConfusableTemporaryModification => "A temporary value is modified in place.",
363 Self::PropertyUsedAsFunction => "A property is called as if it were a function.",
364 Self::ConstantUsedAsFunction => "A constant is called as if it were a function.",
365 Self::FunctionUsedAsProperty => "A function is accessed as if it were a property.",
366 Self::UntypedDeclaration => "A declaration has no type annotation.",
367 Self::InferredDeclaration => "A declaration uses an inferred type (`:=`).",
368 Self::UnsafePropertyAccess => {
369 "A property is not present on the inferred type (but may be on a subtype)."
370 }
371 Self::UnsafeMethodAccess => {
372 "A method is not present on the inferred type (but may be on a subtype)."
373 }
374 Self::UnsafeCast => "A value is cast through `Variant`, which is unsafe.",
375 Self::UnsafeCallArgument => {
376 "An argument needs an unsafe implicit cast into the parameter type."
377 }
378 Self::ReturnValueDiscarded => "A non-`void` call's return value is discarded.",
379 Self::MissingAwait => "An awaitable call's result is not awaited.",
380 Self::InferenceOnVariant => "A type is inferred from a statically-`Variant` value.",
381 Self::NativeMethodOverride => {
382 "A native virtual method is overridden with an incompatible signature."
383 }
384 Self::GetNodeDefaultWithoutOnready => {
385 "A `get_node(...)` default initializer should be `@onready`."
386 }
387 Self::OnreadyWithExport => "`@onready` and `@export` are used together on one member.",
388 }
389 }
390
391 #[must_use]
393 pub fn default_level(self) -> WarnLevel {
394 match self {
395 Self::UntypedDeclaration
397 | Self::InferredDeclaration
398 | Self::UnsafePropertyAccess
399 | Self::UnsafeMethodAccess
400 | Self::UnsafeCast
401 | Self::UnsafeCallArgument
402 | Self::ReturnValueDiscarded
403 | Self::MissingAwait => WarnLevel::Ignore,
404 Self::InferenceOnVariant
406 | Self::NativeMethodOverride
407 | Self::GetNodeDefaultWithoutOnready
408 | Self::OnreadyWithExport => WarnLevel::Error,
409 _ => WarnLevel::Warn,
411 }
412 }
413
414 #[must_use]
417 pub fn is_opt_in(self) -> bool {
418 self.default_level() == WarnLevel::Ignore
419 }
420
421 #[must_use]
423 pub fn since(self) -> Since {
424 match self {
425 Self::ConfusableTemporaryModification | Self::MissingAwait => Since::Master,
426 _ => Since::V4_3,
427 }
428 }
429
430 #[must_use]
433 pub fn from_setting_name(name: &str) -> Option<WarningCode> {
434 Self::ALL
435 .iter()
436 .copied()
437 .find(|c| c.as_str().eq_ignore_ascii_case(name))
438 }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct RawWarning {
445 pub range: TextRange,
447 pub code: WarningCode,
449 pub message: String,
451}
452
453#[allow(clippy::struct_excessive_bools)]
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct WarningSettings {
460 pub enabled: bool,
462 pub treat_as_errors: bool,
464 pub per_code: FxHashMap<WarningCode, WarnLevel>,
466 pub exclude_addons: bool,
468 pub engine: (u32, u32),
470 pub strict_opt_in: bool,
473}
474
475impl WarningSettings {
476 #[must_use]
479 pub fn analyzer_default() -> Self {
480 Self {
481 enabled: true,
482 treat_as_errors: false,
483 per_code: FxHashMap::default(),
484 exclude_addons: false,
485 engine: bundled_version(),
486 strict_opt_in: true,
487 }
488 }
489
490 #[must_use]
496 pub fn with_strict_opt_in(mut self, on: bool) -> Self {
497 self.strict_opt_in = on;
498 self
499 }
500
501 #[must_use]
504 pub fn engine_default(engine: (u32, u32)) -> Self {
505 Self {
506 enabled: true,
507 treat_as_errors: false,
508 per_code: FxHashMap::default(),
509 exclude_addons: true,
510 engine,
511 strict_opt_in: false,
512 }
513 }
514}
515
516#[derive(Debug, Clone, Default, PartialEq, Eq)]
520pub struct SuppressionMap {
521 spans: Vec<(TextRange, Vec<WarningCode>)>,
522}
523
524impl SuppressionMap {
525 #[must_use]
527 pub fn is_suppressed(&self, code: WarningCode, at: TextRange) -> bool {
528 self.spans.iter().any(|(span, codes)| {
529 span.start <= at.start && at.end <= span.end && codes.contains(&code)
530 })
531 }
532
533 pub fn push(&mut self, range: TextRange, codes: Vec<WarningCode>) {
535 self.spans.push((range, codes));
536 }
537}
538
539#[must_use]
545pub fn build_suppression_map(root: &GdNode, source: &str) -> SuppressionMap {
546 let mut map = SuppressionMap::default();
547 let mut anns: Vec<GdNode> = gdscript_syntax::ast::descendants(root)
549 .into_iter()
550 .filter(|n| n.kind() == SyntaxKind::Annotation)
551 .collect();
552 anns.sort_by_key(|n| u32::from(n.text_range().start()));
553
554 let mut open: FxHashMap<WarningCode, u32> = FxHashMap::default();
560 let eof = u32::from(root.text_range().end());
561
562 for ann in &anns {
563 let Some(name) = annotation_name(ann) else {
564 continue;
565 };
566 let codes = annotation_warning_codes(ann);
567 if codes.is_empty() {
568 continue; }
570 match name.as_str() {
571 "warning_ignore" => {
572 if let Some(target) = next_decorated_sibling(ann) {
573 let r = target.text_range();
574 let start = u32::from(r.start());
575 let end = line_end_from(source, u32::from(r.end()));
581 map.push(TextRange::new(start, end), codes);
582 }
583 }
584 "warning_ignore_start" => {
585 let start = u32::from(ann.text_range().end());
586 for c in codes {
587 open.insert(c, start); }
589 }
590 "warning_ignore_restore" => {
591 let end = u32::from(ann.text_range().start());
592 for c in &codes {
593 if let Some(start) = open.remove(c) {
594 map.push(TextRange::new(start, end), vec![*c]);
595 }
596 }
597 }
598 _ => {}
599 }
600 }
601 let mut leftover: Vec<(WarningCode, u32)> = open.into_iter().collect();
604 leftover.sort_by_key(|&(_, start)| start);
605 for (c, start) in leftover {
606 map.push(TextRange::new(start, eof), vec![c]);
607 }
608 map
609}
610
611fn annotation_name(ann: &GdNode) -> Option<String> {
613 ann.children_with_tokens()
614 .filter_map(NodeOrToken::into_token)
615 .find(|t| t.kind() == SyntaxKind::Ident)
616 .map(|t| t.text().to_owned())
617}
618
619fn annotation_warning_codes(ann: &GdNode) -> Vec<WarningCode> {
621 let Some(arglist) = ann.children().find(|c| c.kind() == SyntaxKind::ArgList) else {
622 return Vec::new();
623 };
624 let mut codes = Vec::new();
625 for lit in arglist
626 .children()
627 .filter(|c| c.kind() == SyntaxKind::Literal)
628 {
629 for tok in lit
630 .children_with_tokens()
631 .filter_map(NodeOrToken::into_token)
632 {
633 if tok.kind() == SyntaxKind::String
634 && let Some(c) =
635 WarningCode::from_setting_name(tok.text().trim_matches(['"', '\'']))
636 {
637 codes.push(c);
638 }
639 }
640 }
641 codes
642}
643
644fn line_end_from(source: &str, start: u32) -> u32 {
647 let s = start as usize;
648 match source.get(s..).and_then(|rest| rest.find('\n')) {
649 Some(i) => u32::try_from(s + i).unwrap_or(u32::MAX),
650 None => u32::try_from(source.len()).unwrap_or(u32::MAX),
651 }
652}
653
654fn next_decorated_sibling(ann: &GdNode) -> Option<GdNode> {
657 let parent = ann.parent()?;
658 let after = ann.text_range().start();
659 parent
660 .children()
661 .filter(|c| c.text_range().start() > after && c.kind() != SyntaxKind::Annotation)
662 .min_by_key(|c| u32::from(c.text_range().start()))
663 .cloned()
664}
665
666#[must_use]
670pub fn gate(
671 raw: &RawWarning,
672 settings: &WarningSettings,
673 ignores: &SuppressionMap,
674 path: Option<&str>,
675) -> Option<Diagnostic> {
676 if !settings.enabled {
677 return None;
678 }
679 if raw.code.since().min_version() > settings.engine {
681 return None;
682 }
683 let mut level = settings
686 .per_code
687 .get(&raw.code)
688 .copied()
689 .unwrap_or_else(|| {
690 let d = raw.code.default_level();
691 if settings.strict_opt_in && d == WarnLevel::Ignore {
692 WarnLevel::Warn
693 } else {
694 d
695 }
696 });
697 if level == WarnLevel::Ignore {
698 return None;
699 }
700 if settings.treat_as_errors && level == WarnLevel::Warn {
701 level = WarnLevel::Error;
702 }
703 if settings.exclude_addons && path.is_some_and(is_addon_path) {
704 return None;
705 }
706 if ignores.is_suppressed(raw.code, raw.range) {
707 return None;
708 }
709 Some(Diagnostic {
710 range: raw.range,
711 severity: match level {
712 WarnLevel::Error => Severity::Error,
713 _ => Severity::Warning,
715 },
716 code: raw.code.as_str().to_owned(),
717 message: raw.message.clone(),
718 source: DiagnosticSource::Type,
719 fixes: Vec::new(),
720 })
721}
722
723#[must_use]
727pub fn render_warning_reference() -> String {
728 use std::fmt::Write as _;
729 let mut codes: Vec<WarningCode> = WarningCode::ALL.to_vec();
730 codes.sort_by_key(|c| c.as_str());
731
732 let mut s = String::new();
733 s.push_str("<!-- @generated by `gdscript-hir` (warnings::render_warning_reference); do not edit by hand. -->\n");
734 s.push_str("<!-- Regenerate: `GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current` -->\n\n");
735 s.push_str("# Warning Reference\n\n");
736 s.push_str(
737 "Every gateable GDScript warning the analyzer can emit, with its `project.godot` setting key, \
738 engine-default level, and the earliest Godot version it applies to. Configure these under \
739 `[debug]` as `gdscript/warnings/<key>` (`0` = ignore, `1` = warn, `2` = error), or suppress \
740 inline with `@warning_ignore(\"<key>\")`. See [Configuration](./configuration.md).\n\n",
741 );
742 s.push_str("| Code | Setting key | Default | Since | Description |\n");
743 s.push_str("|---|---|---|---|---|\n");
744 for c in codes {
745 let default = match c.default_level() {
746 WarnLevel::Ignore => "Ignore",
747 WarnLevel::Warn => "Warn",
748 WarnLevel::Error => "Error",
749 };
750 let since = match c.since() {
751 Since::V4_3 => "4.3",
752 Since::Master => "master",
753 };
754 let _ = writeln!(
755 s,
756 "| `{}` | `{}` | {default} | {since} | {} |",
757 c.as_str(),
758 c.setting_name(),
759 c.description(),
760 );
761 }
762 s
763}
764
765fn is_addon_path(path: &str) -> bool {
770 path.starts_with("res://addons/")
771}
772
773#[must_use]
777pub fn bundled_version() -> (u32, u32) {
778 parse_major_minor(gdscript_api::godot_version()).unwrap_or((4, 5))
779}
780
781fn parse_major_minor(s: &str) -> Option<(u32, u32)> {
783 let mut parts = s.split('.');
784 let major = parts.next()?.parse().ok()?;
785 let minor: u32 = parts
786 .next()?
787 .chars()
788 .take_while(char::is_ascii_digit)
789 .collect::<String>()
790 .parse()
791 .ok()?;
792 Some((major, minor))
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798 use gdscript_syntax::parse;
799 use std::collections::HashSet;
800
801 fn off(src: &str, needle: &str) -> u32 {
802 u32::try_from(src.find(needle).unwrap()).unwrap()
803 }
804
805 #[test]
806 fn warning_reference_doc_is_current() {
807 let path = concat!(
809 env!("CARGO_MANIFEST_DIR"),
810 "/../../docs/src/reference/warnings.md"
811 );
812 let generated = render_warning_reference();
813 if std::env::var("GDSCRIPT_UPDATE_DOCS").is_ok() {
814 if let Some(parent) = std::path::Path::new(path).parent() {
815 std::fs::create_dir_all(parent).unwrap();
816 }
817 std::fs::write(path, &generated).unwrap();
818 return;
819 }
820 let on_disk = std::fs::read_to_string(path).unwrap_or_default();
821 assert_eq!(
822 on_disk, generated,
823 "docs/src/reference/warnings.md is stale — regenerate with \
824 `GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current`",
825 );
826 }
827
828 #[test]
829 fn warning_ignore_suppresses_the_next_statement() {
830 let src = "func f():\n\t@warning_ignore(\"integer_division\")\n\tvar x = 5 / 2\n";
831 let map = build_suppression_map(&parse(src).syntax_node(), src);
832 let at = off(src, "5 / 2");
833 assert!(map.is_suppressed(WarningCode::IntegerDivision, TextRange::new(at, at + 5)));
834 assert!(!map.is_suppressed(WarningCode::NarrowingConversion, TextRange::new(at, at + 5)));
836 }
837
838 #[test]
839 fn warning_ignore_covers_semicolon_joined_statements_on_the_line() {
840 let src = "func f():\n\t@warning_ignore(\"unused_variable\")\n\tvar a = 1; var b = 2\n\tvar c = 3\n";
843 let map = build_suppression_map(&parse(src).syntax_node(), src);
844 let a = off(src, "var a");
845 let b = off(src, "var b");
846 let c = off(src, "var c");
847 assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
848 assert!(
849 map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)),
850 "the second `;`-joined statement on the line must be covered"
851 );
852 assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(c, c + 1)));
854 }
855
856 #[test]
857 fn warning_ignore_start_restore_suppresses_a_region() {
858 let src = "@warning_ignore_start(\"unused_variable\")\nfunc f():\n\tvar a = 1\n@warning_ignore_restore(\"unused_variable\")\nfunc g():\n\tvar b = 2\n";
859 let map = build_suppression_map(&parse(src).syntax_node(), src);
860 let a = off(src, "var a");
861 let b = off(src, "var b");
862 assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
863 assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)));
865 }
866
867 #[test]
868 fn repeated_start_for_one_code_overwrites_and_does_not_leak_past_restore() {
869 let src = "@warning_ignore_start(\"unused_variable\")\nvar before = 1\n@warning_ignore_start(\"unused_variable\")\nvar inside = 2\n@warning_ignore_restore(\"unused_variable\")\nvar after = 3\n";
873 let map = build_suppression_map(&parse(src).syntax_node(), src);
874 let before = off(src, "before");
875 let inside = off(src, "inside");
876 let after = off(src, "after");
877 assert!(
878 map.is_suppressed(
879 WarningCode::UnusedVariable,
880 TextRange::new(inside, inside + 1)
881 ),
882 "the active [start2 .. restore] region must be suppressed"
883 );
884 assert!(
885 !map.is_suppressed(
886 WarningCode::UnusedVariable,
887 TextRange::new(after, after + 1)
888 ),
889 "code after the restore must NOT be suppressed (no leak to EOF)"
890 );
891 assert!(
892 !map.is_suppressed(
893 WarningCode::UnusedVariable,
894 TextRange::new(before, before + 1)
895 ),
896 "code before the overwriting start must NOT be suppressed"
897 );
898 }
899
900 #[test]
901 fn exclude_addons_only_matches_the_root_addons_dir() {
902 let none = SuppressionMap::default();
903 let mut s = WarningSettings::engine_default((4, 5));
904 s.per_code
905 .insert(WarningCode::IntegerDivision, WarnLevel::Warn);
906 assert!(
908 gate(
909 &raw(WarningCode::IntegerDivision),
910 &s,
911 &none,
912 Some("res://game/addons/spawner.gd")
913 )
914 .is_some(),
915 "a nested addons/ dir must still be checked"
916 );
917 assert!(
919 gate(
920 &raw(WarningCode::IntegerDivision),
921 &s,
922 &none,
923 Some("res://addons/plugin/x.gd")
924 )
925 .is_none()
926 );
927 }
928
929 fn raw(code: WarningCode) -> RawWarning {
930 RawWarning {
931 range: TextRange::new(10, 20),
932 code,
933 message: "msg".to_owned(),
934 }
935 }
936
937 #[test]
938 fn every_code_has_a_unique_uppercase_string_that_round_trips() {
939 let mut seen = HashSet::new();
940 for &c in WarningCode::ALL {
941 assert!(seen.insert(c.as_str()), "duplicate as_str: {}", c.as_str());
942 assert_eq!(c.as_str(), c.as_str().to_ascii_uppercase());
943 assert_eq!(WarningCode::from_setting_name(&c.setting_name()), Some(c));
944 }
945 assert_eq!(seen.len(), 49);
947 }
948
949 #[test]
950 fn disabled_drops_everything() {
951 let mut s = WarningSettings::analyzer_default();
952 s.enabled = false;
953 assert!(
954 gate(
955 &raw(WarningCode::IntegerDivision),
956 &s,
957 &SuppressionMap::default(),
958 None
959 )
960 .is_none()
961 );
962 }
963
964 #[test]
965 fn opt_in_group_is_silent_under_engine_default_but_warns_under_strict() {
966 let none = SuppressionMap::default();
967 let engine = WarningSettings::engine_default((4, 5));
968 assert!(gate(&raw(WarningCode::UnsafeMethodAccess), &engine, &none, None).is_none());
969 let strict = WarningSettings::analyzer_default(); let d = gate(&raw(WarningCode::UnsafeMethodAccess), &strict, &none, None).unwrap();
971 assert_eq!(d.severity, Severity::Warning);
972 assert_eq!(d.code, "UNSAFE_METHOD_ACCESS");
973 }
974
975 #[test]
976 fn error_default_stays_error() {
977 let d = gate(
978 &raw(WarningCode::InferenceOnVariant),
979 &WarningSettings::analyzer_default(),
980 &SuppressionMap::default(),
981 None,
982 )
983 .unwrap();
984 assert_eq!(d.severity, Severity::Error);
985 }
986
987 #[test]
988 fn treat_as_errors_escalates_warn_only() {
989 let none = SuppressionMap::default();
990 let mut s = WarningSettings::analyzer_default();
991 s.treat_as_errors = true;
992 let d = gate(&raw(WarningCode::IntegerDivision), &s, &none, None).unwrap();
994 assert_eq!(d.severity, Severity::Error);
995 s.per_code
997 .insert(WarningCode::IntegerDivision, WarnLevel::Ignore);
998 assert!(gate(&raw(WarningCode::IntegerDivision), &s, &none, None).is_none());
999 }
1000
1001 #[test]
1002 fn per_code_override_sets_level() {
1003 let none = SuppressionMap::default();
1004 let mut s = WarningSettings::engine_default((4, 5));
1005 s.per_code
1006 .insert(WarningCode::UnsafeMethodAccess, WarnLevel::Error);
1007 let d = gate(&raw(WarningCode::UnsafeMethodAccess), &s, &none, None).unwrap();
1008 assert_eq!(d.severity, Severity::Error);
1009 }
1010
1011 #[test]
1012 fn exclude_addons_suppresses_by_path() {
1013 let mut s = WarningSettings::analyzer_default();
1014 s.exclude_addons = true;
1015 assert!(
1016 gate(
1017 &raw(WarningCode::IntegerDivision),
1018 &s,
1019 &SuppressionMap::default(),
1020 Some("res://addons/x/y.gd")
1021 )
1022 .is_none()
1023 );
1024 assert!(
1025 gate(
1026 &raw(WarningCode::IntegerDivision),
1027 &s,
1028 &SuppressionMap::default(),
1029 Some("res://game/y.gd")
1030 )
1031 .is_some()
1032 );
1033 }
1034
1035 #[test]
1036 fn suppression_map_drops_covered_range() {
1037 let mut map = SuppressionMap::default();
1038 map.push(TextRange::new(0, 100), vec![WarningCode::IntegerDivision]);
1039 assert!(
1040 gate(
1041 &raw(WarningCode::IntegerDivision),
1042 &WarningSettings::analyzer_default(),
1043 &map,
1044 None
1045 )
1046 .is_none()
1047 );
1048 assert!(
1050 gate(
1051 &raw(WarningCode::NarrowingConversion),
1052 &WarningSettings::analyzer_default(),
1053 &map,
1054 None
1055 )
1056 .is_some()
1057 );
1058 }
1059
1060 #[test]
1061 fn master_only_codes_gate_on_engine_version() {
1062 let none = SuppressionMap::default();
1063 let mut old = WarningSettings::engine_default((4, 3));
1065 old.strict_opt_in = false;
1066 assert!(
1067 gate(
1068 &raw(WarningCode::ConfusableTemporaryModification),
1069 &old,
1070 &none,
1071 None
1072 )
1073 .is_none()
1074 );
1075 let new = WarningSettings::engine_default((4, 5));
1076 assert!(
1077 gate(
1078 &raw(WarningCode::ConfusableTemporaryModification),
1079 &new,
1080 &none,
1081 None
1082 )
1083 .is_some()
1084 );
1085 }
1086}