use cstree::util::NodeOrToken;
use gdscript_base::{Diagnostic, DiagnosticSource, Severity, TextRange};
use gdscript_syntax::{GdNode, SyntaxKind};
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningCode {
UnassignedVariable,
UnassignedVariableOpAssign,
UnusedVariable,
UnusedLocalConstant,
UnusedPrivateClassVariable,
UnusedParameter,
UnusedSignal,
ShadowedVariable,
ShadowedVariableBaseClass,
ShadowedGlobalIdentifier,
UnreachableCode,
UnreachablePattern,
StandaloneExpression,
StandaloneTernary,
IncompatibleTernary,
UnsafeVoidReturn,
StaticCalledOnInstance,
MissingTool,
RedundantStaticUnload,
RedundantAwait,
AssertAlwaysTrue,
AssertAlwaysFalse,
IntegerDivision,
NarrowingConversion,
IntAsEnumWithoutCast,
IntAsEnumWithoutMatch,
EnumVariableWithoutDefault,
EmptyFile,
DeprecatedKeyword,
ConfusableIdentifier,
ConfusableLocalDeclaration,
ConfusableLocalUsage,
ConfusableCaptureReassignment,
ConfusableTemporaryModification,
PropertyUsedAsFunction,
ConstantUsedAsFunction,
FunctionUsedAsProperty,
UntypedDeclaration,
InferredDeclaration,
UnsafePropertyAccess,
UnsafeMethodAccess,
UnsafeCast,
UnsafeCallArgument,
ReturnValueDiscarded,
MissingAwait,
InferenceOnVariant,
NativeMethodOverride,
GetNodeDefaultWithoutOnready,
OnreadyWithExport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarnLevel {
Ignore,
Warn,
Error,
}
impl WarnLevel {
#[must_use]
pub fn from_int(n: u32) -> Option<Self> {
match n {
0 => Some(Self::Ignore),
1 => Some(Self::Warn),
2 => Some(Self::Error),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Since {
V4_3,
Master,
}
impl Since {
#[must_use]
pub fn min_version(self) -> (u32, u32) {
match self {
Self::V4_3 => (4, 3),
Self::Master => bundled_version(),
}
}
}
impl WarningCode {
pub const ALL: &'static [WarningCode] = &[
Self::UnassignedVariable,
Self::UnassignedVariableOpAssign,
Self::UnusedVariable,
Self::UnusedLocalConstant,
Self::UnusedPrivateClassVariable,
Self::UnusedParameter,
Self::UnusedSignal,
Self::ShadowedVariable,
Self::ShadowedVariableBaseClass,
Self::ShadowedGlobalIdentifier,
Self::UnreachableCode,
Self::UnreachablePattern,
Self::StandaloneExpression,
Self::StandaloneTernary,
Self::IncompatibleTernary,
Self::UnsafeVoidReturn,
Self::StaticCalledOnInstance,
Self::MissingTool,
Self::RedundantStaticUnload,
Self::RedundantAwait,
Self::AssertAlwaysTrue,
Self::AssertAlwaysFalse,
Self::IntegerDivision,
Self::NarrowingConversion,
Self::IntAsEnumWithoutCast,
Self::IntAsEnumWithoutMatch,
Self::EnumVariableWithoutDefault,
Self::EmptyFile,
Self::DeprecatedKeyword,
Self::ConfusableIdentifier,
Self::ConfusableLocalDeclaration,
Self::ConfusableLocalUsage,
Self::ConfusableCaptureReassignment,
Self::ConfusableTemporaryModification,
Self::PropertyUsedAsFunction,
Self::ConstantUsedAsFunction,
Self::FunctionUsedAsProperty,
Self::UntypedDeclaration,
Self::InferredDeclaration,
Self::UnsafePropertyAccess,
Self::UnsafeMethodAccess,
Self::UnsafeCast,
Self::UnsafeCallArgument,
Self::ReturnValueDiscarded,
Self::MissingAwait,
Self::InferenceOnVariant,
Self::NativeMethodOverride,
Self::GetNodeDefaultWithoutOnready,
Self::OnreadyWithExport,
];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::UnassignedVariable => "UNASSIGNED_VARIABLE",
Self::UnassignedVariableOpAssign => "UNASSIGNED_VARIABLE_OP_ASSIGN",
Self::UnusedVariable => "UNUSED_VARIABLE",
Self::UnusedLocalConstant => "UNUSED_LOCAL_CONSTANT",
Self::UnusedPrivateClassVariable => "UNUSED_PRIVATE_CLASS_VARIABLE",
Self::UnusedParameter => "UNUSED_PARAMETER",
Self::UnusedSignal => "UNUSED_SIGNAL",
Self::ShadowedVariable => "SHADOWED_VARIABLE",
Self::ShadowedVariableBaseClass => "SHADOWED_VARIABLE_BASE_CLASS",
Self::ShadowedGlobalIdentifier => "SHADOWED_GLOBAL_IDENTIFIER",
Self::UnreachableCode => "UNREACHABLE_CODE",
Self::UnreachablePattern => "UNREACHABLE_PATTERN",
Self::StandaloneExpression => "STANDALONE_EXPRESSION",
Self::StandaloneTernary => "STANDALONE_TERNARY",
Self::IncompatibleTernary => "INCOMPATIBLE_TERNARY",
Self::UnsafeVoidReturn => "UNSAFE_VOID_RETURN",
Self::StaticCalledOnInstance => "STATIC_CALLED_ON_INSTANCE",
Self::MissingTool => "MISSING_TOOL",
Self::RedundantStaticUnload => "REDUNDANT_STATIC_UNLOAD",
Self::RedundantAwait => "REDUNDANT_AWAIT",
Self::AssertAlwaysTrue => "ASSERT_ALWAYS_TRUE",
Self::AssertAlwaysFalse => "ASSERT_ALWAYS_FALSE",
Self::IntegerDivision => "INTEGER_DIVISION",
Self::NarrowingConversion => "NARROWING_CONVERSION",
Self::IntAsEnumWithoutCast => "INT_AS_ENUM_WITHOUT_CAST",
Self::IntAsEnumWithoutMatch => "INT_AS_ENUM_WITHOUT_MATCH",
Self::EnumVariableWithoutDefault => "ENUM_VARIABLE_WITHOUT_DEFAULT",
Self::EmptyFile => "EMPTY_FILE",
Self::DeprecatedKeyword => "DEPRECATED_KEYWORD",
Self::ConfusableIdentifier => "CONFUSABLE_IDENTIFIER",
Self::ConfusableLocalDeclaration => "CONFUSABLE_LOCAL_DECLARATION",
Self::ConfusableLocalUsage => "CONFUSABLE_LOCAL_USAGE",
Self::ConfusableCaptureReassignment => "CONFUSABLE_CAPTURE_REASSIGNMENT",
Self::ConfusableTemporaryModification => "CONFUSABLE_TEMPORARY_MODIFICATION",
Self::PropertyUsedAsFunction => "PROPERTY_USED_AS_FUNCTION",
Self::ConstantUsedAsFunction => "CONSTANT_USED_AS_FUNCTION",
Self::FunctionUsedAsProperty => "FUNCTION_USED_AS_PROPERTY",
Self::UntypedDeclaration => "UNTYPED_DECLARATION",
Self::InferredDeclaration => "INFERRED_DECLARATION",
Self::UnsafePropertyAccess => "UNSAFE_PROPERTY_ACCESS",
Self::UnsafeMethodAccess => "UNSAFE_METHOD_ACCESS",
Self::UnsafeCast => "UNSAFE_CAST",
Self::UnsafeCallArgument => "UNSAFE_CALL_ARGUMENT",
Self::ReturnValueDiscarded => "RETURN_VALUE_DISCARDED",
Self::MissingAwait => "MISSING_AWAIT",
Self::InferenceOnVariant => "INFERENCE_ON_VARIANT",
Self::NativeMethodOverride => "NATIVE_METHOD_OVERRIDE",
Self::GetNodeDefaultWithoutOnready => "GET_NODE_DEFAULT_WITHOUT_ONREADY",
Self::OnreadyWithExport => "ONREADY_WITH_EXPORT",
}
}
#[must_use]
pub fn setting_name(self) -> String {
self.as_str().to_ascii_lowercase()
}
#[must_use]
pub fn description(self) -> &'static str {
match self {
Self::UnassignedVariable => "A typed local is read before it is assigned a value.",
Self::UnassignedVariableOpAssign => {
"A compound assignment (`+=`, …) is applied to a still-unassigned local."
}
Self::UnusedVariable => "A local variable is declared but never read.",
Self::UnusedLocalConstant => "A local constant is declared but never read.",
Self::UnusedPrivateClassVariable => {
"A `_`-prefixed class member is never read within the class."
}
Self::UnusedParameter => "A function parameter is never used (prefix it with `_`).",
Self::UnusedSignal => "A signal is never emitted or connected in the file.",
Self::ShadowedVariable => "A local shadows an outer local or parameter.",
Self::ShadowedVariableBaseClass => "A member shadows a member of a base class.",
Self::ShadowedGlobalIdentifier => {
"A `class_name`, member, or local shadows a global identifier."
}
Self::UnreachableCode => {
"A statement follows an unconditional `return`/`break`/`continue` (or an exhaustive `match`)."
}
Self::UnreachablePattern => {
"A `match` pattern can never match (it follows a wildcard)."
}
Self::StandaloneExpression => "An expression statement has no effect.",
Self::StandaloneTernary => {
"A ternary conditional is used as a statement; its value is discarded."
}
Self::IncompatibleTernary => {
"The two values of a ternary conditional have no common type."
}
Self::UnsafeVoidReturn => "A `Variant` value is returned from a `-> void` function.",
Self::StaticCalledOnInstance => "A static method is called through an instance.",
Self::MissingTool => "A class extends a `@tool` class but is not itself `@tool`.",
Self::RedundantStaticUnload => {
"`@static_unload` is used on a class with no static variables."
}
Self::RedundantAwait => "`await` is applied to a non-coroutine, non-signal value.",
Self::AssertAlwaysTrue => "An `assert(...)` condition is always true.",
Self::AssertAlwaysFalse => "An `assert(...)` condition is always false.",
Self::IntegerDivision => "Integer division discards the fractional part.",
Self::NarrowingConversion => "A `float` is stored into an `int`, losing precision.",
Self::IntAsEnumWithoutCast => "An integer is assigned to an enum value without a cast.",
Self::IntAsEnumWithoutMatch => "An integer is compared to an enum value in a `match`.",
Self::EnumVariableWithoutDefault => {
"An enum-typed variable has no explicit default value."
}
Self::EmptyFile => "The script file has no members, `class_name`, or `extends`.",
Self::DeprecatedKeyword => "A deprecated keyword (e.g. `yield`) is used.",
Self::ConfusableIdentifier => {
"An identifier mixes scripts / uses confusable characters."
}
Self::ConfusableLocalDeclaration => "A local is declared after a same-name outer use.",
Self::ConfusableLocalUsage => {
"A local shadowing a member is used before its declaration."
}
Self::ConfusableCaptureReassignment => {
"A captured variable is reassigned inside a lambda."
}
Self::ConfusableTemporaryModification => "A temporary value is modified in place.",
Self::PropertyUsedAsFunction => "A property is called as if it were a function.",
Self::ConstantUsedAsFunction => "A constant is called as if it were a function.",
Self::FunctionUsedAsProperty => "A function is accessed as if it were a property.",
Self::UntypedDeclaration => "A declaration has no type annotation.",
Self::InferredDeclaration => "A declaration uses an inferred type (`:=`).",
Self::UnsafePropertyAccess => {
"A property is not present on the inferred type (but may be on a subtype)."
}
Self::UnsafeMethodAccess => {
"A method is not present on the inferred type (but may be on a subtype)."
}
Self::UnsafeCast => "A value is cast through `Variant`, which is unsafe.",
Self::UnsafeCallArgument => {
"An argument needs an unsafe implicit cast into the parameter type."
}
Self::ReturnValueDiscarded => "A non-`void` call's return value is discarded.",
Self::MissingAwait => "An awaitable call's result is not awaited.",
Self::InferenceOnVariant => "A type is inferred from a statically-`Variant` value.",
Self::NativeMethodOverride => {
"A native virtual method is overridden with an incompatible signature."
}
Self::GetNodeDefaultWithoutOnready => {
"A `get_node(...)` default initializer should be `@onready`."
}
Self::OnreadyWithExport => "`@onready` and `@export` are used together on one member.",
}
}
#[must_use]
pub fn default_level(self) -> WarnLevel {
match self {
Self::UntypedDeclaration
| Self::InferredDeclaration
| Self::UnsafePropertyAccess
| Self::UnsafeMethodAccess
| Self::UnsafeCast
| Self::UnsafeCallArgument
| Self::ReturnValueDiscarded
| Self::MissingAwait => WarnLevel::Ignore,
Self::InferenceOnVariant
| Self::NativeMethodOverride
| Self::GetNodeDefaultWithoutOnready
| Self::OnreadyWithExport => WarnLevel::Error,
_ => WarnLevel::Warn,
}
}
#[must_use]
pub fn is_opt_in(self) -> bool {
self.default_level() == WarnLevel::Ignore
}
#[must_use]
pub fn since(self) -> Since {
match self {
Self::ConfusableTemporaryModification | Self::MissingAwait => Since::Master,
_ => Since::V4_3,
}
}
#[must_use]
pub fn from_setting_name(name: &str) -> Option<WarningCode> {
Self::ALL
.iter()
.copied()
.find(|c| c.as_str().eq_ignore_ascii_case(name))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawWarning {
pub range: TextRange,
pub code: WarningCode,
pub message: String,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WarningSettings {
pub enabled: bool,
pub treat_as_errors: bool,
pub per_code: FxHashMap<WarningCode, WarnLevel>,
pub exclude_addons: bool,
pub engine: (u32, u32),
pub strict_opt_in: bool,
}
impl WarningSettings {
#[must_use]
pub fn analyzer_default() -> Self {
Self {
enabled: true,
treat_as_errors: false,
per_code: FxHashMap::default(),
exclude_addons: false,
engine: bundled_version(),
strict_opt_in: true,
}
}
#[must_use]
pub fn with_strict_opt_in(mut self, on: bool) -> Self {
self.strict_opt_in = on;
self
}
#[must_use]
pub fn engine_default(engine: (u32, u32)) -> Self {
Self {
enabled: true,
treat_as_errors: false,
per_code: FxHashMap::default(),
exclude_addons: true,
engine,
strict_opt_in: false,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SuppressionMap {
spans: Vec<(TextRange, Vec<WarningCode>)>,
}
impl SuppressionMap {
#[must_use]
pub fn is_suppressed(&self, code: WarningCode, at: TextRange) -> bool {
self.spans.iter().any(|(span, codes)| {
span.start <= at.start && at.end <= span.end && codes.contains(&code)
})
}
pub fn push(&mut self, range: TextRange, codes: Vec<WarningCode>) {
self.spans.push((range, codes));
}
}
#[must_use]
pub fn build_suppression_map(root: &GdNode, source: &str) -> SuppressionMap {
let mut map = SuppressionMap::default();
let mut anns: Vec<GdNode> = gdscript_syntax::ast::descendants(root)
.into_iter()
.filter(|n| n.kind() == SyntaxKind::Annotation)
.collect();
anns.sort_by_key(|n| u32::from(n.text_range().start()));
let mut open: FxHashMap<WarningCode, u32> = FxHashMap::default();
let eof = u32::from(root.text_range().end());
for ann in &anns {
let Some(name) = annotation_name(ann) else {
continue;
};
let codes = annotation_warning_codes(ann);
if codes.is_empty() {
continue; }
match name.as_str() {
"warning_ignore" => {
if let Some(target) = next_decorated_sibling(ann) {
let r = target.text_range();
let start = u32::from(r.start());
let end = line_end_from(source, u32::from(r.end()));
map.push(TextRange::new(start, end), codes);
}
}
"warning_ignore_start" => {
let start = u32::from(ann.text_range().end());
for c in codes {
open.insert(c, start); }
}
"warning_ignore_restore" => {
let end = u32::from(ann.text_range().start());
for c in &codes {
if let Some(start) = open.remove(c) {
map.push(TextRange::new(start, end), vec![*c]);
}
}
}
_ => {}
}
}
let mut leftover: Vec<(WarningCode, u32)> = open.into_iter().collect();
leftover.sort_by_key(|&(_, start)| start);
for (c, start) in leftover {
map.push(TextRange::new(start, eof), vec![c]);
}
map
}
fn annotation_name(ann: &GdNode) -> Option<String> {
ann.children_with_tokens()
.filter_map(NodeOrToken::into_token)
.find(|t| t.kind() == SyntaxKind::Ident)
.map(|t| t.text().to_owned())
}
fn annotation_warning_codes(ann: &GdNode) -> Vec<WarningCode> {
let Some(arglist) = ann.children().find(|c| c.kind() == SyntaxKind::ArgList) else {
return Vec::new();
};
let mut codes = Vec::new();
for lit in arglist
.children()
.filter(|c| c.kind() == SyntaxKind::Literal)
{
for tok in lit
.children_with_tokens()
.filter_map(NodeOrToken::into_token)
{
if tok.kind() == SyntaxKind::String
&& let Some(c) =
WarningCode::from_setting_name(tok.text().trim_matches(['"', '\'']))
{
codes.push(c);
}
}
}
codes
}
fn line_end_from(source: &str, start: u32) -> u32 {
let s = start as usize;
match source.get(s..).and_then(|rest| rest.find('\n')) {
Some(i) => u32::try_from(s + i).unwrap_or(u32::MAX),
None => u32::try_from(source.len()).unwrap_or(u32::MAX),
}
}
fn next_decorated_sibling(ann: &GdNode) -> Option<GdNode> {
let parent = ann.parent()?;
let after = ann.text_range().start();
parent
.children()
.filter(|c| c.text_range().start() > after && c.kind() != SyntaxKind::Annotation)
.min_by_key(|c| u32::from(c.text_range().start()))
.cloned()
}
#[must_use]
pub fn gate(
raw: &RawWarning,
settings: &WarningSettings,
ignores: &SuppressionMap,
path: Option<&str>,
) -> Option<Diagnostic> {
if !settings.enabled {
return None;
}
if raw.code.since().min_version() > settings.engine {
return None;
}
let mut level = settings
.per_code
.get(&raw.code)
.copied()
.unwrap_or_else(|| {
let d = raw.code.default_level();
if settings.strict_opt_in && d == WarnLevel::Ignore {
WarnLevel::Warn
} else {
d
}
});
if level == WarnLevel::Ignore {
return None;
}
if settings.treat_as_errors && level == WarnLevel::Warn {
level = WarnLevel::Error;
}
if settings.exclude_addons && path.is_some_and(is_addon_path) {
return None;
}
if ignores.is_suppressed(raw.code, raw.range) {
return None;
}
Some(Diagnostic {
range: raw.range,
severity: match level {
WarnLevel::Error => Severity::Error,
_ => Severity::Warning,
},
code: raw.code.as_str().to_owned(),
message: raw.message.clone(),
source: DiagnosticSource::Type,
fixes: Vec::new(),
})
}
#[must_use]
pub fn render_warning_reference() -> String {
use std::fmt::Write as _;
let mut codes: Vec<WarningCode> = WarningCode::ALL.to_vec();
codes.sort_by_key(|c| c.as_str());
let mut s = String::new();
s.push_str("<!-- @generated by `gdscript-hir` (warnings::render_warning_reference); do not edit by hand. -->\n");
s.push_str("<!-- Regenerate: `GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current` -->\n\n");
s.push_str("# Warning Reference\n\n");
s.push_str(
"Every gateable GDScript warning the analyzer can emit, with its `project.godot` setting key, \
engine-default level, and the earliest Godot version it applies to. Configure these under \
`[debug]` as `gdscript/warnings/<key>` (`0` = ignore, `1` = warn, `2` = error), or suppress \
inline with `@warning_ignore(\"<key>\")`. See [Configuration](./configuration.md).\n\n",
);
s.push_str("| Code | Setting key | Default | Since | Description |\n");
s.push_str("|---|---|---|---|---|\n");
for c in codes {
let default = match c.default_level() {
WarnLevel::Ignore => "Ignore",
WarnLevel::Warn => "Warn",
WarnLevel::Error => "Error",
};
let since = match c.since() {
Since::V4_3 => "4.3",
Since::Master => "master",
};
let _ = writeln!(
s,
"| `{}` | `{}` | {default} | {since} | {} |",
c.as_str(),
c.setting_name(),
c.description(),
);
}
s
}
fn is_addon_path(path: &str) -> bool {
path.starts_with("res://addons/")
}
#[must_use]
pub fn bundled_version() -> (u32, u32) {
parse_major_minor(gdscript_api::godot_version()).unwrap_or((4, 5))
}
fn parse_major_minor(s: &str) -> Option<(u32, u32)> {
let mut parts = s.split('.');
let major = parts.next()?.parse().ok()?;
let minor: u32 = parts
.next()?
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
.parse()
.ok()?;
Some((major, minor))
}
#[cfg(test)]
mod tests {
use super::*;
use gdscript_syntax::parse;
use std::collections::HashSet;
fn off(src: &str, needle: &str) -> u32 {
u32::try_from(src.find(needle).unwrap()).unwrap()
}
#[test]
fn warning_reference_doc_is_current() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/src/reference/warnings.md"
);
let generated = render_warning_reference();
if std::env::var("GDSCRIPT_UPDATE_DOCS").is_ok() {
if let Some(parent) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, &generated).unwrap();
return;
}
let on_disk = std::fs::read_to_string(path).unwrap_or_default();
assert_eq!(
on_disk, generated,
"docs/src/reference/warnings.md is stale — regenerate with \
`GDSCRIPT_UPDATE_DOCS=1 cargo test -p gdscript-hir warning_reference_doc_is_current`",
);
}
#[test]
fn warning_ignore_suppresses_the_next_statement() {
let src = "func f():\n\t@warning_ignore(\"integer_division\")\n\tvar x = 5 / 2\n";
let map = build_suppression_map(&parse(src).syntax_node(), src);
let at = off(src, "5 / 2");
assert!(map.is_suppressed(WarningCode::IntegerDivision, TextRange::new(at, at + 5)));
assert!(!map.is_suppressed(WarningCode::NarrowingConversion, TextRange::new(at, at + 5)));
}
#[test]
fn warning_ignore_covers_semicolon_joined_statements_on_the_line() {
let src = "func f():\n\t@warning_ignore(\"unused_variable\")\n\tvar a = 1; var b = 2\n\tvar c = 3\n";
let map = build_suppression_map(&parse(src).syntax_node(), src);
let a = off(src, "var a");
let b = off(src, "var b");
let c = off(src, "var c");
assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
assert!(
map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)),
"the second `;`-joined statement on the line must be covered"
);
assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(c, c + 1)));
}
#[test]
fn warning_ignore_start_restore_suppresses_a_region() {
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";
let map = build_suppression_map(&parse(src).syntax_node(), src);
let a = off(src, "var a");
let b = off(src, "var b");
assert!(map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(a, a + 1)));
assert!(!map.is_suppressed(WarningCode::UnusedVariable, TextRange::new(b, b + 1)));
}
#[test]
fn repeated_start_for_one_code_overwrites_and_does_not_leak_past_restore() {
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";
let map = build_suppression_map(&parse(src).syntax_node(), src);
let before = off(src, "before");
let inside = off(src, "inside");
let after = off(src, "after");
assert!(
map.is_suppressed(
WarningCode::UnusedVariable,
TextRange::new(inside, inside + 1)
),
"the active [start2 .. restore] region must be suppressed"
);
assert!(
!map.is_suppressed(
WarningCode::UnusedVariable,
TextRange::new(after, after + 1)
),
"code after the restore must NOT be suppressed (no leak to EOF)"
);
assert!(
!map.is_suppressed(
WarningCode::UnusedVariable,
TextRange::new(before, before + 1)
),
"code before the overwriting start must NOT be suppressed"
);
}
#[test]
fn exclude_addons_only_matches_the_root_addons_dir() {
let none = SuppressionMap::default();
let mut s = WarningSettings::engine_default((4, 5));
s.per_code
.insert(WarningCode::IntegerDivision, WarnLevel::Warn);
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&s,
&none,
Some("res://game/addons/spawner.gd")
)
.is_some(),
"a nested addons/ dir must still be checked"
);
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&s,
&none,
Some("res://addons/plugin/x.gd")
)
.is_none()
);
}
fn raw(code: WarningCode) -> RawWarning {
RawWarning {
range: TextRange::new(10, 20),
code,
message: "msg".to_owned(),
}
}
#[test]
fn every_code_has_a_unique_uppercase_string_that_round_trips() {
let mut seen = HashSet::new();
for &c in WarningCode::ALL {
assert!(seen.insert(c.as_str()), "duplicate as_str: {}", c.as_str());
assert_eq!(c.as_str(), c.as_str().to_ascii_uppercase());
assert_eq!(WarningCode::from_setting_name(&c.setting_name()), Some(c));
}
assert_eq!(seen.len(), 49);
}
#[test]
fn disabled_drops_everything() {
let mut s = WarningSettings::analyzer_default();
s.enabled = false;
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&s,
&SuppressionMap::default(),
None
)
.is_none()
);
}
#[test]
fn opt_in_group_is_silent_under_engine_default_but_warns_under_strict() {
let none = SuppressionMap::default();
let engine = WarningSettings::engine_default((4, 5));
assert!(gate(&raw(WarningCode::UnsafeMethodAccess), &engine, &none, None).is_none());
let strict = WarningSettings::analyzer_default(); let d = gate(&raw(WarningCode::UnsafeMethodAccess), &strict, &none, None).unwrap();
assert_eq!(d.severity, Severity::Warning);
assert_eq!(d.code, "UNSAFE_METHOD_ACCESS");
}
#[test]
fn error_default_stays_error() {
let d = gate(
&raw(WarningCode::InferenceOnVariant),
&WarningSettings::analyzer_default(),
&SuppressionMap::default(),
None,
)
.unwrap();
assert_eq!(d.severity, Severity::Error);
}
#[test]
fn treat_as_errors_escalates_warn_only() {
let none = SuppressionMap::default();
let mut s = WarningSettings::analyzer_default();
s.treat_as_errors = true;
let d = gate(&raw(WarningCode::IntegerDivision), &s, &none, None).unwrap();
assert_eq!(d.severity, Severity::Error);
s.per_code
.insert(WarningCode::IntegerDivision, WarnLevel::Ignore);
assert!(gate(&raw(WarningCode::IntegerDivision), &s, &none, None).is_none());
}
#[test]
fn per_code_override_sets_level() {
let none = SuppressionMap::default();
let mut s = WarningSettings::engine_default((4, 5));
s.per_code
.insert(WarningCode::UnsafeMethodAccess, WarnLevel::Error);
let d = gate(&raw(WarningCode::UnsafeMethodAccess), &s, &none, None).unwrap();
assert_eq!(d.severity, Severity::Error);
}
#[test]
fn exclude_addons_suppresses_by_path() {
let mut s = WarningSettings::analyzer_default();
s.exclude_addons = true;
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&s,
&SuppressionMap::default(),
Some("res://addons/x/y.gd")
)
.is_none()
);
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&s,
&SuppressionMap::default(),
Some("res://game/y.gd")
)
.is_some()
);
}
#[test]
fn suppression_map_drops_covered_range() {
let mut map = SuppressionMap::default();
map.push(TextRange::new(0, 100), vec![WarningCode::IntegerDivision]);
assert!(
gate(
&raw(WarningCode::IntegerDivision),
&WarningSettings::analyzer_default(),
&map,
None
)
.is_none()
);
assert!(
gate(
&raw(WarningCode::NarrowingConversion),
&WarningSettings::analyzer_default(),
&map,
None
)
.is_some()
);
}
#[test]
fn master_only_codes_gate_on_engine_version() {
let none = SuppressionMap::default();
let mut old = WarningSettings::engine_default((4, 3));
old.strict_opt_in = false;
assert!(
gate(
&raw(WarningCode::ConfusableTemporaryModification),
&old,
&none,
None
)
.is_none()
);
let new = WarningSettings::engine_default((4, 5));
assert!(
gate(
&raw(WarningCode::ConfusableTemporaryModification),
&new,
&none,
None
)
.is_some()
);
}
}