Skip to content

feat(vba-extractor): model DoCmd.OpenReport and DoCmd.OpenQuery like OpenForm (closes #48) - #69

Merged
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-48-docmd-open
Jul 4, 2026
Merged

feat(vba-extractor): model DoCmd.OpenReport and DoCmd.OpenQuery like OpenForm (closes #48)#69
ardelperal merged 1 commit into
mainfrom
chore/2026-07-04-issue-48-docmd-open

Conversation

@ardelperal

Copy link
Copy Markdown
Owner

Closes #48.

What

Hueco 6 (DoCmd.OpenForm "X" -> opens-form edge, hueco B4) was always labelled follow-up work for OpenReport and OpenQuery. This PR lands both. The OpenForm pipeline is preserved byte-identically (existing tests stay green without modification); OpenReport mirrors it through a small dispatch table; OpenQuery opts for an UnresolvedReference so the resolver binds to the REAL query node emitted by SqlQueryExtractor.

Why

In a real Dysflow-managed Access project, every DoCmd.OpenReport "InformeMensual" and DoCmd.OpenQuery "ConsultaDepuracion" line produced zero graph edges. Cross-report and cross-query UI traffic was invisible to the resolver, even though .report.txt and queries/*.sql are first-class files in the index. This PR closes that gap for OpenReport (synthetic report-layout stub + heuristic edge) and OpenQuery (UnresolvedReference resolving to the existing query node).

Diff

File Lines
src/extraction/vba-extractor.ts +264 / −69
__tests__/extraction-vba.test.ts +236 / −0
src/types.ts +12 / −0
Total +512 / −69

Inside the 400-line source-budget; the PR is reviewable as ONE unit (one logical refactor + two new emissions sharing one pipeline).

Design

OpenForm + OpenReport: shared pipeline via dispatch table

New DOCMD_OPEN_DISPATCH constant (a ReadonlyArray<...> of typed entries) carries everything scanDoCmdOpenCalls + emitOpensStubEdge need:

{
  method, re,
  edgeKind, stubKind,
  syntheticPrefix, syntheticExtension,
  moduleNamePrefix, cacheKey,
  metadataTargetKey, synthesizedBy,
}
  • OpenForm: edgeKind opens-form, stubKind form-layout, synthetic prefix synthetic:opensFormStub, extension .form.txt, qualifiedName prefix Form_, metadata key targetFormName, tag vba-opens-form.
  • OpenReport: edgeKind opens-report, stubKind report-layout, synthetic prefix synthetic:opensReportStub, extension .report.txt, qualifiedName prefix Report_, metadata key targetReportName, tag vba-opens-report.
  • OpenQuery is intentionally NOT in this dispatch — see below.

The scan/emit pipeline iterates the dispatch; OpenForm's emit is byte-identical to pre-#48 (same regex, same arg resolution, same stub id formula, same metadata key — pin tests stay green). The cache key is ${cacheKey}:${loweredName} so OpenForm and OpenReport de-dup buckets stay disjoint within a file. The synthetic path's file extension mirrors the real file extension per Issue #48 spec.

OpenQuery: UnresolvedReference pattern (NOT a stub + edge)

Following the vba-me-control and vba-forms-bang precedent. scanDoCmdOpenQuery is a separate scanner (not in the dispatch table) that pushes ONE UnresolvedReference per match:

  • referenceName = resolved query name
  • referenceKind = 'references'
  • metadata.synthesizedBy = 'vba-opens-query'
  • NO synthetic function or query node — SqlQueryExtractor already emits the real query node for queries/<Name>.sql, and the resolver binds the reference to it via name match.

Type system

NodeKind extended with 'report-layout'. EdgeKind extended with 'opens-report'. Distinct from form-layout / opens-form so downstream tooling can filter form vs report without inspecting qualifiedName.

Test coverage

9 new atoms in __tests__/extraction-vba.test.ts (Issue #48 describe block):

OpenReport (5 atoms):

  1. Literal "InformeMensual" → emits opens-report edge + report-layout stub
  2. Const-resolved REPORT_MENSUAL + unknown REPORT_UNKNOWN (mirrors OpenForm Const-fallback)
  3. Stub id deterministic across re-index (asserted against generateNodeId('synthetic:opensReportStub/<Name>.report.txt', 'report-layout', '<Name>', 0))
  4. W4 no-synthetic-function-node guard
  5. De-dup: two calls produce exactly ONE stub

OpenQuery (4 atoms):
6. Literal "Consulta1" → ONE UnresolvedReference, no synthetic nodes
7. Const-resolved CONSULTA_DEPURACION → UnresolvedReference with resolved name
8. Const-unknown CONSULTA_UNKNOWN → falls back to bare identifier
9. W4 no-synthetic-function-node guard for OpenQuery too

Regression guards (existing tests, all green without modification):

  • extraction-vba-control-modeling.test.ts hueco-6 test
  • extraction-vba.test.ts "DoCmd.OpenForm does not emit a synthetic function node" (W4)
  • extraction-vba.test.ts "resolves local string constants in DoCmd.OpenForm and falls back..." (Const-fallback)

Validation

  • pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #48" → 9/9 pass in 681 ms
  • Full VBA suite (6 files: extraction-vba, extraction-vba-control-modeling, extraction-vba-form, extraction-vba-enums-consts, extraction-vba-realfixtures, extraction-vba-roadmap-25-26) → 236/236 pass in 5.82 s — zero regressions in OpenForm or any other extractor path
  • pnpm run build → exit 0, no TS errors

Out of scope (intentional)

  • OpenTable and other DoCmd.Open* siblings. Same dispatch pattern is ready to absorb them later; not adding them now keeps the diff focused.
  • Arbitrary variable data-flow for OpenForm/OpenReport/OpenQuery arguments — bare identifiers resolve only through local Const declarations, consistent with the existing OpenForm scope.

Not done

n/a — issue complete in this PR.

…OpenForm (closes #48)

Hueco 6 (DoCmd.OpenForm "X" -> opens-form edge) was always labelled follow-up work
for OpenReport and OpenQuery. This lands both. The OpenForm pipeline is preserved
byte-identically (existing hueco-6 + Const-fallback + W4-guard tests stay green
without modification) while OpenReport mirrors it through a small dispatch table
and OpenQuery opts for an UnresolvedReference so the resolver binds to the REAL
query node emitted by SqlQueryExtractor.

Changes:

* `NodeKind` extended with `'report-layout'`; `EdgeKind` extended with
  `'opens-report'` (`src/types.ts`). Distinct from `form-layout` /
  `opens-form` so downstream tooling can filter form vs report without
  inspecting qualifiedName.

* `OPEN_FORM_ARG_RE` retained byte-identical. New `OPEN_REPORT_ARG_RE`
  mirrors it for `DoCmd.OpenReport "X"`; new `OPEN_QUERY_ARG_RE` covers
  `DoCmd.OpenQuery "X"` (Issue #48 spec acceptance).

* `DOCMD_OPEN_DISPATCH` table introduced (`{ method, re, edgeKind,
  stubKind, syntheticPrefix, syntheticExtension, moduleNamePrefix,
  cacheKey, metadataTargetKey, synthesizedBy }`). OpenForm and OpenReport
  share the entire scanner + emitter pipeline; only the per-method
  metadata differs. OpenQuery intentionally is NOT in this dispatch
  because it emits an UnresolvedReference, not a stub + edge.

* `scanOpenFormCalls` generalized into `scanDoCmdOpenCalls` — iterates
  the dispatch table; OpenForm behavior byte-identical (same regex,
  same arg resolution, same stub id formula).
* `emitOpensFormEdge` generalized into `emitOpensStubEdge` — parametric
  on the dispatch entry. Cache key is now `${cacheKey}:${loweredName}`
  so OpenForm and OpenReport de-dup buckets stay disjoint within a file.
  Stub synthetic path uses `${syntheticPrefix}/${Name}${syntheticExtension}`
  — `.form.txt` for OpenForm, `.report.txt` for OpenReport (mirrors the
  real file extension per Issue #48 spec).

* New `scanDoCmdOpenQuery` — pushes ONE UnresolvedReference per match
  (`referenceName` = resolved query name, `referenceKind` = 'references',
  `metadata.synthesizedBy` = 'vba-opens-query'). Falls back to bare
  identifier when a local Const isn't found. The reference resolves to
  the REAL `query` node SqlQueryExtractor emits for `queries/<Name>.sql`
  — no synthetic stub, no synthetic `query` node, no synthetic
  function node (W4 graph-pollution invariant preserved).

* Wiring updated in the line-scanner: `scanDoCmdOpenCalls` replaces the
  old `scanOpenFormCalls` call; `scanDoCmdOpenQuery` runs alongside it
  on the original unmasked line (consistent with OpenForm — the
  literal `"X"` form has the form/query name in a string literal that
  masking would destroy).

* 9 regression tests in `__tests__/extraction-vba.test.ts` (Issue #48
  describe block):
  - OpenReport literal + Const-resolved + Const-fallback (mirrors
    OpenForm's Const-fallback test at line ~2268)
  - OpenReport deterministic stub id across re-index (asserted against
    `generateNodeId('synthetic:opensReportStub/<Name>.report.txt', ...)`)
  - OpenReport W4 no-synthetic-fn guard
  - OpenReport de-dup: two calls produce exactly ONE stub
  - OpenQuery literal + Const-resolved + Const-fallback
  - OpenQuery W4 no-synthetic-fn guard

  Existing OpenForm tests (hueco-6 in
  `__tests__/extraction-vba-control-modeling.test.ts`; the
  "DoCmd.OpenForm does not emit a synthetic function node" and the
  "resolves local string constants in DoCmd.OpenForm..." tests in
  extraction-vba.test.ts) stay green without modification — the
  dispatch refactor preserves OpenForm emission exactly.

Disjointness note: OpenForm and OpenReport live in separate ID spaces
(different synthetic prefixes, different stub kinds, different
qualifiedName prefixes `Form_<Name>` vs `Report_<Name>`). The cache key
prefix `OpenForm:` vs `OpenReport:` further guarantees the two stubs in
the same file don't collide.

Validation:

* `pnpm exec vitest run __tests__/extraction-vba.test.ts -t "Issue #48"`
  -> 9/9 pass in 681 ms
* Full VBA suite (6 files): **236/236 pass** in 5.82 s — zero
  regressions in OpenForm or any other extractor path
* `pnpm run build` -> exit 0, no TS errors
@ardelperal ardelperal added the type:feature New feature label Jul 4, 2026
@ardelperal
ardelperal merged commit e461afb into main Jul 4, 2026
5 checks passed
@ardelperal
ardelperal deleted the chore/2026-07-04-issue-48-docmd-open branch July 4, 2026 09:18
ardelperal added a commit that referenced this pull request Jul 4, 2026
… 7 issues, update README banner + CLAUDE.md schema

Publishes the 7 VBA-extractor issues closed this session (#44, #48,
#49, #50, #51, #52, #53) plus the test-infra flake fix from
PR #72.

CHANGELOG:
- Populate the previously-empty `## [Unreleased]` section with 5
  New Features (bang operator, OpenReport/OpenQuery, RecordSource/
  RowSource, TempVars, encoding robustness) and 2 Fixes
  (conditional-compilation evaluator, procedure-local Const
  scoping). Each entry is plain-language, no internal paths, with
  PR numbers for traceability. Per the project's CLAUDE.md
  convention, the actual version section (`## [1.5.0] - <date>`)
  is auto-promoted by `scripts/prepare-release.mjs` when the
  release workflow runs — we do NOT pre-create the section.
- The `(#NN)` PR numbers after each bullet auto-link in the
  published release notes.

package.json:
- `"version": "1.4.0"` → `"1.5.0"`. This triggers the GitHub
  Actions "Sync package-lock.json" step (`npm install
  --package-lock-only --ignore-scripts`) which rewrites the lock
  on the version-bump commit.

README.md:
- Banner line 5: `## 🎉 1.2 Released — Access forms are first-class`
  → `## 🎉 1.5 Released — VBA conditional-compilation is now
  correct + TempVars/RecordSource/RowSource indexed`. The
  "Already installed? Run codegraph-vba upgrade to update in
  place" line below is unchanged.

CLAUDE.md:
- Append `report-layout` to the NodeKind list and `'opens-report'`
  to the EdgeKind list — both added in #48 (PR #69) but missing
  from the docs. Also append the related chain (form-layout,
  form-instance-control, event-handler, opens-form,
  raises-event, subscribes-event, type-member) so CLAUDE.md
  matches the source of truth at `src/types.ts` (the prior
  abbreviated list was already drifting from the schema).
  No other CLAUDE.md changes.

Not in this commit (out of scope per user brief):
- Test-infra PRs #70 / #72 are correctly absent from
  CHANGELOG (per user's "not in changelog body but mention in
  known-issues if applicable" — no known-issues section
  exists in this repo's CHANGELOG; vitest.testTimeout bump
  is test infrastructure, not a user-visible feature).
- Scriv fragment — this is the codegraph-vba repo, not the
  dysflow-managed one; Scriv doesn't apply.

Next step: trigger the Release workflow via gh workflow run
(or via the Actions → Release → Run workflow UI on the `main`
branch). The workflow will:
1. Sync `package-lock.json` to match `package.json` (auto-commit).
2. Run `scripts/prepare-release.mjs` to promote [Unreleased] →
   `## [1.5.0] - 2026-07-04` + append the link reference.
3. Build per-platform bundles, generate SHA256SUMS, create the
   GitHub Release, publish the npm packages (requires NPM_TOKEN
   repo secret).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(vba): model DoCmd.OpenReport and DoCmd.OpenQuery like OpenForm

1 participant