mirror of
https://github.com/imjasonh/nescript
synced 2026-07-19 07:05:58 +00:00
Analyzer: W0104 warn on dead code after terminator
When a block contains a statement after \`return\`, \`break\`, \`continue\`, or \`transition\`, the trailing statement is unreachable and the analyzer now emits W0104 pointing at it. The warning includes a secondary label on the terminator showing where execution stops. Adds \`Statement::span\` helper to the AST so diagnostics can point at arbitrary statements cleanly. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
e688397594
commit
f8743cf95e
3 changed files with 112 additions and 0 deletions
|
|
@ -573,8 +573,26 @@ impl Analyzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_block(&mut self, block: &Block, state_names: &[&str]) {
|
fn check_block(&mut self, block: &Block, state_names: &[&str]) {
|
||||||
|
let mut terminated_by: Option<Span> = None;
|
||||||
|
let mut warned_dead_code = false;
|
||||||
for stmt in &block.statements {
|
for stmt in &block.statements {
|
||||||
|
if let Some(term_span) = terminated_by {
|
||||||
|
if !warned_dead_code {
|
||||||
|
self.diagnostics.push(
|
||||||
|
Diagnostic::warning(
|
||||||
|
ErrorCode::W0104,
|
||||||
|
"unreachable code after return / break / transition",
|
||||||
|
stmt.span(),
|
||||||
|
)
|
||||||
|
.with_label(term_span, "execution stops here"),
|
||||||
|
);
|
||||||
|
warned_dead_code = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.check_statement(stmt, state_names);
|
self.check_statement(stmt, state_names);
|
||||||
|
if stmt_is_terminator(stmt) && terminated_by.is_none() {
|
||||||
|
terminated_by = Some(stmt.span());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1008,6 +1026,18 @@ fn is_small_constant(expr: &Expr) -> bool {
|
||||||
matches!(expr, Expr::IntLiteral(_, _))
|
matches!(expr, Expr::IntLiteral(_, _))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if this statement unconditionally ends block execution —
|
||||||
|
/// subsequent statements in the same block cannot be reached.
|
||||||
|
fn stmt_is_terminator(stmt: &Statement) -> bool {
|
||||||
|
matches!(
|
||||||
|
stmt,
|
||||||
|
Statement::Return(_, _)
|
||||||
|
| Statement::Break(_)
|
||||||
|
| Statement::Continue(_)
|
||||||
|
| Statement::Transition(_, _)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn block_can_exit_or_yield(block: &Block) -> bool {
|
fn block_can_exit_or_yield(block: &Block) -> bool {
|
||||||
block.statements.iter().any(stmt_can_exit_or_yield)
|
block.statements.iter().any(stmt_can_exit_or_yield)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,64 @@ fn analyze_return_wrong_type() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_dead_code_after_break() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
var x: u8 = 0
|
||||||
|
on frame {
|
||||||
|
loop {
|
||||||
|
break
|
||||||
|
x += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let errors = analyze_errors(src);
|
||||||
|
assert!(
|
||||||
|
errors.contains(&ErrorCode::W0104),
|
||||||
|
"code after break should trigger W0104, got: {errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_dead_code_after_transition() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
state A {
|
||||||
|
on frame {
|
||||||
|
transition B
|
||||||
|
wait_frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state B { on frame { wait_frame } }
|
||||||
|
start A
|
||||||
|
"#;
|
||||||
|
let errors = analyze_errors(src);
|
||||||
|
assert!(
|
||||||
|
errors.contains(&ErrorCode::W0104),
|
||||||
|
"code after transition should trigger W0104, got: {errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn analyze_dead_code_after_return_in_fn() {
|
||||||
|
let src = r#"
|
||||||
|
game "Test" { mapper: NROM }
|
||||||
|
fun foo() -> u8 {
|
||||||
|
return 5
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
on frame { wait_frame }
|
||||||
|
start Main
|
||||||
|
"#;
|
||||||
|
let errors = analyze_errors(src);
|
||||||
|
assert!(
|
||||||
|
errors.contains(&ErrorCode::W0104),
|
||||||
|
"code after return should trigger W0104, got: {errors:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analyze_ram_overflow_emits_e0301() {
|
fn analyze_ram_overflow_emits_e0301() {
|
||||||
// Two arrays totalling >2 KB cannot fit in NES RAM, triggering
|
// Two arrays totalling >2 KB cannot fit in NES RAM, triggering
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,30 @@ pub enum Statement {
|
||||||
DebugAssert(Expr, Span),
|
DebugAssert(Expr, Span),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Statement {
|
||||||
|
pub fn span(&self) -> Span {
|
||||||
|
match self {
|
||||||
|
Self::VarDecl(v) => v.span,
|
||||||
|
Self::Draw(d) => d.span,
|
||||||
|
Self::Assign(_, _, _, s)
|
||||||
|
| Self::If(_, _, _, _, s)
|
||||||
|
| Self::While(_, _, s)
|
||||||
|
| Self::Loop(_, s)
|
||||||
|
| Self::Break(s)
|
||||||
|
| Self::Continue(s)
|
||||||
|
| Self::Return(_, s)
|
||||||
|
| Self::Transition(_, s)
|
||||||
|
| Self::WaitFrame(s)
|
||||||
|
| Self::Call(_, _, s)
|
||||||
|
| Self::LoadBackground(_, s)
|
||||||
|
| Self::SetPalette(_, s)
|
||||||
|
| Self::Scroll(_, _, s)
|
||||||
|
| Self::DebugLog(_, s)
|
||||||
|
| Self::DebugAssert(_, s) => *s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DrawStmt {
|
pub struct DrawStmt {
|
||||||
pub sprite_name: String,
|
pub sprite_name: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue