Skip to content

fix(vba): stop reading On Error GoTo as a reference to a variable named Error - #293

Merged
ardelperal merged 1 commit into
mainfrom
fix/issue-292
Sep 3, 2026
Merged

fix(vba): stop reading On Error GoTo as a reference to a variable named Error#293
ardelperal merged 1 commit into
mainfrom
fix/issue-292

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

scanModuleVariableReferences walks every identifier on a line and emits a reference for any that names a module-level variable. It guarded a . / ! prefix and procedure-local shadowing (#205), but not VBA keyword context — so in a module declaring Public Error As String, the word Error in On Error GoTo errores was read as an access to that variable.

Public Error As String is this codebase's error-channel convention and appears in dozens of classes, so this fired constantly.

Why now

On its own it is a stray edge. #261 (task E4) labels channel references with errorChannel: true, and Error is in the default channel list — which would have turned every one of these into a confident claim that an On Error statement takes part in error propagation. That is the failure mode CLAUDE.md and guardrail 1 of docs/vba-error-handling-plan.md both name as the worst available here. #261 is held until this lands so it is measured on clean data.

The fix

Blank the On Error keyword pair out of the line before the identifier walk, replacing it with spaces of the same length.

The same-length part is load-bearing, not incidental: the emitted reference carries column: m.index, so a substitution that shifted offsets would corrupt every column on the line. There is a fixture that pins the column of a genuine reference sharing a line with On Error GoTo (On Error GoTo errores: Error = "x" → column 25, the real offset).

Scope, deliberately narrow

Only the On Error pair. VBA spells the Error statement (Error 5) and the Error$() function with the same word; both have zero occurrences in this corpus, and telling those from an identically-named variable is a parser problem rather than a masking one. Left for a corpus that contains them.

The alternative fix — dropping Error from #261's default channel list — was rejected: it kills genuine channel matches to suppress false ones, and leaves the underlying edge defect in place for every other consumer.

Corpus measurement

Probe --json over 00_EXPEDIENTES, 00_GESTION_RIESGOS and HPS_SOLICITUDES, this branch versus origin/main:

main branch
nodes, all kinds 30,000 30,000
edges, all kinds 37,456 37,456
unresolved references 26,755 25,211
property-get 5,636 4,092

1,544 false reads removed. The entire delta is property-get; no other reference kind, node kind or edge kind moves — which is the shape you want, since this removes references and never nodes.

The measured figure is larger than the issue's estimate, and that is expected

The issue estimated ~909. The measurement says 1,544. The estimate counted handler bodies; the sweep de-duplicates per (procedure, variable, direction), so every procedure whose only apparent read of Error came from its own On Error line contributed one — including procedures the estimate never looked at. Reporting the measured number, not the forecast.

Verification

  • npx tsc --noEmit — clean.
  • New suite __tests__/extraction-vba-on-error-keyword.test.ts — 10/10.
  • Four adjacent suites together (on-error keyword, module variables, VBA extraction, error policy, labels) — 314 passed.

One test pins a behaviour it does not fix: on On Error GoTo errores: Error = "x" the write is reported as property-get, because after a colon the prefix isDirectAssignment sees is not blank. That is a pre-existing limitation of the shared predicate, unrelated to this mask; asserted so it is pinned rather than silently depended on.

Not verified

The full local suite was not run to completion — npx vitest run on this machine dies with Fatal process out of memory, an environment limit unrelated to this change. CI runs the full suite on Ubuntu and Windows.

Closes #292

🤖 Generated with Claude Code

https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d

…ed Error

`scanModuleVariableReferences` walks every identifier on a line and emits a
reference for any that names a module-level variable. It guarded a `.` / `!`
prefix and procedure-local shadowing (#205), but not VBA keyword context — so
in a module declaring `Public Error As String`, the word `Error` in
`On Error GoTo errores` was read as an access to that variable.

`Public Error As String` is this codebase's error-channel convention and
appears in dozens of classes, so this fired constantly. On its own it is a
stray edge; #261 labels channel references with `errorChannel: true`, which
would have turned every one of them into a confident claim that an
`On Error` statement takes part in error propagation — the failure mode
`CLAUDE.md` and guardrail 1 of `docs/vba-error-handling-plan.md` both name as
the worst available here. #261 is held until this lands so it is measured on
clean data.

The `On Error` pair is blanked out before the identifier walk, replaced with
spaces of the SAME length. That is load-bearing rather than incidental: the
emitted reference carries `column: m.index`, so a substitution that shifted
offsets would corrupt every column on the line. A fixture pins the column of a
genuine reference sharing a line with `On Error GoTo`.

Scoped to the `On Error` pair only. VBA spells the `Error` statement
(`Error 5`) and the `Error$()` function with the same word; both have zero
occurrences in this corpus, and telling those from an identically-named
variable is a parser problem rather than a masking one. They are left for a
corpus that contains them.

Measured on the corpus (`00_EXPEDIENTES`, `00_GESTION_RIESGOS`,
`HPS_SOLICITUDES`): unresolved references fall 26,755 -> 25,211. The entire
delta is `property-get`, 5,636 -> 4,092 — 1,544 false reads removed, and no
other reference kind, node kind or edge kind moves. Nodes stay at 30,000 and
edges at 37,456.

The issue estimated ~909; the measured figure is 1,544. The estimate counted
handler bodies, while the sweep de-duplicates per (procedure, variable,
direction) — so every procedure whose ONLY apparent read of `Error` came from
its own `On Error` line contributed one, including procedures the estimate did
not look at.

Closes #292

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d
@ardelperal
ardelperal merged commit 4d395f5 into main Sep 3, 2026
5 checks passed
@ardelperal
ardelperal deleted the fix/issue-292 branch September 3, 2026 06:54
ardelperal added a commit that referenced this pull request Sep 3, 2026
* feat(vba): recognise the module-variable error channel

Error propagation in an Access codebase of this shape does not use VBA's
error mechanism. 16 handlers out of 3,774 re-raise; `Err.Raise 1000`
unwinds exactly one frame and the house guard `If Err.Number <> 1000`
means "an inner procedure already wrote a human-readable message". The
message itself travels through a field the failing procedure writes and
the caller reads.

That is module-variable data flow, which #251 already models as
`property-set` / `property-get` references onto a `variable` node. This
change only labels it: a read or write of a channel variable now carries
`metadata.errorChannel: true`, on the reference and on the resolved edge.
No new node kind, no new edge kind, and no new row — the corpus indexes
to byte-identical `nodesByKind` / `edgesByKind` / `unresolvedByKind`
totals (26,089 / 29,521 / 26,755, unchanged against origin/main).

Decisions taken:

- The channel names and the write matcher move into a new leaf module,
  `src/extraction/vba/error-channel.ts`. `errors.ts` owned both before,
  and its own comment deferred the config knob to this task; leaving the
  list there and importing it from `module-vars.ts` would have forked
  two matchers the moment the knob became config-aware. Both consumers
  now read one compiled object, so `vba.errorChannel` drives the
  reference flag AND `errorPolicy.behavior` rather than only the former.

- `vba.errorChannel` takes bare VBA identifiers, matched as whole names,
  and EXTENDS the built-in list — the same contract `vba.sqlWrappers`
  established in #244. No user-supplied regex: this runs per identifier
  per line, which is exactly where one is a backtracking hazard. Matching
  a name rather than a substring is what keeps `ErrorCount` out.

- The compiled form (a `Set` plus RegExps) lives on the extractor
  context, not in `VbaExtractionOptions`, because the options object
  crosses the `structuredClone` worker boundary.

- The flag is only ever `true`; its absence encodes "not the channel", so
  it is added to a minority of rows instead of a `false` to every one —
  the shape #260 chose for `inErrorHandler`.

Closes #261

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d

* feat(vba): emit label nodes and handles-error edges (#290)

* feat(vba): emit label nodes and handles-error edges

Task E6 of `docs/vba-error-handling-plan.md`. #259 records *whether* a
procedure has an error handler and #260 marks *which* edges come from
inside one, but neither gives the handler an identity you can point at,
search for or traverse to. This does: one `label` node per VBA line label,
and a `handles-error` edge from the procedure to the handler it routes to.

The plan's §4 rejected this design for #259 and §4.3 named the condition
that reopens it — a query `inErrorHandler`'s per-procedure boolean cannot
serve. Addressing a handler as a thing is that query: a stable id per
handler, `kind:label` search, and dangling/duplicate/control-flow-label
detection as a graph query rather than a scan.

This adds no parsing. Every fact published here was already computed by the
error-policy classifier while the procedure body was open — the label
definitions, the `On Error GoTo` targets, the handler region and the
dangling-target resolution. `handlerBehavior` is #260's derived
`errorPolicy.behavior`, copied verbatim. The one genuinely new signal is
the plain-`GoTo` jump, which the policy classifier had no reason to look at
while it emitted nothing, and which arrives as a fifth rule on the same
declarative table rather than as a second scanner.

Decisions taken:

- `qualifiedName` is always `<ModuleOrClass>.<Procedure>.<label>`. VBA
  scopes labels to the procedure and this corpus writes `errores` 3,735
  times; without the procedure segment every handler in a project collapses
  into one symbol. Same shape #257's parameters and #251's module variables
  chose.
- `handles-error` is not deduplicated per procedure. 47 procedures issue
  more than one `On Error GoTo`, and each is a distinct routing decision
  with its own line, so each emits its own edge.
- A plain `GoTo` reuses the generic `references` kind tagged `vba-goto`.
  A jump is not an error-handling fact and 192 sites do not justify a
  second kind; the synthesizer tag keeps them filterable.
- A `GoTo` whose target the procedure never defines emits an
  `UnresolvedReference` and **no node**. A graph that invents its own
  targets cannot be used to find that defect, which is the only reason to
  look for it.
- Calls inside a handler stay attributed to the enclosing procedure. The
  label is addressable, not a container; re-parenting would change
  `callers`/`callees` for the 3,774 procedures that have a handler.
- Only the label whose region `errorPolicy` actually resolved carries
  `handlerBehavior` and the region lines. A procedure that swaps to a
  second handler label has a second region nobody classified, and deriving
  one here would be exactly the drift this split avoids.
- A numeric `GoTo` target is a VBA line number, not a line label. The label
  detector cannot define one, so referencing it would fabricate a permanent
  dangling reference for legal code.
- `label` stays out of `HIGH_VALUE_NODE_KINDS` and `CONTAINER_NODE_KINDS`,
  for the reason #257 kept `parameter` out of both: it is now the most
  numerous VBA symbol in the graph.

Measured on the three-project Access corpus with the committed probe: 3,911
label nodes, 3,832 `handles-error` edges, 192 `vba-goto` references, zero
new unresolved references, and no other node or edge kind moved. That is
+15.0% nodes and +26.9% edges — the extractor matches the probe's census
exactly on all three counts.

`EXTRACTION_VERSION` is bumped to 26: a new node kind and a new edge kind
change what a re-index would produce.

Closes #263

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d

* docs(vba): correct the E6 landing-order claim in the plan

The E6 section said it landed "after E1-E5 were built". E4 (#261) is still
in flight and E5 (#262) has not started, so that sentence asserted an order
that did not happen. Corrected to what is true: E6 landed after E1-E3,
alongside E4, and before E5.

The rest of the section — the three §4.3 conditions, the measured budget, and
the warning not to read it as licence to add a kind elsewhere — is unchanged
and accurate.

Refs #263

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(vba): stop reading On Error GoTo as a reference to a variable named Error (#293)

`scanModuleVariableReferences` walks every identifier on a line and emits a
reference for any that names a module-level variable. It guarded a `.` / `!`
prefix and procedure-local shadowing (#205), but not VBA keyword context — so
in a module declaring `Public Error As String`, the word `Error` in
`On Error GoTo errores` was read as an access to that variable.

`Public Error As String` is this codebase's error-channel convention and
appears in dozens of classes, so this fired constantly. On its own it is a
stray edge; #261 labels channel references with `errorChannel: true`, which
would have turned every one of them into a confident claim that an
`On Error` statement takes part in error propagation — the failure mode
`CLAUDE.md` and guardrail 1 of `docs/vba-error-handling-plan.md` both name as
the worst available here. #261 is held until this lands so it is measured on
clean data.

The `On Error` pair is blanked out before the identifier walk, replaced with
spaces of the SAME length. That is load-bearing rather than incidental: the
emitted reference carries `column: m.index`, so a substitution that shifted
offsets would corrupt every column on the line. A fixture pins the column of a
genuine reference sharing a line with `On Error GoTo`.

Scoped to the `On Error` pair only. VBA spells the `Error` statement
(`Error 5`) and the `Error$()` function with the same word; both have zero
occurrences in this corpus, and telling those from an identically-named
variable is a parser problem rather than a masking one. They are left for a
corpus that contains them.

Measured on the corpus (`00_EXPEDIENTES`, `00_GESTION_RIESGOS`,
`HPS_SOLICITUDES`): unresolved references fall 26,755 -> 25,211. The entire
delta is `property-get`, 5,636 -> 4,092 — 1,544 false reads removed, and no
other reference kind, node kind or edge kind moves. Nodes stay at 30,000 and
edges at 37,456.

The issue estimated ~909; the measured figure is 1,544. The estimate counted
handler bodies, while the sweep de-duplicates per (procedure, variable,
direction) — so every procedure whose ONLY apparent read of `Error` came from
its own `On Error` line contributed one, including procedures the estimate did
not look at.

Closes #292


Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(vba): recognise the module-variable error channel

Error propagation in an Access codebase of this shape does not use VBA's
error mechanism. 16 handlers out of 3,774 re-raise; `Err.Raise 1000`
unwinds exactly one frame and the house guard `If Err.Number <> 1000`
means "an inner procedure already wrote a human-readable message". The
message itself travels through a field the failing procedure writes and
the caller reads.

That is module-variable data flow, which #251 already models as
`property-set` / `property-get` references onto a `variable` node. This
change only labels it: a read or write of a channel variable now carries
`metadata.errorChannel: true`, on the reference and on the resolved edge.
No new node kind, no new edge kind, and no new row — the corpus indexes
to byte-identical `nodesByKind` / `edgesByKind` / `unresolvedByKind`
totals (26,089 / 29,521 / 26,755, unchanged against origin/main).

Decisions taken:

- The channel names and the write matcher move into a new leaf module,
  `src/extraction/vba/error-channel.ts`. `errors.ts` owned both before,
  and its own comment deferred the config knob to this task; leaving the
  list there and importing it from `module-vars.ts` would have forked
  two matchers the moment the knob became config-aware. Both consumers
  now read one compiled object, so `vba.errorChannel` drives the
  reference flag AND `errorPolicy.behavior` rather than only the former.

- `vba.errorChannel` takes bare VBA identifiers, matched as whole names,
  and EXTENDS the built-in list — the same contract `vba.sqlWrappers`
  established in #244. No user-supplied regex: this runs per identifier
  per line, which is exactly where one is a backtracking hazard. Matching
  a name rather than a substring is what keeps `ErrorCount` out.

- The compiled form (a `Set` plus RegExps) lives on the extractor
  context, not in `VbaExtractionOptions`, because the options object
  crosses the `structuredClone` worker boundary.

- The flag is only ever `true`; its absence encodes "not the channel", so
  it is added to a minority of rows instead of a `false` to every one —
  the shape #260 chose for `inErrorHandler`.

Closes #261

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(vba): On Error GoTo is read as a reference to a module variable named Error

1 participant