@aroman22/codegraph-vba-darwin-arm64 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/dist/db/queries.d.ts +2 -0
  2. package/lib/dist/db/queries.d.ts.map +1 -1
  3. package/lib/dist/db/queries.js +12 -0
  4. package/lib/dist/db/queries.js.map +1 -1
  5. package/lib/dist/extraction/vba/controls.d.ts.map +1 -1
  6. package/lib/dist/extraction/vba/controls.js +77 -0
  7. package/lib/dist/extraction/vba/controls.js.map +1 -1
  8. package/lib/dist/extraction/vba/events.d.ts +16 -0
  9. package/lib/dist/extraction/vba/events.d.ts.map +1 -0
  10. package/lib/dist/extraction/vba/events.js +90 -0
  11. package/lib/dist/extraction/vba/events.js.map +1 -0
  12. package/lib/dist/extraction/vba/text-utils.d.ts +0 -6
  13. package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -1
  14. package/lib/dist/extraction/vba/text-utils.js +9 -6
  15. package/lib/dist/extraction/vba/text-utils.js.map +1 -1
  16. package/lib/dist/extraction/vba-form-extractor.d.ts +26 -114
  17. package/lib/dist/extraction/vba-form-extractor.d.ts.map +1 -1
  18. package/lib/dist/extraction/vba-form-extractor.js +293 -297
  19. package/lib/dist/extraction/vba-form-extractor.js.map +1 -1
  20. package/lib/dist/index.d.ts.map +1 -1
  21. package/lib/dist/index.js +4 -0
  22. package/lib/dist/index.js.map +1 -1
  23. package/lib/dist/resolution/index.d.ts +7 -0
  24. package/lib/dist/resolution/index.d.ts.map +1 -1
  25. package/lib/dist/resolution/index.js +155 -11
  26. package/lib/dist/resolution/index.js.map +1 -1
  27. package/lib/dist/resolution/name-matcher.d.ts +6 -0
  28. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  29. package/lib/dist/resolution/name-matcher.js +81 -0
  30. package/lib/dist/resolution/name-matcher.js.map +1 -1
  31. package/lib/dist/resolution/vba-access-object-name.d.ts +7 -0
  32. package/lib/dist/resolution/vba-access-object-name.d.ts.map +1 -0
  33. package/lib/dist/resolution/vba-access-object-name.js +15 -0
  34. package/lib/dist/resolution/vba-access-object-name.js.map +1 -0
  35. package/lib/node_modules/.modules.yaml +1 -1
  36. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  37. package/lib/package.json +1 -1
  38. package/package.json +1 -1
@@ -44,12 +44,10 @@ exports.VbaFormExtractor = void 0;
44
44
  * not here. Dysflow overwrites `.form.txt`'s embedded `CodeBehindForm`
45
45
  * block on the next import, so any code emitted from this file would be
46
46
  * wrong AND ephemeral.
47
- * - It emits one `form-layout` node per file (named per `Attribute VB_Name`
48
- * when present, otherwise the file basename). The `form-layout` kind
49
- * (B2 hueco 4) replaces what was historically emitted as `kind: 'module'`
50
- * so consumers can dispatch on a UI-specific kind and avoid confusing
51
- * form/report UI files with `.bas` modules. `module` remains the kind
52
- * for `.bas` standard modules emitted by `VbaExtractor`.
47
+ * - It emits one layout node per file (named per `Attribute VB_Name` when
48
+ * present, otherwise the file basename): `form-layout` for forms and
49
+ * `report-layout` for reports. `module` remains the kind for `.bas`
50
+ * standard modules emitted by `VbaExtractor`.
53
51
  * - It emits one `property` node per Access control declaration with
54
52
  * `metadata.controlType` set to the control type (e.g. `'TextBox'`,
55
53
  * `'CommandButton'`).
@@ -77,6 +75,7 @@ exports.VbaFormExtractor = void 0;
77
75
  const path = __importStar(require("path"));
78
76
  const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
79
77
  const vba_preprocess_1 = require("./vba-preprocess");
78
+ const events_1 = require("./vba/events");
80
79
  class VbaFormExtractor {
81
80
  filePath;
82
81
  source;
@@ -137,15 +136,9 @@ class VbaFormExtractor {
137
136
  language: 'vba',
138
137
  metadata: { synthesizedBy: 'vba-form-binding' },
139
138
  });
140
- // Control declarations property nodes (REQ-FORM-2).
141
- this.sweepControls(cleaned);
142
- // Issue #49: RecordSource/RowSource bindings → references edges to
143
- // placeholder class nodes (one per table/query). Swept AFTER
144
- // sweepControls so the `form-instance-control` nodes for any
145
- // enclosing controls are already in `this.nodes` and
146
- // `sweepRowSources` can attribute each edge to its source node.
147
- this.sweepRecordSources(cleaned, formLayoutNode.id);
148
- this.sweepRowSources(cleaned, formLayoutNode.id);
139
+ // A SaveAsText document is a recursive Begin/End block tree. Walk it
140
+ // once so names, bindings, and section membership share one scope model.
141
+ this.walkBlocks(cleaned, formLayoutNode.id);
149
142
  }
150
143
  catch (error) {
151
144
  this.errors.push({
@@ -216,21 +209,23 @@ class VbaFormExtractor {
216
209
  }
217
210
  /**
218
211
  * Build the per-file node for a `.form.txt` / `.report.txt` source.
219
- * Emits `kind: 'form-layout'` (B2 hueco 4), replacing the historical
220
- * `kind: 'module'` so consumers can dispatch on a UI-specific kind.
212
+ * Emits `form-layout` for forms and `report-layout` for reports, replacing
213
+ * the historical `module` kind with a UI-specific layout kind.
221
214
  *
222
- * The deterministic id formula `generateNodeId(filePath, 'form-layout',
223
- * name, 1)` is preserved so cross-extractor stubs (e.g. event-handler
224
- * synthesis on the `.cls` side) can produce a matching id when needed
225
- * though the immediate use case is the `module` `form-layout`
226
- * rename. `metadata.containerKind` keeps the historical `'module'`
227
- * label as a back-compat marker; consumers should prefer `node.kind`.
215
+ * The deterministic id formula remains
216
+ * `generateNodeId(filePath, layoutKind, name, 1)`. Report ids intentionally
217
+ * use `report-layout` so real reports agree with DoCmd.OpenReport stubs.
218
+ * `metadata.containerKind` keeps the historical `module` label as a
219
+ * back-compat marker; consumers should prefer `node.kind`.
228
220
  */
229
221
  createFormLayoutNode(name) {
230
222
  const lines = this.source.split('\n');
223
+ const kind = /\.report\.txt$/i.test(this.filePath)
224
+ ? 'report-layout'
225
+ : 'form-layout';
231
226
  return {
232
- id: (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'form-layout', name, 1),
233
- kind: 'form-layout',
227
+ id: (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, kind, name, 1),
228
+ kind,
234
229
  name,
235
230
  qualifiedName: name,
236
231
  filePath: this.filePath,
@@ -252,166 +247,271 @@ class VbaFormExtractor {
252
247
  * Name = "txtFoo"
253
248
  * End
254
249
  *
255
- * We do not try to balance `Begin`/`End` blocks each `Begin <Type>` on
256
- * its own line is a control. Form/report files are shallow enough that
257
- * the extra `Begin`/`End` for the form's root block are matched but
258
- * filtered by the control-type blacklist (see below).
250
+ * The block walker balances every `Begin`/`End` pair, including untyped
251
+ * root blocks and non-control Form/Report/Section containers.
259
252
  */
260
- static BEGIN_RE = /^\s*Begin\s+(\p{L}[\p{L}\p{N}_]*)\s*$/u;
261
- /**
262
- * `Name = "..."` attribute line — emits the Access control instance name
263
- * (e.g. `lblTitulo`, `ComandoAltaPM`). Capture group 1 is the name.
264
- * The Dysflow SaveAsText format always wraps the value in double quotes
265
- * even when the name is a simple identifier — so we anchor on `"…"`
266
- * without trying to handle unquoted forms.
267
- */
268
- static NAME_RE = /^\s*Name\s*=\s*"([^"]+)"\s*$/u;
269
- /**
270
- * Control type tokens that are NOT user-visible Access controls and must be
271
- * filtered out so they don't appear as `property` nodes.
272
- *
273
- * - `Form` — the form's own root `Begin Form` / `End` container.
274
- * - `Section` — Access section containers (Header / Detail / Footer).
275
- *
276
- * `Rectangle` and `Image` are real Access controls and must NOT appear here.
277
- */
278
- static NON_CONTROL_TYPES = new Set([
279
- 'Form',
280
- 'Section',
281
- ]);
282
- /**
283
- * Maximum scan window for the `Name = "..."` attribute after a
284
- * `Begin <Type>` line. Real Dysflow exports have at most a handful of
285
- * whitespace-only lines and the `Name` line within the first 3–6 lines
286
- * of the block. 16 is a generous bound; if `Name` is missing within
287
- * that window, the control is treated as a nameless container and only
288
- * the legacy `property` node is emitted (preserves REQ-FORM-2 for
289
- * pre-Name `.form.txt` files exported by older Dysflow versions).
290
- */
291
- static NAME_SCAN_WINDOW = 16;
292
- sweepControls(src) {
253
+ static BEGIN_RE = /^\s*Begin(?:\s+(.+?))?\s*$/i;
254
+ /** Access also serializes binary/GUID values as `Property = Begin ... End`. */
255
+ static PROPERTY_BLOCK_BEGIN_RE = /^\s*\p{L}[\p{L}\p{N}_]*\s*=\s*Begin\s*$/iu;
256
+ /** Quoted SaveAsText property captured inside the current block frame. */
257
+ static QUOTED_PROPERTY_RE = /^\s*(\p{L}[\p{L}\p{N}_]*)\s*=\s*"((?:[^"]|"")*)"/u;
258
+ walkBlocks(src, formLayoutNodeId) {
293
259
  const lines = src.split('\n');
260
+ const stack = [];
261
+ let recordSource;
294
262
  for (let i = 0; i < lines.length; i++) {
295
263
  const line = lines[i] ?? '';
296
- const m = VbaFormExtractor.BEGIN_RE.exec(line);
297
- if (!m)
264
+ const lineNum = i + 1;
265
+ const begin = VbaFormExtractor.BEGIN_RE.exec(line);
266
+ if (begin) {
267
+ const rawType = (begin[1] ?? '').trim();
268
+ const controlType = /^\p{L}[\p{L}\p{N}_]*$/u.test(rawType)
269
+ ? rawType
270
+ : '';
271
+ stack.push({
272
+ controlType,
273
+ beginLine: lineNum,
274
+ beginColumnLength: line.length,
275
+ name: '',
276
+ nameLine: lineNum,
277
+ section: this.enclosingSection(stack),
278
+ properties: new Map(),
279
+ });
298
280
  continue;
299
- const controlType = m[1] ?? '';
300
- if (!controlType)
281
+ }
282
+ if (VbaFormExtractor.PROPERTY_BLOCK_BEGIN_RE.test(line)) {
283
+ stack.push({
284
+ controlType: '',
285
+ beginLine: lineNum,
286
+ beginColumnLength: line.length,
287
+ name: '',
288
+ nameLine: lineNum,
289
+ section: this.enclosingSection(stack),
290
+ properties: new Map(),
291
+ });
301
292
  continue;
302
- if (VbaFormExtractor.NON_CONTROL_TYPES.has(controlType))
293
+ }
294
+ if (/^\s*End\s*$/i.test(line)) {
295
+ const frame = stack.pop();
296
+ if (frame)
297
+ this.emitBlock(frame, formLayoutNodeId, recordSource);
303
298
  continue;
304
- // Skip lines whose captured token is the GUID-prefix form of the root
305
- // Begin (some Dysflow exports write `Begin {XXXXXXXX-XXXX-...}` with
306
- // a CLSID, not a control type). The GUID pattern won't match
307
- // [A-Za-z_]\w* — it starts with `{`, so the regex naturally rejects
308
- // it.
309
- const lineNum = i + 1;
310
- // ---- Legacy `property` node (REQ-FORM-2, unchanged). ---------------
311
- // This node's `name` is the control TYPE (e.g. "CommandButton"). Kept
312
- // intact for the 11 existing extraction-vba-form.test.ts tests and
313
- // for the 4 realfixture tests that assert on property-kind counts.
314
- const nodeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'property', controlType, lineNum);
315
- this.nodes.push({
316
- id: nodeId,
317
- kind: 'property',
318
- name: controlType,
319
- qualifiedName: `${this.filePath}::${controlType}`,
320
- filePath: this.filePath,
321
- language: 'vba',
322
- startLine: lineNum,
323
- endLine: lineNum,
324
- startColumn: 0,
325
- endColumn: line.length,
326
- metadata: { controlType },
327
- updatedAt: Date.now(),
328
- });
329
- // ---- Hueco 2: emit a `form-instance-control` node per NAME. -------
330
- // Scan ahead up to NAME_SCAN_WINDOW lines for the first
331
- // `Name = "..."` attribute. The control's `Name` (e.g. "lblTitulo",
332
- // "ComandoAltaPM") is what the .cls side references via
333
- // `Me.<ControlName>` (hueco 1) and what event handlers are wired to
334
- // via the `<ControlName>_<Event>` naming convention (hueco 3).
335
- // line=0 in the generated id keeps the id STABLE across re-indexes
336
- // of the same control — the VbaExtractor side synthesizes the
337
- // matching event-handler edge using the same id formula (see
338
- // vba-extractor.ts: synthesizeEventHandlerEdge).
339
- const { name: controlName, nameLine } = this.findControlName(lines, i, lineNum);
340
- if (!controlName)
299
+ }
300
+ const property = VbaFormExtractor.QUOTED_PROPERTY_RE.exec(line);
301
+ if (!property)
341
302
  continue;
342
- const controlNodeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'form-instance-control', controlName, 0);
343
- this.nodes.push({
344
- id: controlNodeId,
345
- kind: 'form-instance-control',
346
- name: controlName,
347
- qualifiedName: `${this.filePath}::${controlName}`,
348
- filePath: this.filePath,
349
- language: 'vba',
350
- startLine: lineNum,
351
- endLine: nameLine, // spans from Begin to the Name attribute line
352
- startColumn: 0,
353
- endColumn: 0,
354
- metadata: { controlType },
355
- updatedAt: Date.now(),
356
- });
303
+ const key = (property[1] ?? '').toLowerCase();
304
+ const value = (property[2] ?? '').replace(/""/g, '"');
305
+ const frame = stack[stack.length - 1];
306
+ if (!frame) {
307
+ if (key === 'recordsource') {
308
+ this.emitBinding(formLayoutNodeId, value, lineNum, 'vba-record-source');
309
+ }
310
+ else if (key === 'rowsource') {
311
+ this.emitBinding(formLayoutNodeId, value, lineNum, 'vba-row-source');
312
+ }
313
+ this.emitExpressionHandler(formLayoutNodeId, key, value, lineNum);
314
+ continue;
315
+ }
316
+ frame.properties.set(key, { value, line: lineNum });
317
+ if (key === 'name') {
318
+ frame.name = value;
319
+ frame.nameLine = lineNum;
320
+ }
321
+ // RecordSource is a layout-level binding even though SaveAsText places
322
+ // it inside the root Form/Report block.
323
+ if (key === 'recordsource') {
324
+ recordSource = value;
325
+ this.emitBinding(formLayoutNodeId, value, lineNum, 'vba-record-source');
326
+ }
327
+ }
328
+ // Malformed/truncated exports still yield the facts from every complete
329
+ // frame accumulated so far, matching the previous tolerant extractor.
330
+ while (stack.length > 0) {
331
+ this.emitBlock(stack.pop(), formLayoutNodeId, recordSource);
357
332
  }
358
333
  }
359
- /**
360
- * Scan ahead from a `Begin <Type>` line for the first `Name = "…"`
361
- * attribute. Returns the captured name and the line number where it
362
- * was found, or `{ name: '', nameLine: 0 }` when no Name is present
363
- * within the scan window (e.g. the `Begin Form` root block which has
364
- * `Caption = "..."` but no `Name`, or pre-Name legacy exports).
365
- *
366
- * Stops at the next `Begin` or `End` boundary so a misaligned scan
367
- * never crosses into a sibling control's attribute block.
368
- */
369
- findControlName(lines, beginLineIndex, beginLineNum) {
370
- const end = Math.min(lines.length, beginLineIndex + 1 + VbaFormExtractor.NAME_SCAN_WINDOW);
371
- for (let j = beginLineIndex + 1; j < end; j++) {
372
- const line = lines[j] ?? '';
373
- // Boundary check: a sibling Begin or End closes this block. Don't
374
- // look past it (a missing Name line is the common case for the
375
- // root `Begin Form` block — its `Caption` is the visible label,
376
- // not a `Name`).
377
- if (/^\s*(Begin|End)\b/i.test(line))
334
+ enclosingSection(stack) {
335
+ for (let i = stack.length - 1; i >= 0; i--) {
336
+ const frame = stack[i];
337
+ if (frame?.controlType.toLowerCase() === 'section' && frame.name) {
338
+ return frame.name;
339
+ }
340
+ }
341
+ return undefined;
342
+ }
343
+ emitBlock(frame, formLayoutNodeId, recordSource) {
344
+ const { controlType, beginLine: lineNum } = frame;
345
+ if (!controlType) {
346
+ const rowSource = frame.properties.get('rowsource');
347
+ if (rowSource) {
348
+ this.emitBinding(formLayoutNodeId, rowSource.value, rowSource.line, 'vba-row-source');
349
+ }
350
+ this.emitExpressionHandlers(formLayoutNodeId, frame.properties);
351
+ return;
352
+ }
353
+ if (controlType.toLowerCase() === 'section')
354
+ return;
355
+ if (/^(form|report)$/i.test(controlType)) {
356
+ const rowSource = frame.properties.get('rowsource');
357
+ if (rowSource) {
358
+ this.emitBinding(formLayoutNodeId, rowSource.value, rowSource.line, 'vba-row-source');
359
+ }
360
+ this.emitExpressionHandlers(formLayoutNodeId, frame.properties);
361
+ return;
362
+ }
363
+ // ---- Legacy `property` node (REQ-FORM-2, unchanged). ---------------
364
+ // This node's `name` is the control TYPE (e.g. "CommandButton"). Kept
365
+ // intact for the 11 existing extraction-vba-form.test.ts tests and
366
+ // for the 4 realfixture tests that assert on property-kind counts.
367
+ const nodeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'property', controlType, lineNum);
368
+ this.nodes.push({
369
+ id: nodeId,
370
+ kind: 'property',
371
+ name: controlType,
372
+ qualifiedName: `${this.filePath}::${controlType}`,
373
+ filePath: this.filePath,
374
+ language: 'vba',
375
+ startLine: lineNum,
376
+ endLine: lineNum,
377
+ startColumn: 0,
378
+ endColumn: frame.beginColumnLength,
379
+ metadata: { controlType },
380
+ updatedAt: Date.now(),
381
+ });
382
+ // ---- Hueco 2: emit a `form-instance-control` node per NAME. -------
383
+ // The block-scoped walk records the Name property at any distance.
384
+ // The control's `Name` (e.g. "lblTitulo",
385
+ // "ComandoAltaPM") is what the .cls side references via
386
+ // `Me.<ControlName>` (hueco 1) and what event handlers are wired to
387
+ // via the `<ControlName>_<Event>` naming convention (hueco 3).
388
+ // line=0 in the generated id keeps the id STABLE across re-indexes
389
+ // of the same control — the VbaExtractor side synthesizes the
390
+ // matching event-handler edge using the same id formula (see
391
+ // vba-extractor.ts: synthesizeEventHandlerEdge).
392
+ const controlName = frame.name;
393
+ if (!controlName)
394
+ return;
395
+ const controlNodeId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'form-instance-control', controlName, 0);
396
+ const controlSource = frame.properties.get('controlsource');
397
+ const sourceObject = frame.properties.get('sourceobject');
398
+ this.nodes.push({
399
+ id: controlNodeId,
400
+ kind: 'form-instance-control',
401
+ name: controlName,
402
+ qualifiedName: `${this.filePath}::${controlName}`,
403
+ filePath: this.filePath,
404
+ language: 'vba',
405
+ startLine: lineNum,
406
+ endLine: frame.nameLine, // spans from Begin to the Name attribute line
407
+ startColumn: 0,
408
+ endColumn: 0,
409
+ metadata: {
410
+ controlType,
411
+ ...(frame.section ? { section: frame.section } : {}),
412
+ ...(controlSource ? { controlSource: controlSource.value } : {}),
413
+ ...(sourceObject ? { sourceObject: sourceObject.value } : {}),
414
+ },
415
+ updatedAt: Date.now(),
416
+ });
417
+ this.edges.push({
418
+ source: formLayoutNodeId,
419
+ target: controlNodeId,
420
+ kind: 'contains',
421
+ provenance: 'parser',
422
+ });
423
+ this.emitExpressionHandlers(controlNodeId, frame.properties);
424
+ if (controlSource) {
425
+ this.emitControlSourceReference(controlNodeId, controlSource.value, controlSource.line, recordSource);
426
+ }
427
+ if (sourceObject) {
428
+ this.emitSourceObjectReference(controlNodeId, sourceObject.value, sourceObject.line);
429
+ }
430
+ const rowSource = frame.properties.get('rowsource');
431
+ const rowSourceType = frame.properties.get('rowsourcetype');
432
+ if (rowSource &&
433
+ rowSourceType?.value.toLowerCase() !== 'value list') {
434
+ this.emitBinding(controlNodeId, rowSource.value, rowSource.line, 'vba-row-source');
435
+ }
436
+ }
437
+ emitExpressionHandlers(wiringSiteNodeId, properties) {
438
+ for (const [propertyName, property] of properties) {
439
+ this.emitExpressionHandler(wiringSiteNodeId, propertyName, property.value, property.line);
440
+ }
441
+ }
442
+ emitExpressionHandler(wiringSiteNodeId, propertyName, rawValue, lineNum) {
443
+ const eventName = events_1.ACCESS_EVENT_PROPERTIES.get(propertyName.toLowerCase());
444
+ if (!eventName)
445
+ return;
446
+ // `[Event Procedure]` is handled by the existing code-behind naming path.
447
+ // Bare values name Access macros, which are not graph nodes; silent beats
448
+ // inventing a function edge for either form.
449
+ const expression = rawValue.trim();
450
+ const match = /^=\s*([\p{L}_][\p{L}\p{N}_]*)\s*\(/u.exec(expression);
451
+ if (!match)
452
+ return;
453
+ let depth = 0;
454
+ let quoted = false;
455
+ let completeAt = -1;
456
+ for (let i = expression.indexOf('(', match.index); i < expression.length; i++) {
457
+ const char = expression[i];
458
+ if (char === '"')
459
+ quoted = !quoted;
460
+ if (quoted)
461
+ continue;
462
+ if (char === '(')
463
+ depth++;
464
+ if (char === ')' && --depth === 0) {
465
+ completeAt = i;
378
466
  break;
379
- const m = VbaFormExtractor.NAME_RE.exec(line);
380
- if (m) {
381
- const name = m[1] ?? '';
382
- if (name) {
383
- return { name, nameLine: j + 1 };
384
- }
385
467
  }
386
468
  }
387
- // No Name within the window that's the case for the `Begin Form`
388
- // root block (which has `Caption`, not `Name`) and for the
389
- // `Begin Section` Access section blocks (which group controls but
390
- // carry no Name of their own). The legacy `property` node was
391
- // already emitted above; we simply skip the form-instance-control
392
- // emission so hueco-4 stays RED for the .form.txt module node
393
- // transition (a separate B2 task).
394
- return { name: '', nameLine: beginLineNum };
469
+ if (completeAt < 0 || expression.slice(completeAt + 1).trim() !== '')
470
+ return;
471
+ this.unresolvedReferences.push({
472
+ fromNodeId: wiringSiteNodeId,
473
+ referenceName: match[1],
474
+ referenceKind: 'event-handler',
475
+ line: lineNum,
476
+ column: 0,
477
+ filePath: this.filePath,
478
+ language: 'vba',
479
+ metadata: {
480
+ eventName,
481
+ synthesizedBy: 'vba-expression-handler',
482
+ },
483
+ });
484
+ }
485
+ emitSourceObjectReference(controlNodeId, rawValue, lineNum) {
486
+ const match = /^(?:(Form|Report|Table|Query)\.)?(.*)$/i.exec(rawValue.trim());
487
+ if (!match)
488
+ return;
489
+ const prefix = match[1]?.toLowerCase();
490
+ const target = match[2]?.trim() ?? '';
491
+ if (!target)
492
+ return;
493
+ if (prefix === 'table' || prefix === 'query') {
494
+ this.emitTableReference(controlNodeId, target, lineNum, 'vba-source-object', { sourceObjectType: prefix });
495
+ return;
496
+ }
497
+ this.unresolvedReferences.push({
498
+ fromNodeId: controlNodeId,
499
+ referenceName: target,
500
+ referenceKind: 'references',
501
+ line: lineNum,
502
+ column: 0,
503
+ filePath: this.filePath,
504
+ language: 'vba',
505
+ metadata: {
506
+ synthesizedBy: 'vba-source-object',
507
+ embeds: true,
508
+ accessObjectKind: prefix === 'report' ? 'report' : 'form',
509
+ },
510
+ });
395
511
  }
396
512
  // ---------------------------------------------------------------------------
397
513
  // RecordSource / RowSource edge emission (Issue #49)
398
514
  // ---------------------------------------------------------------------------
399
- /**
400
- * Issue #49 — `RecordSource = "..."` line regex. The Dysflow SaveAsText
401
- * format always wraps the value in double quotes; the inner `(?:[^"]|"")*`
402
- * body tolerates the doubled-quote escape (`""` → `"`) so the captured
403
- * group carries the literal text exactly as Access stores it.
404
- *
405
- * Anchored at the start of the line — properties in the SaveAsText
406
- * format are always indented but the attribute name is unambiguous, so
407
- * a leading whitespace-tolerant anchor is enough.
408
- */
409
- static RECORD_SOURCE_RE = /^\s*RecordSource\s*=\s*"((?:[^"]|"")*)"/iu;
410
- /** Issue #49 — `RowSource = "..."` line regex. Mirrors `RECORD_SOURCE_RE`. */
411
- static ROW_SOURCE_RE = /^\s*RowSource\s*=\s*"((?:[^"]|"")*)"/iu;
412
- /** Issue #49 — `RowSourceType = "..."` line regex. Used by the control-block
413
- * scan to detect value-list controls whose RowSource is a literal list. */
414
- static ROW_SOURCE_TYPE_RE = /^\s*RowSourceType\s*=\s*"([^"]*)"/iu;
415
515
  /**
416
516
  * Issue #49 — copy of `VbaExtractor.SQL_TABLE_RE`. Same source / flags:
417
517
  * captures the table name that follows `FROM`/`JOIN`/`INTO`/`UPDATE`,
@@ -453,7 +553,7 @@ class VbaFormExtractor {
453
553
  * (a real `query` node takes precedence when both exist for the same
454
554
  * name — same dual-match `vba-sql-impact`'s `extractFormBindings` does).
455
555
  */
456
- emitTableReference(sourceNodeId, tableName, lineNum, synthesizedBy) {
556
+ emitTableReference(sourceNodeId, tableName, lineNum, synthesizedBy, extraMetadata = {}) {
457
557
  if (!tableName)
458
558
  return;
459
559
  const targetId = (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'class', tableName, 0);
@@ -483,11 +583,34 @@ class VbaFormExtractor {
483
583
  // the metadata.access field uniformly present across every SQL-derived
484
584
  // table reference (the in-code SQL sweep classifies read vs write from
485
585
  // the statement verb; a binding is always a read).
486
- metadata: { synthesizedBy, access: 'read' },
586
+ metadata: { synthesizedBy, access: 'read', ...extraMetadata },
487
587
  line: lineNum,
488
588
  column: 0,
489
589
  });
490
590
  }
591
+ /**
592
+ * Link a bound control to its enclosing form/report's single bare table.
593
+ * Expressions and SQL/absent RecordSource values stay metadata-only: column
594
+ * lineage through expressions or SELECT projections cannot be inferred here
595
+ * without risking false graph edges.
596
+ */
597
+ emitControlSourceReference(controlNodeId, controlSource, lineNum, recordSource) {
598
+ const rawField = controlSource.trim();
599
+ if (!rawField || rawField.startsWith('=') || !recordSource)
600
+ return;
601
+ const bracketedField = /^\[([^\]]+)\]$/.exec(rawField);
602
+ const field = bracketedField?.[1] ?? rawField;
603
+ if (!bracketedField && !/^\p{L}[\p{L}\p{N}_]*$/u.test(rawField))
604
+ return;
605
+ const source = recordSource.trim();
606
+ if (!source || this.isLikelySql(source))
607
+ return;
608
+ const bracketedSource = /^\[([^\]]+)\]$/.exec(source);
609
+ const tableName = bracketedSource?.[1] ?? source;
610
+ if (!bracketedSource && !/^\p{L}[\p{L}\p{N}_]*$/u.test(source))
611
+ return;
612
+ this.emitTableReference(controlNodeId, tableName, lineNum, 'vba-control-source', { column: field });
613
+ }
491
614
  /**
492
615
  * Issue #49 — dispatch the value of a RecordSource/RowSource binding.
493
616
  * If SQL, run `SQL_TABLE_RE` over the value and emit one edge per
@@ -527,133 +650,6 @@ class VbaFormExtractor {
527
650
  return;
528
651
  this.emitTableReference(sourceNodeId, name, lineNum, synthesizedBy);
529
652
  }
530
- /**
531
- * Issue #49 — sweep for the form-level `RecordSource` line. RecordSource
532
- * always attributes to the form-layout node (even if the line is
533
- * written inside a control's Begin block — defensive: in real
534
- * SaveAsText exports the form's RecordSource sits at the root, but
535
- * the agent's spec says "emit from the form-layout node" regardless).
536
- *
537
- * The sweep intentionally matches ONLY `RecordSource` here;
538
- * `RowSource` is handled by `sweepRowSources` so the per-binding
539
- * attribution logic (control vs. form-layout) stays in one place.
540
- */
541
- sweepRecordSources(src, formLayoutNodeId) {
542
- const lines = src.split('\n');
543
- for (let i = 0; i < lines.length; i++) {
544
- const line = lines[i] ?? '';
545
- const m = VbaFormExtractor.RECORD_SOURCE_RE.exec(line);
546
- if (!m)
547
- continue;
548
- const rawValue = m[1] ?? '';
549
- const lineNum = i + 1;
550
- this.emitBinding(formLayoutNodeId, rawValue, lineNum, 'vba-record-source');
551
- }
552
- }
553
- /**
554
- * Issue #49 — sweep for per-control `RowSource` lines. RowSource
555
- * attributes to the enclosing control's `form-instance-control` node
556
- * when one is in scope; if the line is found outside any control
557
- * Begin block (defensive — unusual in real exports), it falls back to
558
- * the form-layout node. The tag is always `'vba-row-source'` so
559
- * consumers can distinguish a control-level data binding from the
560
- * form-level RecordSource even when both happen to share the same
561
- * source node.
562
- *
563
- * Control-block tracking: a stack of `{ controlName, rowSourceType }`
564
- * entries mirrors the current scope as the sweep walks lines in order.
565
- * `Begin <Type>` pushes; `End` pops. `Begin Form` / `Begin Section`
566
- * push a non-control entry so a RowSource written at the form root
567
- * correctly falls through to the form-layout fallback.
568
- *
569
- * Value-list skip: when the CURRENT TOP scope's `RowSourceType` is
570
- * `"Value List"` (captured by the same scan-window as the control's
571
- * `Name`), the control's RowSource is a literal list and we skip the
572
- * emission — the data is in code, not a table.
573
- */
574
- sweepRowSources(src, formLayoutNodeId) {
575
- const lines = src.split('\n');
576
- const stack = [];
577
- for (let i = 0; i < lines.length; i++) {
578
- const line = lines[i] ?? '';
579
- const lineNum = i + 1;
580
- const beginM = VbaFormExtractor.BEGIN_RE.exec(line);
581
- if (beginM) {
582
- const controlType = beginM[1] ?? '';
583
- if (VbaFormExtractor.NON_CONTROL_TYPES.has(controlType)) {
584
- stack.push({ controlName: '', rowSourceType: '' });
585
- }
586
- else {
587
- // Reuse findControlName so the control-name attribution stays
588
- // in lockstep with the form-instance-control node id we
589
- // already produced in sweepControls. Issue #41's event-handler
590
- // edges and Issue #49's references edges then point at the
591
- // same id consistently.
592
- const { name: controlName } = this.findControlName(lines, i, lineNum);
593
- const rowSourceType = this.findRowSourceType(lines, i);
594
- stack.push({ controlName, rowSourceType });
595
- }
596
- continue;
597
- }
598
- if (/^\s*End\s*$/i.test(line)) {
599
- if (stack.length > 0)
600
- stack.pop();
601
- continue;
602
- }
603
- const rowM = VbaFormExtractor.ROW_SOURCE_RE.exec(line);
604
- if (!rowM)
605
- continue;
606
- // Find the nearest enclosing control scope. We don't just look at
607
- // the stack top because a row could conceivably be written inside
608
- // a `Begin Section` (controlName='' entry on top) — in that case
609
- // the section entry hides the actual control below it. Walk the
610
- // stack from top to bottom and pick the first controlName we see.
611
- let currentControl = '';
612
- for (let j = stack.length - 1; j >= 0; j--) {
613
- if (stack[j]?.controlName) {
614
- currentControl = stack[j].controlName;
615
- break;
616
- }
617
- }
618
- // Skip value-list controls. The check is on the TOP scope's
619
- // rowSourceType, which is the most-recent control's type — even
620
- // if a nested Begin/End hid it from the stack-walk above, the
621
- // RowSource line we're matching was written inside that scope so
622
- // the top of stack carries the right value.
623
- const top = stack[stack.length - 1];
624
- if (top && top.rowSourceType === 'Value List')
625
- continue;
626
- const rawValue = rowM[1] ?? '';
627
- const sourceId = currentControl
628
- ? (0, tree_sitter_helpers_1.generateNodeId)(this.filePath, 'form-instance-control', currentControl, 0)
629
- : formLayoutNodeId;
630
- this.emitBinding(sourceId, rawValue, lineNum, 'vba-row-source');
631
- }
632
- }
633
- /**
634
- * Issue #49 — companion helper to `findControlName`: scan ahead from a
635
- * `Begin <Type>` line for the first `RowSourceType = "..."` attribute
636
- * within the same `NAME_SCAN_WINDOW` (so a missing attribute gracefully
637
- * no-ops instead of mis-attributing). Returns the captured value (e.g.
638
- * `"Value List"`, `"Table/Query"`) or `''` when absent.
639
- *
640
- * Stops at the next `Begin` or `End` boundary — same scan-window
641
- * discipline as `findControlName`, so a misaligned scan never crosses
642
- * into a sibling control's attribute block.
643
- */
644
- findRowSourceType(lines, beginLineIndex) {
645
- const end = Math.min(lines.length, beginLineIndex + 1 + VbaFormExtractor.NAME_SCAN_WINDOW);
646
- for (let j = beginLineIndex + 1; j < end; j++) {
647
- const line = lines[j] ?? '';
648
- if (/^\s*(Begin|End)\b/i.test(line))
649
- break;
650
- const m = VbaFormExtractor.ROW_SOURCE_TYPE_RE.exec(line);
651
- if (m) {
652
- return m[1] ?? '';
653
- }
654
- }
655
- return '';
656
- }
657
653
  }
658
654
  exports.VbaFormExtractor = VbaFormExtractor;
659
655
  //# sourceMappingURL=vba-form-extractor.js.map