@filipebraida/adonis-function-points 0.5.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 +102 -0
- package/README.md +101 -292
- package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DUbHiifm.js → fp_calibrate-EAuAtdbq.js} +1 -1
- package/build/{fp_count-ChtblhZV.js → fp_count-CZ0cUUBQ.js} +1 -1
- package/build/{fp_diff-Dt7J4IWu.js → fp_diff-BTg_LX0r.js} +1 -1
- package/build/{fp_explain-DZJ--0-S.js → fp_explain-D6QvDLKQ.js} +1 -1
- package/build/{fp_inventory-CPtmuuke.js → fp_inventory-C43fU39x.js} +1 -1
- package/build/{fp_metrics-et8F1Wvt.js → fp_metrics-DEMPk4xC.js} +1 -1
- package/build/index.d.ts +8 -4
- package/build/index.js +4 -4
- package/build/{pipeline-CNTBhs6o.js → pipeline-Cq4dNTNE.js} +763 -313
- package/build/{resolvers-PJwo2Z8R.js → resolvers-DlKJOZnk.js} +328 -63
- package/build/{runners-DIt1G85i.js → runners-FYmPIPub.js} +6 -3
- 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 +29 -0
- package/build/src/inventory/graph/output_fields.d.ts +99 -0
- package/build/src/inventory/paths.d.ts +2 -0
- package/build/src/inventory/resolvers/index.d.ts +21 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +30 -1
- package/build/stubs/config.stub +29 -16
- 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;
|
|
@@ -750,81 +756,284 @@ const staticServiceResolver = {
|
|
|
750
756
|
}
|
|
751
757
|
};
|
|
752
758
|
//#endregion
|
|
753
|
-
//#region src/inventory/
|
|
754
|
-
/** BaseTransformer's public API; all of it funnels through `toObject` */
|
|
755
|
-
const TRANSFORMER_METHODS = new Set([
|
|
756
|
-
"transform",
|
|
757
|
-
"paginate",
|
|
758
|
-
"toJSON",
|
|
759
|
-
"useVariant",
|
|
760
|
-
"withVariant"
|
|
761
|
-
]);
|
|
762
|
-
/** what the application-side body is called */
|
|
763
|
-
const APPLICATION_BODY = "toObject";
|
|
759
|
+
//#region src/inventory/graph/output_fields.ts
|
|
764
760
|
/**
|
|
765
|
-
*
|
|
766
|
-
* supplies the body.
|
|
767
|
-
*
|
|
768
|
-
* class InviteTransformer extends BaseTransformer<Invite> {
|
|
769
|
-
* toObject() { … }
|
|
770
|
-
* }
|
|
761
|
+
* Does this class extend a transformer base from a package?
|
|
771
762
|
*
|
|
772
|
-
*
|
|
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.
|
|
773
780
|
*
|
|
774
|
-
* `
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* 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.
|
|
779
785
|
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
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
|
|
782
794
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* 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.
|
|
786
797
|
*/
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
+
}
|
|
794
872
|
/**
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
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.
|
|
798
876
|
*/
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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);
|
|
809
899
|
}
|
|
810
|
-
|
|
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
|
+
}
|
|
811
939
|
/**
|
|
812
|
-
*
|
|
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.
|
|
813
942
|
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
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.
|
|
817
950
|
*/
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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;
|
|
825
982
|
}
|
|
826
|
-
|
|
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
|
+
};
|
|
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;
|
|
827
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";
|
|
828
1037
|
//#endregion
|
|
829
1038
|
//#region src/inventory/resolvers/index.ts
|
|
830
1039
|
/**
|
|
@@ -839,7 +1048,35 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
839
1048
|
actionObjectResolver,
|
|
840
1049
|
eventDispatchResolver,
|
|
841
1050
|
jobDispatchResolver,
|
|
842
|
-
|
|
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
|
+
},
|
|
843
1080
|
staticServiceResolver,
|
|
844
1081
|
propertyServiceResolver,
|
|
845
1082
|
moduleFunctionResolver
|
|
@@ -881,5 +1118,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
881
1118
|
}
|
|
882
1119
|
return null;
|
|
883
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
|
+
}
|
|
884
1149
|
//#endregion
|
|
885
|
-
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";
|
|
@@ -225,6 +225,8 @@ function renderDiff(diff) {
|
|
|
225
225
|
}
|
|
226
226
|
lines.push("");
|
|
227
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}`);
|
|
228
230
|
/**
|
|
229
231
|
* Before the per-function list, not after it.
|
|
230
232
|
*
|
|
@@ -415,6 +417,7 @@ async function runDiff(options) {
|
|
|
415
417
|
from: options.previous,
|
|
416
418
|
to
|
|
417
419
|
},
|
|
420
|
+
preset: config.diff?.preset,
|
|
418
421
|
factors: config.diff?.factors,
|
|
419
422
|
reasonFactors: config.diff?.reasonFactors
|
|
420
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
|
/**
|
|
@@ -37,6 +37,29 @@ export declare class IncomparableRulesetsError extends Error {
|
|
|
37
37
|
*/
|
|
38
38
|
export type ChangeFactors = Record<ChangeType, number>;
|
|
39
39
|
export declare const AEP_FACTORS: ChangeFactors;
|
|
40
|
+
/**
|
|
41
|
+
* The Roteiro de Métricas de Software do SISP, v3.0 (Portaria SGD/MGI nº 3656,
|
|
42
|
+
* de 2026), §7.3 "Projeto de Melhoria" — what a Brazilian public contract names
|
|
43
|
+
* instead of AEP:
|
|
44
|
+
*
|
|
45
|
+
* PF_MELHORIA = PF_INCLUÍDO + FI × PF_ALTERADO + 0,50 × PF_EXCLUÍDO + PF_CONVERSÃO
|
|
46
|
+
*
|
|
47
|
+
* where FI, the impact factor on an altered function, is 63% when the contractor
|
|
48
|
+
* developed or already maintains the function, and 84% when it did not (and must
|
|
49
|
+
* document it). This preset carries the 63% — a factory billing maintenance of
|
|
50
|
+
* its own work — and `diff.factors: { changed: 0.84 }` is the other case.
|
|
51
|
+
* PF_CONVERSÃO is data conversion, which this package does not count.
|
|
52
|
+
*
|
|
53
|
+
* Read from the guide's own PDF, not from memory: an earlier draft of this
|
|
54
|
+
* preset said 0,50 / 0,30, and v2.0 (2012) priced exclusion at 0,40. A contract
|
|
55
|
+
* binds to a revision, so the report prints which preset priced the total.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SISP_FACTORS: ChangeFactors;
|
|
58
|
+
export type FactorPreset = 'aep' | 'sisp';
|
|
59
|
+
export declare const FACTOR_PRESETS: Record<FactorPreset, {
|
|
60
|
+
label: string;
|
|
61
|
+
factors: ChangeFactors;
|
|
62
|
+
}>;
|
|
40
63
|
/**
|
|
41
64
|
* Factors for a modified function, by WHAT changed about it.
|
|
42
65
|
*
|
|
@@ -52,6 +75,8 @@ export declare const AEP_FACTORS: ChangeFactors;
|
|
|
52
75
|
*/
|
|
53
76
|
export type ChangeReasonFactors = Partial<Record<ChangeReason, number>>;
|
|
54
77
|
export type DiffOptions = {
|
|
78
|
+
/** which published set of factors to start from; `factors` overrides it field by field */
|
|
79
|
+
preset?: FactorPreset;
|
|
55
80
|
factors?: Partial<ChangeFactors>;
|
|
56
81
|
/** per-reason factors for modified functions; each falls back to `factors.changed` */
|
|
57
82
|
reasonFactors?: ChangeReasonFactors;
|
|
@@ -64,6 +89,8 @@ export type DiffOptions = {
|
|
|
64
89
|
export type FunctionPointDiff = DiffResult & {
|
|
65
90
|
/** function points weighted by the factors — this is what gets billed */
|
|
66
91
|
billable: number;
|
|
92
|
+
/** the preset the factors started from — printed, because the total is quoted under it */
|
|
93
|
+
preset: FactorPreset;
|
|
67
94
|
factors: ChangeFactors;
|
|
68
95
|
/** what the modified functions were actually billed at, by reason */
|
|
69
96
|
reasonFactors: ChangeReasonFactors;
|