Skip to content

LINT-INDENT desyncs on flat otherwise check if: 776 of 780 lines falsely warned on a conforming program (exact-string dedent misses the flat form) #706

Description

@logbie

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.

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