Skip to content

ANALYZE-UNUSED does not count expect as a use: correct test code warns, including WFL's own gated suite (33 of 83 statement variants unhandled) #711

Description

@logbie

Summary

ANALYZE-UNUSED does not model expect … to … as a use, so a variable bound in a test and referenced only by its assertion is falsely reported unused. ExpectStatement is one of 33 of 83 Statement variants that fall into the _ => {} catch-all in the analyzer's use-tracking visitor, so the same false positive also hits create file, create list, and container instantiation. This is the unfinished remainder of the sweep that #468 recommended when it was fixed.

Reproduction

describe "demo":
    test "a value used only inside expect":
        store v as "c"
        expect v to equal "c"
    end test
end describe

Command:

wfl --analyze l4_expect_not_a_use.wfl

Expected

No static analysis warnings found, exit 0. v is read by the assertion; the
test passes 1/1 under wfl --test, so the variable is genuinely used at runtime.

Docs/guides/testing-guide.md documents describe/test/expect as the testing
form, and root testing.md §8.3 requires asserting outcomes rather than "did
not crash" — so binding a value and asserting on it, with no other reference, is
the mandated style, not an edge case.

Control: adding display v before the expect clears the warning entirely
(No static analysis warnings found), confirming the assertion itself contributes
nothing to use-tracking.

Actual

Static analysis warnings:
warning[ANALYZE-UNUSED]: Unused variable 'v'
 = Consider removing this variable if it's not needed

Exit code: 1. wfl --test on the same file: Total: 1 / Passed: 1 / Failed: 0.

The advice is actively wrong — following "Consider removing this variable if it's
not needed" deletes the subject of the assertion.

Root cause

mark_used_variables (src/analyzer/static_analyzer.rs:1003) is a match over
Statement with one arm per handled kind and a catch-all that marks nothing:

// src/analyzer/static_analyzer.rs:1457
            _ => {}

There is no Statement::ExpectStatement arm — searching for Expect across
the whole function returns nothing — so neither its subject nor the assertion's
operand is ever visited:

// src/parser/ast.rs:732-737
ExpectStatement {
    subject: Expression,
    assertion: Assertion,
    ...
}

DescribeBlock and TestBlock do have arms, so the analyzer walks into the
test body correctly and then drops the one statement that matters.

Scope, measured

All 11 assertion spellings are affected — it is the statement kind that is
unhandled, not the assertion kind. Each row is the six-line repro with the
assertion swapped; every one passes 1/1 under --test:

assertion result
to equal "c" Unused variable 'v'
to be "c" Unused variable 'v'
to be greater than 1 Unused variable 'v'
to be less than 9 Unused variable 'v'
to be yes Unused variable 'v'
to be no Unused variable 'v'
to exist Unused variable 'v'
to contain "c" Unused variable 'v'
to be empty Unused variable 'v'
to have length 1 Unused variable 'v'
to be of type "text" Unused variable 'v'

Both operands are uncounted, not just the subject:

store v as "c"
store w as "c"
expect v to equal w

Unused variable 'v' and Unused variable 'w'; passes 1/1.

The whole subject expression tree is unvisited, not just a bare variable:

store base_val as 5
expect twice of base_val to equal 10

Unused variable 'base_val'; passes 1/1.

It is wider than expect

Diffing the 83 Statement variants in src/parser/ast.rs against the arms in
mark_used_variables: 50 handled, 33 not. Verified false positives on three
further constructs, all of which parse and run correctly:

program falsely reported
create file at target_path with body_text both target_path and body_text
create list nums: / add seed_val / end list seed_val
create new P as p1: / nm is who_name / end who_name

Other unhandled variants that carry expressions and are therefore likely affected
(not individually reproduced): CreateDirectoryStatement, DeleteFileStatement,
ExecuteCommandStatement, SpawnProcessStatement, HttpGetStatement,
HttpPostStatement, HttpRequestStatement, IncludeStatement,
ReadProcessOutputStatement, KillProcessStatement, EventTrigger,
ParentMethodCall, MapCreation. (A few of the 33 — BreakStatement,
ContinueStatement, ExitStatement — carry no operands and are correctly
no-ops.)

The repository's own test suite trips this

TestPrograms/natural_language_constructs.test.wfl — a gated regression suite
that passes 12/12 under wfl --test — emits 6 false ANALYZE-UNUSED
warnings: boiling, hot, inside, quotient, remainder, value. Each is
the exact pattern, e.g. at :63-64:

store quotient as 10 / 4
expect quotient to equal 2.5

TestPrograms/math_operations.test.wfl emits 4 more. So the analyzer currently
reports false warnings against the project's own required tests.

Precedent and lineage — not a duplicate

#468 ("Analyzer: add 1 to X not counted as a use of X (ANALYZE-UNUSED
false positive)", labelled bug, CLOSED/fixed) is the same rule, the same
root cause, and a different statement kind. Its fix added the missing arms —
AddToListStatement and RespondStatement are both in the handled list today,
and I re-ran its repro to confirm it stays clean (No static analysis warnings found), so this is not a regression of #468.

What matters is #468's own closing paragraph:

"So the analyzer may be missing a whole family of uses — not just add ... to ...
but also uses inside respond ... with <var> .... Might be worth a single sweep
through the analyzer's use-tracking to verify every statement…"

That sweep was never done; 33 variants remain. This issue is that sweep, with
expect as the entry point. It also settles the label: the maintainer already
triaged this exact class as bug and shipped a fix for it.

Should it go on the #578 reopen instead? No, and I would argue against it.
#578 is a heterogeneous rough-edges list with no common cause; this has a single
root cause, one identified line, a direct precedent issue, and a concrete fix
shape. Folding it in would bury a mechanical fix inside a tracker that is already
suppressing reports.

Suggested fix

Add a Statement::ExpectStatement { subject, assertion, .. } arm that calls
mark_used_in_expression on subject and on the Assertion's operand
(Equal/Be/GreaterThan/LessThan/Contain/HaveLength each carry an
Expression; BeYes/BeNo/Exist/BeEmpty/BeOfType carry none). Then do the
sweep #468 asked for across the remaining 32 variants. A regression test that
would have caught this: run --analyze over TestPrograms/*.test.wfl and assert
zero ANALYZE-UNUSED.

Worth considering: replacing the _ => {} catch-all with an exhaustive match
would stop this recurring every time a statement kind is added — that is what let
expect slip in silently after #468 was closed.

Environment

  • wfl --version: WebFirst Language (WFL) version 26.8.4
  • binary: system install C:\Program Files\wfl\bin\wfl.exe
  • commit: c277d8f
  • OS: Windows 11 Pro 10.0.26200
  • build: release

Also reproduces identically on the repo build G:\repos\wfl\target\release\wfl.exe
(version 26.8.2, same warning, exit 1) — not a regression.
No .wflcfg in scope for the repro.

Context

Found while packaging the WFL port of G:/repos/JShrink/src/JShrink/Minifier.php
(a 738-line PHP JavaScript minifier). The port ships 71 test blocks, and the
assertion-only binding is the dominant shape in them.

This did not block the port — the tests pass and nothing was stubbed — but it
makes --analyze unusable as a clean gate on a well-written test suite, and
--analyze exits 1 on the warning.

The reason it is worth fixing rather than working around: the only way to silence
it is to add a display that exists solely to appease the analyzer. That is noise
in the test output, and it means the checker is steering authors away from the
assertion style that root testing.md mandates. Against the Fundamentals it is
worse than noise — a beginner following the testing guide writes correct code and
is told by the tooling to delete the thing being tested (Fundamental 4, clear and
actionable errors; Fundamental 18, encouragement of best practices).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions