@filipebraida/adonis-function-points 0.1.0 → 0.2.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 +99 -0
- package/README.md +103 -2
- package/build/commands/fp_metrics.d.ts +10 -0
- package/build/commands/main.d.ts +6 -5
- package/build/commands/main.js +48 -114
- package/build/decorate-D6enDn9D.js +24 -0
- package/build/fp_calibrate-Cm079xWL.js +25 -0
- package/build/fp_count-CfcXPuj5.js +22 -0
- package/build/fp_diff-DE_t3twv.js +25 -0
- package/build/fp_explain-MKyEoi0h.js +24 -0
- package/build/fp_inventory-DSrCVhEy.js +18 -0
- package/build/fp_metrics-BUWj9dLw.js +20 -0
- package/build/index.d.ts +2 -1
- package/build/index.js +3 -2
- package/build/{pipeline-BzP-ITGN.js → pipeline-DySlMWcN.js} +298 -40
- package/build/{resolvers-CU9HKYpn.js → resolvers-MFjRl2ef.js} +225 -5
- package/build/{runners-Bt8tbISi.js → runners-DpMd-yZM.js} +252 -64
- package/build/src/albrecht/counter.d.ts +17 -1
- package/build/src/albrecht/data_functions.d.ts +6 -0
- package/build/src/albrecht/diff.d.ts +19 -1
- package/build/src/cli/runners.d.ts +12 -0
- package/build/src/cli.js +11 -2
- package/build/src/define_config.d.ts +35 -1
- package/build/src/inventory/graph/call_graph.d.ts +27 -0
- package/build/src/inventory/graph/noise.d.ts +13 -0
- package/build/src/inventory/resolvers/event_dispatch.d.ts +17 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/inventory/resolvers/types.d.ts +35 -0
- package/build/src/inventory/sources/event_bindings.d.ts +44 -0
- package/build/src/metrics/structure.d.ts +16 -2
- package/build/src/pipeline.js +1 -1
- package/build/src/reporters/table.d.ts +11 -1
- package/build/src/types.d.ts +10 -0
- package/build/stubs/config.stub +27 -1
- package/package.json +2 -1
- package/build/scripts/smoke_package.d.ts +0 -1
- package/build/tmp/probe.d.ts +0 -1
- package/build/tmp/probe_cli.d.ts +0 -1
- package/build/tmp/probe_cmp.d.ts +0 -1
- package/build/tmp/probe_count.d.ts +0 -1
- package/build/tmp/probe_data.d.ts +0 -1
- package/build/tmp/probe_diff.d.ts +0 -1
- package/build/tmp/probe_gap.d.ts +0 -1
- package/build/tmp/probe_graph.d.ts +0 -1
- package/build/tmp/probe_metrics.d.ts +0 -1
- package/build/tmp/probe_miss.d.ts +0 -1
- package/build/tmp/probe_names.d.ts +0 -1
- package/build/tmp/probe_nodata.d.ts +0 -1
- package/build/tmp/probe_one.d.ts +0 -1
- package/build/tmp/probe_perf.d.ts +0 -1
- package/build/tmp/probe_routes.d.ts +0 -1
- package/build/tmp/probe_unres.d.ts +0 -1
- package/build/tmp/probe_vazquez.d.ts +0 -1
- package/build/tsdown.config.d.ts +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Node, SyntaxKind } from "ts-morph";
|
|
1
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
2
2
|
//#region src/inventory/resolvers/action_object.ts
|
|
3
3
|
/**
|
|
4
4
|
* "Action object" pattern: the transaction delegates to an action instantiated
|
|
@@ -50,14 +50,211 @@ function classOfReceiver(receiver) {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
+
//#region src/inventory/paths.ts
|
|
54
|
+
/**
|
|
55
|
+
* One canonical spelling for every path the inventory emits.
|
|
56
|
+
*
|
|
57
|
+
* Two path styles meet in this package. ts-morph always returns forward
|
|
58
|
+
* slashes, including on Windows; node's `path.join` returns backslashes there.
|
|
59
|
+
* Both end up in `HandlerRef.file`, and the call graph uses that string as a
|
|
60
|
+
* cache key:
|
|
61
|
+
*
|
|
62
|
+
* const key = `${ref.file}#${ref.member ?? ref.line ?? '*'}`
|
|
63
|
+
*
|
|
64
|
+
* Two spellings of the same file are two keys, so the same body would be
|
|
65
|
+
* analysed twice and pushed twice into the trace and the implementation scope
|
|
66
|
+
* — and a repeated scope entry changes the hash `fp:diff` compares.
|
|
67
|
+
*
|
|
68
|
+
* Forward slashes win because ts-morph cannot be told otherwise, node's `fs`
|
|
69
|
+
* accepts them on Windows, and `path.relative` normalises mixed input anyway.
|
|
70
|
+
* Normalising at the boundary where a path is created costs one call; leaving
|
|
71
|
+
* it to each comparison costs vigilance forever.
|
|
72
|
+
*/
|
|
73
|
+
const toPosix = (value) => value.split("\\").join("/");
|
|
74
|
+
/** Compares two paths that may have come from different sources. */
|
|
75
|
+
const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/inventory/sources/event_bindings.ts
|
|
78
|
+
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
79
|
+
const LISTENER_METHOD = "handle";
|
|
80
|
+
function collectEventBindings(app) {
|
|
81
|
+
const project = new Project({
|
|
82
|
+
skipAddingFilesFromTsConfig: true,
|
|
83
|
+
skipFileDependencyResolution: true,
|
|
84
|
+
compilerOptions: { allowJs: false }
|
|
85
|
+
});
|
|
86
|
+
for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
87
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
88
|
+
for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
89
|
+
const expression = call.getExpression();
|
|
90
|
+
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
91
|
+
if (expression.getName() !== "on") continue;
|
|
92
|
+
const [event, handlers] = call.getArguments();
|
|
93
|
+
if (!event || !handlers) continue;
|
|
94
|
+
const eventFile = resolveEventClass(event, file, app);
|
|
95
|
+
if (!eventFile) continue;
|
|
96
|
+
const refs = listenersOf(handlers, file, app);
|
|
97
|
+
if (refs.length === 0) continue;
|
|
98
|
+
bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
|
|
99
|
+
}
|
|
100
|
+
return bindings;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The event class a dispatch or a binding names.
|
|
104
|
+
*
|
|
105
|
+
* Two shapes reach here: the class imported directly, and the generated
|
|
106
|
+
* registry (`events.OrderPlaced`), which is what `node ace make:event` produces
|
|
107
|
+
* and therefore the common one. Exported because the resolver has to ask the
|
|
108
|
+
* same question of a call site, and two implementations of "which event is
|
|
109
|
+
* this" would drift.
|
|
110
|
+
*/
|
|
111
|
+
function resolveEventClass(expression, from, app) {
|
|
112
|
+
if (Node.isIdentifier(expression)) {
|
|
113
|
+
const target = importedFrom(expression.getText(), from, app);
|
|
114
|
+
return target ? toPosix(target) : null;
|
|
115
|
+
}
|
|
116
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
117
|
+
const root = expression.getExpression();
|
|
118
|
+
if (!Node.isIdentifier(root)) return null;
|
|
119
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
120
|
+
if (!registry) return null;
|
|
121
|
+
return registryEntry(registry, expression.getName(), from.getProject(), app);
|
|
122
|
+
}
|
|
123
|
+
/** listener bodies named by the second argument of `emitter.on` */
|
|
124
|
+
function listenersOf(handlers, from, app) {
|
|
125
|
+
const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
|
|
126
|
+
const refs = [];
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
/**
|
|
129
|
+
* `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
|
|
130
|
+
* and taking `handle` on faith there would look for a body that is not
|
|
131
|
+
* the one bound.
|
|
132
|
+
*/
|
|
133
|
+
if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
134
|
+
const [target, member] = entry.getElements();
|
|
135
|
+
const file = target ? listenerFile(target, from, app) : null;
|
|
136
|
+
if (!file) continue;
|
|
137
|
+
const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
138
|
+
refs.push({
|
|
139
|
+
file,
|
|
140
|
+
member: named ?? LISTENER_METHOD
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const file = listenerFile(entry, from, app);
|
|
145
|
+
if (file) refs.push({
|
|
146
|
+
file,
|
|
147
|
+
member: LISTENER_METHOD
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return refs;
|
|
151
|
+
}
|
|
152
|
+
function listenerFile(entry, from, app) {
|
|
153
|
+
if (Node.isPropertyAccessExpression(entry)) {
|
|
154
|
+
const root = entry.getExpression();
|
|
155
|
+
if (!Node.isIdentifier(root)) return null;
|
|
156
|
+
const registry = importedFrom(root.getText(), from, app);
|
|
157
|
+
return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
|
|
158
|
+
}
|
|
159
|
+
if (Node.isIdentifier(entry)) {
|
|
160
|
+
const target = importedFrom(entry.getText(), from, app);
|
|
161
|
+
return target ? toPosix(target) : null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/** where a local identifier was imported from, resolved through the alias map */
|
|
166
|
+
function importedFrom(local, from, app) {
|
|
167
|
+
for (const declaration of from.getImportDeclarations()) {
|
|
168
|
+
const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
|
|
169
|
+
const isDefault = declaration.getDefaultImport()?.getText() === local;
|
|
170
|
+
if (!named && !isDefault) continue;
|
|
171
|
+
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The file a key of a generated registry points at.
|
|
177
|
+
*
|
|
178
|
+
* Both shapes the generators emit are handled: a direct reference to an
|
|
179
|
+
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
180
|
+
* per artefact and per framework version, and reading only one of them silently
|
|
181
|
+
* lost half the graph.
|
|
182
|
+
*/
|
|
183
|
+
function registryEntry(registryFile, key, project, app) {
|
|
184
|
+
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
185
|
+
if (!file) return null;
|
|
186
|
+
for (const declaration of file.getVariableDeclarations()) {
|
|
187
|
+
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
188
|
+
if (!value) continue;
|
|
189
|
+
if (Node.isIdentifier(value)) {
|
|
190
|
+
const target = importedFrom(value.getText(), file, app);
|
|
191
|
+
return target ? toPosix(target) : null;
|
|
192
|
+
}
|
|
193
|
+
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
194
|
+
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
195
|
+
return target ? toPosix(target) : null;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/inventory/resolvers/event_dispatch.ts
|
|
201
|
+
/**
|
|
202
|
+
* "Event" pattern: the handler announces, and listeners act.
|
|
203
|
+
*
|
|
204
|
+
* await events.OrderPlaced.dispatch(order.id)
|
|
205
|
+
*
|
|
206
|
+
* Runs BEFORE `job-dispatch`, which matches `Identifier.dispatch(args)` — the
|
|
207
|
+
* shape the direct form takes. Left to it, the event class was resolved and
|
|
208
|
+
* searched for a `handle` it does not declare (`dispatch` comes from
|
|
209
|
+
* `BaseEvent`), so the call was reported as an unknown and the listeners' reads
|
|
210
|
+
* and writes went uncounted.
|
|
211
|
+
*
|
|
212
|
+
* COUNTING DECISION: the same one taken for a job. The user clicks, the effect
|
|
213
|
+
* happens, and AFP §6.5.3 requires aggregating every path the transaction
|
|
214
|
+
* reaches — the emitter is an implementation detail of how it gets there.
|
|
215
|
+
*/
|
|
216
|
+
const eventDispatchResolver = {
|
|
217
|
+
name: "event-dispatch",
|
|
218
|
+
order: 12,
|
|
219
|
+
resolve(call, ctx) {
|
|
220
|
+
if (ctx.eventBindings.size === 0) return [];
|
|
221
|
+
const expression = call.getExpression();
|
|
222
|
+
if (!expression.isKind(SyntaxKind.PropertyAccessExpression)) return [];
|
|
223
|
+
if (expression.getName() !== "dispatch") return [];
|
|
224
|
+
const receiver = expression.getExpression();
|
|
225
|
+
if (!Node.isIdentifier(receiver) && !Node.isPropertyAccessExpression(receiver)) return [];
|
|
226
|
+
const eventFile = resolveEventClass(receiver, ctx.file, { resolveSpecifier: ctx.resolveSpecifier });
|
|
227
|
+
return eventFile ? ctx.eventBindings.get(eventFile) ?? [] : [];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
//#endregion
|
|
53
231
|
//#region src/inventory/resolvers/job_dispatch.ts
|
|
54
232
|
const DISPATCH_METHODS = new Set([
|
|
55
233
|
"dispatch",
|
|
234
|
+
"dispatchMany",
|
|
56
235
|
"dispatchLater",
|
|
57
236
|
"enqueue",
|
|
58
237
|
"later"
|
|
59
238
|
]);
|
|
60
239
|
/**
|
|
240
|
+
* The method that actually runs the job, by queue package.
|
|
241
|
+
*
|
|
242
|
+
* There is no single name: `@adonisjs/queue` and `@rlanz/bull-queue` call it
|
|
243
|
+
* `handle`, `@nemoventures/adonis-jobs` calls it `process`. Looking only for
|
|
244
|
+
* `handle` meant every job in an application using the second one resolved to
|
|
245
|
+
* a file and then to no body, so the dispatch was reported as an unknown while
|
|
246
|
+
* the writes inside it went uncounted — the worst of both outcomes.
|
|
247
|
+
*
|
|
248
|
+
* Ordered: a class declaring more than one is answering the dispatcher with the
|
|
249
|
+
* first, and `handle` is the most common.
|
|
250
|
+
*/
|
|
251
|
+
const EXECUTION_METHODS = [
|
|
252
|
+
"handle",
|
|
253
|
+
"process",
|
|
254
|
+
"run",
|
|
255
|
+
"perform"
|
|
256
|
+
];
|
|
257
|
+
/**
|
|
61
258
|
* "Job" pattern: the write happens asynchronously.
|
|
62
259
|
*
|
|
63
260
|
* await CreateUserJob.dispatch({ userId })
|
|
@@ -86,9 +283,16 @@ const jobDispatchResolver = {
|
|
|
86
283
|
if (ctx.dataStoresBySymbol.has(symbol)) return [];
|
|
87
284
|
const file = ctx.imports.get(symbol);
|
|
88
285
|
if (!file) return [];
|
|
286
|
+
/**
|
|
287
|
+
* `dispatch` enqueues; the execution method is what touches data. When the
|
|
288
|
+
* class declares one, that is the body that matters — and when it declares
|
|
289
|
+
* none, the dispatch name is kept so the gap stays visible instead of being
|
|
290
|
+
* quietly attributed to a body nobody found.
|
|
291
|
+
*/
|
|
292
|
+
const classes = ctx.sourceFile(file)?.getClasses() ?? [];
|
|
89
293
|
return [{
|
|
90
294
|
file,
|
|
91
|
-
member:
|
|
295
|
+
member: EXECUTION_METHODS.find((name) => classes.some((c) => c.getMethod(name))) ?? expr.getName()
|
|
92
296
|
}];
|
|
93
297
|
}
|
|
94
298
|
};
|
|
@@ -109,11 +313,17 @@ const moduleFunctionResolver = {
|
|
|
109
313
|
resolve(call, ctx) {
|
|
110
314
|
const expr = call.getExpression();
|
|
111
315
|
if (!expr.isKind(SyntaxKind.Identifier)) return [];
|
|
112
|
-
const
|
|
316
|
+
const local = expr.getText();
|
|
317
|
+
const file = ctx.imports.get(local);
|
|
113
318
|
if (!file) return [];
|
|
319
|
+
/**
|
|
320
|
+
* The body carries the exported name, not the local one. Following the
|
|
321
|
+
* local name through an alias finds nothing and reports the call as
|
|
322
|
+
* unresolved for a reason that is not true.
|
|
323
|
+
*/
|
|
114
324
|
return [{
|
|
115
325
|
file,
|
|
116
|
-
member:
|
|
326
|
+
member: ctx.exportedAs.get(local) ?? local
|
|
117
327
|
}];
|
|
118
328
|
}
|
|
119
329
|
};
|
|
@@ -526,6 +736,7 @@ function extendsTransformer(file) {
|
|
|
526
736
|
const BUILTIN_CALL_RESOLVERS = [
|
|
527
737
|
sameClassMethodResolver,
|
|
528
738
|
actionObjectResolver,
|
|
739
|
+
eventDispatchResolver,
|
|
529
740
|
jobDispatchResolver,
|
|
530
741
|
transformerResolver,
|
|
531
742
|
staticServiceResolver,
|
|
@@ -543,6 +754,15 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
543
754
|
*/
|
|
544
755
|
function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
545
756
|
for (const resolver of resolvers) {
|
|
757
|
+
/**
|
|
758
|
+
* Asked before `resolve`, and in the same order: a strategy that claims a
|
|
759
|
+
* call as data-free must not be overtaken by a later, more generic one
|
|
760
|
+
* following it into a body it has no business reading.
|
|
761
|
+
*/
|
|
762
|
+
if (resolver.ignores?.(call, ctx)) return {
|
|
763
|
+
by: resolver.name,
|
|
764
|
+
refs: []
|
|
765
|
+
};
|
|
546
766
|
const refs = resolver.resolve(call, ctx);
|
|
547
767
|
if (refs.length > 0) return {
|
|
548
768
|
by: resolver.name,
|
|
@@ -552,4 +772,4 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
552
772
|
return null;
|
|
553
773
|
}
|
|
554
774
|
//#endregion
|
|
555
|
-
export { rootSymbolOf as a, hooksFiredBy as i, resolveCall as n, detectAccess as r, BUILTIN_CALL_RESOLVERS as t };
|
|
775
|
+
export { rootSymbolOf as a, toPosix as c, hooksFiredBy as i, resolveCall as n, collectEventBindings as o, detectAccess as r, samePath as s, BUILTIN_CALL_RESOLVERS as t };
|