@filipebraida/adonis-function-points 0.6.0 → 0.8.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 +138 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-EAuAtdbq.js → fp_calibrate-B_oD9b02.js} +1 -1
- package/build/{fp_count-CZ0cUUBQ.js → fp_count-B_KPNKMO.js} +1 -1
- package/build/{fp_diff-BTg_LX0r.js → fp_diff-B3NhXzrb.js} +1 -1
- package/build/{fp_explain-D6QvDLKQ.js → fp_explain-C2s6Kpaj.js} +1 -1
- package/build/{fp_inventory-C43fU39x.js → fp_inventory-B322MvZT.js} +1 -1
- package/build/{fp_metrics-DEMPk4xC.js → fp_metrics-DjADb7F6.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-Cq4dNTNE.js → pipeline-C6kHKB9-.js} +2363 -114
- package/build/{resolvers-DlKJOZnk.js → resolvers-DhJO-qvQ.js} +339 -151
- package/build/{runners-FYmPIPub.js → runners-BpBJsGMj.js} +2 -2
- package/build/src/albrecht/counter.d.ts +12 -1
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +22 -0
- package/build/src/inventory/graph/deliveries.d.ts +94 -0
- package/build/src/inventory/graph/output_fields.d.ts +45 -1
- package/build/src/inventory/graph/pages.d.ts +44 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- 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 +23 -0
- package/package.json +1 -1
|
@@ -83,129 +83,6 @@ function isApplicationCode(root, file) {
|
|
|
83
83
|
return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
|
|
84
84
|
}
|
|
85
85
|
//#endregion
|
|
86
|
-
//#region src/inventory/sources/event_bindings.ts
|
|
87
|
-
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
88
|
-
const LISTENER_METHOD = "handle";
|
|
89
|
-
function collectEventBindings(app) {
|
|
90
|
-
const project = new Project({
|
|
91
|
-
skipAddingFilesFromTsConfig: true,
|
|
92
|
-
skipFileDependencyResolution: true,
|
|
93
|
-
compilerOptions: { allowJs: false }
|
|
94
|
-
});
|
|
95
|
-
for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
96
|
-
const bindings = /* @__PURE__ */ new Map();
|
|
97
|
-
for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
98
|
-
const expression = call.getExpression();
|
|
99
|
-
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
100
|
-
if (expression.getName() !== "on") continue;
|
|
101
|
-
const [event, handlers] = call.getArguments();
|
|
102
|
-
if (!event || !handlers) continue;
|
|
103
|
-
const eventFile = resolveEventClass(event, file, app);
|
|
104
|
-
if (!eventFile) continue;
|
|
105
|
-
const refs = listenersOf(handlers, file, app);
|
|
106
|
-
if (refs.length === 0) continue;
|
|
107
|
-
bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
|
|
108
|
-
}
|
|
109
|
-
return bindings;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* The event class a dispatch or a binding names.
|
|
113
|
-
*
|
|
114
|
-
* Two shapes reach here: the class imported directly, and the generated
|
|
115
|
-
* registry (`events.OrderPlaced`), which is what `node ace make:event` produces
|
|
116
|
-
* and therefore the common one. Exported because the resolver has to ask the
|
|
117
|
-
* same question of a call site, and two implementations of "which event is
|
|
118
|
-
* this" would drift.
|
|
119
|
-
*/
|
|
120
|
-
function resolveEventClass(expression, from, app) {
|
|
121
|
-
if (Node.isIdentifier(expression)) {
|
|
122
|
-
const target = importedFrom(expression.getText(), from, app);
|
|
123
|
-
return target ? toPosix(target) : null;
|
|
124
|
-
}
|
|
125
|
-
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
126
|
-
const root = expression.getExpression();
|
|
127
|
-
if (!Node.isIdentifier(root)) return null;
|
|
128
|
-
const registry = importedFrom(root.getText(), from, app);
|
|
129
|
-
if (!registry) return null;
|
|
130
|
-
return registryEntry(registry, expression.getName(), from.getProject(), app);
|
|
131
|
-
}
|
|
132
|
-
/** listener bodies named by the second argument of `emitter.on` */
|
|
133
|
-
function listenersOf(handlers, from, app) {
|
|
134
|
-
const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
|
|
135
|
-
const refs = [];
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
/**
|
|
138
|
-
* `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
|
|
139
|
-
* and taking `handle` on faith there would look for a body that is not
|
|
140
|
-
* the one bound.
|
|
141
|
-
*/
|
|
142
|
-
if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
143
|
-
const [target, member] = entry.getElements();
|
|
144
|
-
const file = target ? listenerFile(target, from, app) : null;
|
|
145
|
-
if (!file) continue;
|
|
146
|
-
const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
147
|
-
refs.push({
|
|
148
|
-
file,
|
|
149
|
-
member: named ?? LISTENER_METHOD
|
|
150
|
-
});
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
const file = listenerFile(entry, from, app);
|
|
154
|
-
if (file) refs.push({
|
|
155
|
-
file,
|
|
156
|
-
member: LISTENER_METHOD
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return refs;
|
|
160
|
-
}
|
|
161
|
-
function listenerFile(entry, from, app) {
|
|
162
|
-
if (Node.isPropertyAccessExpression(entry)) {
|
|
163
|
-
const root = entry.getExpression();
|
|
164
|
-
if (!Node.isIdentifier(root)) return null;
|
|
165
|
-
const registry = importedFrom(root.getText(), from, app);
|
|
166
|
-
return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
|
|
167
|
-
}
|
|
168
|
-
if (Node.isIdentifier(entry)) {
|
|
169
|
-
const target = importedFrom(entry.getText(), from, app);
|
|
170
|
-
return target ? toPosix(target) : null;
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
/** where a local identifier was imported from, resolved through the alias map */
|
|
175
|
-
function importedFrom(local, from, app) {
|
|
176
|
-
for (const declaration of from.getImportDeclarations()) {
|
|
177
|
-
const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
|
|
178
|
-
const isDefault = declaration.getDefaultImport()?.getText() === local;
|
|
179
|
-
if (!named && !isDefault) continue;
|
|
180
|
-
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The file a key of a generated registry points at.
|
|
186
|
-
*
|
|
187
|
-
* Both shapes the generators emit are handled: a direct reference to an
|
|
188
|
-
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
189
|
-
* per artefact and per framework version, and reading only one of them silently
|
|
190
|
-
* lost half the graph.
|
|
191
|
-
*/
|
|
192
|
-
function registryEntry(registryFile, key, project, app) {
|
|
193
|
-
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
194
|
-
if (!file) return null;
|
|
195
|
-
for (const declaration of file.getVariableDeclarations()) {
|
|
196
|
-
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
197
|
-
if (!value) continue;
|
|
198
|
-
if (Node.isIdentifier(value)) {
|
|
199
|
-
const target = importedFrom(value.getText(), file, app);
|
|
200
|
-
return target ? toPosix(target) : null;
|
|
201
|
-
}
|
|
202
|
-
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
203
|
-
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
204
|
-
return target ? toPosix(target) : null;
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
//#endregion
|
|
209
86
|
//#region src/inventory/detectors/lucid.ts
|
|
210
87
|
const WRITE_METHODS = new Set([
|
|
211
88
|
"save",
|
|
@@ -503,11 +380,149 @@ function classOfReceiver(receiver) {
|
|
|
503
380
|
return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
|
|
504
381
|
}
|
|
505
382
|
if (receiver.isKind(SyntaxKind.Identifier)) {
|
|
506
|
-
|
|
383
|
+
let init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
|
|
384
|
+
while (init?.isKind(SyntaxKind.AwaitExpression) || init?.isKind(SyntaxKind.ParenthesizedExpression)) init = init.getExpression();
|
|
507
385
|
if (init?.isKind(SyntaxKind.NewExpression)) {
|
|
508
386
|
const target = init.getExpression();
|
|
509
387
|
return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
|
|
510
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* `const svc = await app.container.make(IntakeService)`: the container hands
|
|
391
|
+
* back an instance of the class named — the same binding as `new`, written the
|
|
392
|
+
* way a controller writes it when the service has dependencies of its own. On a
|
|
393
|
+
* reviewed application this shape carried the write of `POST /gestao/atribuicao`
|
|
394
|
+
* and 31 more sites, and none was followed (plan 0.8 §B).
|
|
395
|
+
*/
|
|
396
|
+
if (init?.isKind(SyntaxKind.CallExpression)) {
|
|
397
|
+
const callee = init.getExpression();
|
|
398
|
+
if (callee.isKind(SyntaxKind.PropertyAccessExpression) && callee.getName() === "make" && callee.getExpression().isKind(SyntaxKind.PropertyAccessExpression) && callee.getExpression().asKind(SyntaxKind.PropertyAccessExpression).getName() === "container") {
|
|
399
|
+
const made = init.getArguments()[0];
|
|
400
|
+
return made?.isKind(SyntaxKind.Identifier) ? made.getText() : null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/inventory/sources/event_bindings.ts
|
|
408
|
+
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
409
|
+
const LISTENER_METHOD = "handle";
|
|
410
|
+
function collectEventBindings(app) {
|
|
411
|
+
const project = new Project({
|
|
412
|
+
skipAddingFilesFromTsConfig: true,
|
|
413
|
+
skipFileDependencyResolution: true,
|
|
414
|
+
compilerOptions: { allowJs: false }
|
|
415
|
+
});
|
|
416
|
+
for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
417
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
419
|
+
const expression = call.getExpression();
|
|
420
|
+
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
421
|
+
if (expression.getName() !== "on") continue;
|
|
422
|
+
const [event, handlers] = call.getArguments();
|
|
423
|
+
if (!event || !handlers) continue;
|
|
424
|
+
const eventFile = resolveEventClass(event, file, app);
|
|
425
|
+
if (!eventFile) continue;
|
|
426
|
+
const refs = listenersOf(handlers, file, app);
|
|
427
|
+
if (refs.length === 0) continue;
|
|
428
|
+
bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
|
|
429
|
+
}
|
|
430
|
+
return bindings;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The event class a dispatch or a binding names.
|
|
434
|
+
*
|
|
435
|
+
* Two shapes reach here: the class imported directly, and the generated
|
|
436
|
+
* registry (`events.OrderPlaced`), which is what `node ace make:event` produces
|
|
437
|
+
* and therefore the common one. Exported because the resolver has to ask the
|
|
438
|
+
* same question of a call site, and two implementations of "which event is
|
|
439
|
+
* this" would drift.
|
|
440
|
+
*/
|
|
441
|
+
function resolveEventClass(expression, from, app) {
|
|
442
|
+
if (Node.isIdentifier(expression)) {
|
|
443
|
+
const target = importedFrom(expression.getText(), from, app);
|
|
444
|
+
return target ? toPosix(target) : null;
|
|
445
|
+
}
|
|
446
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
447
|
+
const root = expression.getExpression();
|
|
448
|
+
if (!Node.isIdentifier(root)) return null;
|
|
449
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
450
|
+
if (!registry) return null;
|
|
451
|
+
return registryEntry(registry, expression.getName(), from.getProject(), app);
|
|
452
|
+
}
|
|
453
|
+
/** listener bodies named by the second argument of `emitter.on` */
|
|
454
|
+
function listenersOf(handlers, from, app) {
|
|
455
|
+
const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
|
|
456
|
+
const refs = [];
|
|
457
|
+
for (const entry of entries) {
|
|
458
|
+
/**
|
|
459
|
+
* `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
|
|
460
|
+
* and taking `handle` on faith there would look for a body that is not
|
|
461
|
+
* the one bound.
|
|
462
|
+
*/
|
|
463
|
+
if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
464
|
+
const [target, member] = entry.getElements();
|
|
465
|
+
const file = target ? listenerFile(target, from, app) : null;
|
|
466
|
+
if (!file) continue;
|
|
467
|
+
const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
468
|
+
refs.push({
|
|
469
|
+
file,
|
|
470
|
+
member: named ?? LISTENER_METHOD
|
|
471
|
+
});
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const file = listenerFile(entry, from, app);
|
|
475
|
+
if (file) refs.push({
|
|
476
|
+
file,
|
|
477
|
+
member: LISTENER_METHOD
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return refs;
|
|
481
|
+
}
|
|
482
|
+
function listenerFile(entry, from, app) {
|
|
483
|
+
if (Node.isPropertyAccessExpression(entry)) {
|
|
484
|
+
const root = entry.getExpression();
|
|
485
|
+
if (!Node.isIdentifier(root)) return null;
|
|
486
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
487
|
+
return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
|
|
488
|
+
}
|
|
489
|
+
if (Node.isIdentifier(entry)) {
|
|
490
|
+
const target = importedFrom(entry.getText(), from, app);
|
|
491
|
+
return target ? toPosix(target) : null;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
/** where a local identifier was imported from, resolved through the alias map */
|
|
496
|
+
function importedFrom(local, from, app) {
|
|
497
|
+
for (const declaration of from.getImportDeclarations()) {
|
|
498
|
+
const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
|
|
499
|
+
const isDefault = declaration.getDefaultImport()?.getText() === local;
|
|
500
|
+
if (!named && !isDefault) continue;
|
|
501
|
+
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The file a key of a generated registry points at.
|
|
507
|
+
*
|
|
508
|
+
* Both shapes the generators emit are handled: a direct reference to an
|
|
509
|
+
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
510
|
+
* per artefact and per framework version, and reading only one of them silently
|
|
511
|
+
* lost half the graph.
|
|
512
|
+
*/
|
|
513
|
+
function registryEntry(registryFile, key, project, app) {
|
|
514
|
+
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
515
|
+
if (!file) return null;
|
|
516
|
+
for (const declaration of file.getVariableDeclarations()) {
|
|
517
|
+
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
518
|
+
if (!value) continue;
|
|
519
|
+
if (Node.isIdentifier(value)) {
|
|
520
|
+
const target = importedFrom(value.getText(), file, app);
|
|
521
|
+
return target ? toPosix(target) : null;
|
|
522
|
+
}
|
|
523
|
+
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
524
|
+
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
525
|
+
return target ? toPosix(target) : null;
|
|
511
526
|
}
|
|
512
527
|
return null;
|
|
513
528
|
}
|
|
@@ -619,6 +634,68 @@ const jobDispatchResolver = {
|
|
|
619
634
|
}
|
|
620
635
|
};
|
|
621
636
|
//#endregion
|
|
637
|
+
//#region src/inventory/resolvers/local_function.ts
|
|
638
|
+
/**
|
|
639
|
+
* "Local function" pattern: a helper declared in the same file, not imported.
|
|
640
|
+
*
|
|
641
|
+
* const lista = await proximos(id) // function proximos() { … }
|
|
642
|
+
* return rows.map(paraLinha) // const paraLinha = (row) => …
|
|
643
|
+
*
|
|
644
|
+
* A query object that keeps its helpers beside it is common, and before this
|
|
645
|
+
* every such call was unresolved: the store a helper read was reached by nobody
|
|
646
|
+
* from that route, and a value built by it left as a whole table handed in.
|
|
647
|
+
*
|
|
648
|
+
* Only module-level declarations: a closure declared inside a body is part of
|
|
649
|
+
* that body, and the graph already walks it.
|
|
650
|
+
*/
|
|
651
|
+
const localFunctionResolver = {
|
|
652
|
+
name: "local-function",
|
|
653
|
+
order: 45,
|
|
654
|
+
resolve(call, ctx) {
|
|
655
|
+
const named = calledFunctionOf(call);
|
|
656
|
+
if (!named) return [];
|
|
657
|
+
const name = named.getText();
|
|
658
|
+
if (ctx.imports.has(name)) return [];
|
|
659
|
+
if (!declaresFunction(ctx.file, name)) return [];
|
|
660
|
+
return [{
|
|
661
|
+
file: ctx.file.getFilePath(),
|
|
662
|
+
member: name
|
|
663
|
+
}];
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
/** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
|
|
667
|
+
const APPLIES_CALLBACK = new Set([
|
|
668
|
+
"map",
|
|
669
|
+
"flatMap",
|
|
670
|
+
"forEach",
|
|
671
|
+
"filter",
|
|
672
|
+
"find",
|
|
673
|
+
"some",
|
|
674
|
+
"every",
|
|
675
|
+
"sort",
|
|
676
|
+
"reduce"
|
|
677
|
+
]);
|
|
678
|
+
/**
|
|
679
|
+
* The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
|
|
680
|
+
* names `paraLinha`, called once per row — the body the graph must read is the
|
|
681
|
+
* same, whichever way it was reached.
|
|
682
|
+
*/
|
|
683
|
+
function calledFunctionOf(call) {
|
|
684
|
+
const expression = call.getExpression();
|
|
685
|
+
if (Node.isIdentifier(expression)) return expression;
|
|
686
|
+
if (Node.isPropertyAccessExpression(expression) && APPLIES_CALLBACK.has(expression.getName())) {
|
|
687
|
+
const callback = call.getArguments()[0];
|
|
688
|
+
if (callback && Node.isIdentifier(callback)) return callback;
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
/** `function name() {}` or `const name = () => {}` / `function () {}` at module level */
|
|
693
|
+
function declaresFunction(file, name) {
|
|
694
|
+
if (file.getFunction(name)) return true;
|
|
695
|
+
const initializer = file.getVariableDeclaration(name)?.getInitializer();
|
|
696
|
+
return !!initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer));
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
622
699
|
//#region src/inventory/resolvers/module_function.ts
|
|
623
700
|
/**
|
|
624
701
|
* "Module function" pattern: no class at all.
|
|
@@ -633,8 +710,8 @@ const moduleFunctionResolver = {
|
|
|
633
710
|
name: "module-function",
|
|
634
711
|
order: 50,
|
|
635
712
|
resolve(call, ctx) {
|
|
636
|
-
const expr = call
|
|
637
|
-
if (!expr
|
|
713
|
+
const expr = calledFunctionOf(call);
|
|
714
|
+
if (!expr) return [];
|
|
638
715
|
const local = expr.getText();
|
|
639
716
|
const file = ctx.imports.get(local);
|
|
640
717
|
if (!file) return [];
|
|
@@ -809,11 +886,37 @@ function outputFieldsIn(body, owner, stores, followed) {
|
|
|
809
886
|
* data function applies (§6).
|
|
810
887
|
*/
|
|
811
888
|
const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
|
|
889
|
+
const { leaves, opaque } = collectLeaves(returnedLiteralsOf(body), {
|
|
890
|
+
qualifier,
|
|
891
|
+
excluded,
|
|
892
|
+
followed
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
outputs: leaves,
|
|
896
|
+
opaqueOutputs: opaque,
|
|
897
|
+
resource: resource && stores.has(resource) ? resource : null
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* The leaves of object literals, by the §7 rules:
|
|
902
|
+
*
|
|
903
|
+
* { titulo: l.titulo } 1 — `titulo`
|
|
904
|
+
* { autor: AutorTransformer.transform } 0 here; the followed body contributes
|
|
905
|
+
* { endereco: { rua, cidade } } leaves individually
|
|
906
|
+
* { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
|
|
907
|
+
* { itens: xs.map((i) => ({ a, b })) } the leaves, once
|
|
908
|
+
* ...this.pick(this.resource, [...]) the listed names
|
|
909
|
+
* ...this.toObject() 0 here; the followed body contributes
|
|
910
|
+
* ...anythingElse 1, opaque, reported
|
|
911
|
+
*/
|
|
912
|
+
function collectLeaves(literals, options) {
|
|
913
|
+
const { qualifier, excluded, followed } = options;
|
|
812
914
|
const outputs = /* @__PURE__ */ new Set();
|
|
813
915
|
const opaque = /* @__PURE__ */ new Set();
|
|
916
|
+
const q = qualifier ? `${qualifier}.` : "";
|
|
814
917
|
const leaf = (prefix, name) => {
|
|
815
918
|
if (prefix === "" && excluded.has(name)) return;
|
|
816
|
-
outputs.add(`${
|
|
919
|
+
outputs.add(`${q}${prefix ? `${prefix}.${name}` : name}`);
|
|
817
920
|
};
|
|
818
921
|
const walk = (literal, prefix) => {
|
|
819
922
|
for (const property of literal.getProperties()) {
|
|
@@ -830,7 +933,7 @@ function outputFieldsIn(body, owner, stores, followed) {
|
|
|
830
933
|
continue;
|
|
831
934
|
}
|
|
832
935
|
if (!Node.isPropertyAssignment(property)) continue;
|
|
833
|
-
const name = property.getName().replace(/^['"]|['"]$/g, "");
|
|
936
|
+
const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
|
|
834
937
|
const value = unwrap(property.getInitializer());
|
|
835
938
|
if (!value) {
|
|
836
939
|
leaf(prefix, name);
|
|
@@ -874,15 +977,14 @@ function outputFieldsIn(body, owner, stores, followed) {
|
|
|
874
977
|
* One DET as a floor, and reported — the placeholder carries the expression
|
|
875
978
|
* so the report can name what could not be read.
|
|
876
979
|
*/
|
|
877
|
-
const placeholder = `${
|
|
980
|
+
const placeholder = `${q}${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
|
|
878
981
|
outputs.add(placeholder);
|
|
879
982
|
opaque.add(placeholder);
|
|
880
983
|
};
|
|
881
|
-
for (const literal of
|
|
984
|
+
for (const literal of literals) walk(literal, "");
|
|
882
985
|
return {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
resource: resource && stores.has(resource) ? resource : null
|
|
986
|
+
leaves: [...outputs],
|
|
987
|
+
opaque: [...opaque]
|
|
886
988
|
};
|
|
887
989
|
}
|
|
888
990
|
/**
|
|
@@ -1034,6 +1136,76 @@ const TRANSFORMER_METHODS = new Set([
|
|
|
1034
1136
|
]);
|
|
1035
1137
|
/** what the application-side body is called */
|
|
1036
1138
|
const APPLICATION_BODY = "toObject";
|
|
1139
|
+
/** the variant named in these is a method of the transformer, and part of what leaves */
|
|
1140
|
+
const VARIANT_METHODS = new Set(["useVariant", "withVariant"]);
|
|
1141
|
+
/**
|
|
1142
|
+
* "Transformer" pattern: the package supplies the API, the application
|
|
1143
|
+
* supplies the body.
|
|
1144
|
+
*
|
|
1145
|
+
* class InviteTransformer extends BaseTransformer<Invite> {
|
|
1146
|
+
* toObject() { … }
|
|
1147
|
+
* }
|
|
1148
|
+
*
|
|
1149
|
+
* InviteTransformer.transform(invite)
|
|
1150
|
+
*
|
|
1151
|
+
* `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
|
|
1152
|
+
* symbol lands on the application file and finds no body there. The naive
|
|
1153
|
+
* reading is that the tracer must step into node_modules; it does not. Those
|
|
1154
|
+
* methods call BACK into `toObject()`, which the application writes, so the
|
|
1155
|
+
* body worth analysing was in the application all along.
|
|
1156
|
+
*
|
|
1157
|
+
* It is the same shape as `job-dispatch`, where `dispatch` enqueues and
|
|
1158
|
+
* `handle` executes.
|
|
1159
|
+
*
|
|
1160
|
+
* COUNTING DECISION: the write a transformer performs belongs to the
|
|
1161
|
+
* transaction that serialised through it. Without this, a table written only
|
|
1162
|
+
* inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
|
|
1163
|
+
*/
|
|
1164
|
+
/** is `.useVariant('x')` / `.withVariant('x')` the next link of this call's chain? */
|
|
1165
|
+
function variantFollows(call) {
|
|
1166
|
+
let node = call;
|
|
1167
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
1168
|
+
const parent = node.getParent();
|
|
1169
|
+
if (!parent) return false;
|
|
1170
|
+
if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
|
|
1171
|
+
node = parent;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) return false;
|
|
1175
|
+
const next = parent.getParent();
|
|
1176
|
+
if (!next || !Node.isCallExpression(next) || next.getExpression() !== parent) return false;
|
|
1177
|
+
if (VARIANT_METHODS.has(parent.getName())) return next.getArguments()[0]?.getKind() === SyntaxKind.StringLiteral;
|
|
1178
|
+
node = next;
|
|
1179
|
+
}
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
/** the transformer class a chain call is on, if it is one: `X.transform(p)`, `X.transform(p).useVariant(v)` */
|
|
1183
|
+
function transformerOf(call, ctx) {
|
|
1184
|
+
const expression = call.getExpression();
|
|
1185
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
1186
|
+
if (!TRANSFORMER_METHODS.has(expression.getName())) return null;
|
|
1187
|
+
/**
|
|
1188
|
+
* The root of the chain, so `X.transform(p).useVariant(v)` resolves as
|
|
1189
|
+
* well as `X.transform(p)`. Analysing the same body twice is free: the
|
|
1190
|
+
* graph dedupes by file and member.
|
|
1191
|
+
*/
|
|
1192
|
+
const symbol = rootSymbolOf(expression.getExpression());
|
|
1193
|
+
if (!symbol) return null;
|
|
1194
|
+
const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
|
|
1195
|
+
if (!file) return null;
|
|
1196
|
+
/**
|
|
1197
|
+
* One definition of what a transformer is, shared with the output walker
|
|
1198
|
+
* (`graph/output_fields.ts`): the resolver must not follow a body whose keys
|
|
1199
|
+
* the walker would refuse to read, or the other way round.
|
|
1200
|
+
*/
|
|
1201
|
+
const declared = ctx.sourceFile(file);
|
|
1202
|
+
if (!declared || !declared.getClasses().some(isTransformerClass)) return null;
|
|
1203
|
+
const owner = declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY));
|
|
1204
|
+
return owner ? {
|
|
1205
|
+
file,
|
|
1206
|
+
owner
|
|
1207
|
+
} : null;
|
|
1208
|
+
}
|
|
1037
1209
|
//#endregion
|
|
1038
1210
|
//#region src/inventory/resolvers/index.ts
|
|
1039
1211
|
/**
|
|
@@ -1051,34 +1223,50 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
1051
1223
|
{
|
|
1052
1224
|
name: "transformer",
|
|
1053
1225
|
order: 18,
|
|
1226
|
+
/**
|
|
1227
|
+
* The `transform` before a `useVariant`: claimed and followed nowhere, or
|
|
1228
|
+
* `static-service` would take it next, look for a `transform` body in the
|
|
1229
|
+
* application file and report the package method as a gap.
|
|
1230
|
+
*/
|
|
1231
|
+
ignores(call, ctx) {
|
|
1232
|
+
const expression = call.getExpression();
|
|
1233
|
+
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
1234
|
+
if (VARIANT_METHODS.has(expression.getName()) || !variantFollows(call)) return false;
|
|
1235
|
+
return transformerOf(call, ctx) !== null;
|
|
1236
|
+
},
|
|
1054
1237
|
resolve(call, ctx) {
|
|
1055
1238
|
const expression = call.getExpression();
|
|
1056
1239
|
if (!Node.isPropertyAccessExpression(expression)) return [];
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
* well as `X.transform(p)`. Analysing the same body twice is free: the
|
|
1061
|
-
* graph dedupes by file and member.
|
|
1062
|
-
*/
|
|
1063
|
-
const symbol = rootSymbolOf(expression.getExpression());
|
|
1064
|
-
if (!symbol) return [];
|
|
1065
|
-
const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
|
|
1066
|
-
if (!file) return [];
|
|
1240
|
+
const found = transformerOf(call, ctx);
|
|
1241
|
+
if (!found) return [];
|
|
1242
|
+
const { file, owner } = found;
|
|
1067
1243
|
/**
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
* the
|
|
1244
|
+
* `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
|
|
1245
|
+
* transformer, named after it, and it REPLACES `toObject()` — the shape that
|
|
1246
|
+
* leaves is the variant's. The chain is visited call by call: the `useVariant`
|
|
1247
|
+
* call resolves the variant's body, and the `transform` call before it resolves
|
|
1248
|
+
* nothing, or both shapes would leave and the page would count twice.
|
|
1071
1249
|
*/
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1250
|
+
if (VARIANT_METHODS.has(expression.getName())) {
|
|
1251
|
+
const variant = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
1252
|
+
return variant && owner.getMethod(variant) ? [{
|
|
1253
|
+
file,
|
|
1254
|
+
member: variant
|
|
1255
|
+
}] : [{
|
|
1256
|
+
file,
|
|
1257
|
+
member: APPLICATION_BODY
|
|
1258
|
+
}];
|
|
1259
|
+
}
|
|
1260
|
+
if (variantFollows(call)) return [];
|
|
1261
|
+
return [{
|
|
1075
1262
|
file,
|
|
1076
1263
|
member: APPLICATION_BODY
|
|
1077
|
-
}]
|
|
1264
|
+
}];
|
|
1078
1265
|
}
|
|
1079
1266
|
},
|
|
1080
1267
|
staticServiceResolver,
|
|
1081
1268
|
propertyServiceResolver,
|
|
1269
|
+
localFunctionResolver,
|
|
1082
1270
|
moduleFunctionResolver
|
|
1083
1271
|
];
|
|
1084
1272
|
/**
|
|
@@ -1147,4 +1335,4 @@ function ignoreCalls(options) {
|
|
|
1147
1335
|
};
|
|
1148
1336
|
}
|
|
1149
1337
|
//#endregion
|
|
1150
|
-
export { chainShapeOf as a,
|
|
1338
|
+
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as IncomparableSourcesError, d as DEFAULTS, f as defineConfig, i as measureStructure, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as analyze } from "./pipeline-
|
|
2
|
+
import { y as toPosix } from "./resolvers-DhJO-qvQ.js";
|
|
3
|
+
import { n as analyze } from "./pipeline-C6kHKB9-.js";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -3,6 +3,7 @@ import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
|
3
3
|
import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
4
4
|
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
5
5
|
import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
|
|
6
|
+
import type { CollectedJob } from '../inventory/sources/jobs.js';
|
|
6
7
|
import type { OpaqueDeclaration } from './opaque.js';
|
|
7
8
|
import type { Complexity, CountResult, FunctionType } from '../types.js';
|
|
8
9
|
import type { FunctionOverride } from '../define_config.js';
|
|
@@ -36,12 +37,20 @@ export declare const RULESET = "afp";
|
|
|
36
37
|
* column; system timestamps and `serializeAs: null` columns leaving the DETs;
|
|
37
38
|
* master-detail folded into one data function; identity by table; token tables
|
|
38
39
|
* technical; and opaque declarations reaching every function carrying the origin.
|
|
40
|
+
* Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
|
|
41
|
+
* props, the response payload, what a command prints) read back to their origin;
|
|
42
|
+
* a function of the same file and a `.map(fn)` by reference are followed, so
|
|
43
|
+
* FTRs move; and ace commands are transactions, with flags as input. One in
|
|
44
|
+
* 1.7.0, wide: a write binds to what the variable IS — a destructured named
|
|
45
|
+
* interface, a followed method's return, a relation off a row, a loop over rows,
|
|
46
|
+
* the guard's user, a service the container made — and a write nobody can type is
|
|
47
|
+
* an unresolved call instead of silence.
|
|
39
48
|
*
|
|
40
49
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
41
50
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
42
51
|
* exists for exactly that, and only this constant arms it.
|
|
43
52
|
*/
|
|
44
|
-
export declare const RULESET_VERSION = "1.
|
|
53
|
+
export declare const RULESET_VERSION = "1.7.0";
|
|
45
54
|
export type CountInput = {
|
|
46
55
|
app: AppContext;
|
|
47
56
|
stores: CollectedDataStore[];
|
|
@@ -50,6 +59,8 @@ export type CountInput = {
|
|
|
50
59
|
behaviors: Map<string, Behavior>;
|
|
51
60
|
/** JSON Schema literals found in the code, for `detFromSchema` — §8 */
|
|
52
61
|
jsonSchemas?: Map<string, DiscoveredSchema>;
|
|
62
|
+
/** the queue jobs and who dispatches each — a job no transaction reaches is reported (plan 0.7 §D) */
|
|
63
|
+
jobs?: CollectedJob[];
|
|
53
64
|
/**
|
|
54
65
|
* Stores written anywhere in the application's code, reachable from an entry
|
|
55
66
|
* point or not — AFP §6.5.4 asks who MAINTAINS the store, and a job or a
|
package/build/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CoverageTooLowError } from "../pipeline-
|
|
2
|
-
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-
|
|
1
|
+
import { t as CoverageTooLowError } from "../pipeline-C6kHKB9-.js";
|
|
2
|
+
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-BpBJsGMj.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -53,6 +53,8 @@ export type Behavior = {
|
|
|
53
53
|
* not a validator.
|
|
54
54
|
*/
|
|
55
55
|
requestFields: string[];
|
|
56
|
+
/** an ace command's `@flags.*` / `@args.*`: `flags.limite`, `args.name` — its input DETs */
|
|
57
|
+
commandFields: string[];
|
|
56
58
|
/** the transaction reads the request in a way that enumerates nothing */
|
|
57
59
|
opaqueRequest: boolean;
|
|
58
60
|
/**
|
|
@@ -82,6 +84,26 @@ export type Behavior = {
|
|
|
82
84
|
/** stores it was preloaded through */
|
|
83
85
|
via: string[];
|
|
84
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* What the transaction DELIVERS — plan 0.7 §A′. `any` says a delivery point was
|
|
89
|
+
* found at all; without one the output falls back to the stores read. `fields`
|
|
90
|
+
* are derived values and the leaves of literals a followed body returned,
|
|
91
|
+
* `render:total`; `stores` are the ones whose rows were handed on, raw or through
|
|
92
|
+
* a query object; `opaqueFields` are values nobody could read, 1 DET each.
|
|
93
|
+
*/
|
|
94
|
+
delivered: {
|
|
95
|
+
any: boolean;
|
|
96
|
+
fields: string[];
|
|
97
|
+
opaqueFields: string[];
|
|
98
|
+
stores: string[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Of the stores delivered raw to a page, the columns the page reads off them
|
|
102
|
+
* (plan 0.8 §D). A store absent here leaves whole; one in `unreadablePages` leaves
|
|
103
|
+
* whole because the page could not be read, and says why.
|
|
104
|
+
*/
|
|
105
|
+
pageReads: Record<string, string[]>;
|
|
106
|
+
unreadablePages: Record<string, string>;
|
|
85
107
|
trace: TraceStep[];
|
|
86
108
|
/** bodies reached, for `fp:diff` */
|
|
87
109
|
scope: ScopeEntry[];
|