feat(vba): mark edges emitted from inside an error handler - #289
Merged
Merged
Conversation
Every call a procedure made inside its error handler was attributed to the enclosing procedure exactly like a call on the happy path, so `Riesgo.Guardar -> MsgBox` and `Riesgo.Guardar -> Escribir` were indistinguishable in the graph when only one of them runs on failure. This is §3.4 of `docs/vba-error-handling-plan.md`, and E2 already resolved the handler region — the information was there, it just was not on the rows. Two things land, both as fields on rows that already existed: `metadata.inErrorHandler: true` on every edge and every unresolved reference emitted from inside the region. It is stamped at ONE place — `closeErrorPolicy`, over the procedure's own slice of the two accumulators, bounded by the `[handlerStartLine, handlerEndLine]` the node publishes — rather than in each of the six emitters that can fire inside a handler. Six emitters each remembering a flag is six places to forget it; here a new emitter inherits the behaviour with no knowledge of the feature. `ctx.inErrorHandler(lineNum)` exposes the same predicate for anything that needs it mid-walk. The upper bound is the open procedure itself: the accumulator is cleared at `End Sub`, so the next procedure cannot inherit the previous one's region. That is the off-by-one guard, by construction rather than by arithmetic. `errorPolicy.behavior`, which E2 shipped as a deliberate `null`. It is derived from three independently collected signals over the handler body — a write to an error-channel variable, a `MsgBox` / `Debug.Print`, an `Err.Raise` — and `mixed` is what more than one of them means. Collecting them independently is the point: the pre-probe classifier tested one after another and let evaluation order hide the 921 handlers that both record the error and show it. The classifier mirrors `scripts/vba-coverage-probe.mjs` by hand, because the probe is the instrument the corpus census was measured with and the probe cannot import the extractor (it must also run against a shipped `dist/`). Same channel matcher, same display list, same statement splitter with its `If … Then` guard strip — which is what makes the corpus's fifth most common handler shape, `DoCmd.Hourglass False` cleanup followed by a guarded channel write, classify as `channel` instead of being thrown by the leading call. A test runs both classifiers over one fixture and asserts they agree procedure by procedure, so a future fork fails there rather than in a re-measurement nobody runs. Measured over the three corpus projects the two agree on all 4,817 procedures, with zero disagreements: 2,681 `channel`, 921 `mixed`, 106 `unknown`, 52 `display`, 14 `reraise`, 1,043 `null`. Node, edge and unresolved-reference totals are byte-identical to `origin/main` (26,089 / 29,521 / 26,755) with identical per-kind breakdowns; 57 edges and 2,823 references gained the flag. Closes #260 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d
Owner
Author
|
Rebased onto Two notes on your honest caveats, both now settled from my side:
|
ardelperal
force-pushed
the
feat/issue-260
branch
from
September 3, 2026 05:42
89d6547 to
1d9b2ae
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task E3 of
docs/vba-error-handling-plan.md. Closes #260.What this does
Every call a procedure made inside its error handler was attributed to the enclosing procedure exactly like a call on the happy path, so
Riesgo.Guardar -> MsgBoxandRiesgo.Guardar -> Escribirwere indistinguishable in the graph when only one of them runs on failure (section 3.4). E2 (#259) already resolved the handler region; this PR puts that information onto the rows.Two fields, both on rows that already existed. No new node kind, no new edge kind, no new row:
metadata.inErrorHandler: trueon every edge and everyUnresolvedReferenceemitted from inside the handler region.errorPolicy.behavior—channel/display/reraise/mixed/unknown, filling in thenullE2 deliberately shipped.Decisions worth reviewing
The flag is stamped at one place.
closeErrorPolicyinsrc/extraction/vba/errors.tswalks the procedure's own slice ofctx.edges/ctx.unresolvedReferences(bounded by marks taken when the body opened, so a module with N procedures stays linear) and flags the rows whose line falls in[handlerStartLine, handlerEndLine], the exact region published on the node. The issue asked for this explicitly, and it is why the six emitters that can fire inside a handler needed no edit: a new emitter inherits the behaviour without knowing the feature exists.ctx.inErrorHandler(lineNum)exposes the same predicate for anything that needs it mid-walk.The off-by-one guard is structural, not arithmetic. The region's upper bound is the open procedure itself —
vbaErrorPolicyis cleared atEnd Sub/End Function/End Property, so the next procedure starts with a fresh accumulator and can never inherit the previous one's region. Pinned by a test either way.The flag is only ever
true; absence is the "not in a handler" encoding. That adds a key to ~2,880 rows instead of afalseto 56,276.behaviormirrors the probe by hand.scripts/vba-coverage-probe.mjsis the instrument the section 2.3 census was measured with, and it cannot import the extractor (it must also run against a shippeddist/). So the channel-write matcher, the display-call list, theErr.Raisetest and the statement splitter with itsIf ... Thenguard strip are copied verbatim, the same way #259 copiedLINE_LABEL_RE/NOT_A_LABEL— change one, change the other. The guard strip is what makes the corpus's fifth most common handler shape classify correctly:errores: DoCmd.Hourglass False If Err.Number <> 1000 Then m_Error = "..." & Err.DescriptionSignals are collected over the whole region, per statement, so the leading
DoCmdcannot shadow the later write —channel, notunknown. There is a fixture for exactly this.The three signals are collected independently and
mixedis what more than one means. The pre-probe classifier tested them in sequence and let evaluation order hide the 921 handlers that both record the error and display it.behaviorisnullonly when there is no handler to describe (resume-next/none), andunknownwhen a handler exists but nothing in it is recognised. Two different facts, and the probe's rule.The error-channel names are still the hard-coded four (
m_Error,p_Error,g_Error,Error), identical to the probe's defaults. E4 (#261) is the task that turns them into thevba.errorChannelconfig knob; making them configurable here would have forked the knob from its owner.Corpus measurements
00_EXPEDIENTES,00_GESTION_RIESGOS,HPS_SOLICITUDES— 4,817 procedures, read-only.behavior— extractor vs probe, procedure by procedure: 0 disagreementsbehaviorchannelmixedunknowndisplayreraisenull(227 resume-next + 816 unprotected)This matches the probe's exclusive table in section 2.3 exactly. It deliberately does not match the "~970 display / ~2,788 channel" numbers the issue's acceptance box quotes: those are the raw non-exclusive signal totals (3,602 channel / 971 display / 16 re-raise), which the same probe run reproduces unchanged. The plan's section 2.3 already records that the exclusive table supersedes them, and
mixedis where the difference lives — 921 handlers both record and display.Cross-checked in CI too: a test runs both classifiers over one fixture and asserts they agree procedure by procedure, so a future edit that forks them fails there rather than in a re-measurement nobody runs.
Totals unchanged (the merge-blocking invariant)
Measured by running the extractor over the same corpus twice — once on this branch, once with
src/extraction/vba/errors.tsandsrc/extraction/vba/context.tsrestored toorigin/main:origin/mainnodesByKind,edgesByKindandunresolvedByKindare identical between the two runs. Onorigin/mainbehaviorisnullfor all 4,817 procedures and no row carriesinErrorHandler.What gained the flag
57 edges — 51
calls, 2references / vba-tempvar, 2opens-form, 1references / vba-sql-table, 1references / vba-set-new.2,823 unresolved references — 2,047
calls, 690property-get, 40unqualified-ident, 26references, 20property-set.The
callsshare depends on #265, which landed first: before it, statement-form calls (MsgBox "x",DoCmd.Hourglass False) were classifiedunqualified-identand the handler's dominant construct would have been mis-shaped.Tests
New
__tests__/extraction-vba-error-handler-region.test.ts— 22 tests: the happy-path/handler split, no-handler and untargeted-label cases, an SQL table reference inside a handler still emitted and now flagged, aDoCmd.OpenFormedge from a different emitter inheriting the flag, the off-by-one guard,ctx.inErrorHandlerdirectly, the full behaviour matrix including cleanup-then-record, channel read-vs-write,ErrorCountnot matchingError, a string-literalMsgBox, and the probe cross-check. Plus an "adds no node, no edge and no unresolved reference" assertion.__tests__/extraction-vba-error-policy.test.ts— the one test that pinnedbehavior: nullfor a handler is now the E3 assertion; a new test keeps thenull-when-no-handler case covered.Suite runs (in batches — a full
npx vitest runOOMs on this machine, which is an environment limit, not this change):afterEachEPERM/EBUSYtemp-dir removals inworktree-detection(15),multi-repo-workspace(2),extraction(2),npm-sdk(2)). No other failure.npx tsc --noEmitclean.What I could not verify
afterEachon Windows), not from a run onorigin/main.npm testas a single invocation. It dies withFatal process out of memoryhere; everything was run in batches instead.errores: MsgBox "x") occurs 0 times in the corpus's 3,774 handlers. Thebehaviorsignals read that trailing statement (the probe does, so this must too); theinErrorHandlerflag uses[handlerStartLine, handlerEndLine], which starts on the line after the label — so an edge emitted on the label's own line would not be flagged. On this corpus the two can never disagree, and I chose consistency with the region published on the node over covering a shape that does not occur. It is documented in the plan and in the code.metadata.inErrorHandlerunchanged is inherited from how every other edge-metadata field is stored, not re-proved here.🤖 Generated with Claude Code
https://claude.ai/code/session_019gmKKUq1ng5ESk6Qhxu77d