This document is the canonical reference for the seven reference_kind literals
that survive round-3 (issue #108) and that consumer-side lints, audit scripts
(audit_v113_compare.py and its successors), and the post-extraction stub
resolver all key on. It complements — but does not duplicate — the sibling doc
docs/vba-stub-repoint-decision.md (which
covers repointDecision) and docs/vba-extraction-perf.md
(which covers per-stage timing). The single TypeScript source of truth is the
JSDoc block above export type ReferenceKind in
src/types.ts (around line 362–399); every row in the table
below is anchored there.
The seven reference_kind literals a consumer sees in unresolved_refs after
the v1.7.0 round-4 round-trip:
reference_kind |
Syntactic shape | Example | Resolves to user code? | stdlib allowlist catches? |
|---|---|---|---|---|
unqualified-ident |
bare identifier — no (), no Call keyword and no arguments; default for If HayError…-style reads |
If HayErrorEnRiesgo Then … |
Only via Const-read disambiguation (FR-3.1) | No — name-only gate required (issue #188) |
calls |
paren-form Name(...), or a statement-form Sub call whose syntax rules out a Const read — the Call keyword, or an argument list |
MiFuncion(x) / Call LimpiaBuffer / Escribir 1, 2 |
Yes, same-file; cross-module via resolver | No — stdlib lives in declined-runtime |
qualified-call |
Receiver.Member(...) (paren) or Receiver.Member args (statement) |
getdb().Execute "SELECT …" |
Yes, when receiver type matches user module | No — receiver may be runtime (DAO, fso) |
member-with |
.Member inside a With <receiver> block |
With rs: .MoveFirst: .EOF |
Yes, when receiver is local/typed | No — same caveat as qualified-call |
property-get |
dot read: Me.Name, obj.Prop |
Debug.Print Me.Name |
Yes, Me-control reads | No (controls layer) |
property-set |
dot assignment: Me.Name = value |
Me.Name = "X" |
Yes, Me-control writes | No (controls layer) |
references |
legacy literal — anything the round did not reclassify (e.g. Implements) | Implements IClass |
Yes, interface name | No — preserved for back-compat |
Why a bare identifier is not a calls row (issue #265). The two rows above
split the three statement-form shapes on one question: could this have been a
Const read instead of a call?
| Statement form | Can it be a Const read? |
reference_kind |
|---|---|---|
Escribir |
Yes — a bare identifier read is indistinguishable from a no-argument Sub call | unqualified-ident |
Call Escribir |
No — the Call keyword is only valid on a procedure |
calls |
Escribir 1, 2 |
No — a constant cannot take an argument list | calls |
The bare form stays in unqualified-ident deliberately: the const-first
disambiguation rule (FR-3.1, issue #108) resolves that bucket against local
constants before it treats a name as a missing callee, and the name-only
runtime gates for DAO enum values and VBA intrinsic constants (issue #188) key
on the same kind. Moving bare reads into calls would break both. The other
two forms carry information the parser already had, so they get the canonical
calls literal — the same one a resolved call edge uses.
Two additional literals (function_ref, bang-get, bang-set, dao-query)
are emitted by the extractor but are out of scope for the consumer-side filters
in this doc — see src/types.ts:362-399 for the full union and rationale.
declined-runtime is never emitted by the extractor. It is a status the
post-extraction resolver at src/resolution/index.ts (lines 1184, 1235, 1435)
stamps on a row when classifyVbaReferenceAsRuntime(reference) returns
true. The classifier consults two allowlists in
src/resolution/vba-runtime-objects.ts:
RUNTIME_OBJECTS— the receiver is a known VBA/Access runtime object (DAO,fso,err,ListBox,Collection,VBA,Application,Screen,DoCmd,CurrentDb,Forms,Reports,Debug,Modules,References,CommandBars,SysCmd,CurrentProject,CodeData,CodeProject,TextBox,ComboBox,CreateObject,GetObject). These never link to user code by definition — the runtime ships them.VBA_STDLIB_FUNCTIONSandDAO_ENUM_VALUES— the call site is a bare built-in (CStr,IsNull,MsgBox,Shell,vbCrLf,dbFailOnError, …). Same reasoning.
When either allowlist matches, the row's status flips from failed to
declined-runtime; the row stays in unresolved_refs (it does not turn
into an edge) but the consumer filter WHERE status = 'failed' no longer
catches it. That is the entire point — failed becomes the actionable bucket,
declined-runtime is the documented noise.
-- Diagnostic: distribution of unresolved-ref statuses.
-- A healthy corpus shows `declined-runtime` dominating, `failed` small.
SELECT r.status,
r.reference_kind,
COUNT(*) AS n
FROM unresolved_refs r
GROUP BY r.status, r.reference_kind
ORDER BY n DESC;
-- Actionable: the genuine-missing-callees list. THIS is the bucket a
-- parser-bug audit should iterate over.
SELECT r.reference_kind,
COUNT(*) AS n
FROM unresolved_refs r
WHERE r.status = 'failed'
GROUP BY r.reference_kind
ORDER BY n DESC;The first query should show declined-runtime at the top (DAO / fso / stdlib
noise); the second should show only the kinds where a real callee is
unfindable.
These are the expected failed rate per reference_kind on the
00_VBA_TOOLKIT_BENCH corpus at v1.13.0 — i.e. what fraction of each kind
survives the declined-runtime classifier and lands in the actionable
failed bucket. They are the lower-bound on residual noise for any new
parser-bug audit; if a future change drops a kind's failed count below these
numbers, that is not a win — it almost certainly means the classifier
swallowed a real callee.
reference_kind |
Total emitted | failed count |
failed / total |
Source PRs |
|---|---|---|---|---|
unqualified-ident |
649 | 65 | ~10% | #183 (DAO enum gate) + #188 (VBA intrinsics gate) — split superseded by #265, see below |
calls |
689 | 207 | ~30% | #184 (stdlib statement-form gate) + #195 (statement-form stdlib) — split superseded by #265, see below |
qualified-call |
560 | 56 | ~80% | #185 (qualified-call DAO escape) — most resolve via vba-name-resolution |
member-with |
702 | 35 | ~95% | #186 (With-block runtime-object escape) — almost all With rs/With fso patterns |
property-get |
140 | 70 | ~50% | inherits from cross-line property-set fix; residual is dynamic Me.Controls!X |
Issue #265 moved the two syntactically unambiguous statement forms (Call Foo
and Foo 1, 2) out of unqualified-ident and into calls. That reclassifies
rows; it never adds or removes one, so the combined total and the
combined failed count of the two kinds are invariant — which is exactly
what the measurement below is for. The 00_VBA_TOOLKIT_BENCH corpus that
produced the v1.13.0 table above is no longer available, so the two affected
rows were re-measured on two production Access projects (246 .bas / .cls
modules). Every other kind came out identical before and after and is omitted.
Extractor output, via the committed probe
(npm run probe:vba -- <root> ..., see scripts/vba-coverage-probe.mjs):
reference_kind |
Emitted (before → after) |
|---|---|
calls |
7,679 → 8,976 |
unqualified-ident |
1,459 → 162 |
| combined | 9,138 → 9,138 |
Every other line of the probe report — files, all node kinds, declared procedures, stub nodes, every edge kind and synthesizer, the top-30 stub targets, SQL tables, forms — is identical between the two runs.
After the resolver, from a full index of the same two projects (indexed
separately), which is what the failed column above measures:
reference_kind |
Total (before → after) | failed (before → after) |
failed / total (after) |
|---|---|---|---|
calls |
6,194 → 6,351 | 693 → 777 | ~12% |
unqualified-ident |
166 → 9 | 85 → 1 | ~11% |
| combined | 6,360 → 6,360 | 778 → 778 | ~12% |
The combined row is the invariant to re-check after any future change to these two emitters: if it moves, rows were created or dropped, not reclassified.
Because calls is the kind the stdlib gate (#184/#195) already accepts by name
alone, the reclassified statement-form built-ins (MsgBox "x", Shell "calc",
…) keep landing in declined-runtime rather than the actionable bucket. What
changes is the reverse case the issue was filed for: a genuine missing callee
written Call Foo no longer sits behind the bare-identifier const/intrinsic
gates, so it can no longer be silenced for the wrong reason.
Two derived facts from this table:
declined-runtimeis the dominant bucket on every kind that reaches the classifier. The aggregatefailedrate is well under 20%; the rest is documented runtime noise. An audit that reportsunresolved_refswithout filtering onstatus='failed'is reporting noise.member-withandqualified-callresolve best because they preserve the receiver type and the resolver can use it.unqualified-identandcallsresolve worst because they strip the receiver — disambiguation falls back to Const-read priority (FR-3.1) and name matching, which the runtime allowlist catches the rest of.
Each PR above is additive: it added a new allowlist entry or
classifier branch and shrank the failed bucket without touching the
emitters. Future PRs that want to improve resolution should follow the same
shape (add a classifier → measure before/after on this corpus → cite the
delta in the changelog) — never modify an emitter without re-running this
table.
- TypeScript source of truth —
src/types.ts:362-399(JSDoc aboveexport type ReferenceKind). The union literal there is the canonical shape; this doc mirrors it for human readers. - Extractors that emit each kind:
src/extraction/vba/calls.ts— emitscallsandqualified-callfor the paren forms, and classifies the statement form viadetectStatementCallsrc/extraction/vba/call-sweep.ts— emits the statement-form split (callswhen theCallkeyword or an argument list makes the shape unambiguous, otherwiseunqualified-ident) andmember-withsrc/extraction/vba/controls.ts— emitsproperty-get/property-set(line 106) and the legacyreferences(line 131)src/extraction/vba/context.ts,src/extraction/vba/dims.ts,src/extraction/vba/rules.ts— the supporting state, type map, and rule table the emissions above consult; noreferenceKindliterals of their own.
- Runtime-object + stdlib allowlists —
src/resolution/vba-runtime-objects.ts(RUNTIME_OBJECTS,VBA_STDLIB_FUNCTIONS,DAO_ENUM_VALUES, and theisRuntimeObject/isVbaStdlibFunctionpredicates). These are whatdeclined-runtimekeys on. - Three
declined-runtimegates in the resolver —src/resolution/index.ts:1184,:1235,:1435. Each is thestatus: classifyVbaReferenceAsRuntime(r) ? 'declined-runtime' : 'failed'ternary at the point where unresolved refs are persisted tomarkReferencesFailed. - Sibling doc on
repointDecision—docs/vba-stub-repoint-decision.md. Thereponted-to-real/declined-runtime/declined-ambiguous/declined-not-foundtaxonomy is orthogonal toreference_kindbut shares thedeclined-runtimelabel — read both when debugging "why didn't this edge get repointed?" - Sibling doc on per-stage timing —
docs/vba-extraction-perf.md. The classifiers that emit the seven kinds each have a per-stage ms cost in theCODEGRAPH_VBA_TIMING=2instrumentation; thecallsAndSqlclassifier alone is ~32% ofACAuditoriaOperaciones.clstotal time. - Issue #265 — the statement-form
calls/unqualified-identsplit, and the source of the re-measured table above. - PRs that produced the bench numbers — #183 (DAO enum gate, v1.13.0), #184 (stdlib statement-form gate, v1.13.0), #185 (qualified-call DAO escape, v1.13.0), #186 (With-block runtime-object escape, v1.13.0), #188 (VBA intrinsic constants — closed by #196), #195 (statement-form stdlib, v1.13.0).