@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.
- package/LICENSE.md +21 -0
- package/README.md +427 -0
- package/bin/cli.js +4 -0
- package/build/commands/fp_calibrate.d.ts +16 -0
- package/build/commands/fp_count.d.ts +11 -0
- package/build/commands/fp_diff.d.ts +16 -0
- package/build/commands/fp_explain.d.ts +15 -0
- package/build/commands/fp_inventory.d.ts +9 -0
- package/build/commands/main.d.ts +5 -0
- package/build/commands/main.js +120 -0
- package/build/commands/printer.d.ts +9 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +10 -0
- package/build/define_config-DOqWyPwV.js +19 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +4 -0
- package/build/pipeline-BzP-ITGN.js +2306 -0
- package/build/resolvers-CU9HKYpn.js +555 -0
- package/build/runners-Bt8tbISi.js +630 -0
- package/build/scripts/smoke_package.d.ts +1 -0
- package/build/src/albrecht/calibration.d.ts +62 -0
- package/build/src/albrecht/counter.d.ts +48 -0
- package/build/src/albrecht/data_functions.d.ts +32 -0
- package/build/src/albrecht/diff.d.ts +66 -0
- package/build/src/albrecht/index.d.ts +13 -0
- package/build/src/albrecht/tables.d.ts +19 -0
- package/build/src/albrecht/technical_filter.d.ts +25 -0
- package/build/src/albrecht/transactional_functions.d.ts +35 -0
- package/build/src/cli/load_config.d.ts +28 -0
- package/build/src/cli/print.d.ts +19 -0
- package/build/src/cli/runners.d.ts +52 -0
- package/build/src/cli.d.ts +19 -0
- package/build/src/cli.js +198 -0
- package/build/src/define_config.d.ts +137 -0
- package/build/src/inventory/app_context.d.ts +73 -0
- package/build/src/inventory/detectors/lucid.d.ts +77 -0
- package/build/src/inventory/graph/call_graph.d.ts +80 -0
- package/build/src/inventory/graph/noise.d.ts +9 -0
- package/build/src/inventory/index.d.ts +15 -0
- package/build/src/inventory/paths.d.ts +22 -0
- package/build/src/inventory/resolvers/action_object.d.ts +14 -0
- package/build/src/inventory/resolvers/index.d.ts +23 -0
- package/build/src/inventory/resolvers/index.js +2 -0
- package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
- package/build/src/inventory/resolvers/module_function.d.ts +11 -0
- package/build/src/inventory/resolvers/property_service.d.ts +18 -0
- package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
- package/build/src/inventory/resolvers/static_service.d.ts +13 -0
- package/build/src/inventory/resolvers/transformer.d.ts +25 -0
- package/build/src/inventory/resolvers/types.d.ts +65 -0
- package/build/src/inventory/source.d.ts +39 -0
- package/build/src/inventory/sources/data_stores.d.ts +31 -0
- package/build/src/inventory/sources/json_schemas.d.ts +32 -0
- package/build/src/inventory/sources/routes_ast.d.ts +28 -0
- package/build/src/metrics/structure.d.ts +72 -0
- package/build/src/pipeline.d.ts +44 -0
- package/build/src/pipeline.js +2 -0
- package/build/src/reporters/table.d.ts +6 -0
- package/build/src/types.d.ts +256 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config.stub +37 -0
- package/build/tmp/probe.d.ts +1 -0
- package/build/tmp/probe_cli.d.ts +1 -0
- package/build/tmp/probe_cmp.d.ts +1 -0
- package/build/tmp/probe_count.d.ts +1 -0
- package/build/tmp/probe_data.d.ts +1 -0
- package/build/tmp/probe_diff.d.ts +1 -0
- package/build/tmp/probe_gap.d.ts +1 -0
- package/build/tmp/probe_graph.d.ts +1 -0
- package/build/tmp/probe_metrics.d.ts +1 -0
- package/build/tmp/probe_miss.d.ts +1 -0
- package/build/tmp/probe_names.d.ts +1 -0
- package/build/tmp/probe_nodata.d.ts +1 -0
- package/build/tmp/probe_one.d.ts +1 -0
- package/build/tmp/probe_perf.d.ts +1 -0
- package/build/tmp/probe_routes.d.ts +1 -0
- package/build/tmp/probe_unres.d.ts +1 -0
- package/build/tmp/probe_vazquez.d.ts +1 -0
- package/build/tsdown.config.d.ts +2 -0
- 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 };
|