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]
493 pub fn engine_default(engine: (u32, u32)) -> Self {
494 Self {
495 enabled: true,
496 treat_as_errors: false,
497 per_code: FxHashMap::default(),
498 exclude_addons: true,
499 engine,
500 strict_opt_in: false,
501 }
502 }
503}
504
505#[derive(Debug, Clone, Default, PartialEq, Eq)]
509pub struct SuppressionMap {
510 spans: Vec<(TextRange, Vec<WarningCode>)>,
511}
512
513impl SuppressionMap {
514 #[must_use]
516 pub fn is_suppressed(&self, code: WarningCode, at: TextRange) -> bool {
517 self.spans.iter().any(|(span, codes)| {
518 span.start <= at.start && at.end <= span.end && codes.contains(&code)
519 })
520 }
521
522 pub fn push(&mut self, range: TextRange, codes: Vec<WarningCode>) {
524 self.spans.push((range, codes));
525 }
526}
527
528#[must_use]
534pub fn build_suppression_map(root: &GdNode, source: &str) -> SuppressionMap {
535 let mut map = SuppressionMap::default();
536 let mut anns: Vec<GdNode> = gdscript_syntax::ast::descendants(root)
538 .into_iter()
539 .filter(|n| n.kind() == SyntaxKind::Annotation)
540 .collect();
541 anns.sort_by_key(|n| u32::from(n.text_range().start()));
542
543 let mut open: FxHashMap<WarningCode, u32> = FxHashMap::default();
549 let eof = u32::from(root.text_range().end());
550
551 for ann in &anns {
552 let Some(name) = annotation_name(ann) else {
553 continue;
554 };
555 let codes = annotation_warning_codes(ann);
556 if codes.is_empty() {
557 continue; }
559 match name.as_str() {
560 "warning_ignore" => {
561 if let Some(target) = next_decorated_sibling(ann) {
562 let r = target.text_range();
563 let start = u32::from(r.start());
564 let end = line_end_from(source, u32::from(r.end()));
570 map.push(TextRange::new(start, end), codes);
571 }
572 }
573 "warning_ignore_start" => {
574 let start = u32::from(ann.text_range().end());
575 for c in codes {
576 open.insert(c, start); }
578 }
579 "warning_ignore_restore" => {
580 let end = u32::from(ann.text_range().start());
581 for c in &codes {
582 if let Some(start) = open.remove(c) {
583 map.push(TextRange::new(start, end), vec![*c]);
584 }
585 }
586 }
587 _ => {}
588 }
589 }
590 let mut leftover: Vec<(WarningCode, u32)> = open.into_iter().collect();
593 leftover.sort_by_key(|&(_, start)| start);
594 for (c, start) in leftover {
595 map.push(TextRange::new(start, eof), vec![c]);
596 }
597 map
598}
599
600fn annotation_name(ann: &GdNode) -> Option<String> {
602 ann.children_with_tokens()
603 .filter_map(NodeOrToken::into_token)
604 .find(|t| t.kind() == SyntaxKind::Ident)
605 .map(|t| t.text().to_owned())
606}
607
608fn annotation_warning_codes(ann: &GdNode) -> Vec<WarningCode> {
610 let Some(arglist) = ann.children().find(|c| c.kind() == SyntaxKind::ArgList) else {
611 return Vec::new();
612 };
613 let mut codes = Vec::new();
614 for lit in arglist
615 .children()
616 .filter(|c| c.kind() == SyntaxKind::Literal)
617 {
618 for tok in lit
619 .children_with_tokens()
620 .filter_map(NodeOrToken::into_token)
621 {
622 if tok.kind() == SyntaxKind::String
623 && let Some(c) =
624 WarningCode::from_setting_name(tok.text().trim_matches(['"', '\'']))
625 {
626 codes.push(c);
627 }
628 }
629 }
630 codes
631}
632
633fn line_end_from(source: &str, start: u32) -> u32 {
636 let s = start as usize;
637 match source.get(s..).and_then(|rest| rest.find('\n')) {
638 Some(i) => u32::try_from(s + i).unwrap_or(u32::MAX),
639 None => u32::try_from(source.len()).unwrap_or(u32::MAX),
640 }
641}
642
643fn next_decorated_sibling(ann: &GdNode) -> Option<GdNode> {
646 let parent = ann.parent()?;
647 let after = ann.text_range().start();
648 parent
649 .children()
650 .filter(|c| c.text_range().start() > after && c.kind() != SyntaxKind::Annotation)
651 .min_by_key(|c| u32::from(c.text_range().start()))
652 .cloned()
653}
654
655#[must_use]
659pub fn gate(
660 raw: &RawWarning,
661 settings: &WarningSettings,
662 ignores: &SuppressionMap,
663 path: Option<&str>,
664) -> Option<Diagnostic> {
665 if !settings.enabled {
666 return None;
667 }
668 if raw.code.since().min_version() > settings.engine {
670 return None;
671 }
672 let mut level = settings
675 .per_code
676 .get(&raw.code)
677 .copied()
678 .unwrap_or_else(|| {
679 let d = raw.code.default_level();
680 if settings.strict_opt_in && d == WarnLevel::Ignore {
681 WarnLevel::Warn
682 } else {
683 d
684 }
685 });
686 if level == WarnLevel::Ignore {
687 return None;
688 }
689 if settings.treat_as_errors && level == WarnLevel::Warn {
690 level = WarnLevel::Error;
691 }
692 if settings.exclude_addons && path.is_some_and(is_addon_path) {
693 return None;
694 }
695 if ignores.is_suppressed(raw.code, raw.range) {
696 return None;
697 }
698 Some(Diagnostic {
699 range: raw.range,
700 severity: match level {
701 WarnLevel::Error => Severity::Error,
702 _ => Severity::Warning,
704 },
705 code: raw.code.as_str().to_owned(),
706 message: raw.message.clone(),
707 source: DiagnosticSource::Type,
708 fixes: Vec::new(),
709 })
710}
711
712#[must_use]
716pub fn render_warning_reference() -> String {
717 use std::fmt::Write as _;
718 let mut codes: Vec<WarningCode> = WarningCode::ALL.to_vec();
719 codes.sort_by_key(|c| c.as_str());
720
721 let mut s = String::new();
722 s.push_str("<!-- @generated by `gdscript-hir` (warnings::render_warning_reference); do not edit by hand. -->\n");
723 s.push_str("<!-- Regenerate: `GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current` -->\n\n");
724 s.push_str("# Warning Reference\n\n");
725 s.push_str(
726 "Every gateable GDScript warning the analyzer can emit, with its `project.godot` setting key, \
727 engine-default level, and the earliest Godot version it applies to. Configure these under \
728 `[debug]` as `gdscript/warnings/<key>` (`0` = ignore, `1` = warn, `2` = error), or suppress \
729 inline with `@warning_ignore(\"<key>\")`. See [Configuration](./configuration.md).\n\n",
730 );
731 s.push_str("| Code | Setting key | Default | Since | Description |\n");
732 s.push_str("|---|---|---|---|---|\n");
733 for c in codes {
734 let default = match c.default_level() {
735 WarnLevel::Ignore => "Ignore",
736 WarnLevel::Warn => "Warn",
737 WarnLevel::Error => "Error",
738 };
739 let since = match c.since() {
740 Since::V4_3 => "4.3",
741 Since::Master => "master",
742 };
743 let _ = writeln!(
744 s,
745 "| `{}` | `{}` | {default} | {since} | {} |",
746 c.as_str(),
747 c.setting_name(),
748 c.description(),
749 );
750 }
751 s
752}
753
754fn is_addon_path(path: &str) -> bool {
759 path.starts_with("res://addons/")
760}
761
762#[must_use]
766pub fn bundled_version() -> (u32, u32) {
767 parse_major_minor(gdscript_api::godot_version()).unwrap_or((4, 5))
768}
769
770fn parse_major_minor(s: &str) -> Option<(u32, u32)> {
772 let mut parts = s.split('.');
773 let major = parts.next()?.parse().ok()?;
774 let minor: u32 = parts
775 .next()?
776 .chars()
777 .take_while(char::is_ascii_digit)
778 .collect::<String>()
779 .parse()
780 .ok()?;
781 Some((major, minor))
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use gdscript_syntax::parse;
788 use std::collections::HashSet;
789
790 fn off(src: &str, needle: &str) -> u32 {
791 u32::try_from(src.find(needle).unwrap()).unwrap()
792 }
793
794 #[test]
795 fn warning_reference_doc_is_current() {
796 let path = concat!(
798 env!("CARGO_MANIFEST_DIR"),
799 "/../../docs/src/reference/warnings.md"
800 );
801 let generated = render_warning_reference();
802 if std::env::var("GDSCRIPT_UPDATE_DOCS").is_ok() {
803 if let Some(parent) = std::path::Path::new(path).parent() {
804 std::fs::create_dir_all(parent).unwrap();
805 }
806 std::fs::write(path, &generated).unwrap();
807 return;
808 }
809 let on_disk = std::fs::read_to_string(path).unwrap_or_default();
810 assert_eq!(
811 on_disk, generated,
812 "docs/src/reference/warnings.md is stale — regenerate with \
813 `GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current`",
814 );
815 }
816
817 #[test]
818 fn warning_ignore_suppresses_the_next_statement() {
819 let src = "func f():\n\t@warning_ignore(\"integer_division\")\n\tvar x = 5 / 2\n";
820 let map = build_suppression_map(&parse(src).syntax_node(), src);
821 let at = off(src, "5 / 2");
822 assert!(map.is_suppressed(WarningCode::IntegerDivision, TextRange::new(at, at + 5)));
823 assert!(!map.is_suppressed(WarningCode::NarrowingConversion, TextRange::new(at, at + 5)));
825 }
826
827 #[test]
828 fn warning_ignore_covers_semicolon_joined_statements_on_the_line() {
829 let src = "func f():\n\t@warning_ignore(\"unused_variable\")\n\tvar a = 1; var b = 2\n\tvar c = 3\n";
832 let map = build_suppression_map(&parse(src).syntax_node(), src);
833 let a = off(src, "var a");
834 let b = off(src, "var b");
835 let c = off(src, "var c");
836 assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
837 assert!(
838 map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)),
839 "the second `;`-joined statement on the line must be covered"
840 );
841 assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(c, c + 1)));
843 }
844
845 #[test]
846 fn warning_ignore_start_restore_suppresses_a_region() {
847 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";
848 let map = build_suppression_map(&parse(src).syntax_node(), src);
849 let a = off(src, "var a");
850 let b = off(src, "var b");
851 assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
852 assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)));
854 }
855
856 #[test]
857 fn repeated_start_for_one_code_overwrites_and_does_not_leak_past_restore() {
858 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";
862 let map = build_suppression_map(&parse(src).syntax_node(), src);
863 let before = off(src, "before");
864 let inside = off(src, "inside");
865 let after = off(src, "after");
866 assert!(
867 map.is_suppressed(
868 WarningCode::UnusedVariable,
869 TextRange::new(inside, inside + 1)
870 ),
871 "the active [start2 .. restore] region must be suppressed"
872 );
873 assert!(
874 !map.is_suppressed(
875 WarningCode::UnusedVariable,
876 TextRange::new(after, after + 1)
877 ),
878 "code after the restore must NOT be suppressed (no leak to EOF)"
879 );
880 assert!(
881 !map.is_suppressed(
882 WarningCode::UnusedVariable,
883 TextRange::new(before, before + 1)
884 ),
885 "code before the overwriting start must NOT be suppressed"
886 );
887 }
888
889 #[test]
890 fn exclude_addons_only_matches_the_root_addons_dir() {
891 let none = SuppressionMap::default();
892 let mut s = WarningSettings::engine_default((4, 5));
893 s.per_code
894 .insert(WarningCode::IntegerDivision, WarnLevel::Warn);
895 assert!(
897 gate(
898 &raw(WarningCode::IntegerDivision),
899 &s,
900 &none,
901 Some("res://game/addons/spawner.gd")
902 )
903 .is_some(),
904 "a nested addons/ dir must still be checked"
905 );
906 assert!(
908 gate(
909 &raw(WarningCode::IntegerDivision),
910 &s,
911 &none,
912 Some("res://addons/plugin/x.gd")
913 )
914 .is_none()
915 );
916 }
917
918 fn raw(code: WarningCode) -> RawWarning {
919 RawWarning {
920 range: TextRange::new(10, 20),
921 code,
922 message: "msg".to_owned(),
923 }
924 }
925
926 #[test]
927 fn every_code_has_a_unique_uppercase_string_that_round_trips() {
928 let mut seen = HashSet::new();
929 for &c in WarningCode::ALL {
930 assert!(seen.insert(c.as_str()), "duplicate as_str: {}", c.as_str());
931 assert_eq!(c.as_str(), c.as_str().to_ascii_uppercase());
932 assert_eq!(WarningCode::from_setting_name(&c.setting_name()), Some(c));
933 }
934 assert_eq!(seen.len(), 49);
936 }
937
938 #[test]
939 fn disabled_drops_everything() {
940 let mut s = WarningSettings::analyzer_default();
941 s.enabled = false;
942 assert!(
943 gate(
944 &raw(WarningCode::IntegerDivision),
945 &s,
946 &SuppressionMap::default(),
947 None
948 )
949 .is_none()
950 );
951 }
952
953 #[test]
954 fn opt_in_group_is_silent_under_engine_default_but_warns_under_strict() {
955 let none = SuppressionMap::default();
956 let engine = WarningSettings::engine_default((4, 5));
957 assert!(gate(&raw(WarningCode::UnsafeMethodAccess), &engine, &none, None).is_none());
958 let strict = WarningSettings::analyzer_default(); let d = gate(&raw(WarningCode::UnsafeMethodAccess), &strict, &none, None).unwrap();
960 assert_eq!(d.severity, Severity::Warning);
961 assert_eq!(d.code, "UNSAFE_METHOD_ACCESS");
962 }
963
964 #[test]
965 fn error_default_stays_error() {
966 let d = gate(
967 &raw(WarningCode::InferenceOnVariant),
968 &WarningSettings::analyzer_default(),
969 &SuppressionMap::default(),
970 None,
971 )
972 .unwrap();
973 assert_eq!(d.severity, Severity::Error);
974 }
975
976 #[test]
977 fn treat_as_errors_escalates_warn_only() {
978 let none = SuppressionMap::default();
979 let mut s = WarningSettings::analyzer_default();
980 s.treat_as_errors = true;
981 let d = gate(&raw(WarningCode::IntegerDivision), &s, &none, None).unwrap();
983 assert_eq!(d.severity, Severity::Error);
984 s.per_code
986 .insert(WarningCode::IntegerDivision, WarnLevel::Ignore);
987 assert!(gate(&raw(WarningCode::IntegerDivision), &s, &none, None).is_none());
988 }
989
990 #[test]
991 fn per_code_override_sets_level() {
992 let none = SuppressionMap::default();
993 let mut s = WarningSettings::engine_default((4, 5));
994 s.per_code
995 .insert(WarningCode::UnsafeMethodAccess, WarnLevel::Error);
996 let d = gate(&raw(WarningCode::UnsafeMethodAccess), &s, &none, None).unwrap();
997 assert_eq!(d.severity, Severity::Error);
998 }
999
1000 #[test]
1001 fn exclude_addons_suppresses_by_path() {
1002 let mut s = WarningSettings::analyzer_default();
1003 s.exclude_addons = true;
1004 assert!(
1005 gate(
1006 &raw(WarningCode::IntegerDivision),
1007 &s,
1008 &SuppressionMap::default(),
1009 Some("res://addons/x/y.gd")
1010 )
1011 .is_none()
1012 );
1013 assert!(
1014 gate(
1015 &raw(WarningCode::IntegerDivision),
1016 &s,
1017 &SuppressionMap::default(),
1018 Some("res://game/y.gd")
1019 )
1020 .is_some()
1021 );
1022 }
1023
1024 #[test]
1025 fn suppression_map_drops_covered_range() {
1026 let mut map = SuppressionMap::default();
1027 map.push(TextRange::new(0, 100), vec![WarningCode::IntegerDivision]);
1028 assert!(
1029 gate(
1030 &raw(WarningCode::IntegerDivision),
1031 &WarningSettings::analyzer_default(),
1032 &map,
1033 None
1034 )
1035 .is_none()
1036 );
1037 assert!(
1039 gate(
1040 &raw(WarningCode::NarrowingConversion),
1041 &WarningSettings::analyzer_default(),
1042 &map,
1043 None
1044 )
1045 .is_some()
1046 );
1047 }
1048
1049 #[test]
1050 fn master_only_codes_gate_on_engine_version() {
1051 let none = SuppressionMap::default();
1052 let mut old = WarningSettings::engine_default((4, 3));
1054 old.strict_opt_in = false;
1055 assert!(
1056 gate(
1057 &raw(WarningCode::ConfusableTemporaryModification),
1058 &old,
1059 &none,
1060 None
1061 )
1062 .is_none()
1063 );
1064 let new = WarningSettings::engine_default((4, 5));
1065 assert!(
1066 gate(
1067 &raw(WarningCode::ConfusableTemporaryModification),
1068 &new,
1069 &none,
1070 None
1071 )
1072 .is_some()
1073 );
1074 }
1075}