1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-18 14:45:58 +00:00

Analyzer: W0103 also fires on unused state-local variables

Previously the unused-variable warning only ran on globals. Now it
walks \`state.locals\` too, so state-scoped variables that are
declared and never read also emit W0103. Factored out to
\`check_unused_var\` to share the \`_\`-prefix exemption and help text.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 11:21:53 +00:00
parent b7c806523d
commit 3f87aa1e30
No known key found for this signature in database
2 changed files with 53 additions and 18 deletions

View file

@ -211,25 +211,15 @@ impl Analyzer {
// Compute max call depths from entry points (state handlers) // Compute max call depths from entry points (state handlers)
self.compute_max_depths(program); self.compute_max_depths(program);
// Check for unused global variables (W0103). Variables whose names // Check for unused variables (W0103). Variables whose names
// start with '_' are exempt by convention. State-local variables are // start with '_' are exempt by convention. Both globals and
// left out for now to avoid noise during early development. // state-local variables are checked.
for var in &program.globals { for var in &program.globals {
if var.name.starts_with('_') { self.check_unused_var(var);
continue;
} }
if !self.used_vars.contains(&var.name) { for state in &program.states {
self.diagnostics.push(Diagnostic { for var in &state.locals {
level: Level::Warning, self.check_unused_var(var);
code: ErrorCode::W0103,
message: format!("unused variable '{}'", var.name),
span: var.span,
labels: Vec::<Label>::new(),
help: Some(
"prefix with '_' to silence this warning, or remove the declaration".into(),
),
note: None,
});
} }
} }
@ -242,6 +232,26 @@ impl Analyzer {
self.used_vars.insert(name.to_string()); self.used_vars.insert(name.to_string());
} }
/// Emit W0103 if `var` is never read anywhere. Variables named
/// with a leading `_` are exempt by convention.
fn check_unused_var(&mut self, var: &VarDecl) {
if var.name.starts_with('_') {
return;
}
if self.used_vars.contains(&var.name) {
return;
}
self.diagnostics.push(Diagnostic {
level: Level::Warning,
code: ErrorCode::W0103,
message: format!("unused variable '{}'", var.name),
span: var.span,
labels: Vec::<Label>::new(),
help: Some("prefix with '_' to silence this warning, or remove the declaration".into()),
note: None,
});
}
/// Recursively walk an expression tree and mark every identifier that /// Recursively walk an expression tree and mark every identifier that
/// appears as an `Expr::Ident` (or as an `Expr::ArrayIndex` base) as /// appears as an `Expr::Ident` (or as an `Expr::ArrayIndex` base) as
/// "read". Used by the W0103 unused-variable analysis. Also emits /// "read". Used by the W0103 unused-variable analysis. Also emits

View file

@ -562,6 +562,31 @@ fn analyze_unused_variable_warning() {
); );
} }
#[test]
fn analyze_unused_state_local_warning() {
// State-local `bonus` is declared but never read — W0103 should fire.
let (prog, diags) = parser::parse(
r#"
game "Test" { mapper: NROM }
state Main {
var bonus: u8 = 0
on frame { wait_frame }
}
start Main
"#,
);
assert!(diags.is_empty(), "parse errors: {diags:?}");
let result = analyze(&prog.unwrap());
assert!(
result
.diagnostics
.iter()
.any(|d| d.code == ErrorCode::W0103 && d.message.contains("bonus")),
"expected W0103 for unused state-local 'bonus', got: {:?}",
result.diagnostics
);
}
#[test] #[test]
fn analyze_unused_variable_no_warning_when_read() { fn analyze_unused_variable_no_warning_when_read() {
// `counter` is both written and read (in the `if` condition), // `counter` is both written and read (in the `if` condition),