@filipebraida/adonis-function-points 0.5.0 → 0.7.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/CHANGELOG.md +170 -0
- package/README.md +101 -292
- package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DUbHiifm.js → fp_calibrate-3TxGdS1b.js} +1 -1
- package/build/{fp_count-ChtblhZV.js → fp_count-arGLnVlY.js} +1 -1
- package/build/{fp_diff-Dt7J4IWu.js → fp_diff-DBBvzq5x.js} +1 -1
- package/build/{fp_explain-DZJ--0-S.js → fp_explain-aNFApwiT.js} +1 -1
- package/build/{fp_inventory-CPtmuuke.js → fp_inventory-DIjKIC9t.js} +1 -1
- package/build/{fp_metrics-et8F1Wvt.js → fp_metrics-BpU61waG.js} +1 -1
- package/build/index.d.ts +8 -4
- package/build/index.js +4 -4
- package/build/{pipeline-CNTBhs6o.js → pipeline-DO2301fV.js} +2131 -389
- package/build/{resolvers-PJwo2Z8R.js → resolvers-DaU4uAqT.js} +603 -165
- package/build/{runners-DIt1G85i.js → runners-Dm7cWGa-.js} +6 -3
- package/build/src/albrecht/counter.d.ts +38 -5
- package/build/src/albrecht/data_functions.d.ts +49 -3
- package/build/src/albrecht/diff.d.ts +27 -0
- package/build/src/albrecht/index.d.ts +1 -0
- package/build/src/albrecht/opaque.d.ts +90 -0
- package/build/src/albrecht/technical_filter.d.ts +18 -11
- package/build/src/albrecht/transactional_functions.d.ts +7 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +55 -57
- package/build/src/inventory/graph/call_graph.d.ts +44 -0
- package/build/src/inventory/graph/deliveries.d.ts +88 -0
- package/build/src/inventory/graph/output_fields.d.ts +143 -0
- package/build/src/inventory/paths.d.ts +2 -0
- package/build/src/inventory/resolvers/index.d.ts +21 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
- package/build/src/inventory/resolvers/local_function.d.ts +24 -0
- package/build/src/inventory/resolvers/transformer.d.ts +0 -23
- package/build/src/inventory/sources/commands.d.ts +14 -0
- package/build/src/inventory/sources/jobs.d.ts +27 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +49 -1
- package/build/stubs/config.stub +29 -16
- package/package.json +1 -1
|
@@ -68,6 +68,12 @@ const SCAFFOLDING = new Set([
|
|
|
68
68
|
"migrations",
|
|
69
69
|
"factories"
|
|
70
70
|
]);
|
|
71
|
+
/** a seeder, by the directory `make:seeder` writes to — scaffolding, but a fact the report uses */
|
|
72
|
+
function isSeeder(root, file) {
|
|
73
|
+
const relative = relativeTo(root, file);
|
|
74
|
+
if (relative.startsWith("..")) return false;
|
|
75
|
+
return relative.split("/").slice(0, -1).some((segment) => segment === "seeders" || segment === "seeder");
|
|
76
|
+
}
|
|
71
77
|
function isApplicationCode(root, file) {
|
|
72
78
|
const relative = relativeTo(root, file);
|
|
73
79
|
if (relative.startsWith("..")) return false;
|
|
@@ -77,129 +83,6 @@ function isApplicationCode(root, file) {
|
|
|
77
83
|
return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
|
|
78
84
|
}
|
|
79
85
|
//#endregion
|
|
80
|
-
//#region src/inventory/sources/event_bindings.ts
|
|
81
|
-
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
82
|
-
const LISTENER_METHOD = "handle";
|
|
83
|
-
function collectEventBindings(app) {
|
|
84
|
-
const project = new Project({
|
|
85
|
-
skipAddingFilesFromTsConfig: true,
|
|
86
|
-
skipFileDependencyResolution: true,
|
|
87
|
-
compilerOptions: { allowJs: false }
|
|
88
|
-
});
|
|
89
|
-
for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
90
|
-
const bindings = /* @__PURE__ */ new Map();
|
|
91
|
-
for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
92
|
-
const expression = call.getExpression();
|
|
93
|
-
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
94
|
-
if (expression.getName() !== "on") continue;
|
|
95
|
-
const [event, handlers] = call.getArguments();
|
|
96
|
-
if (!event || !handlers) continue;
|
|
97
|
-
const eventFile = resolveEventClass(event, file, app);
|
|
98
|
-
if (!eventFile) continue;
|
|
99
|
-
const refs = listenersOf(handlers, file, app);
|
|
100
|
-
if (refs.length === 0) continue;
|
|
101
|
-
bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
|
|
102
|
-
}
|
|
103
|
-
return bindings;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* The event class a dispatch or a binding names.
|
|
107
|
-
*
|
|
108
|
-
* Two shapes reach here: the class imported directly, and the generated
|
|
109
|
-
* registry (`events.OrderPlaced`), which is what `node ace make:event` produces
|
|
110
|
-
* and therefore the common one. Exported because the resolver has to ask the
|
|
111
|
-
* same question of a call site, and two implementations of "which event is
|
|
112
|
-
* this" would drift.
|
|
113
|
-
*/
|
|
114
|
-
function resolveEventClass(expression, from, app) {
|
|
115
|
-
if (Node.isIdentifier(expression)) {
|
|
116
|
-
const target = importedFrom(expression.getText(), from, app);
|
|
117
|
-
return target ? toPosix(target) : null;
|
|
118
|
-
}
|
|
119
|
-
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
120
|
-
const root = expression.getExpression();
|
|
121
|
-
if (!Node.isIdentifier(root)) return null;
|
|
122
|
-
const registry = importedFrom(root.getText(), from, app);
|
|
123
|
-
if (!registry) return null;
|
|
124
|
-
return registryEntry(registry, expression.getName(), from.getProject(), app);
|
|
125
|
-
}
|
|
126
|
-
/** listener bodies named by the second argument of `emitter.on` */
|
|
127
|
-
function listenersOf(handlers, from, app) {
|
|
128
|
-
const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
|
|
129
|
-
const refs = [];
|
|
130
|
-
for (const entry of entries) {
|
|
131
|
-
/**
|
|
132
|
-
* `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
|
|
133
|
-
* and taking `handle` on faith there would look for a body that is not
|
|
134
|
-
* the one bound.
|
|
135
|
-
*/
|
|
136
|
-
if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
137
|
-
const [target, member] = entry.getElements();
|
|
138
|
-
const file = target ? listenerFile(target, from, app) : null;
|
|
139
|
-
if (!file) continue;
|
|
140
|
-
const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
141
|
-
refs.push({
|
|
142
|
-
file,
|
|
143
|
-
member: named ?? LISTENER_METHOD
|
|
144
|
-
});
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const file = listenerFile(entry, from, app);
|
|
148
|
-
if (file) refs.push({
|
|
149
|
-
file,
|
|
150
|
-
member: LISTENER_METHOD
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return refs;
|
|
154
|
-
}
|
|
155
|
-
function listenerFile(entry, from, app) {
|
|
156
|
-
if (Node.isPropertyAccessExpression(entry)) {
|
|
157
|
-
const root = entry.getExpression();
|
|
158
|
-
if (!Node.isIdentifier(root)) return null;
|
|
159
|
-
const registry = importedFrom(root.getText(), from, app);
|
|
160
|
-
return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
|
|
161
|
-
}
|
|
162
|
-
if (Node.isIdentifier(entry)) {
|
|
163
|
-
const target = importedFrom(entry.getText(), from, app);
|
|
164
|
-
return target ? toPosix(target) : null;
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
/** where a local identifier was imported from, resolved through the alias map */
|
|
169
|
-
function importedFrom(local, from, app) {
|
|
170
|
-
for (const declaration of from.getImportDeclarations()) {
|
|
171
|
-
const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
|
|
172
|
-
const isDefault = declaration.getDefaultImport()?.getText() === local;
|
|
173
|
-
if (!named && !isDefault) continue;
|
|
174
|
-
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* The file a key of a generated registry points at.
|
|
180
|
-
*
|
|
181
|
-
* Both shapes the generators emit are handled: a direct reference to an
|
|
182
|
-
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
183
|
-
* per artefact and per framework version, and reading only one of them silently
|
|
184
|
-
* lost half the graph.
|
|
185
|
-
*/
|
|
186
|
-
function registryEntry(registryFile, key, project, app) {
|
|
187
|
-
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
188
|
-
if (!file) return null;
|
|
189
|
-
for (const declaration of file.getVariableDeclarations()) {
|
|
190
|
-
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
191
|
-
if (!value) continue;
|
|
192
|
-
if (Node.isIdentifier(value)) {
|
|
193
|
-
const target = importedFrom(value.getText(), file, app);
|
|
194
|
-
return target ? toPosix(target) : null;
|
|
195
|
-
}
|
|
196
|
-
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
197
|
-
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
198
|
-
return target ? toPosix(target) : null;
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
//#endregion
|
|
203
86
|
//#region src/inventory/detectors/lucid.ts
|
|
204
87
|
const WRITE_METHODS = new Set([
|
|
205
88
|
"save",
|
|
@@ -506,6 +389,129 @@ function classOfReceiver(receiver) {
|
|
|
506
389
|
return null;
|
|
507
390
|
}
|
|
508
391
|
//#endregion
|
|
392
|
+
//#region src/inventory/sources/event_bindings.ts
|
|
393
|
+
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
394
|
+
const LISTENER_METHOD = "handle";
|
|
395
|
+
function collectEventBindings(app) {
|
|
396
|
+
const project = new Project({
|
|
397
|
+
skipAddingFilesFromTsConfig: true,
|
|
398
|
+
skipFileDependencyResolution: true,
|
|
399
|
+
compilerOptions: { allowJs: false }
|
|
400
|
+
});
|
|
401
|
+
for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
402
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
404
|
+
const expression = call.getExpression();
|
|
405
|
+
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
406
|
+
if (expression.getName() !== "on") continue;
|
|
407
|
+
const [event, handlers] = call.getArguments();
|
|
408
|
+
if (!event || !handlers) continue;
|
|
409
|
+
const eventFile = resolveEventClass(event, file, app);
|
|
410
|
+
if (!eventFile) continue;
|
|
411
|
+
const refs = listenersOf(handlers, file, app);
|
|
412
|
+
if (refs.length === 0) continue;
|
|
413
|
+
bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
|
|
414
|
+
}
|
|
415
|
+
return bindings;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The event class a dispatch or a binding names.
|
|
419
|
+
*
|
|
420
|
+
* Two shapes reach here: the class imported directly, and the generated
|
|
421
|
+
* registry (`events.OrderPlaced`), which is what `node ace make:event` produces
|
|
422
|
+
* and therefore the common one. Exported because the resolver has to ask the
|
|
423
|
+
* same question of a call site, and two implementations of "which event is
|
|
424
|
+
* this" would drift.
|
|
425
|
+
*/
|
|
426
|
+
function resolveEventClass(expression, from, app) {
|
|
427
|
+
if (Node.isIdentifier(expression)) {
|
|
428
|
+
const target = importedFrom(expression.getText(), from, app);
|
|
429
|
+
return target ? toPosix(target) : null;
|
|
430
|
+
}
|
|
431
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
432
|
+
const root = expression.getExpression();
|
|
433
|
+
if (!Node.isIdentifier(root)) return null;
|
|
434
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
435
|
+
if (!registry) return null;
|
|
436
|
+
return registryEntry(registry, expression.getName(), from.getProject(), app);
|
|
437
|
+
}
|
|
438
|
+
/** listener bodies named by the second argument of `emitter.on` */
|
|
439
|
+
function listenersOf(handlers, from, app) {
|
|
440
|
+
const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
|
|
441
|
+
const refs = [];
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
/**
|
|
444
|
+
* `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
|
|
445
|
+
* and taking `handle` on faith there would look for a body that is not
|
|
446
|
+
* the one bound.
|
|
447
|
+
*/
|
|
448
|
+
if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
449
|
+
const [target, member] = entry.getElements();
|
|
450
|
+
const file = target ? listenerFile(target, from, app) : null;
|
|
451
|
+
if (!file) continue;
|
|
452
|
+
const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
453
|
+
refs.push({
|
|
454
|
+
file,
|
|
455
|
+
member: named ?? LISTENER_METHOD
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const file = listenerFile(entry, from, app);
|
|
460
|
+
if (file) refs.push({
|
|
461
|
+
file,
|
|
462
|
+
member: LISTENER_METHOD
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return refs;
|
|
466
|
+
}
|
|
467
|
+
function listenerFile(entry, from, app) {
|
|
468
|
+
if (Node.isPropertyAccessExpression(entry)) {
|
|
469
|
+
const root = entry.getExpression();
|
|
470
|
+
if (!Node.isIdentifier(root)) return null;
|
|
471
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
472
|
+
return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
|
|
473
|
+
}
|
|
474
|
+
if (Node.isIdentifier(entry)) {
|
|
475
|
+
const target = importedFrom(entry.getText(), from, app);
|
|
476
|
+
return target ? toPosix(target) : null;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
/** where a local identifier was imported from, resolved through the alias map */
|
|
481
|
+
function importedFrom(local, from, app) {
|
|
482
|
+
for (const declaration of from.getImportDeclarations()) {
|
|
483
|
+
const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
|
|
484
|
+
const isDefault = declaration.getDefaultImport()?.getText() === local;
|
|
485
|
+
if (!named && !isDefault) continue;
|
|
486
|
+
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The file a key of a generated registry points at.
|
|
492
|
+
*
|
|
493
|
+
* Both shapes the generators emit are handled: a direct reference to an
|
|
494
|
+
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
495
|
+
* per artefact and per framework version, and reading only one of them silently
|
|
496
|
+
* lost half the graph.
|
|
497
|
+
*/
|
|
498
|
+
function registryEntry(registryFile, key, project, app) {
|
|
499
|
+
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
500
|
+
if (!file) return null;
|
|
501
|
+
for (const declaration of file.getVariableDeclarations()) {
|
|
502
|
+
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
503
|
+
if (!value) continue;
|
|
504
|
+
if (Node.isIdentifier(value)) {
|
|
505
|
+
const target = importedFrom(value.getText(), file, app);
|
|
506
|
+
return target ? toPosix(target) : null;
|
|
507
|
+
}
|
|
508
|
+
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
509
|
+
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
510
|
+
return target ? toPosix(target) : null;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
509
515
|
//#region src/inventory/resolvers/event_dispatch.ts
|
|
510
516
|
/**
|
|
511
517
|
* "Event" pattern: the handler announces, and listeners act.
|
|
@@ -613,6 +619,68 @@ const jobDispatchResolver = {
|
|
|
613
619
|
}
|
|
614
620
|
};
|
|
615
621
|
//#endregion
|
|
622
|
+
//#region src/inventory/resolvers/local_function.ts
|
|
623
|
+
/**
|
|
624
|
+
* "Local function" pattern: a helper declared in the same file, not imported.
|
|
625
|
+
*
|
|
626
|
+
* const lista = await proximos(id) // function proximos() { … }
|
|
627
|
+
* return rows.map(paraLinha) // const paraLinha = (row) => …
|
|
628
|
+
*
|
|
629
|
+
* A query object that keeps its helpers beside it is common, and before this
|
|
630
|
+
* every such call was unresolved: the store a helper read was reached by nobody
|
|
631
|
+
* from that route, and a value built by it left as a whole table handed in.
|
|
632
|
+
*
|
|
633
|
+
* Only module-level declarations: a closure declared inside a body is part of
|
|
634
|
+
* that body, and the graph already walks it.
|
|
635
|
+
*/
|
|
636
|
+
const localFunctionResolver = {
|
|
637
|
+
name: "local-function",
|
|
638
|
+
order: 45,
|
|
639
|
+
resolve(call, ctx) {
|
|
640
|
+
const named = calledFunctionOf(call);
|
|
641
|
+
if (!named) return [];
|
|
642
|
+
const name = named.getText();
|
|
643
|
+
if (ctx.imports.has(name)) return [];
|
|
644
|
+
if (!declaresFunction(ctx.file, name)) return [];
|
|
645
|
+
return [{
|
|
646
|
+
file: ctx.file.getFilePath(),
|
|
647
|
+
member: name
|
|
648
|
+
}];
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
/** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
|
|
652
|
+
const APPLIES_CALLBACK = new Set([
|
|
653
|
+
"map",
|
|
654
|
+
"flatMap",
|
|
655
|
+
"forEach",
|
|
656
|
+
"filter",
|
|
657
|
+
"find",
|
|
658
|
+
"some",
|
|
659
|
+
"every",
|
|
660
|
+
"sort",
|
|
661
|
+
"reduce"
|
|
662
|
+
]);
|
|
663
|
+
/**
|
|
664
|
+
* The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
|
|
665
|
+
* names `paraLinha`, called once per row — the body the graph must read is the
|
|
666
|
+
* same, whichever way it was reached.
|
|
667
|
+
*/
|
|
668
|
+
function calledFunctionOf(call) {
|
|
669
|
+
const expression = call.getExpression();
|
|
670
|
+
if (Node.isIdentifier(expression)) return expression;
|
|
671
|
+
if (Node.isPropertyAccessExpression(expression) && APPLIES_CALLBACK.has(expression.getName())) {
|
|
672
|
+
const callback = call.getArguments()[0];
|
|
673
|
+
if (callback && Node.isIdentifier(callback)) return callback;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
/** `function name() {}` or `const name = () => {}` / `function () {}` at module level */
|
|
678
|
+
function declaresFunction(file, name) {
|
|
679
|
+
if (file.getFunction(name)) return true;
|
|
680
|
+
const initializer = file.getVariableDeclaration(name)?.getInitializer();
|
|
681
|
+
return !!initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer));
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
616
684
|
//#region src/inventory/resolvers/module_function.ts
|
|
617
685
|
/**
|
|
618
686
|
* "Module function" pattern: no class at all.
|
|
@@ -627,8 +695,8 @@ const moduleFunctionResolver = {
|
|
|
627
695
|
name: "module-function",
|
|
628
696
|
order: 50,
|
|
629
697
|
resolve(call, ctx) {
|
|
630
|
-
const expr = call
|
|
631
|
-
if (!expr
|
|
698
|
+
const expr = calledFunctionOf(call);
|
|
699
|
+
if (!expr) return [];
|
|
632
700
|
const local = expr.getText();
|
|
633
701
|
const file = ctx.imports.get(local);
|
|
634
702
|
if (!file) return [];
|
|
@@ -750,6 +818,298 @@ const staticServiceResolver = {
|
|
|
750
818
|
}
|
|
751
819
|
};
|
|
752
820
|
//#endregion
|
|
821
|
+
//#region src/inventory/graph/output_fields.ts
|
|
822
|
+
/**
|
|
823
|
+
* Does this class extend a transformer base from a package?
|
|
824
|
+
*
|
|
825
|
+
* Decided by the base's name AND by its import being a bare specifier, so an
|
|
826
|
+
* application class that merely happens to own a `transform` method is not
|
|
827
|
+
* mistaken for one. Shared with the `transformer` resolver: one definition of
|
|
828
|
+
* what a transformer is, or the resolver follows a body this walker refuses.
|
|
829
|
+
*/
|
|
830
|
+
function isTransformerClass(cls) {
|
|
831
|
+
const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
|
|
832
|
+
if (!name?.endsWith("Transformer")) return false;
|
|
833
|
+
const imported = cls.getSourceFile().getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
|
|
834
|
+
return imported !== void 0 && !imported.getModuleSpecifierValue().startsWith("#");
|
|
835
|
+
}
|
|
836
|
+
/** `class X extends BaseTransformer<Livro>` -> 'Livro' */
|
|
837
|
+
function transformerResourceOf(cls) {
|
|
838
|
+
return (cls.getExtends()?.getTypeArguments()[0])?.asKind(SyntaxKind.TypeReference)?.getTypeName().getText() ?? null;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* The keys a transformer method returns.
|
|
842
|
+
*
|
|
843
|
+
* `followed` says whether a call inside the literal is a body the graph walks —
|
|
844
|
+
* `AutorTransformer.transform(x)`, `this.toObject()` — in which case its keys
|
|
845
|
+
* arrive through that body and the key holding it is not a DET of its own: the
|
|
846
|
+
* user sees the author's name, not an "autor" field.
|
|
847
|
+
*
|
|
848
|
+
* { titulo: l.titulo } 1 — `titulo`
|
|
849
|
+
* { autor: AutorTransformer.transform } 0 here; the nested body contributes
|
|
850
|
+
* { endereco: { rua, cidade } } leaves individually
|
|
851
|
+
* { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
|
|
852
|
+
* { itens: xs.map((i) => ({ a, b })) } the leaves, once
|
|
853
|
+
* ...this.pick(this.resource, [...]) the listed names
|
|
854
|
+
* ...this.toObject() 0 here; the followed body contributes
|
|
855
|
+
* ...anythingElse 1, opaque, reported
|
|
856
|
+
*
|
|
857
|
+
* A key that is the identifier of the transformer's resource is not a DET, for
|
|
858
|
+
* the same reason `isPrimary` is not one on the data function.
|
|
859
|
+
*/
|
|
860
|
+
function outputFieldsIn(body, owner, stores, followed) {
|
|
861
|
+
if (!owner || !isTransformerClass(owner)) return {
|
|
862
|
+
outputs: [],
|
|
863
|
+
opaqueOutputs: [],
|
|
864
|
+
resource: null
|
|
865
|
+
};
|
|
866
|
+
const qualifier = owner.getName() ?? "Transformer";
|
|
867
|
+
const resource = transformerResourceOf(owner);
|
|
868
|
+
/**
|
|
869
|
+
* The resource's key and its system timestamps: re-emitted for links and
|
|
870
|
+
* sorting, and not something the user recognises — the same two exclusions the
|
|
871
|
+
* data function applies (§6).
|
|
872
|
+
*/
|
|
873
|
+
const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
|
|
874
|
+
const { leaves, opaque } = collectLeaves(returnedLiteralsOf(body), {
|
|
875
|
+
qualifier,
|
|
876
|
+
excluded,
|
|
877
|
+
followed
|
|
878
|
+
});
|
|
879
|
+
return {
|
|
880
|
+
outputs: leaves,
|
|
881
|
+
opaqueOutputs: opaque,
|
|
882
|
+
resource: resource && stores.has(resource) ? resource : null
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* The leaves of object literals, by the §7 rules:
|
|
887
|
+
*
|
|
888
|
+
* { titulo: l.titulo } 1 — `titulo`
|
|
889
|
+
* { autor: AutorTransformer.transform } 0 here; the followed body contributes
|
|
890
|
+
* { endereco: { rua, cidade } } leaves individually
|
|
891
|
+
* { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
|
|
892
|
+
* { itens: xs.map((i) => ({ a, b })) } the leaves, once
|
|
893
|
+
* ...this.pick(this.resource, [...]) the listed names
|
|
894
|
+
* ...this.toObject() 0 here; the followed body contributes
|
|
895
|
+
* ...anythingElse 1, opaque, reported
|
|
896
|
+
*/
|
|
897
|
+
function collectLeaves(literals, options) {
|
|
898
|
+
const { qualifier, excluded, followed } = options;
|
|
899
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
900
|
+
const opaque = /* @__PURE__ */ new Set();
|
|
901
|
+
const q = qualifier ? `${qualifier}.` : "";
|
|
902
|
+
const leaf = (prefix, name) => {
|
|
903
|
+
if (prefix === "" && excluded.has(name)) return;
|
|
904
|
+
outputs.add(`${q}${prefix ? `${prefix}.${name}` : name}`);
|
|
905
|
+
};
|
|
906
|
+
const walk = (literal, prefix) => {
|
|
907
|
+
for (const property of literal.getProperties()) {
|
|
908
|
+
if (Node.isShorthandPropertyAssignment(property)) {
|
|
909
|
+
leaf(prefix, property.getName());
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (Node.isMethodDeclaration(property) || Node.isGetAccessorDeclaration(property)) {
|
|
913
|
+
leaf(prefix, property.getName());
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (Node.isSpreadAssignment(property)) {
|
|
917
|
+
spread(property.getExpression(), prefix);
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (!Node.isPropertyAssignment(property)) continue;
|
|
921
|
+
const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
|
|
922
|
+
const value = unwrap(property.getInitializer());
|
|
923
|
+
if (!value) {
|
|
924
|
+
leaf(prefix, name);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
928
|
+
walk(value, prefix ? `${prefix}.${name}` : name);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (Node.isCallExpression(value) && followed(value)) continue;
|
|
932
|
+
const mapped = mappedLiteralOf(value);
|
|
933
|
+
if (mapped) {
|
|
934
|
+
walk(mapped, prefix ? `${prefix}.${name}` : name);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
leaf(prefix, name);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const spread = (expression, prefix) => {
|
|
941
|
+
const value = unwrap(expression);
|
|
942
|
+
if (!value) return;
|
|
943
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
944
|
+
walk(value, prefix);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (Node.isConditionalExpression(value)) {
|
|
948
|
+
spread(value.getWhenTrue(), prefix);
|
|
949
|
+
spread(value.getWhenFalse(), prefix);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (Node.isCallExpression(value)) {
|
|
953
|
+
const picked = pickedNamesOf(value);
|
|
954
|
+
if (picked) {
|
|
955
|
+
for (const name of picked) leaf(prefix, name);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (followed(value)) return;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* `...this.resource.serialize()`, `...this.extras`: whatever the model has.
|
|
962
|
+
* One DET as a floor, and reported — the placeholder carries the expression
|
|
963
|
+
* so the report can name what could not be read.
|
|
964
|
+
*/
|
|
965
|
+
const placeholder = `${q}${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
|
|
966
|
+
outputs.add(placeholder);
|
|
967
|
+
opaque.add(placeholder);
|
|
968
|
+
};
|
|
969
|
+
for (const literal of literals) walk(literal, "");
|
|
970
|
+
return {
|
|
971
|
+
leaves: [...outputs],
|
|
972
|
+
opaque: [...opaque]
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Object literals the body itself returns — not the ones returned by arrow
|
|
977
|
+
* functions inside it, which belong to `.map()` callbacks and are read as
|
|
978
|
+
* repeating groups where they occur.
|
|
979
|
+
*/
|
|
980
|
+
function returnedLiteralsOf(body) {
|
|
981
|
+
const literals = [];
|
|
982
|
+
for (const statement of body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
983
|
+
if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== body) continue;
|
|
984
|
+
const value = unwrap(statement.getExpression());
|
|
985
|
+
if (value && Node.isObjectLiteralExpression(value)) literals.push(value);
|
|
986
|
+
}
|
|
987
|
+
return literals;
|
|
988
|
+
}
|
|
989
|
+
/** `this.pick(this.resource, ['a', 'b'])` -> ['a', 'b']; null when it is not that call */
|
|
990
|
+
function pickedNamesOf(call) {
|
|
991
|
+
const callee = call.getExpression();
|
|
992
|
+
if (!Node.isPropertyAccessExpression(callee)) return null;
|
|
993
|
+
if (callee.getName() !== "pick") return null;
|
|
994
|
+
if (callee.getExpression().getKind() !== SyntaxKind.ThisKeyword) return null;
|
|
995
|
+
const list = call.getArguments()[1]?.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
996
|
+
if (!list) return null;
|
|
997
|
+
const names = [];
|
|
998
|
+
for (const element of list.getElements()) {
|
|
999
|
+
const name = element.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
1000
|
+
if (name === void 0) return null;
|
|
1001
|
+
names.push(name);
|
|
1002
|
+
}
|
|
1003
|
+
return names;
|
|
1004
|
+
}
|
|
1005
|
+
/** `xs.map((x) => ({ a, b }))` -> the literal; null for a scalar map or anything else */
|
|
1006
|
+
function mappedLiteralOf(value) {
|
|
1007
|
+
if (!Node.isCallExpression(value)) return null;
|
|
1008
|
+
const callee = value.getExpression();
|
|
1009
|
+
if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "map") return null;
|
|
1010
|
+
const callback = value.getArguments()[0];
|
|
1011
|
+
if (!callback || !Node.isArrowFunction(callback)) return null;
|
|
1012
|
+
const returned = unwrap(callback.getBody().asKind(SyntaxKind.Block) ? null : callback.getBody());
|
|
1013
|
+
if (returned && Node.isObjectLiteralExpression(returned)) return returned;
|
|
1014
|
+
for (const statement of callback.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
1015
|
+
const expression = unwrap(statement.getExpression());
|
|
1016
|
+
if (expression && Node.isObjectLiteralExpression(expression)) return expression;
|
|
1017
|
+
}
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
/** strips parentheses, `as`, `satisfies` and non-null assertions */
|
|
1021
|
+
function unwrap(node) {
|
|
1022
|
+
let current = node;
|
|
1023
|
+
while (current && (Node.isParenthesizedExpression(current) || Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isNonNullExpression(current))) current = current.getExpression();
|
|
1024
|
+
return current && Node.isExpression(current) ? current : null;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* The columns a fluent chain names in `.select()`, walking UP from the access
|
|
1028
|
+
* the detector recognised (`Livro.query()`) through the chain it belongs to.
|
|
1029
|
+
*
|
|
1030
|
+
* Only calls on the chain itself qualify. A `q.select('nome')` inside a
|
|
1031
|
+
* `preload('autor', (q) => …)` callback narrows the related store, not this
|
|
1032
|
+
* one, and reading every descendant would have attributed it here.
|
|
1033
|
+
*
|
|
1034
|
+
* Both spellings count: `.select(['a', 'b'])` and `.select('a', 'b')`. Anything
|
|
1035
|
+
* that is not a string literal — a variable, a raw expression — is unreadable:
|
|
1036
|
+
* the store falls back to every column, and the chain is reported.
|
|
1037
|
+
*/
|
|
1038
|
+
/** methods that leave ONE derived scalar rather than rows */
|
|
1039
|
+
const AGGREGATES = new Set([
|
|
1040
|
+
"count",
|
|
1041
|
+
"countDistinct",
|
|
1042
|
+
"exists",
|
|
1043
|
+
"sum",
|
|
1044
|
+
"avg",
|
|
1045
|
+
"min",
|
|
1046
|
+
"max"
|
|
1047
|
+
]);
|
|
1048
|
+
function chainShapeOf(access) {
|
|
1049
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1050
|
+
const unreadable = [];
|
|
1051
|
+
let aggregate = false;
|
|
1052
|
+
const inspect = (call) => {
|
|
1053
|
+
const callee = call.getExpression();
|
|
1054
|
+
if (!Node.isPropertyAccessExpression(callee)) return;
|
|
1055
|
+
const name = callee.getName();
|
|
1056
|
+
if (AGGREGATES.has(name)) aggregate = true;
|
|
1057
|
+
if (name !== "select") return;
|
|
1058
|
+
const literal = literalColumnsOf(call);
|
|
1059
|
+
if (literal) for (const column of literal) selected.add(column);
|
|
1060
|
+
else unreadable.push({
|
|
1061
|
+
line: call.getStartLineNumber(),
|
|
1062
|
+
expression: call.getText().replace(/\s+/g, "")
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
let node = access;
|
|
1066
|
+
for (let depth = 0; node && depth < 40; depth++) {
|
|
1067
|
+
if (Node.isCallExpression(node)) inspect(node);
|
|
1068
|
+
node = Node.isCallExpression(node) || Node.isPropertyAccessExpression(node) ? node.getExpression() : void 0;
|
|
1069
|
+
}
|
|
1070
|
+
node = access;
|
|
1071
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
1072
|
+
const parent = node.getParent();
|
|
1073
|
+
if (!parent) break;
|
|
1074
|
+
if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
|
|
1075
|
+
node = parent;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) break;
|
|
1079
|
+
const call = parent.getParent();
|
|
1080
|
+
if (!call || !Node.isCallExpression(call) || call.getExpression() !== parent) break;
|
|
1081
|
+
inspect(call);
|
|
1082
|
+
node = call;
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
selected: [...selected],
|
|
1086
|
+
aggregate,
|
|
1087
|
+
unreadable
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
/** string-literal columns of one `.select(...)`; null when any is not a literal */
|
|
1091
|
+
function literalColumnsOf(call) {
|
|
1092
|
+
const columns = [];
|
|
1093
|
+
const read = (node) => {
|
|
1094
|
+
const value = node.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
1095
|
+
if (value === void 0) return false;
|
|
1096
|
+
const bare = value.includes(".") ? value.split(".").pop() : value;
|
|
1097
|
+
if (bare !== "*") columns.push(camelCase(bare));
|
|
1098
|
+
return true;
|
|
1099
|
+
};
|
|
1100
|
+
for (const argument of call.getArguments()) {
|
|
1101
|
+
const list = argument.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
1102
|
+
if (list) {
|
|
1103
|
+
for (const element of list.getElements()) if (!read(element)) return null;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (!read(argument)) return null;
|
|
1107
|
+
}
|
|
1108
|
+
return columns;
|
|
1109
|
+
}
|
|
1110
|
+
/** `created_at` -> `createdAt`, to match the model's attribute names */
|
|
1111
|
+
const camelCase = (value) => value.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
1112
|
+
//#endregion
|
|
753
1113
|
//#region src/inventory/resolvers/transformer.ts
|
|
754
1114
|
/** BaseTransformer's public API; all of it funnels through `toObject` */
|
|
755
1115
|
const TRANSFORMER_METHODS = new Set([
|
|
@@ -761,6 +1121,8 @@ const TRANSFORMER_METHODS = new Set([
|
|
|
761
1121
|
]);
|
|
762
1122
|
/** what the application-side body is called */
|
|
763
1123
|
const APPLICATION_BODY = "toObject";
|
|
1124
|
+
/** the variant named in these is a method of the transformer, and part of what leaves */
|
|
1125
|
+
const VARIANT_METHODS = new Set(["useVariant", "withVariant"]);
|
|
764
1126
|
/**
|
|
765
1127
|
* "Transformer" pattern: the package supplies the API, the application
|
|
766
1128
|
* supplies the body.
|
|
@@ -784,47 +1146,51 @@ const APPLICATION_BODY = "toObject";
|
|
|
784
1146
|
* transaction that serialised through it. Without this, a table written only
|
|
785
1147
|
* inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
|
|
786
1148
|
*/
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
if (!
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
if (!file) return [];
|
|
803
|
-
const declared = ctx.sourceFile(file);
|
|
804
|
-
if (!declared || !extendsTransformer(declared)) return [];
|
|
805
|
-
return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
|
|
806
|
-
file,
|
|
807
|
-
member: APPLICATION_BODY
|
|
808
|
-
}] : [];
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
/**
|
|
812
|
-
* Does a class here extend a transformer base from a package?
|
|
813
|
-
*
|
|
814
|
-
* Checked by the base's name and by its import being a bare specifier, so an
|
|
815
|
-
* application class that merely happens to own a `transform` method is not
|
|
816
|
-
* mistaken for one.
|
|
817
|
-
*/
|
|
818
|
-
function extendsTransformer(file) {
|
|
819
|
-
if (!file) return false;
|
|
820
|
-
for (const cls of file.getClasses()) {
|
|
821
|
-
const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
|
|
822
|
-
if (!name?.endsWith("Transformer")) continue;
|
|
823
|
-
const imported = file.getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
|
|
824
|
-
if (imported && !imported.getModuleSpecifierValue().startsWith("#")) return true;
|
|
1149
|
+
/** is `.useVariant('x')` / `.withVariant('x')` the next link of this call's chain? */
|
|
1150
|
+
function variantFollows(call) {
|
|
1151
|
+
let node = call;
|
|
1152
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
1153
|
+
const parent = node.getParent();
|
|
1154
|
+
if (!parent) return false;
|
|
1155
|
+
if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
|
|
1156
|
+
node = parent;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) return false;
|
|
1160
|
+
const next = parent.getParent();
|
|
1161
|
+
if (!next || !Node.isCallExpression(next) || next.getExpression() !== parent) return false;
|
|
1162
|
+
if (VARIANT_METHODS.has(parent.getName())) return next.getArguments()[0]?.getKind() === SyntaxKind.StringLiteral;
|
|
1163
|
+
node = next;
|
|
825
1164
|
}
|
|
826
1165
|
return false;
|
|
827
1166
|
}
|
|
1167
|
+
/** the transformer class a chain call is on, if it is one: `X.transform(p)`, `X.transform(p).useVariant(v)` */
|
|
1168
|
+
function transformerOf(call, ctx) {
|
|
1169
|
+
const expression = call.getExpression();
|
|
1170
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
1171
|
+
if (!TRANSFORMER_METHODS.has(expression.getName())) return null;
|
|
1172
|
+
/**
|
|
1173
|
+
* The root of the chain, so `X.transform(p).useVariant(v)` resolves as
|
|
1174
|
+
* well as `X.transform(p)`. Analysing the same body twice is free: the
|
|
1175
|
+
* graph dedupes by file and member.
|
|
1176
|
+
*/
|
|
1177
|
+
const symbol = rootSymbolOf(expression.getExpression());
|
|
1178
|
+
if (!symbol) return null;
|
|
1179
|
+
const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
|
|
1180
|
+
if (!file) return null;
|
|
1181
|
+
/**
|
|
1182
|
+
* One definition of what a transformer is, shared with the output walker
|
|
1183
|
+
* (`graph/output_fields.ts`): the resolver must not follow a body whose keys
|
|
1184
|
+
* the walker would refuse to read, or the other way round.
|
|
1185
|
+
*/
|
|
1186
|
+
const declared = ctx.sourceFile(file);
|
|
1187
|
+
if (!declared || !declared.getClasses().some(isTransformerClass)) return null;
|
|
1188
|
+
const owner = declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY));
|
|
1189
|
+
return owner ? {
|
|
1190
|
+
file,
|
|
1191
|
+
owner
|
|
1192
|
+
} : null;
|
|
1193
|
+
}
|
|
828
1194
|
//#endregion
|
|
829
1195
|
//#region src/inventory/resolvers/index.ts
|
|
830
1196
|
/**
|
|
@@ -839,9 +1205,53 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
839
1205
|
actionObjectResolver,
|
|
840
1206
|
eventDispatchResolver,
|
|
841
1207
|
jobDispatchResolver,
|
|
842
|
-
|
|
1208
|
+
{
|
|
1209
|
+
name: "transformer",
|
|
1210
|
+
order: 18,
|
|
1211
|
+
/**
|
|
1212
|
+
* The `transform` before a `useVariant`: claimed and followed nowhere, or
|
|
1213
|
+
* `static-service` would take it next, look for a `transform` body in the
|
|
1214
|
+
* application file and report the package method as a gap.
|
|
1215
|
+
*/
|
|
1216
|
+
ignores(call, ctx) {
|
|
1217
|
+
const expression = call.getExpression();
|
|
1218
|
+
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
1219
|
+
if (VARIANT_METHODS.has(expression.getName()) || !variantFollows(call)) return false;
|
|
1220
|
+
return transformerOf(call, ctx) !== null;
|
|
1221
|
+
},
|
|
1222
|
+
resolve(call, ctx) {
|
|
1223
|
+
const expression = call.getExpression();
|
|
1224
|
+
if (!Node.isPropertyAccessExpression(expression)) return [];
|
|
1225
|
+
const found = transformerOf(call, ctx);
|
|
1226
|
+
if (!found) return [];
|
|
1227
|
+
const { file, owner } = found;
|
|
1228
|
+
/**
|
|
1229
|
+
* `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
|
|
1230
|
+
* transformer, named after it, and it REPLACES `toObject()` — the shape that
|
|
1231
|
+
* leaves is the variant's. The chain is visited call by call: the `useVariant`
|
|
1232
|
+
* call resolves the variant's body, and the `transform` call before it resolves
|
|
1233
|
+
* nothing, or both shapes would leave and the page would count twice.
|
|
1234
|
+
*/
|
|
1235
|
+
if (VARIANT_METHODS.has(expression.getName())) {
|
|
1236
|
+
const variant = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
1237
|
+
return variant && owner.getMethod(variant) ? [{
|
|
1238
|
+
file,
|
|
1239
|
+
member: variant
|
|
1240
|
+
}] : [{
|
|
1241
|
+
file,
|
|
1242
|
+
member: APPLICATION_BODY
|
|
1243
|
+
}];
|
|
1244
|
+
}
|
|
1245
|
+
if (variantFollows(call)) return [];
|
|
1246
|
+
return [{
|
|
1247
|
+
file,
|
|
1248
|
+
member: APPLICATION_BODY
|
|
1249
|
+
}];
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
843
1252
|
staticServiceResolver,
|
|
844
1253
|
propertyServiceResolver,
|
|
1254
|
+
localFunctionResolver,
|
|
845
1255
|
moduleFunctionResolver
|
|
846
1256
|
];
|
|
847
1257
|
/**
|
|
@@ -881,5 +1291,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
881
1291
|
}
|
|
882
1292
|
return null;
|
|
883
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* A strategy that recognises a family of calls and knows they reach no data
|
|
1296
|
+
* store — a rate limiter, an attachment's URL, an authorisation check.
|
|
1297
|
+
*
|
|
1298
|
+
* Read from a real configuration, every such strategy was the same eight lines:
|
|
1299
|
+
* a helper to get the method name off the ts-morph node (the app does not depend
|
|
1300
|
+
* on ts-morph), a `resolve` that returns nothing, and one comparison. What the
|
|
1301
|
+
* design wants is kept — it is still a NAMED strategy, and `fp:count` still
|
|
1302
|
+
* reports the volume it declared data-free — and the ceremony is not.
|
|
1303
|
+
*/
|
|
1304
|
+
function ignoreCalls(options) {
|
|
1305
|
+
if (!options.methods?.length && !options.matching) throw new Error(`ignoreCalls("${options.name}"): say what it ignores — \`methods\` or \`matching\``);
|
|
1306
|
+
const methods = new Set(options.methods ?? []);
|
|
1307
|
+
return {
|
|
1308
|
+
name: options.name,
|
|
1309
|
+
order: options.order ?? 1,
|
|
1310
|
+
/** follows nothing: the whole point */
|
|
1311
|
+
resolve: () => [],
|
|
1312
|
+
ignores(call) {
|
|
1313
|
+
const callee = call.getExpression();
|
|
1314
|
+
const text = callee.getText();
|
|
1315
|
+
if (options.matching?.test(text)) return true;
|
|
1316
|
+
if (methods.size === 0) return false;
|
|
1317
|
+
const method = callee.getKindName() === "PropertyAccessExpression" ? text.split(".").pop() : text;
|
|
1318
|
+
return method !== void 0 && methods.has(method);
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
884
1322
|
//#endregion
|
|
885
|
-
export {
|
|
1323
|
+
export { relativeTo as _, chainShapeOf as a, unwrap as c, collectEventBindings as d, detectAccess as f, isSeeder as g, isApplicationCode as h, resolveCall as i, DISPATCH_METHODS as l, rootSymbolOf as m, ignoreCalls as n, mappedLiteralOf as o, hooksFiredBy as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, EXECUTION_METHODS as u, samePath as v, toPosix as y };
|