@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +103 -2
  3. package/build/commands/fp_metrics.d.ts +10 -0
  4. package/build/commands/main.d.ts +6 -5
  5. package/build/commands/main.js +48 -114
  6. package/build/decorate-D6enDn9D.js +24 -0
  7. package/build/fp_calibrate-Cm079xWL.js +25 -0
  8. package/build/fp_count-CfcXPuj5.js +22 -0
  9. package/build/fp_diff-DE_t3twv.js +25 -0
  10. package/build/fp_explain-MKyEoi0h.js +24 -0
  11. package/build/fp_inventory-DSrCVhEy.js +18 -0
  12. package/build/fp_metrics-BUWj9dLw.js +20 -0
  13. package/build/index.d.ts +2 -1
  14. package/build/index.js +3 -2
  15. package/build/{pipeline-BzP-ITGN.js → pipeline-DySlMWcN.js} +298 -40
  16. package/build/{resolvers-CU9HKYpn.js → resolvers-MFjRl2ef.js} +225 -5
  17. package/build/{runners-Bt8tbISi.js → runners-DpMd-yZM.js} +252 -64
  18. package/build/src/albrecht/counter.d.ts +17 -1
  19. package/build/src/albrecht/data_functions.d.ts +6 -0
  20. package/build/src/albrecht/diff.d.ts +19 -1
  21. package/build/src/cli/runners.d.ts +12 -0
  22. package/build/src/cli.js +11 -2
  23. package/build/src/define_config.d.ts +35 -1
  24. package/build/src/inventory/graph/call_graph.d.ts +27 -0
  25. package/build/src/inventory/graph/noise.d.ts +13 -0
  26. package/build/src/inventory/resolvers/event_dispatch.d.ts +17 -0
  27. package/build/src/inventory/resolvers/index.js +1 -1
  28. package/build/src/inventory/resolvers/types.d.ts +35 -0
  29. package/build/src/inventory/sources/event_bindings.d.ts +44 -0
  30. package/build/src/metrics/structure.d.ts +16 -2
  31. package/build/src/pipeline.js +1 -1
  32. package/build/src/reporters/table.d.ts +11 -1
  33. package/build/src/types.d.ts +10 -0
  34. package/build/stubs/config.stub +27 -1
  35. package/package.json +2 -1
  36. package/build/scripts/smoke_package.d.ts +0 -1
  37. package/build/tmp/probe.d.ts +0 -1
  38. package/build/tmp/probe_cli.d.ts +0 -1
  39. package/build/tmp/probe_cmp.d.ts +0 -1
  40. package/build/tmp/probe_count.d.ts +0 -1
  41. package/build/tmp/probe_data.d.ts +0 -1
  42. package/build/tmp/probe_diff.d.ts +0 -1
  43. package/build/tmp/probe_gap.d.ts +0 -1
  44. package/build/tmp/probe_graph.d.ts +0 -1
  45. package/build/tmp/probe_metrics.d.ts +0 -1
  46. package/build/tmp/probe_miss.d.ts +0 -1
  47. package/build/tmp/probe_names.d.ts +0 -1
  48. package/build/tmp/probe_nodata.d.ts +0 -1
  49. package/build/tmp/probe_one.d.ts +0 -1
  50. package/build/tmp/probe_perf.d.ts +0 -1
  51. package/build/tmp/probe_routes.d.ts +0 -1
  52. package/build/tmp/probe_unres.d.ts +0 -1
  53. package/build/tmp/probe_vazquez.d.ts +0 -1
  54. 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: ctx.sourceFile(file)?.getClasses().some((c) => c.getMethod("handle")) ? "handle" : expr.getName()
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 file = ctx.imports.get(expr.getText());
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: expr.getText()
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 };