@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.
- package/lib/dist/db/queries.d.ts +2 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +12 -0
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/extraction/vba/controls.d.ts.map +1 -1
- package/lib/dist/extraction/vba/controls.js +77 -0
- package/lib/dist/extraction/vba/controls.js.map +1 -1
- package/lib/dist/extraction/vba/events.d.ts +16 -0
- package/lib/dist/extraction/vba/events.d.ts.map +1 -0
- package/lib/dist/extraction/vba/events.js +90 -0
- package/lib/dist/extraction/vba/events.js.map +1 -0
- package/lib/dist/extraction/vba/text-utils.d.ts +0 -6
- package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -1
- package/lib/dist/extraction/vba/text-utils.js +9 -6
- package/lib/dist/extraction/vba/text-utils.js.map +1 -1
- package/lib/dist/extraction/vba-form-extractor.d.ts +26 -114
- package/lib/dist/extraction/vba-form-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vba-form-extractor.js +293 -297
- package/lib/dist/extraction/vba-form-extractor.js.map +1 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +4 -0
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +7 -0
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +155 -11
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +6 -0
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +81 -0
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/vba-access-object-name.d.ts +7 -0
- package/lib/dist/resolution/vba-access-object-name.d.ts.map +1 -0
- package/lib/dist/resolution/vba-access-object-name.js +15 -0
- package/lib/dist/resolution/vba-access-object-name.js.map +1 -0
- package/lib/node_modules/.modules.yaml +1 -1
- package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
- package/lib/package.json +1 -1
- 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
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
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 `
|
|
220
|
-
* `
|
|
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
|
|
223
|
-
* name, 1)
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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,
|
|
233
|
-
kind
|
|
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
|
-
*
|
|
256
|
-
*
|
|
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
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|