@filipebraida/adonis-function-points 0.1.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 (80) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +427 -0
  3. package/bin/cli.js +4 -0
  4. package/build/commands/fp_calibrate.d.ts +16 -0
  5. package/build/commands/fp_count.d.ts +11 -0
  6. package/build/commands/fp_diff.d.ts +16 -0
  7. package/build/commands/fp_explain.d.ts +15 -0
  8. package/build/commands/fp_inventory.d.ts +9 -0
  9. package/build/commands/main.d.ts +5 -0
  10. package/build/commands/main.js +120 -0
  11. package/build/commands/printer.d.ts +9 -0
  12. package/build/configure.d.ts +2 -0
  13. package/build/configure.js +10 -0
  14. package/build/define_config-DOqWyPwV.js +19 -0
  15. package/build/index.d.ts +5 -0
  16. package/build/index.js +4 -0
  17. package/build/pipeline-BzP-ITGN.js +2306 -0
  18. package/build/resolvers-CU9HKYpn.js +555 -0
  19. package/build/runners-Bt8tbISi.js +630 -0
  20. package/build/scripts/smoke_package.d.ts +1 -0
  21. package/build/src/albrecht/calibration.d.ts +62 -0
  22. package/build/src/albrecht/counter.d.ts +48 -0
  23. package/build/src/albrecht/data_functions.d.ts +32 -0
  24. package/build/src/albrecht/diff.d.ts +66 -0
  25. package/build/src/albrecht/index.d.ts +13 -0
  26. package/build/src/albrecht/tables.d.ts +19 -0
  27. package/build/src/albrecht/technical_filter.d.ts +25 -0
  28. package/build/src/albrecht/transactional_functions.d.ts +35 -0
  29. package/build/src/cli/load_config.d.ts +28 -0
  30. package/build/src/cli/print.d.ts +19 -0
  31. package/build/src/cli/runners.d.ts +52 -0
  32. package/build/src/cli.d.ts +19 -0
  33. package/build/src/cli.js +198 -0
  34. package/build/src/define_config.d.ts +137 -0
  35. package/build/src/inventory/app_context.d.ts +73 -0
  36. package/build/src/inventory/detectors/lucid.d.ts +77 -0
  37. package/build/src/inventory/graph/call_graph.d.ts +80 -0
  38. package/build/src/inventory/graph/noise.d.ts +9 -0
  39. package/build/src/inventory/index.d.ts +15 -0
  40. package/build/src/inventory/paths.d.ts +22 -0
  41. package/build/src/inventory/resolvers/action_object.d.ts +14 -0
  42. package/build/src/inventory/resolvers/index.d.ts +23 -0
  43. package/build/src/inventory/resolvers/index.js +2 -0
  44. package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
  45. package/build/src/inventory/resolvers/module_function.d.ts +11 -0
  46. package/build/src/inventory/resolvers/property_service.d.ts +18 -0
  47. package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
  48. package/build/src/inventory/resolvers/static_service.d.ts +13 -0
  49. package/build/src/inventory/resolvers/transformer.d.ts +25 -0
  50. package/build/src/inventory/resolvers/types.d.ts +65 -0
  51. package/build/src/inventory/source.d.ts +39 -0
  52. package/build/src/inventory/sources/data_stores.d.ts +31 -0
  53. package/build/src/inventory/sources/json_schemas.d.ts +32 -0
  54. package/build/src/inventory/sources/routes_ast.d.ts +28 -0
  55. package/build/src/metrics/structure.d.ts +72 -0
  56. package/build/src/pipeline.d.ts +44 -0
  57. package/build/src/pipeline.js +2 -0
  58. package/build/src/reporters/table.d.ts +6 -0
  59. package/build/src/types.d.ts +256 -0
  60. package/build/src/types.js +1 -0
  61. package/build/stubs/config.stub +37 -0
  62. package/build/tmp/probe.d.ts +1 -0
  63. package/build/tmp/probe_cli.d.ts +1 -0
  64. package/build/tmp/probe_cmp.d.ts +1 -0
  65. package/build/tmp/probe_count.d.ts +1 -0
  66. package/build/tmp/probe_data.d.ts +1 -0
  67. package/build/tmp/probe_diff.d.ts +1 -0
  68. package/build/tmp/probe_gap.d.ts +1 -0
  69. package/build/tmp/probe_graph.d.ts +1 -0
  70. package/build/tmp/probe_metrics.d.ts +1 -0
  71. package/build/tmp/probe_miss.d.ts +1 -0
  72. package/build/tmp/probe_names.d.ts +1 -0
  73. package/build/tmp/probe_nodata.d.ts +1 -0
  74. package/build/tmp/probe_one.d.ts +1 -0
  75. package/build/tmp/probe_perf.d.ts +1 -0
  76. package/build/tmp/probe_routes.d.ts +1 -0
  77. package/build/tmp/probe_unres.d.ts +1 -0
  78. package/build/tmp/probe_vazquez.d.ts +1 -0
  79. package/build/tsdown.config.d.ts +2 -0
  80. package/package.json +133 -0
@@ -0,0 +1,555 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+ //#region src/inventory/resolvers/action_object.ts
3
+ /**
4
+ * "Action object" pattern: the transaction delegates to an action instantiated
5
+ * at the call site.
6
+ *
7
+ * await new ExpireInvite().handle({ invite })
8
+ *
9
+ * const mark = new MarkContentChanged()
10
+ * await mark.handle({ documentId })
11
+ *
12
+ * The second form keeps the instance in a local variable, so the declaration
13
+ * has to be followed back to the `new` — that is what `classOfReceiver` does.
14
+ */
15
+ const actionObjectResolver = {
16
+ name: "action-object",
17
+ order: 10,
18
+ resolve(call, ctx) {
19
+ const expr = call.getExpression();
20
+ if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
21
+ const member = expr.getName();
22
+ const className = classOfReceiver(expr.getExpression());
23
+ if (!className) return [];
24
+ const file = ctx.imports.get(className);
25
+ if (!file) return [];
26
+ return [{
27
+ file,
28
+ member
29
+ }];
30
+ }
31
+ };
32
+ /**
33
+ * Finds the class behind a call receiver.
34
+ *
35
+ * new Foo().handle() -> 'Foo'
36
+ * foo.handle() where const foo = new Foo() -> 'Foo'
37
+ */
38
+ function classOfReceiver(receiver) {
39
+ if (receiver.isKind(SyntaxKind.NewExpression)) {
40
+ const target = receiver.getExpression();
41
+ return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
42
+ }
43
+ if (receiver.isKind(SyntaxKind.Identifier)) {
44
+ const init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
45
+ if (init?.isKind(SyntaxKind.NewExpression)) {
46
+ const target = init.getExpression();
47
+ return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ //#endregion
53
+ //#region src/inventory/resolvers/job_dispatch.ts
54
+ const DISPATCH_METHODS = new Set([
55
+ "dispatch",
56
+ "dispatchLater",
57
+ "enqueue",
58
+ "later"
59
+ ]);
60
+ /**
61
+ * "Job" pattern: the write happens asynchronously.
62
+ *
63
+ * await CreateUserJob.dispatch({ userId })
64
+ *
65
+ * Runs before `static-service` on purpose: the syntactic shape is identical
66
+ * (`Identifier.method(args)`) and the generic strategy would swallow the job.
67
+ * The distinction is semantic, and it matters because the counting decision
68
+ * differs.
69
+ *
70
+ * COUNTING DECISION: a job dispatched by a handler is followed as part of the
71
+ * SAME transactional function, because IFPUG counts what the user recognises —
72
+ * they click and the effect happens, even if execution is asynchronous. A
73
+ * SCHEDULED job, which nobody dispatches, is a different thing: it is an entry
74
+ * point of its own, and out of v1 scope — only HTTP routes are collected.
75
+ */
76
+ const jobDispatchResolver = {
77
+ name: "job-dispatch",
78
+ order: 15,
79
+ resolve(call, ctx) {
80
+ const expr = call.getExpression();
81
+ if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
82
+ if (!DISPATCH_METHODS.has(expr.getName())) return [];
83
+ const receiver = expr.getExpression();
84
+ if (!receiver.isKind(SyntaxKind.Identifier)) return [];
85
+ const symbol = receiver.getText();
86
+ if (ctx.dataStoresBySymbol.has(symbol)) return [];
87
+ const file = ctx.imports.get(symbol);
88
+ if (!file) return [];
89
+ return [{
90
+ file,
91
+ member: ctx.sourceFile(file)?.getClasses().some((c) => c.getMethod("handle")) ? "handle" : expr.getName()
92
+ }];
93
+ }
94
+ };
95
+ //#endregion
96
+ //#region src/inventory/resolvers/module_function.ts
97
+ /**
98
+ * "Module function" pattern: no class at all.
99
+ *
100
+ * await createUser(payload)
101
+ * await syncWithProvider(order)
102
+ *
103
+ * Runs last: it matches any call to an imported identifier and would otherwise
104
+ * swallow the more precise patterns.
105
+ */
106
+ const moduleFunctionResolver = {
107
+ name: "module-function",
108
+ order: 50,
109
+ resolve(call, ctx) {
110
+ const expr = call.getExpression();
111
+ if (!expr.isKind(SyntaxKind.Identifier)) return [];
112
+ const file = ctx.imports.get(expr.getText());
113
+ if (!file) return [];
114
+ return [{
115
+ file,
116
+ member: expr.getText()
117
+ }];
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region src/inventory/resolvers/property_service.ts
122
+ /**
123
+ * "Injected dependency" pattern: the call leaves through a class property.
124
+ *
125
+ * @inject()
126
+ * class InvoiceController {
127
+ * constructor(protected billing: BillingService) {}
128
+ * async queue() { await this.billing.enqueue(invoice) }
129
+ * }
130
+ *
131
+ * This is the official AdonisJS pattern, and in applications that use it, it is
132
+ * frequently the only path from a route down to a write.
133
+ *
134
+ * **No type checker required.** `@inject()` only works with an explicit type
135
+ * annotation — that annotation is how the container knows what to inject — so
136
+ * the type is always in the AST as an imported identifier.
137
+ */
138
+ const propertyServiceResolver = {
139
+ name: "property-service",
140
+ order: 30,
141
+ resolve(call, ctx) {
142
+ const expression = call.getExpression();
143
+ if (!Node.isPropertyAccessExpression(expression)) return [];
144
+ const receiver = expression.getExpression();
145
+ if (!Node.isPropertyAccessExpression(receiver)) return [];
146
+ if (receiver.getExpression().getKind() !== SyntaxKind.ThisKeyword) return [];
147
+ const file = ctx.injected.get(receiver.getName());
148
+ if (!file) return [];
149
+ return [{
150
+ file,
151
+ member: expression.getName()
152
+ }];
153
+ }
154
+ };
155
+ //#endregion
156
+ //#region src/inventory/resolvers/same_class_method.ts
157
+ /**
158
+ * "Same class method" pattern: `this.privateMethod()`.
159
+ *
160
+ * async expire(uuid: string) {
161
+ * const invite = await this.findByUuid(uuid)
162
+ * await this.persistExpiration(invite)
163
+ * }
164
+ *
165
+ * A public method delegating to private ones of the same class is where writes
166
+ * often live. No other strategy covers it — `property-service` requires
167
+ * `this.dependency.method()`, with two levels of access.
168
+ *
169
+ * Runs before `property-service` because it is more specific: the receiver is
170
+ * exactly `this`.
171
+ */
172
+ const sameClassMethodResolver = {
173
+ name: "same-class-method",
174
+ order: 5,
175
+ resolve(call, ctx) {
176
+ const expression = call.getExpression();
177
+ if (!Node.isPropertyAccessExpression(expression)) return [];
178
+ if (expression.getExpression().getKind() !== SyntaxKind.ThisKeyword) return [];
179
+ const member = expression.getName();
180
+ const owner = call.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
181
+ if (!owner) return [];
182
+ /**
183
+ * Only claim the call if the method really exists on the class. Otherwise
184
+ * `this.someFunctionProperty()` would be claimed, the body lookup would
185
+ * fail, and the report would blame inheritance from a package — sending the
186
+ * reader to the wrong place.
187
+ */
188
+ if (!owner.getMethod(member) && !owner.getStaticMethod(member)) return [];
189
+ return [{
190
+ file: ctx.file.getFilePath(),
191
+ member
192
+ }];
193
+ }
194
+ };
195
+ //#endregion
196
+ //#region src/inventory/resolvers/static_service.ts
197
+ /**
198
+ * "Static service" pattern: a class method called without instantiating.
199
+ *
200
+ * await UserService.create(payload)
201
+ * await OrderService.finalize(order)
202
+ *
203
+ * Careful: `Order.findByOrFail(...)` has exactly the same syntactic shape. The
204
+ * difference is semantic — a model is a data store, not a body to walk into,
205
+ * and the persistence detector handles it. Hence this resolver depends on
206
+ * `ctx.dataStoresBySymbol` already being populated.
207
+ */
208
+ const staticServiceResolver = {
209
+ name: "static-service",
210
+ order: 20,
211
+ resolve(call, ctx) {
212
+ const expr = call.getExpression();
213
+ if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
214
+ const receiver = expr.getExpression();
215
+ if (!receiver.isKind(SyntaxKind.Identifier)) return [];
216
+ const symbol = receiver.getText();
217
+ if (ctx.dataStoresBySymbol.has(symbol)) return [];
218
+ const file = ctx.imports.get(symbol);
219
+ if (!file) return [];
220
+ return [{
221
+ file,
222
+ member: expr.getName()
223
+ }];
224
+ }
225
+ };
226
+ //#endregion
227
+ //#region src/inventory/detectors/lucid.ts
228
+ const WRITE_METHODS = new Set([
229
+ "save",
230
+ "delete",
231
+ "create",
232
+ "createMany",
233
+ "merge",
234
+ "fill",
235
+ "updateOrCreate",
236
+ "fetchOrCreateMany",
237
+ "firstOrCreate",
238
+ "updateOrCreateMany",
239
+ "attach",
240
+ "detach",
241
+ "sync",
242
+ "increment",
243
+ "decrement",
244
+ "update",
245
+ "truncate",
246
+ "restore",
247
+ "forceDelete"
248
+ ]);
249
+ /**
250
+ * Which hooks each access fires, by decorator name — counting-decisions §3.
251
+ *
252
+ * `save()` fires the save pair AND the create-or-update pair, and which of the
253
+ * two runs is not knowable statically. That is not a compromise here: AFP
254
+ * §6.5.3 requires treating multiple optional paths as part of the same
255
+ * transaction, so following both is the specified behaviour.
256
+ */
257
+ const HOOKS_BY_METHOD = {
258
+ save: [
259
+ "beforeSave",
260
+ "afterSave",
261
+ "beforeCreate",
262
+ "afterCreate",
263
+ "beforeUpdate",
264
+ "afterUpdate"
265
+ ],
266
+ create: [
267
+ "beforeCreate",
268
+ "afterCreate",
269
+ "beforeSave",
270
+ "afterSave"
271
+ ],
272
+ createMany: [
273
+ "beforeCreate",
274
+ "afterCreate",
275
+ "beforeSave",
276
+ "afterSave"
277
+ ],
278
+ firstOrCreate: [
279
+ "beforeCreate",
280
+ "afterCreate",
281
+ "beforeSave",
282
+ "afterSave"
283
+ ],
284
+ fetchOrCreateMany: [
285
+ "beforeCreate",
286
+ "afterCreate",
287
+ "beforeSave",
288
+ "afterSave"
289
+ ],
290
+ updateOrCreate: [
291
+ "beforeCreate",
292
+ "afterCreate",
293
+ "beforeUpdate",
294
+ "afterUpdate",
295
+ "beforeSave",
296
+ "afterSave"
297
+ ],
298
+ updateOrCreateMany: [
299
+ "beforeCreate",
300
+ "afterCreate",
301
+ "beforeUpdate",
302
+ "afterUpdate",
303
+ "beforeSave",
304
+ "afterSave"
305
+ ],
306
+ delete: ["beforeDelete", "afterDelete"],
307
+ forceDelete: ["beforeDelete", "afterDelete"],
308
+ find: ["beforeFind", "afterFind"],
309
+ findOrFail: ["beforeFind", "afterFind"],
310
+ findBy: ["beforeFind", "afterFind"],
311
+ findByOrFail: ["beforeFind", "afterFind"],
312
+ first: ["beforeFind", "afterFind"],
313
+ firstOrFail: ["beforeFind", "afterFind"],
314
+ all: ["beforeFetch", "afterFetch"],
315
+ findMany: ["beforeFetch", "afterFetch"]
316
+ };
317
+ new Set(Object.values(HOOKS_BY_METHOD).flat());
318
+ /**
319
+ * Hook decorators fired by an access, or `[]` when it fires none.
320
+ *
321
+ * `truncate`, `increment`, `decrement` and the pivot operations change rows
322
+ * without instantiating a model, so no hook runs.
323
+ */
324
+ function hooksFiredBy(access) {
325
+ if (!access.firesHooks) return [];
326
+ return HOOKS_BY_METHOD[access.method] ?? [];
327
+ }
328
+ const READ_METHODS = new Set([
329
+ "find",
330
+ "findOrFail",
331
+ "findBy",
332
+ "findByOrFail",
333
+ "findMany",
334
+ "first",
335
+ "firstOrFail",
336
+ "all",
337
+ "query",
338
+ "preload",
339
+ "load",
340
+ "paginate",
341
+ "count",
342
+ "exists",
343
+ "related",
344
+ "where",
345
+ "orderBy"
346
+ ]);
347
+ function detectAccess(call, symbols, relations = /* @__PURE__ */ new Map()) {
348
+ const expression = call.getExpression();
349
+ if (!Node.isPropertyAccessExpression(expression)) return null;
350
+ const method = expression.getName();
351
+ const isWrite = WRITE_METHODS.has(method);
352
+ if (!isWrite && !READ_METHODS.has(method)) return null;
353
+ const receiver = expression.getExpression();
354
+ /**
355
+ * Looks up the PATH before the root: `input.invite.save()` has root `input`,
356
+ * which is no store at all — `input.invite` is.
357
+ *
358
+ * This is the dominant shape in action objects with a typed input, and
359
+ * without it the graph reaches the action and sees no write.
360
+ */
361
+ const store = symbols.get(pathSymbolOf(receiver) ?? "") ?? symbols.get(rootSymbolOf(receiver) ?? "");
362
+ if (!store) return null;
363
+ return {
364
+ mode: isWrite ? "write" : "read",
365
+ store,
366
+ method,
367
+ line: call.getStartLineNumber(),
368
+ viaRelation: relationTargetOf(method, call, store, relations),
369
+ firesHooks: firesHooks(receiver)
370
+ };
371
+ }
372
+ /**
373
+ * An access fires hooks unless it went through the query builder.
374
+ *
375
+ * The signal is a CALL anywhere in the receiver chain: `document.delete()` has
376
+ * none, `Document.query().where(…).delete()` has two. It errs towards NOT
377
+ * following — `(await Document.find(id))!.delete()` is read as bulk — because
378
+ * an FTR that is missing understates, and one that is invented overstates.
379
+ */
380
+ function firesHooks(receiver) {
381
+ let current = receiver;
382
+ for (let depth = 0; depth < 20; depth++) {
383
+ if (Node.isCallExpression(current)) return false;
384
+ if (!Node.isPropertyAccessExpression(current)) return Node.isIdentifier(current);
385
+ current = current.getExpression();
386
+ }
387
+ return false;
388
+ }
389
+ const RELATION_ACCESSORS = new Set([
390
+ "preload",
391
+ "load",
392
+ "related",
393
+ "withCount"
394
+ ]);
395
+ /**
396
+ * `.preload('author')` on a store declaring `{ author: 'Author' }` reaches
397
+ * `Author`.
398
+ */
399
+ function relationTargetOf(method, call, store, relations) {
400
+ if (!RELATION_ACCESSORS.has(method)) return void 0;
401
+ const name = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
402
+ if (!name) return void 0;
403
+ return relations.get(store)?.[name];
404
+ }
405
+ /**
406
+ * Dotted path of a receiver made only of property accesses: `input.invite`
407
+ * yields "input.invite". Any call in between invalidates the path, because the
408
+ * value stops being statically traceable.
409
+ */
410
+ function pathSymbolOf(node) {
411
+ const parts = [];
412
+ let current = node;
413
+ for (let depth = 0; depth < 20; depth++) {
414
+ if (Node.isIdentifier(current)) return [current.getText(), ...parts].join(".");
415
+ if (!Node.isPropertyAccessExpression(current)) return null;
416
+ parts.unshift(current.getName());
417
+ current = current.getExpression();
418
+ }
419
+ return null;
420
+ }
421
+ /**
422
+ * Root of an `a.b().c()` chain — the left-most identifier.
423
+ *
424
+ * It must traverse `await`, calls, property access and `new`, otherwise
425
+ * `await new Action().handle()` and `Invite.query().where().update()` stop at
426
+ * the first node and the write disappears.
427
+ */
428
+ function rootSymbolOf(node) {
429
+ let current = node;
430
+ for (let depth = 0; depth < 60 && current; depth++) {
431
+ if (Node.isIdentifier(current)) return current.getText();
432
+ if (current.getKind() === SyntaxKind.ThisKeyword) return "this";
433
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isCallExpression(current) || Node.isNewExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
434
+ current = current.getExpression();
435
+ continue;
436
+ }
437
+ return null;
438
+ }
439
+ return null;
440
+ }
441
+ //#endregion
442
+ //#region src/inventory/resolvers/transformer.ts
443
+ /** BaseTransformer's public API; all of it funnels through `toObject` */
444
+ const TRANSFORMER_METHODS = new Set([
445
+ "transform",
446
+ "paginate",
447
+ "toJSON",
448
+ "useVariant",
449
+ "withVariant"
450
+ ]);
451
+ /** what the application-side body is called */
452
+ const APPLICATION_BODY = "toObject";
453
+ /**
454
+ * "Transformer" pattern: the package supplies the API, the application
455
+ * supplies the body.
456
+ *
457
+ * class InviteTransformer extends BaseTransformer<Invite> {
458
+ * toObject() { … }
459
+ * }
460
+ *
461
+ * InviteTransformer.transform(invite)
462
+ *
463
+ * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
464
+ * symbol lands on the application file and finds no body there. The naive
465
+ * reading is that the tracer must step into node_modules; it does not. Those
466
+ * methods call BACK into `toObject()`, which the application writes, so the
467
+ * body worth analysing was in the application all along.
468
+ *
469
+ * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
470
+ * `handle` executes.
471
+ *
472
+ * COUNTING DECISION: the write a transformer performs belongs to the
473
+ * transaction that serialised through it. Without this, a table written only
474
+ * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
475
+ */
476
+ const transformerResolver = {
477
+ name: "transformer",
478
+ order: 18,
479
+ resolve(call, ctx) {
480
+ const expression = call.getExpression();
481
+ if (!Node.isPropertyAccessExpression(expression)) return [];
482
+ if (!TRANSFORMER_METHODS.has(expression.getName())) return [];
483
+ /**
484
+ * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
485
+ * well as `X.transform(p)`. Analysing the same body twice is free: the
486
+ * graph dedupes by file and member.
487
+ */
488
+ const symbol = rootSymbolOf(expression.getExpression());
489
+ if (!symbol) return [];
490
+ const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
491
+ if (!file) return [];
492
+ const declared = ctx.sourceFile(file);
493
+ if (!declared || !extendsTransformer(declared)) return [];
494
+ return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
495
+ file,
496
+ member: APPLICATION_BODY
497
+ }] : [];
498
+ }
499
+ };
500
+ /**
501
+ * Does a class here extend a transformer base from a package?
502
+ *
503
+ * Checked by the base's name and by its import being a bare specifier, so an
504
+ * application class that merely happens to own a `transform` method is not
505
+ * mistaken for one.
506
+ */
507
+ function extendsTransformer(file) {
508
+ if (!file) return false;
509
+ for (const cls of file.getClasses()) {
510
+ const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
511
+ if (!name?.endsWith("Transformer")) continue;
512
+ const imported = file.getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
513
+ if (imported && !imported.getModuleSpecifierValue().startsWith("#")) return true;
514
+ }
515
+ return false;
516
+ }
517
+ //#endregion
518
+ //#region src/inventory/resolvers/index.ts
519
+ /**
520
+ * Built-in strategies, ordered from most to least specific.
521
+ *
522
+ * They cover the patterns that appear in real AdonisJS applications. No closed
523
+ * list can cover them all — a project with its own convention registers it in
524
+ * `config/function_points.ts`, and it runs before these.
525
+ */
526
+ const BUILTIN_CALL_RESOLVERS = [
527
+ sameClassMethodResolver,
528
+ actionObjectResolver,
529
+ jobDispatchResolver,
530
+ transformerResolver,
531
+ staticServiceResolver,
532
+ propertyServiceResolver,
533
+ moduleFunctionResolver
534
+ ];
535
+ /**
536
+ * The FIRST strategy that claims a call wins.
537
+ *
538
+ * This is not an implementation detail: syntactically identical shapes carry
539
+ * different meanings. `CreateUserJob.dispatch(p)`, `UserService.create(p)` and
540
+ * `User.find(p)` are all `Identifier.method(args)`, and only ordering tells
541
+ * them apart. Hence specific strategies declare a lower `order` than generic
542
+ * ones, and `module-function` comes last — it would match almost anything.
543
+ */
544
+ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
545
+ for (const resolver of resolvers) {
546
+ const refs = resolver.resolve(call, ctx);
547
+ if (refs.length > 0) return {
548
+ by: resolver.name,
549
+ refs
550
+ };
551
+ }
552
+ return null;
553
+ }
554
+ //#endregion
555
+ export { rootSymbolOf as a, hooksFiredBy as i, resolveCall as n, detectAccess as r, BUILTIN_CALL_RESOLVERS as t };