@filipebraida/adonis-function-points 0.5.0 → 0.7.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 (39) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +101 -292
  3. package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
  4. package/build/commands/main.js +6 -6
  5. package/build/{fp_calibrate-DUbHiifm.js → fp_calibrate-3TxGdS1b.js} +1 -1
  6. package/build/{fp_count-ChtblhZV.js → fp_count-arGLnVlY.js} +1 -1
  7. package/build/{fp_diff-Dt7J4IWu.js → fp_diff-DBBvzq5x.js} +1 -1
  8. package/build/{fp_explain-DZJ--0-S.js → fp_explain-aNFApwiT.js} +1 -1
  9. package/build/{fp_inventory-CPtmuuke.js → fp_inventory-DIjKIC9t.js} +1 -1
  10. package/build/{fp_metrics-et8F1Wvt.js → fp_metrics-BpU61waG.js} +1 -1
  11. package/build/index.d.ts +8 -4
  12. package/build/index.js +4 -4
  13. package/build/{pipeline-CNTBhs6o.js → pipeline-DO2301fV.js} +2131 -389
  14. package/build/{resolvers-PJwo2Z8R.js → resolvers-DaU4uAqT.js} +603 -165
  15. package/build/{runners-DIt1G85i.js → runners-Dm7cWGa-.js} +6 -3
  16. package/build/src/albrecht/counter.d.ts +38 -5
  17. package/build/src/albrecht/data_functions.d.ts +49 -3
  18. package/build/src/albrecht/diff.d.ts +27 -0
  19. package/build/src/albrecht/index.d.ts +1 -0
  20. package/build/src/albrecht/opaque.d.ts +90 -0
  21. package/build/src/albrecht/technical_filter.d.ts +18 -11
  22. package/build/src/albrecht/transactional_functions.d.ts +7 -0
  23. package/build/src/cli.js +2 -2
  24. package/build/src/define_config.d.ts +55 -57
  25. package/build/src/inventory/graph/call_graph.d.ts +44 -0
  26. package/build/src/inventory/graph/deliveries.d.ts +88 -0
  27. package/build/src/inventory/graph/output_fields.d.ts +143 -0
  28. package/build/src/inventory/paths.d.ts +2 -0
  29. package/build/src/inventory/resolvers/index.d.ts +21 -0
  30. package/build/src/inventory/resolvers/index.js +2 -2
  31. package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
  32. package/build/src/inventory/resolvers/local_function.d.ts +24 -0
  33. package/build/src/inventory/resolvers/transformer.d.ts +0 -23
  34. package/build/src/inventory/sources/commands.d.ts +14 -0
  35. package/build/src/inventory/sources/jobs.d.ts +27 -0
  36. package/build/src/pipeline.js +1 -1
  37. package/build/src/types.d.ts +49 -1
  38. package/build/stubs/config.stub +29 -16
  39. package/package.json +1 -1
@@ -68,6 +68,12 @@ const SCAFFOLDING = new Set([
68
68
  "migrations",
69
69
  "factories"
70
70
  ]);
71
+ /** a seeder, by the directory `make:seeder` writes to — scaffolding, but a fact the report uses */
72
+ function isSeeder(root, file) {
73
+ const relative = relativeTo(root, file);
74
+ if (relative.startsWith("..")) return false;
75
+ return relative.split("/").slice(0, -1).some((segment) => segment === "seeders" || segment === "seeder");
76
+ }
71
77
  function isApplicationCode(root, file) {
72
78
  const relative = relativeTo(root, file);
73
79
  if (relative.startsWith("..")) return false;
@@ -77,129 +83,6 @@ function isApplicationCode(root, file) {
77
83
  return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
78
84
  }
79
85
  //#endregion
80
- //#region src/inventory/sources/event_bindings.ts
81
- /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
82
- const LISTENER_METHOD = "handle";
83
- function collectEventBindings(app) {
84
- const project = new Project({
85
- skipAddingFilesFromTsConfig: true,
86
- skipFileDependencyResolution: true,
87
- compilerOptions: { allowJs: false }
88
- });
89
- for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
90
- const bindings = /* @__PURE__ */ new Map();
91
- for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
92
- const expression = call.getExpression();
93
- if (!Node.isPropertyAccessExpression(expression)) continue;
94
- if (expression.getName() !== "on") continue;
95
- const [event, handlers] = call.getArguments();
96
- if (!event || !handlers) continue;
97
- const eventFile = resolveEventClass(event, file, app);
98
- if (!eventFile) continue;
99
- const refs = listenersOf(handlers, file, app);
100
- if (refs.length === 0) continue;
101
- bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
102
- }
103
- return bindings;
104
- }
105
- /**
106
- * The event class a dispatch or a binding names.
107
- *
108
- * Two shapes reach here: the class imported directly, and the generated
109
- * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
110
- * and therefore the common one. Exported because the resolver has to ask the
111
- * same question of a call site, and two implementations of "which event is
112
- * this" would drift.
113
- */
114
- function resolveEventClass(expression, from, app) {
115
- if (Node.isIdentifier(expression)) {
116
- const target = importedFrom(expression.getText(), from, app);
117
- return target ? toPosix(target) : null;
118
- }
119
- if (!Node.isPropertyAccessExpression(expression)) return null;
120
- const root = expression.getExpression();
121
- if (!Node.isIdentifier(root)) return null;
122
- const registry = importedFrom(root.getText(), from, app);
123
- if (!registry) return null;
124
- return registryEntry(registry, expression.getName(), from.getProject(), app);
125
- }
126
- /** listener bodies named by the second argument of `emitter.on` */
127
- function listenersOf(handlers, from, app) {
128
- const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
129
- const refs = [];
130
- for (const entry of entries) {
131
- /**
132
- * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
133
- * and taking `handle` on faith there would look for a body that is not
134
- * the one bound.
135
- */
136
- if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
137
- const [target, member] = entry.getElements();
138
- const file = target ? listenerFile(target, from, app) : null;
139
- if (!file) continue;
140
- const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
141
- refs.push({
142
- file,
143
- member: named ?? LISTENER_METHOD
144
- });
145
- continue;
146
- }
147
- const file = listenerFile(entry, from, app);
148
- if (file) refs.push({
149
- file,
150
- member: LISTENER_METHOD
151
- });
152
- }
153
- return refs;
154
- }
155
- function listenerFile(entry, from, app) {
156
- if (Node.isPropertyAccessExpression(entry)) {
157
- const root = entry.getExpression();
158
- if (!Node.isIdentifier(root)) return null;
159
- const registry = importedFrom(root.getText(), from, app);
160
- return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
161
- }
162
- if (Node.isIdentifier(entry)) {
163
- const target = importedFrom(entry.getText(), from, app);
164
- return target ? toPosix(target) : null;
165
- }
166
- return null;
167
- }
168
- /** where a local identifier was imported from, resolved through the alias map */
169
- function importedFrom(local, from, app) {
170
- for (const declaration of from.getImportDeclarations()) {
171
- const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
172
- const isDefault = declaration.getDefaultImport()?.getText() === local;
173
- if (!named && !isDefault) continue;
174
- return app.resolveSpecifier(declaration.getModuleSpecifierValue());
175
- }
176
- return null;
177
- }
178
- /**
179
- * The file a key of a generated registry points at.
180
- *
181
- * Both shapes the generators emit are handled: a direct reference to an
182
- * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
183
- * per artefact and per framework version, and reading only one of them silently
184
- * lost half the graph.
185
- */
186
- function registryEntry(registryFile, key, project, app) {
187
- const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
188
- if (!file) return null;
189
- for (const declaration of file.getVariableDeclarations()) {
190
- const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
191
- if (!value) continue;
192
- if (Node.isIdentifier(value)) {
193
- const target = importedFrom(value.getText(), file, app);
194
- return target ? toPosix(target) : null;
195
- }
196
- const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
197
- const target = specifier ? app.resolveSpecifier(specifier) : null;
198
- return target ? toPosix(target) : null;
199
- }
200
- return null;
201
- }
202
- //#endregion
203
86
  //#region src/inventory/detectors/lucid.ts
204
87
  const WRITE_METHODS = new Set([
205
88
  "save",
@@ -506,6 +389,129 @@ function classOfReceiver(receiver) {
506
389
  return null;
507
390
  }
508
391
  //#endregion
392
+ //#region src/inventory/sources/event_bindings.ts
393
+ /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
394
+ const LISTENER_METHOD = "handle";
395
+ function collectEventBindings(app) {
396
+ const project = new Project({
397
+ skipAddingFilesFromTsConfig: true,
398
+ skipFileDependencyResolution: true,
399
+ compilerOptions: { allowJs: false }
400
+ });
401
+ for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
402
+ const bindings = /* @__PURE__ */ new Map();
403
+ for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
404
+ const expression = call.getExpression();
405
+ if (!Node.isPropertyAccessExpression(expression)) continue;
406
+ if (expression.getName() !== "on") continue;
407
+ const [event, handlers] = call.getArguments();
408
+ if (!event || !handlers) continue;
409
+ const eventFile = resolveEventClass(event, file, app);
410
+ if (!eventFile) continue;
411
+ const refs = listenersOf(handlers, file, app);
412
+ if (refs.length === 0) continue;
413
+ bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
414
+ }
415
+ return bindings;
416
+ }
417
+ /**
418
+ * The event class a dispatch or a binding names.
419
+ *
420
+ * Two shapes reach here: the class imported directly, and the generated
421
+ * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
422
+ * and therefore the common one. Exported because the resolver has to ask the
423
+ * same question of a call site, and two implementations of "which event is
424
+ * this" would drift.
425
+ */
426
+ function resolveEventClass(expression, from, app) {
427
+ if (Node.isIdentifier(expression)) {
428
+ const target = importedFrom(expression.getText(), from, app);
429
+ return target ? toPosix(target) : null;
430
+ }
431
+ if (!Node.isPropertyAccessExpression(expression)) return null;
432
+ const root = expression.getExpression();
433
+ if (!Node.isIdentifier(root)) return null;
434
+ const registry = importedFrom(root.getText(), from, app);
435
+ if (!registry) return null;
436
+ return registryEntry(registry, expression.getName(), from.getProject(), app);
437
+ }
438
+ /** listener bodies named by the second argument of `emitter.on` */
439
+ function listenersOf(handlers, from, app) {
440
+ const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
441
+ const refs = [];
442
+ for (const entry of entries) {
443
+ /**
444
+ * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
445
+ * and taking `handle` on faith there would look for a body that is not
446
+ * the one bound.
447
+ */
448
+ if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
449
+ const [target, member] = entry.getElements();
450
+ const file = target ? listenerFile(target, from, app) : null;
451
+ if (!file) continue;
452
+ const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
453
+ refs.push({
454
+ file,
455
+ member: named ?? LISTENER_METHOD
456
+ });
457
+ continue;
458
+ }
459
+ const file = listenerFile(entry, from, app);
460
+ if (file) refs.push({
461
+ file,
462
+ member: LISTENER_METHOD
463
+ });
464
+ }
465
+ return refs;
466
+ }
467
+ function listenerFile(entry, from, app) {
468
+ if (Node.isPropertyAccessExpression(entry)) {
469
+ const root = entry.getExpression();
470
+ if (!Node.isIdentifier(root)) return null;
471
+ const registry = importedFrom(root.getText(), from, app);
472
+ return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
473
+ }
474
+ if (Node.isIdentifier(entry)) {
475
+ const target = importedFrom(entry.getText(), from, app);
476
+ return target ? toPosix(target) : null;
477
+ }
478
+ return null;
479
+ }
480
+ /** where a local identifier was imported from, resolved through the alias map */
481
+ function importedFrom(local, from, app) {
482
+ for (const declaration of from.getImportDeclarations()) {
483
+ const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
484
+ const isDefault = declaration.getDefaultImport()?.getText() === local;
485
+ if (!named && !isDefault) continue;
486
+ return app.resolveSpecifier(declaration.getModuleSpecifierValue());
487
+ }
488
+ return null;
489
+ }
490
+ /**
491
+ * The file a key of a generated registry points at.
492
+ *
493
+ * Both shapes the generators emit are handled: a direct reference to an
494
+ * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
495
+ * per artefact and per framework version, and reading only one of them silently
496
+ * lost half the graph.
497
+ */
498
+ function registryEntry(registryFile, key, project, app) {
499
+ const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
500
+ if (!file) return null;
501
+ for (const declaration of file.getVariableDeclarations()) {
502
+ const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
503
+ if (!value) continue;
504
+ if (Node.isIdentifier(value)) {
505
+ const target = importedFrom(value.getText(), file, app);
506
+ return target ? toPosix(target) : null;
507
+ }
508
+ const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
509
+ const target = specifier ? app.resolveSpecifier(specifier) : null;
510
+ return target ? toPosix(target) : null;
511
+ }
512
+ return null;
513
+ }
514
+ //#endregion
509
515
  //#region src/inventory/resolvers/event_dispatch.ts
510
516
  /**
511
517
  * "Event" pattern: the handler announces, and listeners act.
@@ -613,6 +619,68 @@ const jobDispatchResolver = {
613
619
  }
614
620
  };
615
621
  //#endregion
622
+ //#region src/inventory/resolvers/local_function.ts
623
+ /**
624
+ * "Local function" pattern: a helper declared in the same file, not imported.
625
+ *
626
+ * const lista = await proximos(id) // function proximos() { … }
627
+ * return rows.map(paraLinha) // const paraLinha = (row) => …
628
+ *
629
+ * A query object that keeps its helpers beside it is common, and before this
630
+ * every such call was unresolved: the store a helper read was reached by nobody
631
+ * from that route, and a value built by it left as a whole table handed in.
632
+ *
633
+ * Only module-level declarations: a closure declared inside a body is part of
634
+ * that body, and the graph already walks it.
635
+ */
636
+ const localFunctionResolver = {
637
+ name: "local-function",
638
+ order: 45,
639
+ resolve(call, ctx) {
640
+ const named = calledFunctionOf(call);
641
+ if (!named) return [];
642
+ const name = named.getText();
643
+ if (ctx.imports.has(name)) return [];
644
+ if (!declaresFunction(ctx.file, name)) return [];
645
+ return [{
646
+ file: ctx.file.getFilePath(),
647
+ member: name
648
+ }];
649
+ }
650
+ };
651
+ /** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
652
+ const APPLIES_CALLBACK = new Set([
653
+ "map",
654
+ "flatMap",
655
+ "forEach",
656
+ "filter",
657
+ "find",
658
+ "some",
659
+ "every",
660
+ "sort",
661
+ "reduce"
662
+ ]);
663
+ /**
664
+ * The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
665
+ * names `paraLinha`, called once per row — the body the graph must read is the
666
+ * same, whichever way it was reached.
667
+ */
668
+ function calledFunctionOf(call) {
669
+ const expression = call.getExpression();
670
+ if (Node.isIdentifier(expression)) return expression;
671
+ if (Node.isPropertyAccessExpression(expression) && APPLIES_CALLBACK.has(expression.getName())) {
672
+ const callback = call.getArguments()[0];
673
+ if (callback && Node.isIdentifier(callback)) return callback;
674
+ }
675
+ return null;
676
+ }
677
+ /** `function name() {}` or `const name = () => {}` / `function () {}` at module level */
678
+ function declaresFunction(file, name) {
679
+ if (file.getFunction(name)) return true;
680
+ const initializer = file.getVariableDeclaration(name)?.getInitializer();
681
+ return !!initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer));
682
+ }
683
+ //#endregion
616
684
  //#region src/inventory/resolvers/module_function.ts
617
685
  /**
618
686
  * "Module function" pattern: no class at all.
@@ -627,8 +695,8 @@ const moduleFunctionResolver = {
627
695
  name: "module-function",
628
696
  order: 50,
629
697
  resolve(call, ctx) {
630
- const expr = call.getExpression();
631
- if (!expr.isKind(SyntaxKind.Identifier)) return [];
698
+ const expr = calledFunctionOf(call);
699
+ if (!expr) return [];
632
700
  const local = expr.getText();
633
701
  const file = ctx.imports.get(local);
634
702
  if (!file) return [];
@@ -750,6 +818,298 @@ const staticServiceResolver = {
750
818
  }
751
819
  };
752
820
  //#endregion
821
+ //#region src/inventory/graph/output_fields.ts
822
+ /**
823
+ * Does this class extend a transformer base from a package?
824
+ *
825
+ * Decided by the base's name AND by its import being a bare specifier, so an
826
+ * application class that merely happens to own a `transform` method is not
827
+ * mistaken for one. Shared with the `transformer` resolver: one definition of
828
+ * what a transformer is, or the resolver follows a body this walker refuses.
829
+ */
830
+ function isTransformerClass(cls) {
831
+ const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
832
+ if (!name?.endsWith("Transformer")) return false;
833
+ const imported = cls.getSourceFile().getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
834
+ return imported !== void 0 && !imported.getModuleSpecifierValue().startsWith("#");
835
+ }
836
+ /** `class X extends BaseTransformer<Livro>` -> 'Livro' */
837
+ function transformerResourceOf(cls) {
838
+ return (cls.getExtends()?.getTypeArguments()[0])?.asKind(SyntaxKind.TypeReference)?.getTypeName().getText() ?? null;
839
+ }
840
+ /**
841
+ * The keys a transformer method returns.
842
+ *
843
+ * `followed` says whether a call inside the literal is a body the graph walks —
844
+ * `AutorTransformer.transform(x)`, `this.toObject()` — in which case its keys
845
+ * arrive through that body and the key holding it is not a DET of its own: the
846
+ * user sees the author's name, not an "autor" field.
847
+ *
848
+ * { titulo: l.titulo } 1 — `titulo`
849
+ * { autor: AutorTransformer.transform } 0 here; the nested body contributes
850
+ * { endereco: { rua, cidade } } leaves individually
851
+ * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
852
+ * { itens: xs.map((i) => ({ a, b })) } the leaves, once
853
+ * ...this.pick(this.resource, [...]) the listed names
854
+ * ...this.toObject() 0 here; the followed body contributes
855
+ * ...anythingElse 1, opaque, reported
856
+ *
857
+ * A key that is the identifier of the transformer's resource is not a DET, for
858
+ * the same reason `isPrimary` is not one on the data function.
859
+ */
860
+ function outputFieldsIn(body, owner, stores, followed) {
861
+ if (!owner || !isTransformerClass(owner)) return {
862
+ outputs: [],
863
+ opaqueOutputs: [],
864
+ resource: null
865
+ };
866
+ const qualifier = owner.getName() ?? "Transformer";
867
+ const resource = transformerResourceOf(owner);
868
+ /**
869
+ * The resource's key and its system timestamps: re-emitted for links and
870
+ * sorting, and not something the user recognises — the same two exclusions the
871
+ * data function applies (§6).
872
+ */
873
+ const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
874
+ const { leaves, opaque } = collectLeaves(returnedLiteralsOf(body), {
875
+ qualifier,
876
+ excluded,
877
+ followed
878
+ });
879
+ return {
880
+ outputs: leaves,
881
+ opaqueOutputs: opaque,
882
+ resource: resource && stores.has(resource) ? resource : null
883
+ };
884
+ }
885
+ /**
886
+ * The leaves of object literals, by the §7 rules:
887
+ *
888
+ * { titulo: l.titulo } 1 — `titulo`
889
+ * { autor: AutorTransformer.transform } 0 here; the followed body contributes
890
+ * { endereco: { rua, cidade } } leaves individually
891
+ * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
892
+ * { itens: xs.map((i) => ({ a, b })) } the leaves, once
893
+ * ...this.pick(this.resource, [...]) the listed names
894
+ * ...this.toObject() 0 here; the followed body contributes
895
+ * ...anythingElse 1, opaque, reported
896
+ */
897
+ function collectLeaves(literals, options) {
898
+ const { qualifier, excluded, followed } = options;
899
+ const outputs = /* @__PURE__ */ new Set();
900
+ const opaque = /* @__PURE__ */ new Set();
901
+ const q = qualifier ? `${qualifier}.` : "";
902
+ const leaf = (prefix, name) => {
903
+ if (prefix === "" && excluded.has(name)) return;
904
+ outputs.add(`${q}${prefix ? `${prefix}.${name}` : name}`);
905
+ };
906
+ const walk = (literal, prefix) => {
907
+ for (const property of literal.getProperties()) {
908
+ if (Node.isShorthandPropertyAssignment(property)) {
909
+ leaf(prefix, property.getName());
910
+ continue;
911
+ }
912
+ if (Node.isMethodDeclaration(property) || Node.isGetAccessorDeclaration(property)) {
913
+ leaf(prefix, property.getName());
914
+ continue;
915
+ }
916
+ if (Node.isSpreadAssignment(property)) {
917
+ spread(property.getExpression(), prefix);
918
+ continue;
919
+ }
920
+ if (!Node.isPropertyAssignment(property)) continue;
921
+ const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
922
+ const value = unwrap(property.getInitializer());
923
+ if (!value) {
924
+ leaf(prefix, name);
925
+ continue;
926
+ }
927
+ if (Node.isObjectLiteralExpression(value)) {
928
+ walk(value, prefix ? `${prefix}.${name}` : name);
929
+ continue;
930
+ }
931
+ if (Node.isCallExpression(value) && followed(value)) continue;
932
+ const mapped = mappedLiteralOf(value);
933
+ if (mapped) {
934
+ walk(mapped, prefix ? `${prefix}.${name}` : name);
935
+ continue;
936
+ }
937
+ leaf(prefix, name);
938
+ }
939
+ };
940
+ const spread = (expression, prefix) => {
941
+ const value = unwrap(expression);
942
+ if (!value) return;
943
+ if (Node.isObjectLiteralExpression(value)) {
944
+ walk(value, prefix);
945
+ return;
946
+ }
947
+ if (Node.isConditionalExpression(value)) {
948
+ spread(value.getWhenTrue(), prefix);
949
+ spread(value.getWhenFalse(), prefix);
950
+ return;
951
+ }
952
+ if (Node.isCallExpression(value)) {
953
+ const picked = pickedNamesOf(value);
954
+ if (picked) {
955
+ for (const name of picked) leaf(prefix, name);
956
+ return;
957
+ }
958
+ if (followed(value)) return;
959
+ }
960
+ /**
961
+ * `...this.resource.serialize()`, `...this.extras`: whatever the model has.
962
+ * One DET as a floor, and reported — the placeholder carries the expression
963
+ * so the report can name what could not be read.
964
+ */
965
+ const placeholder = `${q}${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
966
+ outputs.add(placeholder);
967
+ opaque.add(placeholder);
968
+ };
969
+ for (const literal of literals) walk(literal, "");
970
+ return {
971
+ leaves: [...outputs],
972
+ opaque: [...opaque]
973
+ };
974
+ }
975
+ /**
976
+ * Object literals the body itself returns — not the ones returned by arrow
977
+ * functions inside it, which belong to `.map()` callbacks and are read as
978
+ * repeating groups where they occur.
979
+ */
980
+ function returnedLiteralsOf(body) {
981
+ const literals = [];
982
+ for (const statement of body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
983
+ if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== body) continue;
984
+ const value = unwrap(statement.getExpression());
985
+ if (value && Node.isObjectLiteralExpression(value)) literals.push(value);
986
+ }
987
+ return literals;
988
+ }
989
+ /** `this.pick(this.resource, ['a', 'b'])` -> ['a', 'b']; null when it is not that call */
990
+ function pickedNamesOf(call) {
991
+ const callee = call.getExpression();
992
+ if (!Node.isPropertyAccessExpression(callee)) return null;
993
+ if (callee.getName() !== "pick") return null;
994
+ if (callee.getExpression().getKind() !== SyntaxKind.ThisKeyword) return null;
995
+ const list = call.getArguments()[1]?.asKind(SyntaxKind.ArrayLiteralExpression);
996
+ if (!list) return null;
997
+ const names = [];
998
+ for (const element of list.getElements()) {
999
+ const name = element.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1000
+ if (name === void 0) return null;
1001
+ names.push(name);
1002
+ }
1003
+ return names;
1004
+ }
1005
+ /** `xs.map((x) => ({ a, b }))` -> the literal; null for a scalar map or anything else */
1006
+ function mappedLiteralOf(value) {
1007
+ if (!Node.isCallExpression(value)) return null;
1008
+ const callee = value.getExpression();
1009
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "map") return null;
1010
+ const callback = value.getArguments()[0];
1011
+ if (!callback || !Node.isArrowFunction(callback)) return null;
1012
+ const returned = unwrap(callback.getBody().asKind(SyntaxKind.Block) ? null : callback.getBody());
1013
+ if (returned && Node.isObjectLiteralExpression(returned)) return returned;
1014
+ for (const statement of callback.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
1015
+ const expression = unwrap(statement.getExpression());
1016
+ if (expression && Node.isObjectLiteralExpression(expression)) return expression;
1017
+ }
1018
+ return null;
1019
+ }
1020
+ /** strips parentheses, `as`, `satisfies` and non-null assertions */
1021
+ function unwrap(node) {
1022
+ let current = node;
1023
+ while (current && (Node.isParenthesizedExpression(current) || Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isNonNullExpression(current))) current = current.getExpression();
1024
+ return current && Node.isExpression(current) ? current : null;
1025
+ }
1026
+ /**
1027
+ * The columns a fluent chain names in `.select()`, walking UP from the access
1028
+ * the detector recognised (`Livro.query()`) through the chain it belongs to.
1029
+ *
1030
+ * Only calls on the chain itself qualify. A `q.select('nome')` inside a
1031
+ * `preload('autor', (q) => …)` callback narrows the related store, not this
1032
+ * one, and reading every descendant would have attributed it here.
1033
+ *
1034
+ * Both spellings count: `.select(['a', 'b'])` and `.select('a', 'b')`. Anything
1035
+ * that is not a string literal — a variable, a raw expression — is unreadable:
1036
+ * the store falls back to every column, and the chain is reported.
1037
+ */
1038
+ /** methods that leave ONE derived scalar rather than rows */
1039
+ const AGGREGATES = new Set([
1040
+ "count",
1041
+ "countDistinct",
1042
+ "exists",
1043
+ "sum",
1044
+ "avg",
1045
+ "min",
1046
+ "max"
1047
+ ]);
1048
+ function chainShapeOf(access) {
1049
+ const selected = /* @__PURE__ */ new Set();
1050
+ const unreadable = [];
1051
+ let aggregate = false;
1052
+ const inspect = (call) => {
1053
+ const callee = call.getExpression();
1054
+ if (!Node.isPropertyAccessExpression(callee)) return;
1055
+ const name = callee.getName();
1056
+ if (AGGREGATES.has(name)) aggregate = true;
1057
+ if (name !== "select") return;
1058
+ const literal = literalColumnsOf(call);
1059
+ if (literal) for (const column of literal) selected.add(column);
1060
+ else unreadable.push({
1061
+ line: call.getStartLineNumber(),
1062
+ expression: call.getText().replace(/\s+/g, "")
1063
+ });
1064
+ };
1065
+ let node = access;
1066
+ for (let depth = 0; node && depth < 40; depth++) {
1067
+ if (Node.isCallExpression(node)) inspect(node);
1068
+ node = Node.isCallExpression(node) || Node.isPropertyAccessExpression(node) ? node.getExpression() : void 0;
1069
+ }
1070
+ node = access;
1071
+ for (let depth = 0; depth < 40; depth++) {
1072
+ const parent = node.getParent();
1073
+ if (!parent) break;
1074
+ if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
1075
+ node = parent;
1076
+ continue;
1077
+ }
1078
+ if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) break;
1079
+ const call = parent.getParent();
1080
+ if (!call || !Node.isCallExpression(call) || call.getExpression() !== parent) break;
1081
+ inspect(call);
1082
+ node = call;
1083
+ }
1084
+ return {
1085
+ selected: [...selected],
1086
+ aggregate,
1087
+ unreadable
1088
+ };
1089
+ }
1090
+ /** string-literal columns of one `.select(...)`; null when any is not a literal */
1091
+ function literalColumnsOf(call) {
1092
+ const columns = [];
1093
+ const read = (node) => {
1094
+ const value = node.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1095
+ if (value === void 0) return false;
1096
+ const bare = value.includes(".") ? value.split(".").pop() : value;
1097
+ if (bare !== "*") columns.push(camelCase(bare));
1098
+ return true;
1099
+ };
1100
+ for (const argument of call.getArguments()) {
1101
+ const list = argument.asKind(SyntaxKind.ArrayLiteralExpression);
1102
+ if (list) {
1103
+ for (const element of list.getElements()) if (!read(element)) return null;
1104
+ continue;
1105
+ }
1106
+ if (!read(argument)) return null;
1107
+ }
1108
+ return columns;
1109
+ }
1110
+ /** `created_at` -> `createdAt`, to match the model's attribute names */
1111
+ const camelCase = (value) => value.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1112
+ //#endregion
753
1113
  //#region src/inventory/resolvers/transformer.ts
754
1114
  /** BaseTransformer's public API; all of it funnels through `toObject` */
755
1115
  const TRANSFORMER_METHODS = new Set([
@@ -761,6 +1121,8 @@ const TRANSFORMER_METHODS = new Set([
761
1121
  ]);
762
1122
  /** what the application-side body is called */
763
1123
  const APPLICATION_BODY = "toObject";
1124
+ /** the variant named in these is a method of the transformer, and part of what leaves */
1125
+ const VARIANT_METHODS = new Set(["useVariant", "withVariant"]);
764
1126
  /**
765
1127
  * "Transformer" pattern: the package supplies the API, the application
766
1128
  * supplies the body.
@@ -784,47 +1146,51 @@ const APPLICATION_BODY = "toObject";
784
1146
  * transaction that serialised through it. Without this, a table written only
785
1147
  * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
786
1148
  */
787
- const transformerResolver = {
788
- name: "transformer",
789
- order: 18,
790
- resolve(call, ctx) {
791
- const expression = call.getExpression();
792
- if (!Node.isPropertyAccessExpression(expression)) return [];
793
- if (!TRANSFORMER_METHODS.has(expression.getName())) return [];
794
- /**
795
- * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
796
- * well as `X.transform(p)`. Analysing the same body twice is free: the
797
- * graph dedupes by file and member.
798
- */
799
- const symbol = rootSymbolOf(expression.getExpression());
800
- if (!symbol) return [];
801
- const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
802
- if (!file) return [];
803
- const declared = ctx.sourceFile(file);
804
- if (!declared || !extendsTransformer(declared)) return [];
805
- return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
806
- file,
807
- member: APPLICATION_BODY
808
- }] : [];
809
- }
810
- };
811
- /**
812
- * Does a class here extend a transformer base from a package?
813
- *
814
- * Checked by the base's name and by its import being a bare specifier, so an
815
- * application class that merely happens to own a `transform` method is not
816
- * mistaken for one.
817
- */
818
- function extendsTransformer(file) {
819
- if (!file) return false;
820
- for (const cls of file.getClasses()) {
821
- const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
822
- if (!name?.endsWith("Transformer")) continue;
823
- const imported = file.getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
824
- if (imported && !imported.getModuleSpecifierValue().startsWith("#")) return true;
1149
+ /** is `.useVariant('x')` / `.withVariant('x')` the next link of this call's chain? */
1150
+ function variantFollows(call) {
1151
+ let node = call;
1152
+ for (let depth = 0; depth < 20; depth++) {
1153
+ const parent = node.getParent();
1154
+ if (!parent) return false;
1155
+ if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
1156
+ node = parent;
1157
+ continue;
1158
+ }
1159
+ if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) return false;
1160
+ const next = parent.getParent();
1161
+ if (!next || !Node.isCallExpression(next) || next.getExpression() !== parent) return false;
1162
+ if (VARIANT_METHODS.has(parent.getName())) return next.getArguments()[0]?.getKind() === SyntaxKind.StringLiteral;
1163
+ node = next;
825
1164
  }
826
1165
  return false;
827
1166
  }
1167
+ /** the transformer class a chain call is on, if it is one: `X.transform(p)`, `X.transform(p).useVariant(v)` */
1168
+ function transformerOf(call, ctx) {
1169
+ const expression = call.getExpression();
1170
+ if (!Node.isPropertyAccessExpression(expression)) return null;
1171
+ if (!TRANSFORMER_METHODS.has(expression.getName())) return null;
1172
+ /**
1173
+ * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
1174
+ * well as `X.transform(p)`. Analysing the same body twice is free: the
1175
+ * graph dedupes by file and member.
1176
+ */
1177
+ const symbol = rootSymbolOf(expression.getExpression());
1178
+ if (!symbol) return null;
1179
+ const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
1180
+ if (!file) return null;
1181
+ /**
1182
+ * One definition of what a transformer is, shared with the output walker
1183
+ * (`graph/output_fields.ts`): the resolver must not follow a body whose keys
1184
+ * the walker would refuse to read, or the other way round.
1185
+ */
1186
+ const declared = ctx.sourceFile(file);
1187
+ if (!declared || !declared.getClasses().some(isTransformerClass)) return null;
1188
+ const owner = declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY));
1189
+ return owner ? {
1190
+ file,
1191
+ owner
1192
+ } : null;
1193
+ }
828
1194
  //#endregion
829
1195
  //#region src/inventory/resolvers/index.ts
830
1196
  /**
@@ -839,9 +1205,53 @@ const BUILTIN_CALL_RESOLVERS = [
839
1205
  actionObjectResolver,
840
1206
  eventDispatchResolver,
841
1207
  jobDispatchResolver,
842
- transformerResolver,
1208
+ {
1209
+ name: "transformer",
1210
+ order: 18,
1211
+ /**
1212
+ * The `transform` before a `useVariant`: claimed and followed nowhere, or
1213
+ * `static-service` would take it next, look for a `transform` body in the
1214
+ * application file and report the package method as a gap.
1215
+ */
1216
+ ignores(call, ctx) {
1217
+ const expression = call.getExpression();
1218
+ if (!Node.isPropertyAccessExpression(expression)) return false;
1219
+ if (VARIANT_METHODS.has(expression.getName()) || !variantFollows(call)) return false;
1220
+ return transformerOf(call, ctx) !== null;
1221
+ },
1222
+ resolve(call, ctx) {
1223
+ const expression = call.getExpression();
1224
+ if (!Node.isPropertyAccessExpression(expression)) return [];
1225
+ const found = transformerOf(call, ctx);
1226
+ if (!found) return [];
1227
+ const { file, owner } = found;
1228
+ /**
1229
+ * `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
1230
+ * transformer, named after it, and it REPLACES `toObject()` — the shape that
1231
+ * leaves is the variant's. The chain is visited call by call: the `useVariant`
1232
+ * call resolves the variant's body, and the `transform` call before it resolves
1233
+ * nothing, or both shapes would leave and the page would count twice.
1234
+ */
1235
+ if (VARIANT_METHODS.has(expression.getName())) {
1236
+ const variant = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1237
+ return variant && owner.getMethod(variant) ? [{
1238
+ file,
1239
+ member: variant
1240
+ }] : [{
1241
+ file,
1242
+ member: APPLICATION_BODY
1243
+ }];
1244
+ }
1245
+ if (variantFollows(call)) return [];
1246
+ return [{
1247
+ file,
1248
+ member: APPLICATION_BODY
1249
+ }];
1250
+ }
1251
+ },
843
1252
  staticServiceResolver,
844
1253
  propertyServiceResolver,
1254
+ localFunctionResolver,
845
1255
  moduleFunctionResolver
846
1256
  ];
847
1257
  /**
@@ -881,5 +1291,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
881
1291
  }
882
1292
  return null;
883
1293
  }
1294
+ /**
1295
+ * A strategy that recognises a family of calls and knows they reach no data
1296
+ * store — a rate limiter, an attachment's URL, an authorisation check.
1297
+ *
1298
+ * Read from a real configuration, every such strategy was the same eight lines:
1299
+ * a helper to get the method name off the ts-morph node (the app does not depend
1300
+ * on ts-morph), a `resolve` that returns nothing, and one comparison. What the
1301
+ * design wants is kept — it is still a NAMED strategy, and `fp:count` still
1302
+ * reports the volume it declared data-free — and the ceremony is not.
1303
+ */
1304
+ function ignoreCalls(options) {
1305
+ if (!options.methods?.length && !options.matching) throw new Error(`ignoreCalls("${options.name}"): say what it ignores — \`methods\` or \`matching\``);
1306
+ const methods = new Set(options.methods ?? []);
1307
+ return {
1308
+ name: options.name,
1309
+ order: options.order ?? 1,
1310
+ /** follows nothing: the whole point */
1311
+ resolve: () => [],
1312
+ ignores(call) {
1313
+ const callee = call.getExpression();
1314
+ const text = callee.getText();
1315
+ if (options.matching?.test(text)) return true;
1316
+ if (methods.size === 0) return false;
1317
+ const method = callee.getKindName() === "PropertyAccessExpression" ? text.split(".").pop() : text;
1318
+ return method !== void 0 && methods.has(method);
1319
+ }
1320
+ };
1321
+ }
884
1322
  //#endregion
885
- export { hooksFiredBy as a, isApplicationCode as c, toPosix as d, detectAccess as i, relativeTo as l, isTechnicalWrite as n, rootSymbolOf as o, resolveCall as r, collectEventBindings as s, BUILTIN_CALL_RESOLVERS as t, samePath as u };
1323
+ export { relativeTo as _, chainShapeOf as a, unwrap as c, collectEventBindings as d, detectAccess as f, isSeeder as g, isApplicationCode as h, resolveCall as i, DISPATCH_METHODS as l, rootSymbolOf as m, ignoreCalls as n, mappedLiteralOf as o, hooksFiredBy as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, EXECUTION_METHODS as u, samePath as v, toPosix as y };