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).
Summary
ANALYZE-UNUSEDdoes not modelexpect … to …as a use, so a variable bound in a test and referenced only by its assertion is falsely reported unused.ExpectStatementis one of 33 of 83Statementvariants that fall into the_ => {}catch-all in the analyzer's use-tracking visitor, so the same false positive also hitscreate file,create list, and container instantiation. This is the unfinished remainder of the sweep that #468 recommended when it was fixed.Reproduction
Command:
Expected
No static analysis warnings found, exit 0.vis read by the assertion; thetest passes 1/1 under
wfl --test, so the variable is genuinely used at runtime.Docs/guides/testing-guide.mddocumentsdescribe/test/expectas the testingform, and root
testing.md§8.3 requires asserting outcomes rather than "didnot crash" — so binding a value and asserting on it, with no other reference, is
the mandated style, not an edge case.
Control: adding
display vbefore theexpectclears the warning entirely(
No static analysis warnings found), confirming the assertion itself contributesnothing to use-tracking.
Actual
Exit code: 1.
wfl --teston 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 amatchoverStatementwith one arm per handled kind and a catch-all that marks nothing:There is no
Statement::ExpectStatementarm — searching forExpectacrossthe whole function returns nothing — so neither its
subjectnor the assertion'soperand is ever visited:
DescribeBlockandTestBlockdo have arms, so the analyzer walks into thetest 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:to equal "c"to be "c"to be greater than 1to be less than 9to be yesto be noto existto contain "c"to be emptyto have length 1to be of type "text"Both operands are uncounted, not just the subject:
→
Unused variable 'v'andUnused variable 'w'; passes 1/1.The whole subject expression tree is unvisited, not just a bare variable:
→
Unused variable 'base_val'; passes 1/1.It is wider than
expectDiffing the 83
Statementvariants insrc/parser/ast.rsagainst the arms inmark_used_variables: 50 handled, 33 not. Verified false positives on threefurther constructs, all of which parse and run correctly:
create file at target_path with body_texttarget_pathandbody_textcreate list nums:/add seed_val/end listseed_valcreate new P as p1:/nm is who_name/endwho_nameOther 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 correctlyno-ops.)
The repository's own test suite trips this
TestPrograms/natural_language_constructs.test.wfl— a gated regression suitethat passes 12/12 under
wfl --test— emits 6 falseANALYZE-UNUSEDwarnings:
boiling,hot,inside,quotient,remainder,value. Each isthe exact pattern, e.g. at
:63-64:TestPrograms/math_operations.test.wflemits 4 more. So the analyzer currentlyreports false warnings against the project's own required tests.
Precedent and lineage — not a duplicate
#468 ("Analyzer:
add 1 to Xnot counted as a use ofX(ANALYZE-UNUSEDfalse positive)", labelled
bug, CLOSED/fixed) is the same rule, the sameroot cause, and a different statement kind. Its fix added the missing arms —
AddToListStatementandRespondStatementare 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:
That sweep was never done; 33 variants remain. This issue is that sweep, with
expectas the entry point. It also settles the label: the maintainer alreadytriaged this exact class as
bugand 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 callsmark_used_in_expressiononsubjectand on theAssertion's operand(
Equal/Be/GreaterThan/LessThan/Contain/HaveLengtheach carry anExpression;BeYes/BeNo/Exist/BeEmpty/BeOfTypecarry none). Then do thesweep #468 asked for across the remaining 32 variants. A regression test that
would have caught this: run
--analyzeoverTestPrograms/*.test.wfland assertzero
ANALYZE-UNUSED.Worth considering: replacing the
_ => {}catch-all with an exhaustive matchwould stop this recurring every time a statement kind is added — that is what let
expectslip in silently after #468 was closed.Environment
WebFirst Language (WFL) version 26.8.4C:\Program Files\wfl\bin\wfl.exeAlso 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
.wflcfgin 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
testblocks, and theassertion-only binding is the dominant shape in them.
This did not block the port — the tests pass and nothing was stubbed — but it
makes
--analyzeunusable as a clean gate on a well-written test suite, and--analyzeexits 1 on the warning.The reason it is worth fixing rather than working around: the only way to silence
it is to add a
displaythat exists solely to appease the analyzer. That is noisein the test output, and it means the checker is steering authors away from the
assertion style that root
testing.mdmandates. Against the Fundamentals it isworse 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).