Summary
LINT-INDENT treats a flat otherwise check if …: as opening a nesting level that never closes, so every line after it is misreported. A correctly indented, correctly running file gets a warning on essentially every remaining line and --lint exits 1 — on the very construct the docs recommend.
Reproduction
store x as 1
check if x is equal to 1:
display "a"
otherwise check if x is equal to 2:
display "b"
end check
Command:
wfl l1_flat_otherwise_lint.wfl # prints "a", exit 0
wfl --analyze l1_flat_otherwise_lint.wfl # clean, exit 0
wfl --lint l1_flat_otherwise_lint.wfl
Expected
Rule LINT-INDENT found 0 issues, exit 0. The file is indented exactly as WFL
prescribes.
Docs/03-language-basics/control-flow.md:69 documents the flat form under
"### Chained Conditionals (Else If)", and the equivalent nested form
(otherwise: with an inner check if) lints clean — verified, 0 issues across
all six rules. Two spellings of the same program should not disagree with the
linter.
Actual
Rule LINT-INDENT found 3 issues
Linting complete. Found 3 issues
Lint warnings:
warning[LINT-INDENT]: Line should be indented with 4 spaces, found 0
= Adjust indentation to 4 spaces
warning[LINT-INDENT]: Line should be indented with 8 spaces, found 4
= Adjust indentation to 8 spaces
warning[LINT-INDENT]: Line should be indented with 4 spaces, found 0
= Adjust indentation to 4 spaces
Exit code: 1. The program itself runs correctly (prints a, exit 0) and
--analyze is clean (exit 0).
Root cause
IndentationRule::apply (src/linter/mod.rs:161-226) is a pure text scanner —
note it ignores the parsed AST (_program: &Program, :174). It dedents only on
an exact string match:
// src/linter/mod.rs:194-199
if (trimmed.starts_with("end ")
|| trimmed == "end action"
|| trimmed == "otherwise:" // <-- exact equality
|| trimmed == "end check")
&& expected_indent >= 4
{
expected_indent -= 4;
}
then indents on any line ending in ::
// src/linter/mod.rs:219-221
if trimmed.ends_with(':') || trimmed.contains("then:") {
expected_indent += 4;
}
otherwise check if x is equal to 2: is not equal to "otherwise:", so it never
dedents — but it does end with :, so it increments. The result is a +4 with no
matching -4, and expected_indent stays permanently offset. Trace of the repro:
| line |
text |
indent |
expected |
result |
| 1 |
store x as 1 |
0 |
0 |
ok |
| 2 |
check if x is equal to 1: |
0 |
0 |
ok → expected 4 |
| 3 |
display "a" |
4 |
4 |
ok |
| 4 |
otherwise check if …: |
0 |
4 |
warn → expected 8 |
| 5 |
display "b" |
4 |
8 |
warn |
| 6 |
end check |
0 |
4 |
warn |
The nested form works only because otherwise: hits the exact-match dedent and
then re-increments — exactly what the flat form should also do.
Severity: it is not three warnings, it is the rest of the file
The drift is cumulative, so every line from the first flat otherwise check if
to EOF is misreported. Measured, ladders of n flat arms in a 3+2n-line file:
| flat arms |
file lines |
LINT-INDENT warnings |
| 1 |
6 |
3 |
| 2 |
8 |
5 |
| 3 |
10 |
7 |
| 4 |
12 |
9 |
At realistic scale — l1_scale_780_lines.wfl, a synthetic 780-line program with
ten 5-arm flat ladders, which runs correctly (exit 0) and is --analyze
clean (exit 0) with 0 issues from all five other lint rules:
Rule LINT-NAME found 0 issues
Rule LINT-INDENT found 776 issues
Rule LINT-KEYWORD found 0 issues
Rule LINT-WHITESPACE found 0 issues
Rule LINT-LENGTH found 0 issues
Rule LINT-COMPLEX found 0 issues
Linting complete. Found 776 issues
Exit code 1. 776 of 780 lines flagged.
Two aggravating details:
- The warnings carry no line or column context in the rendered output, so a
reader cannot even tell which lines are meant.
- Every one of the 776 is wrong, so the rule cannot be used to find a real
indentation error in any file that uses the flat form.
Suggested fix
Change the exact-equality test to a prefix test — trimmed.starts_with("otherwise")
— so the flat form dedents and then re-indents like otherwise: already does.
That is a one-line change and restores parity between the two spellings.
Related
Same root-cause family as the companion LINT-KEYWORD issue in this batch: four
of the six rules take the parsed Program and ignore it
(_program at src/linter/mod.rs:174, :242, :341, :386), scanning raw text
instead. For TrailingWhitespaceRule and LineLengthRule that is legitimate —
they are genuinely line-level. For IndentationRule and KeywordCasingRule it is
the direct cause of both defects, since both need block structure and token
boundaries that only the AST has. Fixing them independently is fine; the shared
cause is noted so they are not triaged as unrelated.
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, 3 issues on the minimal repro) — not a regression.
No .wflcfg in scope for the repro.
Context
Found while porting G:/repos/JShrink/src/JShrink/Minifier.php (a 738-line PHP
JavaScript minifier) to WFL. JShrink is a switch-driven character state machine,
and the flat otherwise check if ladder is what those switch arms map onto 1:1.
This one did not block the port — it runs and analyzes clean, so nothing was
stubbed — but it makes --lint unusable on it, and --lint exits 1, so it would
break any CI gate built on the linter.
The reason it is worth fixing rather than working around: the only workaround is
to rewrite every flat ladder into nested otherwise: blocks, which is precisely
the reshaping a port should not have to do. It would add a level of indentation
per arm, obscure the 1:1 correspondence with the source's switch arms, and — for
a switch with six arms — bury the last arm six levels deep to satisfy a rule
that is itself miscounting. It also pushes users away from the form the
documentation recommends, toward a form chosen only to appease a broken check.
Summary
LINT-INDENTtreats a flatotherwise check if …:as opening a nesting level that never closes, so every line after it is misreported. A correctly indented, correctly running file gets a warning on essentially every remaining line and--lintexits 1 — on the very construct the docs recommend.Reproduction
Command:
Expected
Rule LINT-INDENT found 0 issues, exit 0. The file is indented exactly as WFLprescribes.
Docs/03-language-basics/control-flow.md:69documents the flat form under"### Chained Conditionals (Else If)", and the equivalent nested form
(
otherwise:with an innercheck if) lints clean — verified, 0 issues acrossall six rules. Two spellings of the same program should not disagree with the
linter.
Actual
Exit code: 1. The program itself runs correctly (prints
a, exit 0) and--analyzeis clean (exit 0).Root cause
IndentationRule::apply(src/linter/mod.rs:161-226) is a pure text scanner —note it ignores the parsed AST (
_program: &Program,:174). It dedents only onan exact string match:
then indents on any line ending in
::otherwise check if x is equal to 2:is not equal to"otherwise:", so it neverdedents — but it does end with
:, so it increments. The result is a+4with nomatching
-4, andexpected_indentstays permanently offset. Trace of the repro:store x as 1check if x is equal to 1:display "a"otherwise check if …:display "b"end checkThe nested form works only because
otherwise:hits the exact-match dedent andthen re-increments — exactly what the flat form should also do.
Severity: it is not three warnings, it is the rest of the file
The drift is cumulative, so every line from the first flat
otherwise check ifto EOF is misreported. Measured, ladders of n flat arms in a 3+2n-line file:
At realistic scale —
l1_scale_780_lines.wfl, a synthetic 780-line program withten 5-arm flat ladders, which runs correctly (exit 0) and is
--analyzeclean (exit 0) with 0 issues from all five other lint rules:
Exit code 1. 776 of 780 lines flagged.
Two aggravating details:
reader cannot even tell which lines are meant.
indentation error in any file that uses the flat form.
Suggested fix
Change the exact-equality test to a prefix test —
trimmed.starts_with("otherwise")— so the flat form dedents and then re-indents like
otherwise:already does.That is a one-line change and restores parity between the two spellings.
Related
Same root-cause family as the companion
LINT-KEYWORDissue in this batch: fourof the six rules take the parsed
Programand ignore it(
_programatsrc/linter/mod.rs:174,:242,:341,:386), scanning raw textinstead. For
TrailingWhitespaceRuleandLineLengthRulethat is legitimate —they are genuinely line-level. For
IndentationRuleandKeywordCasingRuleit isthe direct cause of both defects, since both need block structure and token
boundaries that only the AST has. Fixing them independently is fine; the shared
cause is noted so they are not triaged as unrelated.
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, 3 issues on the minimal repro) — not a regression.
No
.wflcfgin scope for the repro.Context
Found while porting
G:/repos/JShrink/src/JShrink/Minifier.php(a 738-line PHPJavaScript minifier) to WFL. JShrink is a
switch-driven character state machine,and the flat
otherwise check ifladder is what thoseswitcharms map onto 1:1.This one did not block the port — it runs and analyzes clean, so nothing was
stubbed — but it makes
--lintunusable on it, and--lintexits 1, so it wouldbreak any CI gate built on the linter.
The reason it is worth fixing rather than working around: the only workaround is
to rewrite every flat ladder into nested
otherwise:blocks, which is preciselythe reshaping a port should not have to do. It would add a level of indentation
per arm, obscure the 1:1 correspondence with the source's
switcharms, and — fora
switchwith six arms — bury the last arm six levels deep to satisfy a rulethat is itself miscounting. It also pushes users away from the form the
documentation recommends, toward a form chosen only to appease a broken check.