bug: errors raised at end of input carry zero-width ranges #3
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
barrettruth/starlark-cst#3
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Version
0.1.1
Dialect
Dialect::Bazel, reproduced onBUILDand.bzl. Not dialect-specific.Which guarantee broke
Neither invariant — round-trip holds and nothing panics. This is a diagnostic
quality bug: every error raised at end of input carries an empty range.
Observed
Every zero-width range is at
src.len(). Errors raised at a real token arefine —
12..16above is correct, andfoo(a, ??? )style errors are onecharacter wide.
Why it matters
An unclosed delimiter is not an edge case; it is the state a
BUILDfile isin for most of the time anyone is editing it.
cc_library(is what existsbetween typing the callee and typing the first argument.
Under LSP, a
Diagnosticwhoserange.start == range.endhas no characters tounderline. Clients disagree on what to do with it: some render nothing at all,
some fall back to a caret on the following character, some to the whole line.
The most common diagnostic this crate produces is therefore the one most likely
to be invisible.
This is also a known trap that this project already committed to avoiding —
the same defect was catalogued in starpls before this crate was written:
and listed as a required change: "Emit non-zero-width error ranges (currently
TextRange::new(pos, pos))." It has been reproduced.Precedent
Neither Bazel nor starlark-rust has a good answer for the EOF case
specifically — starlark-rust degenerates to an empty span at
1:1:But Bazel does have the right idea for unclosed constructs generally, and
applies it consistently to strings: the diagnostic is anchored on the token
that opened the thing that was never closed, not on the point where the parser
noticed.
That anchor is strictly more useful than the truncation point: it points at the
(that needs closing rather than at the end of the buffer, and it is neverempty.
Proposed behaviour
Two rules, in order.
An "expected X to close Y" error anchors on the opening delimiter, and
spans it. The parser already knows the offset — it descended from there.
foo(\nexpected `)` to close arguments3..4(foo(a\nexpected `)` to close arguments3..4(x = [\n4..5[x = {\n4..5{def f(:\n pass\nexpected `)` to close parameters5..6(Every other error at end of input falls back to a non-empty range: the
last non-trivia token, or if the file is empty,
0..0is unavoidable andacceptable.
expected an expressiononx = [\nshould point at the[or at the newline, not at
6..6.A cheap invariant to hold the line, given errors already carry ranges:
Minimal reproduction
Fixed by
a24a8b9.