@filipebraida/adonis-function-points 0.4.0 → 0.6.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 +155 -0
- package/README.md +101 -281
- package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DLZP5bUp.js → fp_calibrate-EAuAtdbq.js} +1 -1
- package/build/{fp_count-DNSwaLUD.js → fp_count-CZ0cUUBQ.js} +1 -1
- package/build/{fp_diff-CCKxqGKh.js → fp_diff-BTg_LX0r.js} +1 -1
- package/build/{fp_explain-Dpiby5Qx.js → fp_explain-D6QvDLKQ.js} +1 -1
- package/build/{fp_inventory-DHwZzEQf.js → fp_inventory-C43fU39x.js} +1 -1
- package/build/{fp_metrics-M84qLYaE.js → fp_metrics-DEMPk4xC.js} +1 -1
- package/build/index.d.ts +8 -4
- package/build/index.js +4 -4
- package/build/{pipeline-Dm9KvUvF.js → pipeline-Cq4dNTNE.js} +841 -264
- package/build/{resolvers-vMahHkAd.js → resolvers-DlKJOZnk.js} +373 -63
- package/build/{runners-DetZGfh5.js → runners-FYmPIPub.js} +12 -4
- package/build/src/albrecht/counter.d.ts +31 -5
- package/build/src/albrecht/data_functions.d.ts +49 -3
- package/build/src/albrecht/diff.d.ts +27 -0
- package/build/src/albrecht/index.d.ts +1 -0
- package/build/src/albrecht/opaque.d.ts +90 -0
- package/build/src/albrecht/technical_filter.d.ts +18 -11
- package/build/src/albrecht/transactional_functions.d.ts +7 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +55 -57
- package/build/src/inventory/graph/call_graph.d.ts +39 -0
- package/build/src/inventory/graph/output_fields.d.ts +99 -0
- package/build/src/inventory/paths.d.ts +3 -0
- package/build/src/inventory/resolvers/index.d.ts +28 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/types.d.ts +19 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +32 -1
- package/build/stubs/config.stub +29 -16
- package/package.json +1 -1
|
@@ -40,6 +40,48 @@ const toPosix = (value) => value.split("\\").join("/");
|
|
|
40
40
|
const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
|
|
41
41
|
/** Compares two paths that may have come from different sources. */
|
|
42
42
|
const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
|
|
43
|
+
/**
|
|
44
|
+
* Is this file the application's own code, as opposed to the scaffolding around it?
|
|
45
|
+
*
|
|
46
|
+
* The top-level filter on `scanRoots` already drops `tests/`, `database/` and the
|
|
47
|
+
* rest — but only at the ROOT. Applications organised by domain module put both
|
|
48
|
+
* inside `app/`:
|
|
49
|
+
*
|
|
50
|
+
* app/billing/tests/functional/invoice.spec.ts
|
|
51
|
+
* app/billing/seeders/plan_seeder.ts
|
|
52
|
+
*
|
|
53
|
+
* so they land in the project, and `writtenAnywhere()` read a seeder's inserts as
|
|
54
|
+
* the application maintaining the table. A reference table only the seed populates
|
|
55
|
+
* came out as an ILF — which the CPM does not allow: data maintained by the
|
|
56
|
+
* development team is at most an EIF, and code data is not counted at all.
|
|
57
|
+
*
|
|
58
|
+
* The segments are AdonisJS's own: `make:test` writes to a suite directory,
|
|
59
|
+
* `make:seeder` to `seeders`, `make:migration` to `migrations`, `make:factory` to
|
|
60
|
+
* `factories`. The `.spec`/`.test` suffixes come from the suite globs in
|
|
61
|
+
* `adonisrc.ts`.
|
|
62
|
+
*/
|
|
63
|
+
const SCAFFOLDING = new Set([
|
|
64
|
+
"tests",
|
|
65
|
+
"test",
|
|
66
|
+
"seeders",
|
|
67
|
+
"seeder",
|
|
68
|
+
"migrations",
|
|
69
|
+
"factories"
|
|
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
|
+
}
|
|
77
|
+
function isApplicationCode(root, file) {
|
|
78
|
+
const relative = relativeTo(root, file);
|
|
79
|
+
if (relative.startsWith("..")) return false;
|
|
80
|
+
const parts = relative.split("/");
|
|
81
|
+
const name = parts.at(-1) ?? "";
|
|
82
|
+
if (/\.(spec|test)\.[jt]s$/.test(name)) return false;
|
|
83
|
+
return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
|
|
84
|
+
}
|
|
43
85
|
//#endregion
|
|
44
86
|
//#region src/inventory/sources/event_bindings.ts
|
|
45
87
|
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
@@ -714,81 +756,284 @@ const staticServiceResolver = {
|
|
|
714
756
|
}
|
|
715
757
|
};
|
|
716
758
|
//#endregion
|
|
717
|
-
//#region src/inventory/
|
|
718
|
-
/** BaseTransformer's public API; all of it funnels through `toObject` */
|
|
719
|
-
const TRANSFORMER_METHODS = new Set([
|
|
720
|
-
"transform",
|
|
721
|
-
"paginate",
|
|
722
|
-
"toJSON",
|
|
723
|
-
"useVariant",
|
|
724
|
-
"withVariant"
|
|
725
|
-
]);
|
|
726
|
-
/** what the application-side body is called */
|
|
727
|
-
const APPLICATION_BODY = "toObject";
|
|
759
|
+
//#region src/inventory/graph/output_fields.ts
|
|
728
760
|
/**
|
|
729
|
-
*
|
|
730
|
-
* supplies the body.
|
|
731
|
-
*
|
|
732
|
-
* class InviteTransformer extends BaseTransformer<Invite> {
|
|
733
|
-
* toObject() { … }
|
|
734
|
-
* }
|
|
761
|
+
* Does this class extend a transformer base from a package?
|
|
735
762
|
*
|
|
736
|
-
*
|
|
763
|
+
* Decided by the base's name AND by its import being a bare specifier, so an
|
|
764
|
+
* application class that merely happens to own a `transform` method is not
|
|
765
|
+
* mistaken for one. Shared with the `transformer` resolver: one definition of
|
|
766
|
+
* what a transformer is, or the resolver follows a body this walker refuses.
|
|
767
|
+
*/
|
|
768
|
+
function isTransformerClass(cls) {
|
|
769
|
+
const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
|
|
770
|
+
if (!name?.endsWith("Transformer")) return false;
|
|
771
|
+
const imported = cls.getSourceFile().getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
|
|
772
|
+
return imported !== void 0 && !imported.getModuleSpecifierValue().startsWith("#");
|
|
773
|
+
}
|
|
774
|
+
/** `class X extends BaseTransformer<Livro>` -> 'Livro' */
|
|
775
|
+
function transformerResourceOf(cls) {
|
|
776
|
+
return (cls.getExtends()?.getTypeArguments()[0])?.asKind(SyntaxKind.TypeReference)?.getTypeName().getText() ?? null;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The keys a transformer method returns.
|
|
737
780
|
*
|
|
738
|
-
* `
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* body worth analysing was in the application all along.
|
|
781
|
+
* `followed` says whether a call inside the literal is a body the graph walks —
|
|
782
|
+
* `AutorTransformer.transform(x)`, `this.toObject()` — in which case its keys
|
|
783
|
+
* arrive through that body and the key holding it is not a DET of its own: the
|
|
784
|
+
* user sees the author's name, not an "autor" field.
|
|
743
785
|
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
786
|
+
* { titulo: l.titulo } 1 — `titulo`
|
|
787
|
+
* { autor: AutorTransformer.transform } 0 here; the nested body contributes
|
|
788
|
+
* { endereco: { rua, cidade } } leaves individually
|
|
789
|
+
* { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
|
|
790
|
+
* { itens: xs.map((i) => ({ a, b })) } the leaves, once
|
|
791
|
+
* ...this.pick(this.resource, [...]) the listed names
|
|
792
|
+
* ...this.toObject() 0 here; the followed body contributes
|
|
793
|
+
* ...anythingElse 1, opaque, reported
|
|
746
794
|
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
|
|
795
|
+
* A key that is the identifier of the transformer's resource is not a DET, for
|
|
796
|
+
* the same reason `isPrimary` is not one on the data function.
|
|
750
797
|
*/
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
798
|
+
function outputFieldsIn(body, owner, stores, followed) {
|
|
799
|
+
if (!owner || !isTransformerClass(owner)) return {
|
|
800
|
+
outputs: [],
|
|
801
|
+
opaqueOutputs: [],
|
|
802
|
+
resource: null
|
|
803
|
+
};
|
|
804
|
+
const qualifier = owner.getName() ?? "Transformer";
|
|
805
|
+
const resource = transformerResourceOf(owner);
|
|
806
|
+
/**
|
|
807
|
+
* The resource's key and its system timestamps: re-emitted for links and
|
|
808
|
+
* sorting, and not something the user recognises — the same two exclusions the
|
|
809
|
+
* data function applies (§6).
|
|
810
|
+
*/
|
|
811
|
+
const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
|
|
812
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
813
|
+
const opaque = /* @__PURE__ */ new Set();
|
|
814
|
+
const leaf = (prefix, name) => {
|
|
815
|
+
if (prefix === "" && excluded.has(name)) return;
|
|
816
|
+
outputs.add(`${qualifier}.${prefix ? `${prefix}.${name}` : name}`);
|
|
817
|
+
};
|
|
818
|
+
const walk = (literal, prefix) => {
|
|
819
|
+
for (const property of literal.getProperties()) {
|
|
820
|
+
if (Node.isShorthandPropertyAssignment(property)) {
|
|
821
|
+
leaf(prefix, property.getName());
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (Node.isMethodDeclaration(property) || Node.isGetAccessorDeclaration(property)) {
|
|
825
|
+
leaf(prefix, property.getName());
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (Node.isSpreadAssignment(property)) {
|
|
829
|
+
spread(property.getExpression(), prefix);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (!Node.isPropertyAssignment(property)) continue;
|
|
833
|
+
const name = property.getName().replace(/^['"]|['"]$/g, "");
|
|
834
|
+
const value = unwrap(property.getInitializer());
|
|
835
|
+
if (!value) {
|
|
836
|
+
leaf(prefix, name);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
840
|
+
walk(value, prefix ? `${prefix}.${name}` : name);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (Node.isCallExpression(value) && followed(value)) continue;
|
|
844
|
+
const mapped = mappedLiteralOf(value);
|
|
845
|
+
if (mapped) {
|
|
846
|
+
walk(mapped, prefix ? `${prefix}.${name}` : name);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
leaf(prefix, name);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
const spread = (expression, prefix) => {
|
|
853
|
+
const value = unwrap(expression);
|
|
854
|
+
if (!value) return;
|
|
855
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
856
|
+
walk(value, prefix);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (Node.isConditionalExpression(value)) {
|
|
860
|
+
spread(value.getWhenTrue(), prefix);
|
|
861
|
+
spread(value.getWhenFalse(), prefix);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (Node.isCallExpression(value)) {
|
|
865
|
+
const picked = pickedNamesOf(value);
|
|
866
|
+
if (picked) {
|
|
867
|
+
for (const name of picked) leaf(prefix, name);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (followed(value)) return;
|
|
871
|
+
}
|
|
758
872
|
/**
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
873
|
+
* `...this.resource.serialize()`, `...this.extras`: whatever the model has.
|
|
874
|
+
* One DET as a floor, and reported — the placeholder carries the expression
|
|
875
|
+
* so the report can name what could not be read.
|
|
762
876
|
*/
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
877
|
+
const placeholder = `${qualifier}.${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
|
|
878
|
+
outputs.add(placeholder);
|
|
879
|
+
opaque.add(placeholder);
|
|
880
|
+
};
|
|
881
|
+
for (const literal of returnedLiteralsOf(body)) walk(literal, "");
|
|
882
|
+
return {
|
|
883
|
+
outputs: [...outputs],
|
|
884
|
+
opaqueOutputs: [...opaque],
|
|
885
|
+
resource: resource && stores.has(resource) ? resource : null
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Object literals the body itself returns — not the ones returned by arrow
|
|
890
|
+
* functions inside it, which belong to `.map()` callbacks and are read as
|
|
891
|
+
* repeating groups where they occur.
|
|
892
|
+
*/
|
|
893
|
+
function returnedLiteralsOf(body) {
|
|
894
|
+
const literals = [];
|
|
895
|
+
for (const statement of body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
896
|
+
if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== body) continue;
|
|
897
|
+
const value = unwrap(statement.getExpression());
|
|
898
|
+
if (value && Node.isObjectLiteralExpression(value)) literals.push(value);
|
|
773
899
|
}
|
|
774
|
-
|
|
900
|
+
return literals;
|
|
901
|
+
}
|
|
902
|
+
/** `this.pick(this.resource, ['a', 'b'])` -> ['a', 'b']; null when it is not that call */
|
|
903
|
+
function pickedNamesOf(call) {
|
|
904
|
+
const callee = call.getExpression();
|
|
905
|
+
if (!Node.isPropertyAccessExpression(callee)) return null;
|
|
906
|
+
if (callee.getName() !== "pick") return null;
|
|
907
|
+
if (callee.getExpression().getKind() !== SyntaxKind.ThisKeyword) return null;
|
|
908
|
+
const list = call.getArguments()[1]?.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
909
|
+
if (!list) return null;
|
|
910
|
+
const names = [];
|
|
911
|
+
for (const element of list.getElements()) {
|
|
912
|
+
const name = element.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
913
|
+
if (name === void 0) return null;
|
|
914
|
+
names.push(name);
|
|
915
|
+
}
|
|
916
|
+
return names;
|
|
917
|
+
}
|
|
918
|
+
/** `xs.map((x) => ({ a, b }))` -> the literal; null for a scalar map or anything else */
|
|
919
|
+
function mappedLiteralOf(value) {
|
|
920
|
+
if (!Node.isCallExpression(value)) return null;
|
|
921
|
+
const callee = value.getExpression();
|
|
922
|
+
if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "map") return null;
|
|
923
|
+
const callback = value.getArguments()[0];
|
|
924
|
+
if (!callback || !Node.isArrowFunction(callback)) return null;
|
|
925
|
+
const returned = unwrap(callback.getBody().asKind(SyntaxKind.Block) ? null : callback.getBody());
|
|
926
|
+
if (returned && Node.isObjectLiteralExpression(returned)) return returned;
|
|
927
|
+
for (const statement of callback.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
928
|
+
const expression = unwrap(statement.getExpression());
|
|
929
|
+
if (expression && Node.isObjectLiteralExpression(expression)) return expression;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
/** strips parentheses, `as`, `satisfies` and non-null assertions */
|
|
934
|
+
function unwrap(node) {
|
|
935
|
+
let current = node;
|
|
936
|
+
while (current && (Node.isParenthesizedExpression(current) || Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isNonNullExpression(current))) current = current.getExpression();
|
|
937
|
+
return current && Node.isExpression(current) ? current : null;
|
|
938
|
+
}
|
|
775
939
|
/**
|
|
776
|
-
*
|
|
940
|
+
* The columns a fluent chain names in `.select()`, walking UP from the access
|
|
941
|
+
* the detector recognised (`Livro.query()`) through the chain it belongs to.
|
|
777
942
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
943
|
+
* Only calls on the chain itself qualify. A `q.select('nome')` inside a
|
|
944
|
+
* `preload('autor', (q) => …)` callback narrows the related store, not this
|
|
945
|
+
* one, and reading every descendant would have attributed it here.
|
|
946
|
+
*
|
|
947
|
+
* Both spellings count: `.select(['a', 'b'])` and `.select('a', 'b')`. Anything
|
|
948
|
+
* that is not a string literal — a variable, a raw expression — is unreadable:
|
|
949
|
+
* the store falls back to every column, and the chain is reported.
|
|
781
950
|
*/
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
951
|
+
/** methods that leave ONE derived scalar rather than rows */
|
|
952
|
+
const AGGREGATES = new Set([
|
|
953
|
+
"count",
|
|
954
|
+
"countDistinct",
|
|
955
|
+
"exists",
|
|
956
|
+
"sum",
|
|
957
|
+
"avg",
|
|
958
|
+
"min",
|
|
959
|
+
"max"
|
|
960
|
+
]);
|
|
961
|
+
function chainShapeOf(access) {
|
|
962
|
+
const selected = /* @__PURE__ */ new Set();
|
|
963
|
+
const unreadable = [];
|
|
964
|
+
let aggregate = false;
|
|
965
|
+
const inspect = (call) => {
|
|
966
|
+
const callee = call.getExpression();
|
|
967
|
+
if (!Node.isPropertyAccessExpression(callee)) return;
|
|
968
|
+
const name = callee.getName();
|
|
969
|
+
if (AGGREGATES.has(name)) aggregate = true;
|
|
970
|
+
if (name !== "select") return;
|
|
971
|
+
const literal = literalColumnsOf(call);
|
|
972
|
+
if (literal) for (const column of literal) selected.add(column);
|
|
973
|
+
else unreadable.push({
|
|
974
|
+
line: call.getStartLineNumber(),
|
|
975
|
+
expression: call.getText().replace(/\s+/g, "")
|
|
976
|
+
});
|
|
977
|
+
};
|
|
978
|
+
let node = access;
|
|
979
|
+
for (let depth = 0; node && depth < 40; depth++) {
|
|
980
|
+
if (Node.isCallExpression(node)) inspect(node);
|
|
981
|
+
node = Node.isCallExpression(node) || Node.isPropertyAccessExpression(node) ? node.getExpression() : void 0;
|
|
789
982
|
}
|
|
790
|
-
|
|
983
|
+
node = access;
|
|
984
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
985
|
+
const parent = node.getParent();
|
|
986
|
+
if (!parent) break;
|
|
987
|
+
if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
|
|
988
|
+
node = parent;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) break;
|
|
992
|
+
const call = parent.getParent();
|
|
993
|
+
if (!call || !Node.isCallExpression(call) || call.getExpression() !== parent) break;
|
|
994
|
+
inspect(call);
|
|
995
|
+
node = call;
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
selected: [...selected],
|
|
999
|
+
aggregate,
|
|
1000
|
+
unreadable
|
|
1001
|
+
};
|
|
791
1002
|
}
|
|
1003
|
+
/** string-literal columns of one `.select(...)`; null when any is not a literal */
|
|
1004
|
+
function literalColumnsOf(call) {
|
|
1005
|
+
const columns = [];
|
|
1006
|
+
const read = (node) => {
|
|
1007
|
+
const value = node.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
1008
|
+
if (value === void 0) return false;
|
|
1009
|
+
const bare = value.includes(".") ? value.split(".").pop() : value;
|
|
1010
|
+
if (bare !== "*") columns.push(camelCase(bare));
|
|
1011
|
+
return true;
|
|
1012
|
+
};
|
|
1013
|
+
for (const argument of call.getArguments()) {
|
|
1014
|
+
const list = argument.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
1015
|
+
if (list) {
|
|
1016
|
+
for (const element of list.getElements()) if (!read(element)) return null;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (!read(argument)) return null;
|
|
1020
|
+
}
|
|
1021
|
+
return columns;
|
|
1022
|
+
}
|
|
1023
|
+
/** `created_at` -> `createdAt`, to match the model's attribute names */
|
|
1024
|
+
const camelCase = (value) => value.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/inventory/resolvers/transformer.ts
|
|
1027
|
+
/** BaseTransformer's public API; all of it funnels through `toObject` */
|
|
1028
|
+
const TRANSFORMER_METHODS = new Set([
|
|
1029
|
+
"transform",
|
|
1030
|
+
"paginate",
|
|
1031
|
+
"toJSON",
|
|
1032
|
+
"useVariant",
|
|
1033
|
+
"withVariant"
|
|
1034
|
+
]);
|
|
1035
|
+
/** what the application-side body is called */
|
|
1036
|
+
const APPLICATION_BODY = "toObject";
|
|
792
1037
|
//#endregion
|
|
793
1038
|
//#region src/inventory/resolvers/index.ts
|
|
794
1039
|
/**
|
|
@@ -803,7 +1048,35 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
803
1048
|
actionObjectResolver,
|
|
804
1049
|
eventDispatchResolver,
|
|
805
1050
|
jobDispatchResolver,
|
|
806
|
-
|
|
1051
|
+
{
|
|
1052
|
+
name: "transformer",
|
|
1053
|
+
order: 18,
|
|
1054
|
+
resolve(call, ctx) {
|
|
1055
|
+
const expression = call.getExpression();
|
|
1056
|
+
if (!Node.isPropertyAccessExpression(expression)) return [];
|
|
1057
|
+
if (!TRANSFORMER_METHODS.has(expression.getName())) return [];
|
|
1058
|
+
/**
|
|
1059
|
+
* The root of the chain, so `X.transform(p).useVariant(v)` resolves as
|
|
1060
|
+
* well as `X.transform(p)`. Analysing the same body twice is free: the
|
|
1061
|
+
* graph dedupes by file and member.
|
|
1062
|
+
*/
|
|
1063
|
+
const symbol = rootSymbolOf(expression.getExpression());
|
|
1064
|
+
if (!symbol) return [];
|
|
1065
|
+
const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
|
|
1066
|
+
if (!file) return [];
|
|
1067
|
+
/**
|
|
1068
|
+
* One definition of what a transformer is, shared with the output walker
|
|
1069
|
+
* (`graph/output_fields.ts`): the resolver must not follow a body whose keys
|
|
1070
|
+
* the walker would refuse to read, or the other way round.
|
|
1071
|
+
*/
|
|
1072
|
+
const declared = ctx.sourceFile(file);
|
|
1073
|
+
if (!declared || !declared.getClasses().some(isTransformerClass)) return [];
|
|
1074
|
+
return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
|
|
1075
|
+
file,
|
|
1076
|
+
member: APPLICATION_BODY
|
|
1077
|
+
}] : [];
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
807
1080
|
staticServiceResolver,
|
|
808
1081
|
propertyServiceResolver,
|
|
809
1082
|
moduleFunctionResolver
|
|
@@ -817,6 +1090,15 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
817
1090
|
* them apart. Hence specific strategies declare a lower `order` than generic
|
|
818
1091
|
* ones, and `module-function` comes last — it would match almost anything.
|
|
819
1092
|
*/
|
|
1093
|
+
/**
|
|
1094
|
+
* Does any strategy call this a technical write?
|
|
1095
|
+
*
|
|
1096
|
+
* Asked separately from resolution, because the strategy that recognises the call as
|
|
1097
|
+
* incidental is not necessarily the one that knows where it goes.
|
|
1098
|
+
*/
|
|
1099
|
+
function isTechnicalWrite(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
1100
|
+
return resolvers.some((resolver) => resolver.technicalWrite?.(call, ctx) === true);
|
|
1101
|
+
}
|
|
820
1102
|
function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
821
1103
|
for (const resolver of resolvers) {
|
|
822
1104
|
/**
|
|
@@ -836,5 +1118,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
836
1118
|
}
|
|
837
1119
|
return null;
|
|
838
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* A strategy that recognises a family of calls and knows they reach no data
|
|
1123
|
+
* store — a rate limiter, an attachment's URL, an authorisation check.
|
|
1124
|
+
*
|
|
1125
|
+
* Read from a real configuration, every such strategy was the same eight lines:
|
|
1126
|
+
* a helper to get the method name off the ts-morph node (the app does not depend
|
|
1127
|
+
* on ts-morph), a `resolve` that returns nothing, and one comparison. What the
|
|
1128
|
+
* design wants is kept — it is still a NAMED strategy, and `fp:count` still
|
|
1129
|
+
* reports the volume it declared data-free — and the ceremony is not.
|
|
1130
|
+
*/
|
|
1131
|
+
function ignoreCalls(options) {
|
|
1132
|
+
if (!options.methods?.length && !options.matching) throw new Error(`ignoreCalls("${options.name}"): say what it ignores — \`methods\` or \`matching\``);
|
|
1133
|
+
const methods = new Set(options.methods ?? []);
|
|
1134
|
+
return {
|
|
1135
|
+
name: options.name,
|
|
1136
|
+
order: options.order ?? 1,
|
|
1137
|
+
/** follows nothing: the whole point */
|
|
1138
|
+
resolve: () => [],
|
|
1139
|
+
ignores(call) {
|
|
1140
|
+
const callee = call.getExpression();
|
|
1141
|
+
const text = callee.getText();
|
|
1142
|
+
if (options.matching?.test(text)) return true;
|
|
1143
|
+
if (methods.size === 0) return false;
|
|
1144
|
+
const method = callee.getKindName() === "PropertyAccessExpression" ? text.split(".").pop() : text;
|
|
1145
|
+
return method !== void 0 && methods.has(method);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
839
1149
|
//#endregion
|
|
840
|
-
export {
|
|
1150
|
+
export { chainShapeOf as a, hooksFiredBy as c, isApplicationCode as d, isSeeder as f, toPosix as h, resolveCall as i, rootSymbolOf as l, samePath as m, ignoreCalls as n, outputFieldsIn as o, relativeTo as p, isTechnicalWrite as r, detectAccess as s, BUILTIN_CALL_RESOLVERS as t, collectEventBindings as u };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
3
|
-
import { n as analyze } from "./pipeline-
|
|
1
|
+
import { c as IncomparableSourcesError, d as DEFAULTS, f as defineConfig, i as measureStructure, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
|
|
2
|
+
import { h as toPosix } from "./resolvers-DlKJOZnk.js";
|
|
3
|
+
import { n as analyze } from "./pipeline-Cq4dNTNE.js";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -91,7 +91,12 @@ function renderCount(result) {
|
|
|
91
91
|
* habit: if it grows, the count comes from a spreadsheet and the tool loses
|
|
92
92
|
* its reason to exist. Printing the share is what keeps that visible.
|
|
93
93
|
*/
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Only entries that DECLARED a number. A review records a decision and declares
|
|
96
|
+
* nothing, so counting it here would read as "35% of the total declared by
|
|
97
|
+
* override" about a count nobody touched.
|
|
98
|
+
*/
|
|
99
|
+
const overridden = result.functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.length > 0));
|
|
95
100
|
if (overridden.length > 0) {
|
|
96
101
|
const points = overridden.reduce((total, fn) => total + fn.points, 0);
|
|
97
102
|
const share = (points / (result.totals.unadjusted || 1) * 100).toFixed(1);
|
|
@@ -220,6 +225,8 @@ function renderDiff(diff) {
|
|
|
220
225
|
}
|
|
221
226
|
lines.push("");
|
|
222
227
|
lines.push(`Billable FP: ${diff.billable}`);
|
|
228
|
+
/** the total is quoted under a set of factors, so the set is named beside it */
|
|
229
|
+
lines.push(`Factors: ${diff.preset} — ${FACTOR_PRESETS[diff.preset].label}`);
|
|
223
230
|
/**
|
|
224
231
|
* Before the per-function list, not after it.
|
|
225
232
|
*
|
|
@@ -410,6 +417,7 @@ async function runDiff(options) {
|
|
|
410
417
|
from: options.previous,
|
|
411
418
|
to
|
|
412
419
|
},
|
|
420
|
+
preset: config.diff?.preset,
|
|
413
421
|
factors: config.diff?.factors,
|
|
414
422
|
reasonFactors: config.diff?.reasonFactors
|
|
415
423
|
}))
|
|
@@ -3,9 +3,12 @@ import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
|
|
|
3
3
|
import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
|
|
4
4
|
import type { Behavior } from '../inventory/graph/call_graph.js';
|
|
5
5
|
import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
|
|
6
|
+
import type { OpaqueDeclaration } from './opaque.js';
|
|
6
7
|
import type { Complexity, CountResult, FunctionType } from '../types.js';
|
|
7
8
|
import type { FunctionOverride } from '../define_config.js';
|
|
8
9
|
import type { ComplexityTable } from './tables.js';
|
|
10
|
+
import type { GroupingStrategy } from './data_functions.js';
|
|
11
|
+
import type { TechnicalPattern } from './technical_filter.js';
|
|
9
12
|
/**
|
|
10
13
|
* Assembles the count from the inventory.
|
|
11
14
|
*
|
|
@@ -26,15 +29,19 @@ export declare const RULESET = "afp";
|
|
|
26
29
|
* that is easy to forget. Four such changes landed in 1.1.0 — maintenance read
|
|
27
30
|
* across the whole project rather than from routes alone, a job followed into
|
|
28
31
|
* `process`, an event followed into its listeners, and `request.input(…)` counted
|
|
29
|
-
* as a DET —
|
|
32
|
+
* as a DET — three more in 1.2.0: an open input object counting 1 instead of 0,
|
|
30
33
|
* `detFromSchema` no longer subtracting a placeholder that was not there, and a
|
|
31
|
-
* write through `related(…)` maintaining the related table.
|
|
34
|
+
* write through `related(…)` maintaining the related table — and six in 1.5.0:
|
|
35
|
+
* output DETs read from transformers, selects and aggregates instead of every
|
|
36
|
+
* column; system timestamps and `serializeAs: null` columns leaving the DETs;
|
|
37
|
+
* master-detail folded into one data function; identity by table; token tables
|
|
38
|
+
* technical; and opaque declarations reaching every function carrying the origin.
|
|
32
39
|
*
|
|
33
40
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
34
41
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
35
42
|
* exists for exactly that, and only this constant arms it.
|
|
36
43
|
*/
|
|
37
|
-
export declare const RULESET_VERSION = "1.
|
|
44
|
+
export declare const RULESET_VERSION = "1.5.0";
|
|
38
45
|
export type CountInput = {
|
|
39
46
|
app: AppContext;
|
|
40
47
|
stores: CollectedDataStore[];
|
|
@@ -49,16 +56,35 @@ export type CountInput = {
|
|
|
49
56
|
* seeder is this application just as much as a route is.
|
|
50
57
|
*/
|
|
51
58
|
writtenAnywhere?: Set<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Stores the application addresses DIRECTLY somewhere in its code — as opposed
|
|
61
|
+
* to reaching only through a parent's relation. Decides which composition
|
|
62
|
+
* children fold into their parent as a RET (counting-decisions §10).
|
|
63
|
+
*/
|
|
64
|
+
addressedAnywhere?: Set<string>;
|
|
65
|
+
/**
|
|
66
|
+
* Stores written by a SEEDER — scaffolding, so not maintenance — kept apart
|
|
67
|
+
* because an EIF only a seed populates is one of two things the code cannot
|
|
68
|
+
* tell: code data the team maintains (not counted, CPM) or a mirror of data
|
|
69
|
+
* another system maintains in production (a legitimate EIF). Reported.
|
|
70
|
+
*/
|
|
71
|
+
seededAnywhere?: Set<string>;
|
|
52
72
|
};
|
|
53
73
|
export type CountOptions = {
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
dataFunctions?: {
|
|
75
|
+
grouping?: GroupingStrategy;
|
|
76
|
+
};
|
|
77
|
+
/** what a person declared about a DET the analysis cannot read, by origin — §8 */
|
|
78
|
+
opaque?: Record<string, OpaqueDeclaration>;
|
|
79
|
+
/** a declared DET or RET for one function — the last resort, see §8 */
|
|
56
80
|
overrides?: Record<string, FunctionOverride>;
|
|
57
81
|
boundary?: {
|
|
58
82
|
infrastructure?: string[];
|
|
59
83
|
externallyMaintained?: string[];
|
|
60
84
|
/** restores what the AFP naming filter caught by accident */
|
|
61
85
|
business?: string[];
|
|
86
|
+
/** replaces the filter's naming conventions — §6.5.2.1.3 treats them as user input */
|
|
87
|
+
technicalPatterns?: TechnicalPattern[];
|
|
62
88
|
ignoreEntryPoints?: string[];
|
|
63
89
|
};
|
|
64
90
|
messageDet?: number;
|
|
@@ -13,7 +13,9 @@ import type { ComplexityTable } from './tables.js';
|
|
|
13
13
|
* application's Transactional Functions, the Data Function shall not be
|
|
14
14
|
* counted in the application." — AFP §6.5.4
|
|
15
15
|
*
|
|
16
|
-
* That is why this module takes usage, not just the stores.
|
|
16
|
+
* That is why this module takes usage, not just the stores. And the same
|
|
17
|
+
* question — how does the application use it? — decides whether a table is a
|
|
18
|
+
* data function at all or a RET of another one (counting-decisions §10).
|
|
17
19
|
*/
|
|
18
20
|
export type StoreUsage = {
|
|
19
21
|
/** does any transaction of the application write to this store? */
|
|
@@ -21,9 +23,53 @@ export type StoreUsage = {
|
|
|
21
23
|
/** does any transaction reach it at all, reading or writing? */
|
|
22
24
|
used: boolean;
|
|
23
25
|
};
|
|
26
|
+
export type GroupingStrategy = 'usage' | 'none';
|
|
27
|
+
export type GroupingOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* `usage` folds a composition child nobody addresses directly into its parent
|
|
30
|
+
* as a RET; `none` keeps every table its own data function, RET 1 — the
|
|
31
|
+
* behaviour of rule sets before 1.5.0, for comparing with an old count.
|
|
32
|
+
*/
|
|
33
|
+
grouping: GroupingStrategy;
|
|
34
|
+
/**
|
|
35
|
+
* Stores the application addresses directly anywhere in its own code —
|
|
36
|
+
* `C.query()`, `C.create()`, `new C()` — as opposed to reaching only through
|
|
37
|
+
* a parent's relation. Same pass as `writtenAnywhere`, same exclusions.
|
|
38
|
+
*/
|
|
39
|
+
addressedAnywhere: Set<string>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* How the stores fold into data functions.
|
|
43
|
+
*
|
|
44
|
+
* rootOf every counted store -> the store whose data function it belongs to
|
|
45
|
+
* members root -> [root, ...children folded in]
|
|
46
|
+
* linkColumns store -> the foreign keys that are the subgroup's LINK to its
|
|
47
|
+
* parent, and therefore not DETs of the group
|
|
48
|
+
*/
|
|
49
|
+
export type StoreGrouping = {
|
|
50
|
+
strategy: GroupingStrategy;
|
|
51
|
+
rootOf: Map<string, string>;
|
|
52
|
+
members: Map<string, string[]>;
|
|
53
|
+
linkColumns: Map<string, Set<string>>;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A store `C` is a RET of `P` when, and only when:
|
|
58
|
+
*
|
|
59
|
+
* 1. `P` declares `hasMany` / `hasOne` -> `C` (collected as `subgroups`);
|
|
60
|
+
* 2. no application code addresses `C` directly — the user only ever reaches
|
|
61
|
+
* it through `P`, so under the CPM it is not a logical file of its own;
|
|
62
|
+
* 3. exactly one `P` satisfies (1). More than one: `C` stays apart, reported.
|
|
63
|
+
*
|
|
64
|
+
* Cascade delete was measured and rejected as the signal: on a real application
|
|
65
|
+
* 11 of 13 cascades pointed at the tenant table. Usage is the rule the rest of
|
|
66
|
+
* the count already runs on.
|
|
67
|
+
*/
|
|
68
|
+
export declare function groupStores(stores: CollectedDataStore[], options: GroupingOptions): StoreGrouping;
|
|
69
|
+
/** the DET attributes of one store: not the key, not a system stamp, not a link to its parent */
|
|
70
|
+
export declare function detAttributesOf(store: CollectedDataStore, links?: Set<string>): import("../types.js").Attribute[];
|
|
24
71
|
export type DataFunctionOptions = {
|
|
25
|
-
|
|
26
|
-
retStrategy: 'constant' | 'composition';
|
|
72
|
+
grouping: StoreGrouping;
|
|
27
73
|
/** stores maintained by another system, by boundary decision */
|
|
28
74
|
externallyMaintained: Set<string>;
|
|
29
75
|
/**
|