@filipebraida/adonis-function-points 0.2.0 → 0.4.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 +105 -0
- package/README.md +1 -1
- package/build/calibration-8eV8CEix.js +403 -0
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-Cm079xWL.js → fp_calibrate-DLZP5bUp.js} +1 -1
- package/build/{fp_count-CfcXPuj5.js → fp_count-DNSwaLUD.js} +1 -1
- package/build/{fp_diff-DE_t3twv.js → fp_diff-CCKxqGKh.js} +1 -1
- package/build/{fp_explain-MKyEoi0h.js → fp_explain-Dpiby5Qx.js} +1 -1
- package/build/{fp_inventory-DSrCVhEy.js → fp_inventory-DHwZzEQf.js} +1 -1
- package/build/{fp_metrics-BUWj9dLw.js → fp_metrics-M84qLYaE.js} +1 -1
- package/build/index.d.ts +28 -0
- package/build/index.js +4 -3
- package/build/{pipeline-DySlMWcN.js → pipeline-Dm9KvUvF.js} +424 -48
- package/build/{resolvers-MFjRl2ef.js → resolvers-vMahHkAd.js} +356 -291
- package/build/{runners-DpMd-yZM.js → runners-DetZGfh5.js} +14 -388
- package/build/src/albrecht/counter.d.ts +9 -5
- package/build/src/albrecht/transactional_functions.d.ts +9 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +26 -1
- package/build/src/inventory/detectors/lucid.d.ts +8 -0
- package/build/src/inventory/graph/call_graph.d.ts +2 -0
- package/build/src/inventory/paths.d.ts +17 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/inventory/source.d.ts +8 -1
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +7 -0
- package/build/stubs/config.stub +7 -1
- package/package.json +1 -1
- package/build/define_config-DOqWyPwV.js +0 -19
|
@@ -1,55 +1,5 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { Node, Project, 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
3
|
//#region src/inventory/paths.ts
|
|
54
4
|
/**
|
|
55
5
|
* One canonical spelling for every path the inventory emits.
|
|
@@ -71,6 +21,23 @@ function classOfReceiver(receiver) {
|
|
|
71
21
|
* it to each comparison costs vigilance forever.
|
|
72
22
|
*/
|
|
73
23
|
const toPosix = (value) => value.split("\\").join("/");
|
|
24
|
+
/**
|
|
25
|
+
* A path as it should appear in an EMITTED artefact: relative to the application.
|
|
26
|
+
*
|
|
27
|
+
* `CountSource.app` is documented as never being the absolute path, because that
|
|
28
|
+
* says where the machine keeps its files and travels with every count sent
|
|
29
|
+
* anywhere. One field below it, `config` shipped the absolute path — and so did
|
|
30
|
+
* every `trace[].file`, 858 times in a single production count. The rule was
|
|
31
|
+
* stated and then applied to one field.
|
|
32
|
+
*
|
|
33
|
+
* Internally the absolute path is the right thing: it is what ts-morph resolves
|
|
34
|
+
* and what the call graph keys its caches on. So this converts at the boundary
|
|
35
|
+
* where a path LEAVES, and nowhere else.
|
|
36
|
+
*
|
|
37
|
+
* A path outside the root keeps its `../` prefix, which describes where it is
|
|
38
|
+
* without naming the home directory.
|
|
39
|
+
*/
|
|
40
|
+
const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
|
|
74
41
|
/** Compares two paths that may have come from different sources. */
|
|
75
42
|
const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
|
|
76
43
|
//#endregion
|
|
@@ -170,29 +137,335 @@ function importedFrom(local, from, app) {
|
|
|
170
137
|
if (!named && !isDefault) continue;
|
|
171
138
|
return app.resolveSpecifier(declaration.getModuleSpecifierValue());
|
|
172
139
|
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The file a key of a generated registry points at.
|
|
144
|
+
*
|
|
145
|
+
* Both shapes the generators emit are handled: a direct reference to an
|
|
146
|
+
* imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
|
|
147
|
+
* per artefact and per framework version, and reading only one of them silently
|
|
148
|
+
* lost half the graph.
|
|
149
|
+
*/
|
|
150
|
+
function registryEntry(registryFile, key, project, app) {
|
|
151
|
+
const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
|
|
152
|
+
if (!file) return null;
|
|
153
|
+
for (const declaration of file.getVariableDeclarations()) {
|
|
154
|
+
const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
|
|
155
|
+
if (!value) continue;
|
|
156
|
+
if (Node.isIdentifier(value)) {
|
|
157
|
+
const target = importedFrom(value.getText(), file, app);
|
|
158
|
+
return target ? toPosix(target) : null;
|
|
159
|
+
}
|
|
160
|
+
const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
161
|
+
const target = specifier ? app.resolveSpecifier(specifier) : null;
|
|
162
|
+
return target ? toPosix(target) : null;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/inventory/detectors/lucid.ts
|
|
168
|
+
const WRITE_METHODS = new Set([
|
|
169
|
+
"save",
|
|
170
|
+
"delete",
|
|
171
|
+
"create",
|
|
172
|
+
"createMany",
|
|
173
|
+
"merge",
|
|
174
|
+
"fill",
|
|
175
|
+
"updateOrCreate",
|
|
176
|
+
"fetchOrCreateMany",
|
|
177
|
+
"firstOrCreate",
|
|
178
|
+
"updateOrCreateMany",
|
|
179
|
+
"attach",
|
|
180
|
+
"detach",
|
|
181
|
+
"sync",
|
|
182
|
+
"increment",
|
|
183
|
+
"decrement",
|
|
184
|
+
"update",
|
|
185
|
+
"truncate",
|
|
186
|
+
"restore",
|
|
187
|
+
"forceDelete"
|
|
188
|
+
]);
|
|
189
|
+
/**
|
|
190
|
+
* Which hooks each access fires, by decorator name — counting-decisions §3.
|
|
191
|
+
*
|
|
192
|
+
* `save()` fires the save pair AND the create-or-update pair, and which of the
|
|
193
|
+
* two runs is not knowable statically. That is not a compromise here: AFP
|
|
194
|
+
* §6.5.3 requires treating multiple optional paths as part of the same
|
|
195
|
+
* transaction, so following both is the specified behaviour.
|
|
196
|
+
*/
|
|
197
|
+
const HOOKS_BY_METHOD = {
|
|
198
|
+
save: [
|
|
199
|
+
"beforeSave",
|
|
200
|
+
"afterSave",
|
|
201
|
+
"beforeCreate",
|
|
202
|
+
"afterCreate",
|
|
203
|
+
"beforeUpdate",
|
|
204
|
+
"afterUpdate"
|
|
205
|
+
],
|
|
206
|
+
create: [
|
|
207
|
+
"beforeCreate",
|
|
208
|
+
"afterCreate",
|
|
209
|
+
"beforeSave",
|
|
210
|
+
"afterSave"
|
|
211
|
+
],
|
|
212
|
+
createMany: [
|
|
213
|
+
"beforeCreate",
|
|
214
|
+
"afterCreate",
|
|
215
|
+
"beforeSave",
|
|
216
|
+
"afterSave"
|
|
217
|
+
],
|
|
218
|
+
firstOrCreate: [
|
|
219
|
+
"beforeCreate",
|
|
220
|
+
"afterCreate",
|
|
221
|
+
"beforeSave",
|
|
222
|
+
"afterSave"
|
|
223
|
+
],
|
|
224
|
+
fetchOrCreateMany: [
|
|
225
|
+
"beforeCreate",
|
|
226
|
+
"afterCreate",
|
|
227
|
+
"beforeSave",
|
|
228
|
+
"afterSave"
|
|
229
|
+
],
|
|
230
|
+
updateOrCreate: [
|
|
231
|
+
"beforeCreate",
|
|
232
|
+
"afterCreate",
|
|
233
|
+
"beforeUpdate",
|
|
234
|
+
"afterUpdate",
|
|
235
|
+
"beforeSave",
|
|
236
|
+
"afterSave"
|
|
237
|
+
],
|
|
238
|
+
updateOrCreateMany: [
|
|
239
|
+
"beforeCreate",
|
|
240
|
+
"afterCreate",
|
|
241
|
+
"beforeUpdate",
|
|
242
|
+
"afterUpdate",
|
|
243
|
+
"beforeSave",
|
|
244
|
+
"afterSave"
|
|
245
|
+
],
|
|
246
|
+
delete: ["beforeDelete", "afterDelete"],
|
|
247
|
+
forceDelete: ["beforeDelete", "afterDelete"],
|
|
248
|
+
find: ["beforeFind", "afterFind"],
|
|
249
|
+
findOrFail: ["beforeFind", "afterFind"],
|
|
250
|
+
findBy: ["beforeFind", "afterFind"],
|
|
251
|
+
findByOrFail: ["beforeFind", "afterFind"],
|
|
252
|
+
first: ["beforeFind", "afterFind"],
|
|
253
|
+
firstOrFail: ["beforeFind", "afterFind"],
|
|
254
|
+
all: ["beforeFetch", "afterFetch"],
|
|
255
|
+
findMany: ["beforeFetch", "afterFetch"]
|
|
256
|
+
};
|
|
257
|
+
new Set(Object.values(HOOKS_BY_METHOD).flat());
|
|
258
|
+
/**
|
|
259
|
+
* Hook decorators fired by an access, or `[]` when it fires none.
|
|
260
|
+
*
|
|
261
|
+
* `truncate`, `increment`, `decrement` and the pivot operations change rows
|
|
262
|
+
* without instantiating a model, so no hook runs.
|
|
263
|
+
*/
|
|
264
|
+
function hooksFiredBy(access) {
|
|
265
|
+
if (!access.firesHooks) return [];
|
|
266
|
+
return HOOKS_BY_METHOD[access.method] ?? [];
|
|
267
|
+
}
|
|
268
|
+
const READ_METHODS = new Set([
|
|
269
|
+
"find",
|
|
270
|
+
"findOrFail",
|
|
271
|
+
"findBy",
|
|
272
|
+
"findByOrFail",
|
|
273
|
+
"findMany",
|
|
274
|
+
"first",
|
|
275
|
+
"firstOrFail",
|
|
276
|
+
"all",
|
|
277
|
+
"query",
|
|
278
|
+
"preload",
|
|
279
|
+
"load",
|
|
280
|
+
"paginate",
|
|
281
|
+
"count",
|
|
282
|
+
"exists",
|
|
283
|
+
"related",
|
|
284
|
+
"where",
|
|
285
|
+
"orderBy"
|
|
286
|
+
]);
|
|
287
|
+
function detectAccess(call, symbols, relations = /* @__PURE__ */ new Map()) {
|
|
288
|
+
const expression = call.getExpression();
|
|
289
|
+
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
290
|
+
const method = expression.getName();
|
|
291
|
+
const isWrite = WRITE_METHODS.has(method);
|
|
292
|
+
if (!isWrite && !READ_METHODS.has(method)) return null;
|
|
293
|
+
const receiver = expression.getExpression();
|
|
294
|
+
/**
|
|
295
|
+
* Looks up the PATH before the root: `input.invite.save()` has root `input`,
|
|
296
|
+
* which is no store at all — `input.invite` is.
|
|
297
|
+
*
|
|
298
|
+
* This is the dominant shape in action objects with a typed input, and
|
|
299
|
+
* without it the graph reaches the action and sees no write.
|
|
300
|
+
*/
|
|
301
|
+
const store = symbols.get(pathSymbolOf(receiver) ?? "") ?? symbols.get(rootSymbolOf(receiver) ?? "");
|
|
302
|
+
if (!store) return null;
|
|
303
|
+
/**
|
|
304
|
+
* `distribution.related('files').create({…})` — the relation is the SUBJECT of
|
|
305
|
+
* the write, not a table read along the way.
|
|
306
|
+
*
|
|
307
|
+
* `relationTargetOf` reads the current method, and here the current method is
|
|
308
|
+
* `create`, whose receiver is the `related(…)` call. Without looking back up the
|
|
309
|
+
* chain the write was attributed to `distributions` alone and `distribution_files`
|
|
310
|
+
* came out as a table this application only reads — an EIF, maintained by
|
|
311
|
+
* somebody else. That is what a production application reported, and it is
|
|
312
|
+
* ordinary Lucid: `related(…)` followed by `create`, `createMany`, `save`,
|
|
313
|
+
* `saveMany`, `attach`, `detach` or `sync` writes the related table.
|
|
314
|
+
*/
|
|
315
|
+
const related = relatedCallIn(receiver);
|
|
316
|
+
const viaRelation = relationTargetOf(method, call, store, relations) ?? (related ? relationTargetOf("related", related, store, relations) : void 0);
|
|
317
|
+
return {
|
|
318
|
+
mode: isWrite ? "write" : "read",
|
|
319
|
+
store,
|
|
320
|
+
method,
|
|
321
|
+
line: call.getStartLineNumber(),
|
|
322
|
+
viaRelation,
|
|
323
|
+
/** the relation is written when the method acting on it writes */
|
|
324
|
+
relationWritten: isWrite,
|
|
325
|
+
firesHooks: firesHooks(receiver)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The `related('x')` call inside a receiver chain, if any.
|
|
330
|
+
*
|
|
331
|
+
* Only `related` qualifies: `preload` and `load` hand back the parent, so a write
|
|
332
|
+
* after them acts on the parent. `related` hands back the relation's own query
|
|
333
|
+
* builder, and that is what makes the difference.
|
|
334
|
+
*/
|
|
335
|
+
function relatedCallIn(receiver) {
|
|
336
|
+
let current = receiver;
|
|
337
|
+
for (let depth = 0; depth < 20 && current; depth++) {
|
|
338
|
+
if (Node.isCallExpression(current)) {
|
|
339
|
+
const expression = current.getExpression();
|
|
340
|
+
if (Node.isPropertyAccessExpression(expression) && expression.getName() === "related") return current;
|
|
341
|
+
current = expression;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (Node.isPropertyAccessExpression(current) || Node.isAwaitExpression(current)) {
|
|
345
|
+
current = current.getExpression();
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* An access fires hooks unless it went through the query builder.
|
|
354
|
+
*
|
|
355
|
+
* The signal is a CALL anywhere in the receiver chain: `document.delete()` has
|
|
356
|
+
* none, `Document.query().where(…).delete()` has two. It errs towards NOT
|
|
357
|
+
* following — `(await Document.find(id))!.delete()` is read as bulk — because
|
|
358
|
+
* an FTR that is missing understates, and one that is invented overstates.
|
|
359
|
+
*/
|
|
360
|
+
function firesHooks(receiver) {
|
|
361
|
+
let current = receiver;
|
|
362
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
363
|
+
if (Node.isCallExpression(current)) return false;
|
|
364
|
+
if (!Node.isPropertyAccessExpression(current)) return Node.isIdentifier(current);
|
|
365
|
+
current = current.getExpression();
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
const RELATION_ACCESSORS = new Set([
|
|
370
|
+
"preload",
|
|
371
|
+
"load",
|
|
372
|
+
"related",
|
|
373
|
+
"withCount"
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* `.preload('author')` on a store declaring `{ author: 'Author' }` reaches
|
|
377
|
+
* `Author`.
|
|
378
|
+
*/
|
|
379
|
+
function relationTargetOf(method, call, store, relations) {
|
|
380
|
+
if (!RELATION_ACCESSORS.has(method)) return void 0;
|
|
381
|
+
const name = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
382
|
+
if (!name) return void 0;
|
|
383
|
+
return relations.get(store)?.[name];
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Dotted path of a receiver made only of property accesses: `input.invite`
|
|
387
|
+
* yields "input.invite". Any call in between invalidates the path, because the
|
|
388
|
+
* value stops being statically traceable.
|
|
389
|
+
*/
|
|
390
|
+
function pathSymbolOf(node) {
|
|
391
|
+
const parts = [];
|
|
392
|
+
let current = node;
|
|
393
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
394
|
+
if (Node.isIdentifier(current)) return [current.getText(), ...parts].join(".");
|
|
395
|
+
if (!Node.isPropertyAccessExpression(current)) return null;
|
|
396
|
+
parts.unshift(current.getName());
|
|
397
|
+
current = current.getExpression();
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Root of an `a.b().c()` chain — the left-most identifier.
|
|
403
|
+
*
|
|
404
|
+
* It must traverse `await`, calls, property access and `new`, otherwise
|
|
405
|
+
* `await new Action().handle()` and `Invite.query().where().update()` stop at
|
|
406
|
+
* the first node and the write disappears.
|
|
407
|
+
*/
|
|
408
|
+
function rootSymbolOf(node) {
|
|
409
|
+
let current = node;
|
|
410
|
+
for (let depth = 0; depth < 60 && current; depth++) {
|
|
411
|
+
if (Node.isIdentifier(current)) return current.getText();
|
|
412
|
+
if (current.getKind() === SyntaxKind.ThisKeyword) return "this";
|
|
413
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isCallExpression(current) || Node.isNewExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
414
|
+
current = current.getExpression();
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/inventory/resolvers/action_object.ts
|
|
423
|
+
/**
|
|
424
|
+
* "Action object" pattern: the transaction delegates to an action instantiated
|
|
425
|
+
* at the call site.
|
|
426
|
+
*
|
|
427
|
+
* await new ExpireInvite().handle({ invite })
|
|
428
|
+
*
|
|
429
|
+
* const mark = new MarkContentChanged()
|
|
430
|
+
* await mark.handle({ documentId })
|
|
431
|
+
*
|
|
432
|
+
* The second form keeps the instance in a local variable, so the declaration
|
|
433
|
+
* has to be followed back to the `new` — that is what `classOfReceiver` does.
|
|
434
|
+
*/
|
|
435
|
+
const actionObjectResolver = {
|
|
436
|
+
name: "action-object",
|
|
437
|
+
order: 10,
|
|
438
|
+
resolve(call, ctx) {
|
|
439
|
+
const expr = call.getExpression();
|
|
440
|
+
if (!expr.isKind(SyntaxKind.PropertyAccessExpression)) return [];
|
|
441
|
+
const member = expr.getName();
|
|
442
|
+
const className = classOfReceiver(expr.getExpression());
|
|
443
|
+
if (!className) return [];
|
|
444
|
+
const file = ctx.imports.get(className);
|
|
445
|
+
if (!file) return [];
|
|
446
|
+
return [{
|
|
447
|
+
file,
|
|
448
|
+
member
|
|
449
|
+
}];
|
|
450
|
+
}
|
|
451
|
+
};
|
|
175
452
|
/**
|
|
176
|
-
*
|
|
453
|
+
* Finds the class behind a call receiver.
|
|
177
454
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* per artefact and per framework version, and reading only one of them silently
|
|
181
|
-
* lost half the graph.
|
|
455
|
+
* new Foo().handle() -> 'Foo'
|
|
456
|
+
* foo.handle() where const foo = new Foo() -> 'Foo'
|
|
182
457
|
*/
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
458
|
+
function classOfReceiver(receiver) {
|
|
459
|
+
if (receiver.isKind(SyntaxKind.NewExpression)) {
|
|
460
|
+
const target = receiver.getExpression();
|
|
461
|
+
return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
|
|
462
|
+
}
|
|
463
|
+
if (receiver.isKind(SyntaxKind.Identifier)) {
|
|
464
|
+
const init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
|
|
465
|
+
if (init?.isKind(SyntaxKind.NewExpression)) {
|
|
466
|
+
const target = init.getExpression();
|
|
467
|
+
return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
|
|
192
468
|
}
|
|
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
469
|
}
|
|
197
470
|
return null;
|
|
198
471
|
}
|
|
@@ -239,17 +512,24 @@ const DISPATCH_METHODS = new Set([
|
|
|
239
512
|
/**
|
|
240
513
|
* The method that actually runs the job, by queue package.
|
|
241
514
|
*
|
|
242
|
-
* There is no single name
|
|
243
|
-
* `handle
|
|
244
|
-
* `
|
|
245
|
-
*
|
|
246
|
-
*
|
|
515
|
+
* There is no single name, and this list grew twice by measurement rather than by
|
|
516
|
+
* reasoning. `@rlanz/bull-queue` uses `handle`; `@nemoventures/adonis-jobs` calls
|
|
517
|
+
* it `process`; `@adonisjs/queue` — the official package — generates
|
|
518
|
+
* `async execute()` in its own `make:job` stub. Each omission cost the same: the
|
|
519
|
+
* file resolved, no body was found, the dispatch was reported as an unknown, and
|
|
520
|
+
* every write inside the job went uncounted.
|
|
521
|
+
*
|
|
522
|
+
* `execute` surfaced only once event dispatch started being followed, because the
|
|
523
|
+
* listener was what enqueued the job and that path had never been walked. Which is
|
|
524
|
+
* the argument for adding a name when a real application shows it: a list written
|
|
525
|
+
* from imagination would have missed this one too.
|
|
247
526
|
*
|
|
248
527
|
* Ordered: a class declaring more than one is answering the dispatcher with the
|
|
249
528
|
* first, and `handle` is the most common.
|
|
250
529
|
*/
|
|
251
530
|
const EXECUTION_METHODS = [
|
|
252
531
|
"handle",
|
|
532
|
+
"execute",
|
|
253
533
|
"process",
|
|
254
534
|
"run",
|
|
255
535
|
"perform"
|
|
@@ -434,221 +714,6 @@ const staticServiceResolver = {
|
|
|
434
714
|
}
|
|
435
715
|
};
|
|
436
716
|
//#endregion
|
|
437
|
-
//#region src/inventory/detectors/lucid.ts
|
|
438
|
-
const WRITE_METHODS = new Set([
|
|
439
|
-
"save",
|
|
440
|
-
"delete",
|
|
441
|
-
"create",
|
|
442
|
-
"createMany",
|
|
443
|
-
"merge",
|
|
444
|
-
"fill",
|
|
445
|
-
"updateOrCreate",
|
|
446
|
-
"fetchOrCreateMany",
|
|
447
|
-
"firstOrCreate",
|
|
448
|
-
"updateOrCreateMany",
|
|
449
|
-
"attach",
|
|
450
|
-
"detach",
|
|
451
|
-
"sync",
|
|
452
|
-
"increment",
|
|
453
|
-
"decrement",
|
|
454
|
-
"update",
|
|
455
|
-
"truncate",
|
|
456
|
-
"restore",
|
|
457
|
-
"forceDelete"
|
|
458
|
-
]);
|
|
459
|
-
/**
|
|
460
|
-
* Which hooks each access fires, by decorator name — counting-decisions §3.
|
|
461
|
-
*
|
|
462
|
-
* `save()` fires the save pair AND the create-or-update pair, and which of the
|
|
463
|
-
* two runs is not knowable statically. That is not a compromise here: AFP
|
|
464
|
-
* §6.5.3 requires treating multiple optional paths as part of the same
|
|
465
|
-
* transaction, so following both is the specified behaviour.
|
|
466
|
-
*/
|
|
467
|
-
const HOOKS_BY_METHOD = {
|
|
468
|
-
save: [
|
|
469
|
-
"beforeSave",
|
|
470
|
-
"afterSave",
|
|
471
|
-
"beforeCreate",
|
|
472
|
-
"afterCreate",
|
|
473
|
-
"beforeUpdate",
|
|
474
|
-
"afterUpdate"
|
|
475
|
-
],
|
|
476
|
-
create: [
|
|
477
|
-
"beforeCreate",
|
|
478
|
-
"afterCreate",
|
|
479
|
-
"beforeSave",
|
|
480
|
-
"afterSave"
|
|
481
|
-
],
|
|
482
|
-
createMany: [
|
|
483
|
-
"beforeCreate",
|
|
484
|
-
"afterCreate",
|
|
485
|
-
"beforeSave",
|
|
486
|
-
"afterSave"
|
|
487
|
-
],
|
|
488
|
-
firstOrCreate: [
|
|
489
|
-
"beforeCreate",
|
|
490
|
-
"afterCreate",
|
|
491
|
-
"beforeSave",
|
|
492
|
-
"afterSave"
|
|
493
|
-
],
|
|
494
|
-
fetchOrCreateMany: [
|
|
495
|
-
"beforeCreate",
|
|
496
|
-
"afterCreate",
|
|
497
|
-
"beforeSave",
|
|
498
|
-
"afterSave"
|
|
499
|
-
],
|
|
500
|
-
updateOrCreate: [
|
|
501
|
-
"beforeCreate",
|
|
502
|
-
"afterCreate",
|
|
503
|
-
"beforeUpdate",
|
|
504
|
-
"afterUpdate",
|
|
505
|
-
"beforeSave",
|
|
506
|
-
"afterSave"
|
|
507
|
-
],
|
|
508
|
-
updateOrCreateMany: [
|
|
509
|
-
"beforeCreate",
|
|
510
|
-
"afterCreate",
|
|
511
|
-
"beforeUpdate",
|
|
512
|
-
"afterUpdate",
|
|
513
|
-
"beforeSave",
|
|
514
|
-
"afterSave"
|
|
515
|
-
],
|
|
516
|
-
delete: ["beforeDelete", "afterDelete"],
|
|
517
|
-
forceDelete: ["beforeDelete", "afterDelete"],
|
|
518
|
-
find: ["beforeFind", "afterFind"],
|
|
519
|
-
findOrFail: ["beforeFind", "afterFind"],
|
|
520
|
-
findBy: ["beforeFind", "afterFind"],
|
|
521
|
-
findByOrFail: ["beforeFind", "afterFind"],
|
|
522
|
-
first: ["beforeFind", "afterFind"],
|
|
523
|
-
firstOrFail: ["beforeFind", "afterFind"],
|
|
524
|
-
all: ["beforeFetch", "afterFetch"],
|
|
525
|
-
findMany: ["beforeFetch", "afterFetch"]
|
|
526
|
-
};
|
|
527
|
-
new Set(Object.values(HOOKS_BY_METHOD).flat());
|
|
528
|
-
/**
|
|
529
|
-
* Hook decorators fired by an access, or `[]` when it fires none.
|
|
530
|
-
*
|
|
531
|
-
* `truncate`, `increment`, `decrement` and the pivot operations change rows
|
|
532
|
-
* without instantiating a model, so no hook runs.
|
|
533
|
-
*/
|
|
534
|
-
function hooksFiredBy(access) {
|
|
535
|
-
if (!access.firesHooks) return [];
|
|
536
|
-
return HOOKS_BY_METHOD[access.method] ?? [];
|
|
537
|
-
}
|
|
538
|
-
const READ_METHODS = new Set([
|
|
539
|
-
"find",
|
|
540
|
-
"findOrFail",
|
|
541
|
-
"findBy",
|
|
542
|
-
"findByOrFail",
|
|
543
|
-
"findMany",
|
|
544
|
-
"first",
|
|
545
|
-
"firstOrFail",
|
|
546
|
-
"all",
|
|
547
|
-
"query",
|
|
548
|
-
"preload",
|
|
549
|
-
"load",
|
|
550
|
-
"paginate",
|
|
551
|
-
"count",
|
|
552
|
-
"exists",
|
|
553
|
-
"related",
|
|
554
|
-
"where",
|
|
555
|
-
"orderBy"
|
|
556
|
-
]);
|
|
557
|
-
function detectAccess(call, symbols, relations = /* @__PURE__ */ new Map()) {
|
|
558
|
-
const expression = call.getExpression();
|
|
559
|
-
if (!Node.isPropertyAccessExpression(expression)) return null;
|
|
560
|
-
const method = expression.getName();
|
|
561
|
-
const isWrite = WRITE_METHODS.has(method);
|
|
562
|
-
if (!isWrite && !READ_METHODS.has(method)) return null;
|
|
563
|
-
const receiver = expression.getExpression();
|
|
564
|
-
/**
|
|
565
|
-
* Looks up the PATH before the root: `input.invite.save()` has root `input`,
|
|
566
|
-
* which is no store at all — `input.invite` is.
|
|
567
|
-
*
|
|
568
|
-
* This is the dominant shape in action objects with a typed input, and
|
|
569
|
-
* without it the graph reaches the action and sees no write.
|
|
570
|
-
*/
|
|
571
|
-
const store = symbols.get(pathSymbolOf(receiver) ?? "") ?? symbols.get(rootSymbolOf(receiver) ?? "");
|
|
572
|
-
if (!store) return null;
|
|
573
|
-
return {
|
|
574
|
-
mode: isWrite ? "write" : "read",
|
|
575
|
-
store,
|
|
576
|
-
method,
|
|
577
|
-
line: call.getStartLineNumber(),
|
|
578
|
-
viaRelation: relationTargetOf(method, call, store, relations),
|
|
579
|
-
firesHooks: firesHooks(receiver)
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* An access fires hooks unless it went through the query builder.
|
|
584
|
-
*
|
|
585
|
-
* The signal is a CALL anywhere in the receiver chain: `document.delete()` has
|
|
586
|
-
* none, `Document.query().where(…).delete()` has two. It errs towards NOT
|
|
587
|
-
* following — `(await Document.find(id))!.delete()` is read as bulk — because
|
|
588
|
-
* an FTR that is missing understates, and one that is invented overstates.
|
|
589
|
-
*/
|
|
590
|
-
function firesHooks(receiver) {
|
|
591
|
-
let current = receiver;
|
|
592
|
-
for (let depth = 0; depth < 20; depth++) {
|
|
593
|
-
if (Node.isCallExpression(current)) return false;
|
|
594
|
-
if (!Node.isPropertyAccessExpression(current)) return Node.isIdentifier(current);
|
|
595
|
-
current = current.getExpression();
|
|
596
|
-
}
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
const RELATION_ACCESSORS = new Set([
|
|
600
|
-
"preload",
|
|
601
|
-
"load",
|
|
602
|
-
"related",
|
|
603
|
-
"withCount"
|
|
604
|
-
]);
|
|
605
|
-
/**
|
|
606
|
-
* `.preload('author')` on a store declaring `{ author: 'Author' }` reaches
|
|
607
|
-
* `Author`.
|
|
608
|
-
*/
|
|
609
|
-
function relationTargetOf(method, call, store, relations) {
|
|
610
|
-
if (!RELATION_ACCESSORS.has(method)) return void 0;
|
|
611
|
-
const name = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
612
|
-
if (!name) return void 0;
|
|
613
|
-
return relations.get(store)?.[name];
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Dotted path of a receiver made only of property accesses: `input.invite`
|
|
617
|
-
* yields "input.invite". Any call in between invalidates the path, because the
|
|
618
|
-
* value stops being statically traceable.
|
|
619
|
-
*/
|
|
620
|
-
function pathSymbolOf(node) {
|
|
621
|
-
const parts = [];
|
|
622
|
-
let current = node;
|
|
623
|
-
for (let depth = 0; depth < 20; depth++) {
|
|
624
|
-
if (Node.isIdentifier(current)) return [current.getText(), ...parts].join(".");
|
|
625
|
-
if (!Node.isPropertyAccessExpression(current)) return null;
|
|
626
|
-
parts.unshift(current.getName());
|
|
627
|
-
current = current.getExpression();
|
|
628
|
-
}
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* Root of an `a.b().c()` chain — the left-most identifier.
|
|
633
|
-
*
|
|
634
|
-
* It must traverse `await`, calls, property access and `new`, otherwise
|
|
635
|
-
* `await new Action().handle()` and `Invite.query().where().update()` stop at
|
|
636
|
-
* the first node and the write disappears.
|
|
637
|
-
*/
|
|
638
|
-
function rootSymbolOf(node) {
|
|
639
|
-
let current = node;
|
|
640
|
-
for (let depth = 0; depth < 60 && current; depth++) {
|
|
641
|
-
if (Node.isIdentifier(current)) return current.getText();
|
|
642
|
-
if (current.getKind() === SyntaxKind.ThisKeyword) return "this";
|
|
643
|
-
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isCallExpression(current) || Node.isNewExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
644
|
-
current = current.getExpression();
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
//#endregion
|
|
652
717
|
//#region src/inventory/resolvers/transformer.ts
|
|
653
718
|
/** BaseTransformer's public API; all of it funnels through `toObject` */
|
|
654
719
|
const TRANSFORMER_METHODS = new Set([
|
|
@@ -772,4 +837,4 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
772
837
|
return null;
|
|
773
838
|
}
|
|
774
839
|
//#endregion
|
|
775
|
-
export { rootSymbolOf as a,
|
|
840
|
+
export { rootSymbolOf as a, samePath as c, hooksFiredBy as i, toPosix as l, resolveCall as n, collectEventBindings as o, detectAccess as r, relativeTo as s, BUILTIN_CALL_RESOLVERS as t };
|