@filipebraida/adonis-function-points 0.6.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.
- package/CHANGELOG.md +68 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-EAuAtdbq.js → fp_calibrate-3TxGdS1b.js} +1 -1
- package/build/{fp_count-CZ0cUUBQ.js → fp_count-arGLnVlY.js} +1 -1
- package/build/{fp_diff-BTg_LX0r.js → fp_diff-DBBvzq5x.js} +1 -1
- package/build/{fp_explain-D6QvDLKQ.js → fp_explain-aNFApwiT.js} +1 -1
- package/build/{fp_inventory-C43fU39x.js → fp_inventory-DIjKIC9t.js} +1 -1
- package/build/{fp_metrics-DEMPk4xC.js → fp_metrics-BpU61waG.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-Cq4dNTNE.js → pipeline-DO2301fV.js} +1414 -122
- package/build/{resolvers-DlKJOZnk.js → resolvers-DaU4uAqT.js} +323 -150
- package/build/{runners-FYmPIPub.js → runners-Dm7cWGa-.js} +2 -2
- package/build/src/albrecht/counter.d.ts +8 -1
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +15 -0
- package/build/src/inventory/graph/deliveries.d.ts +88 -0
- package/build/src/inventory/graph/output_fields.d.ts +45 -1
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
- package/build/src/inventory/resolvers/local_function.d.ts +24 -0
- package/build/src/inventory/resolvers/transformer.d.ts +0 -23
- package/build/src/inventory/sources/commands.d.ts +14 -0
- package/build/src/inventory/sources/jobs.d.ts +27 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +19 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as chainShapeOf, c as
|
|
1
|
+
import { _ as relativeTo, a as chainShapeOf, c as unwrap$1, d as collectEventBindings, f as detectAccess, g as isSeeder, h as isApplicationCode, i as resolveCall, l as DISPATCH_METHODS, m as rootSymbolOf, o as mappedLiteralOf, p as hooksFiredBy, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as EXECUTION_METHODS, v as samePath, y as toPosix } from "./resolvers-DaU4uAqT.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
@@ -965,93 +965,116 @@ const problemAt = (call, expression, reason) => ({
|
|
|
965
965
|
reason
|
|
966
966
|
});
|
|
967
967
|
//#endregion
|
|
968
|
-
//#region src/inventory/sources/
|
|
969
|
-
|
|
968
|
+
//#region src/inventory/sources/commands.ts
|
|
969
|
+
/**
|
|
970
|
+
* Ace commands as entry points — counting-decisions §5, plan 0.7 §C.
|
|
971
|
+
*
|
|
972
|
+
* IFPUG counts batch processes an operator starts. `node ace noticias:importar`
|
|
973
|
+
* reads a feed and writes news: an EI exactly like a `POST`, and until this the
|
|
974
|
+
* collector emitted only `kind: 'http'`, although the type had `command` and §5
|
|
975
|
+
* had decided its identity since 0.1.0.
|
|
976
|
+
*
|
|
977
|
+
* AdonisJS scans `./commands` for the application's own commands; so does this.
|
|
978
|
+
* A command is a class extending `BaseCommand` from `@adonisjs/core/ace` with a
|
|
979
|
+
* literal `static commandName`; its body is `run()`, followed by the same
|
|
980
|
+
* strategies as any handler. What it reaches decides whether it is a transaction
|
|
981
|
+
* at all — a scaffolder that writes files reaches no store and falls out, as a
|
|
982
|
+
* static route does.
|
|
983
|
+
*/
|
|
984
|
+
const ACE_MODULE = "@adonisjs/core/ace";
|
|
985
|
+
const BASE_COMMAND = "BaseCommand";
|
|
986
|
+
/** a generator of test data, by the package everyone uses for it */
|
|
987
|
+
const FAKER = /^@faker-js\/faker/;
|
|
988
|
+
const GENERATES_DATA_HINT = "imports @faker-js/faker: generates data, probably a development tool";
|
|
989
|
+
function collectCommands(app) {
|
|
970
990
|
const project = new Project({
|
|
971
991
|
skipAddingFilesFromTsConfig: true,
|
|
972
992
|
skipFileDependencyResolution: true,
|
|
973
993
|
compilerOptions: { allowJs: false }
|
|
974
994
|
});
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
995
|
+
project.addSourceFilesAtPaths(`${toPosix(app.root)}/commands/**/*.ts`);
|
|
996
|
+
const entryPoints = [];
|
|
997
|
+
for (const file of project.getSourceFiles()) {
|
|
998
|
+
if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
|
|
999
|
+
const baseCommand = baseCommandNameIn(file);
|
|
1000
|
+
if (!baseCommand) continue;
|
|
1001
|
+
for (const cls of file.getClasses()) {
|
|
1002
|
+
if (cls.getExtends()?.getExpression().getText() !== baseCommand) continue;
|
|
1003
|
+
const name = commandNameOf(cls);
|
|
1004
|
+
if (!name) continue;
|
|
1005
|
+
const filePath = file.getFilePath();
|
|
1006
|
+
const hints = importsFaker(file) ? [GENERATES_DATA_HINT] : [];
|
|
1007
|
+
entryPoints.push({
|
|
1008
|
+
id: `ace ${name}`,
|
|
1009
|
+
kind: "command",
|
|
1010
|
+
module: app.moduleOf(filePath),
|
|
1011
|
+
trigger: "ace",
|
|
1012
|
+
signature: name,
|
|
1013
|
+
name,
|
|
1014
|
+
handler: {
|
|
1015
|
+
file: filePath,
|
|
1016
|
+
member: "run"
|
|
1017
|
+
},
|
|
1018
|
+
identity: `ace ${name}`,
|
|
1019
|
+
provenance: {
|
|
1020
|
+
file: filePath,
|
|
1021
|
+
line: cls.getStartLineNumber(),
|
|
1022
|
+
by: "ace-commands"
|
|
1023
|
+
},
|
|
1024
|
+
...hints.length ? { hints } : {}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1006
1027
|
}
|
|
1007
|
-
return
|
|
1028
|
+
return entryPoints.sort((a, b) => a.identity.localeCompare(b.identity));
|
|
1008
1029
|
}
|
|
1009
|
-
/**
|
|
1010
|
-
function
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1030
|
+
/** the local name `BaseCommand` was imported under from `@adonisjs/core/ace`, if it was */
|
|
1031
|
+
function baseCommandNameIn(file) {
|
|
1032
|
+
for (const declaration of file.getImportDeclarations()) {
|
|
1033
|
+
if (declaration.getModuleSpecifierValue() !== ACE_MODULE) continue;
|
|
1034
|
+
const named = declaration.getNamedImports().find((n) => n.getName() === BASE_COMMAND);
|
|
1035
|
+
if (named) return named.getAliasNode()?.getText() ?? BASE_COMMAND;
|
|
1036
|
+
}
|
|
1037
|
+
return null;
|
|
1014
1038
|
}
|
|
1015
|
-
/**
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
return
|
|
1039
|
+
/** `static commandName = 'noticias:importar'` — a literal; a computed name is nobody's identity */
|
|
1040
|
+
function commandNameOf(cls) {
|
|
1041
|
+
const property = cls.getStaticProperty("commandName");
|
|
1042
|
+
if (!property || !Node.isPropertyDeclaration(property)) return null;
|
|
1043
|
+
const initializer = property.getInitializer();
|
|
1044
|
+
if (!initializer) return null;
|
|
1045
|
+
if (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) return initializer.getLiteralValue();
|
|
1046
|
+
return null;
|
|
1023
1047
|
}
|
|
1048
|
+
function importsFaker(file) {
|
|
1049
|
+
return file.getImportDeclarations().some((d) => FAKER.test(d.getModuleSpecifierValue()));
|
|
1050
|
+
}
|
|
1051
|
+
/** the ace decorators that declare what the operator types: `@flags.string()`, `@args.string()` */
|
|
1052
|
+
const INPUT_DECORATORS = new Set(["flags", "args"]);
|
|
1024
1053
|
/**
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
* array of scalar 1 (repeating group)
|
|
1030
|
-
* array of object the object's leaves, once
|
|
1031
|
-
* enum / const 1
|
|
1054
|
+
* The input DETs of a command: its `@flags.*` and `@args.*`, named as the
|
|
1055
|
+
* operator types them — `flagName` / `argumentName` when given, the property
|
|
1056
|
+
* otherwise. Returned as `flags.<name>` / `args.<name>` so the counter can say
|
|
1057
|
+
* which is which.
|
|
1032
1058
|
*/
|
|
1033
|
-
function
|
|
1034
|
-
const
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
if (!
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const inner = nested ? leavesOf$1(nested, path) : [];
|
|
1046
|
-
return inner.length > 0 ? inner : [path];
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
if (items) {
|
|
1050
|
-
const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1051
|
-
const inner = element ? leavesOf$1(element, prefix) : [];
|
|
1052
|
-
return inner.length > 0 ? inner : [prefix];
|
|
1059
|
+
function commandFieldsOf(cls) {
|
|
1060
|
+
const fields = [];
|
|
1061
|
+
for (const property of cls.getProperties()) for (const decorator of property.getDecorators()) {
|
|
1062
|
+
const call = decorator.getCallExpression();
|
|
1063
|
+
const callee = call?.getExpression();
|
|
1064
|
+
if (!call || !callee || !Node.isPropertyAccessExpression(callee)) continue;
|
|
1065
|
+
const kind = callee.getExpression().getText();
|
|
1066
|
+
if (!INPUT_DECORATORS.has(kind)) continue;
|
|
1067
|
+
const options = call.getArguments()[0];
|
|
1068
|
+
const literal = (options && Node.isObjectLiteralExpression(options) ? ["flagName", "argumentName"].map((key) => options.getProperty(key)).find((p) => p && Node.isPropertyAssignment(p)) : void 0)?.getInitializer();
|
|
1069
|
+
const name = literal && Node.isStringLiteral(literal) ? literal.getLiteralValue() : property.getName();
|
|
1070
|
+
fields.push(`${kind}.${name}`);
|
|
1053
1071
|
}
|
|
1054
|
-
return
|
|
1072
|
+
return fields;
|
|
1073
|
+
}
|
|
1074
|
+
/** is this class an ace command? — the graph asks, to read its flags as input */
|
|
1075
|
+
function isCommandClass(cls) {
|
|
1076
|
+
const baseCommand = baseCommandNameIn(cls.getSourceFile());
|
|
1077
|
+
return !!baseCommand && cls.getExtends()?.getExpression().getText() === baseCommand;
|
|
1055
1078
|
}
|
|
1056
1079
|
//#endregion
|
|
1057
1080
|
//#region src/inventory/graph/noise.ts
|
|
@@ -1164,7 +1187,10 @@ const FRAMEWORK_SERVICES = new Set([
|
|
|
1164
1187
|
"encryption",
|
|
1165
1188
|
"i18n",
|
|
1166
1189
|
"ally",
|
|
1167
|
-
"bouncer"
|
|
1190
|
+
"bouncer",
|
|
1191
|
+
"ui",
|
|
1192
|
+
"colors",
|
|
1193
|
+
"app"
|
|
1168
1194
|
]);
|
|
1169
1195
|
/** Is this call one that cannot reach a data store? */
|
|
1170
1196
|
function isNoise(call, owner) {
|
|
@@ -1194,50 +1220,858 @@ function isNoise(call, owner) {
|
|
|
1194
1220
|
* costs nothing and is not the same as silencing an unresolved call: nothing
|
|
1195
1221
|
* was ever there to resolve.
|
|
1196
1222
|
*/
|
|
1197
|
-
function isIterationCall(call) {
|
|
1198
|
-
const expression = call.getExpression();
|
|
1199
|
-
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
1200
|
-
return isIteration(expression.getName(), call);
|
|
1223
|
+
function isIterationCall(call) {
|
|
1224
|
+
const expression = call.getExpression();
|
|
1225
|
+
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
1226
|
+
return isIteration(expression.getName(), call);
|
|
1227
|
+
}
|
|
1228
|
+
/** An iteration method whose first argument is an inline callback. */
|
|
1229
|
+
function isIteration(method, call) {
|
|
1230
|
+
if (!ITERATION_METHODS.has(method)) return false;
|
|
1231
|
+
const first = call.getArguments()[0];
|
|
1232
|
+
return first !== void 0 && (Node.isArrowFunction(first) || Node.isFunctionExpression(first));
|
|
1233
|
+
}
|
|
1234
|
+
/** Does the receiver resolve to a built-in, by its declaration? */
|
|
1235
|
+
function isNativeReceiver(receiver, owner) {
|
|
1236
|
+
if (Node.isArrayLiteralExpression(receiver) || Node.isStringLiteral(receiver)) return true;
|
|
1237
|
+
if (Node.isPropertyAccessExpression(receiver) && receiver.getExpression().getKind()) {
|
|
1238
|
+
const inner = receiver.getExpression();
|
|
1239
|
+
if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName());
|
|
1240
|
+
}
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
function isNativeProperty(owner, name) {
|
|
1244
|
+
/**
|
|
1245
|
+
* Both shapes declare a field: a class property, and a constructor parameter
|
|
1246
|
+
* property. Reading only the first missed every `Map` handed in through the
|
|
1247
|
+
* constructor, which is how a transformer usually receives its lookups.
|
|
1248
|
+
*/
|
|
1249
|
+
const property = owner.getProperty(name) ?? owner.getConstructors()[0]?.getParameters().find((parameter) => parameter.getName() === name);
|
|
1250
|
+
if (!property) return false;
|
|
1251
|
+
const typeNode = property.getTypeNode()?.getText();
|
|
1252
|
+
if (typeNode && NATIVE_TYPES.has(typeNode.replace(/<.*/, "").trim())) return true;
|
|
1253
|
+
const initializer = property.getInitializer();
|
|
1254
|
+
if (initializer && Node.isNewExpression(initializer)) return NATIVE_TYPES.has(initializer.getExpression().getText());
|
|
1255
|
+
return initializer !== void 0 && (Node.isArrayLiteralExpression(initializer) || Node.isStringLiteral(initializer));
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The body-not-found path: a symbol resolved to an application file whose
|
|
1259
|
+
* member is not there. For a framework service that is expected — the
|
|
1260
|
+
* implementation is in the package — and says nothing about tracing quality.
|
|
1261
|
+
*/
|
|
1262
|
+
function isNoiseMember(file, member) {
|
|
1263
|
+
if (member && NEVER_DATA_METHODS.has(member)) return true;
|
|
1264
|
+
const base = file.split("/").pop()?.replace(/\.[jt]s$/, "");
|
|
1265
|
+
return base !== void 0 && FRAMEWORK_SERVICES.has(base);
|
|
1266
|
+
}
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/inventory/graph/deliveries.ts
|
|
1269
|
+
/** `inertia.render(page, props)`, `inertia.modal(page, props)`, `view.render(view, props)` */
|
|
1270
|
+
const RENDERERS = new Set(["inertia", "view"]);
|
|
1271
|
+
const RENDER_METHODS = new Set(["render", "modal"]);
|
|
1272
|
+
/** `response.json(x)`, `.ok(x)`, `.created(x)`, `.send(x)`, `.accepted(x)` */
|
|
1273
|
+
const RESPONSE_METHODS = new Set([
|
|
1274
|
+
"json",
|
|
1275
|
+
"ok",
|
|
1276
|
+
"created",
|
|
1277
|
+
"accepted",
|
|
1278
|
+
"send"
|
|
1279
|
+
]);
|
|
1280
|
+
/**
|
|
1281
|
+
* An ace command prints: `this.ui.table().row([…])`, `this.logger.info(…)`,
|
|
1282
|
+
* `console.log(…)`. What a report hands to the terminal is what leaves — every
|
|
1283
|
+
* argument is delivered, read like a prop (plan 0.7 §C).
|
|
1284
|
+
*/
|
|
1285
|
+
const PRINTERS = new Set([
|
|
1286
|
+
"ui",
|
|
1287
|
+
"logger",
|
|
1288
|
+
"console"
|
|
1289
|
+
]);
|
|
1290
|
+
/** `x.data`, `x.rows` on a paginated / wrapped result hand the collection on */
|
|
1291
|
+
const PASSES_THROUGH = new Set([
|
|
1292
|
+
"data",
|
|
1293
|
+
"rows",
|
|
1294
|
+
"all",
|
|
1295
|
+
"toJSON",
|
|
1296
|
+
"serialize"
|
|
1297
|
+
]);
|
|
1298
|
+
/** keys of a result that carry its rows: picking one of these is not picking one value */
|
|
1299
|
+
const PASSES_ROWS = new Set([
|
|
1300
|
+
"data",
|
|
1301
|
+
"rows",
|
|
1302
|
+
"items",
|
|
1303
|
+
"results",
|
|
1304
|
+
"list",
|
|
1305
|
+
"linhas",
|
|
1306
|
+
"itens"
|
|
1307
|
+
]);
|
|
1308
|
+
/** properties of a result that are one value, not its rows */
|
|
1309
|
+
const SCALAR_PROPS = new Set([
|
|
1310
|
+
"length",
|
|
1311
|
+
"size",
|
|
1312
|
+
"total",
|
|
1313
|
+
"count"
|
|
1314
|
+
]);
|
|
1315
|
+
/**
|
|
1316
|
+
* What Lucid's `paginator.getMeta()` says that a page can show. The URLs are
|
|
1317
|
+
* navigation and `firstPage` a constant: neither is a user-recognisable attribute.
|
|
1318
|
+
*/
|
|
1319
|
+
const PAGINATOR_META = [
|
|
1320
|
+
"total",
|
|
1321
|
+
"perPage",
|
|
1322
|
+
"currentPage",
|
|
1323
|
+
"lastPage"
|
|
1324
|
+
];
|
|
1325
|
+
/** methods that return the same collection, or one of its rows: what leaves is the receiver */
|
|
1326
|
+
const SAME_COLLECTION = new Set([
|
|
1327
|
+
"slice",
|
|
1328
|
+
"filter",
|
|
1329
|
+
"sort",
|
|
1330
|
+
"sortBy",
|
|
1331
|
+
"toSorted",
|
|
1332
|
+
"reverse",
|
|
1333
|
+
"toReversed",
|
|
1334
|
+
"concat",
|
|
1335
|
+
"flat",
|
|
1336
|
+
"find",
|
|
1337
|
+
"findLast",
|
|
1338
|
+
"at",
|
|
1339
|
+
"first",
|
|
1340
|
+
"last"
|
|
1341
|
+
]);
|
|
1342
|
+
/** reads of the request whose result echoes input already counted on entry */
|
|
1343
|
+
const ECHOES_INPUT = new Set([
|
|
1344
|
+
"validateUsing",
|
|
1345
|
+
"input",
|
|
1346
|
+
"only",
|
|
1347
|
+
"all",
|
|
1348
|
+
"body",
|
|
1349
|
+
"qs",
|
|
1350
|
+
"params"
|
|
1351
|
+
]);
|
|
1352
|
+
/** Inertia's lazy props: `inertia.defer(() => q.handle())` — the callback's value is what leaves */
|
|
1353
|
+
const INERTIA_LAZY = new Set([
|
|
1354
|
+
"defer",
|
|
1355
|
+
"lazy",
|
|
1356
|
+
"optional",
|
|
1357
|
+
"always",
|
|
1358
|
+
"merge",
|
|
1359
|
+
"scroll",
|
|
1360
|
+
"once"
|
|
1361
|
+
]);
|
|
1362
|
+
/** a yes/no: an authorisation check, a membership test */
|
|
1363
|
+
const BOOLEAN_METHODS = new Set([
|
|
1364
|
+
"allows",
|
|
1365
|
+
"denies",
|
|
1366
|
+
"can",
|
|
1367
|
+
"cannot",
|
|
1368
|
+
"includes",
|
|
1369
|
+
"has",
|
|
1370
|
+
"startsWith",
|
|
1371
|
+
"endsWith",
|
|
1372
|
+
"test"
|
|
1373
|
+
]);
|
|
1374
|
+
/** a value formatted: still one value */
|
|
1375
|
+
const FORMAT_METHODS = new Set([
|
|
1376
|
+
"join",
|
|
1377
|
+
"toString",
|
|
1378
|
+
"toISO",
|
|
1379
|
+
"toISODate",
|
|
1380
|
+
"toISOTime",
|
|
1381
|
+
"toISOString",
|
|
1382
|
+
"toFormat",
|
|
1383
|
+
"toRFC2822",
|
|
1384
|
+
"toHTTP",
|
|
1385
|
+
"toSQL",
|
|
1386
|
+
"toLocaleString",
|
|
1387
|
+
"toLocaleDateString",
|
|
1388
|
+
"toFixed",
|
|
1389
|
+
"toUnixInteger",
|
|
1390
|
+
"toMillis",
|
|
1391
|
+
"trim",
|
|
1392
|
+
"toUpperCase",
|
|
1393
|
+
"toLowerCase",
|
|
1394
|
+
"padStart",
|
|
1395
|
+
"padEnd",
|
|
1396
|
+
"replace",
|
|
1397
|
+
"slice",
|
|
1398
|
+
"substring"
|
|
1399
|
+
]);
|
|
1400
|
+
/** framework services whose calls hand back one value: a translation, a session key, a URL */
|
|
1401
|
+
const SCALAR_SERVICES = new Set([
|
|
1402
|
+
"i18n",
|
|
1403
|
+
"session",
|
|
1404
|
+
"env",
|
|
1405
|
+
"router",
|
|
1406
|
+
"encryption",
|
|
1407
|
+
"hash",
|
|
1408
|
+
"config",
|
|
1409
|
+
"app"
|
|
1410
|
+
]);
|
|
1411
|
+
/** built-ins whose static calls are one value or a constant list: `Object.values(Enum)`, `JSON.stringify(x)` */
|
|
1412
|
+
const NATIVE_GLOBALS = new Set([
|
|
1413
|
+
"Object",
|
|
1414
|
+
"Array",
|
|
1415
|
+
"JSON",
|
|
1416
|
+
"Math",
|
|
1417
|
+
"Number",
|
|
1418
|
+
"String",
|
|
1419
|
+
"Boolean",
|
|
1420
|
+
"Date",
|
|
1421
|
+
"Intl"
|
|
1422
|
+
]);
|
|
1423
|
+
/**
|
|
1424
|
+
* The deliveries of a body: every props argument handed to a renderer or a
|
|
1425
|
+
* response method, and what a `return` hands back that is not one of those calls.
|
|
1426
|
+
*/
|
|
1427
|
+
function deliveriesIn(ctx) {
|
|
1428
|
+
const deliveries = [];
|
|
1429
|
+
const returns = [];
|
|
1430
|
+
let any = false;
|
|
1431
|
+
let anyReturn = false;
|
|
1432
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1433
|
+
for (const call of ctx.body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1434
|
+
const callee = call.getExpression();
|
|
1435
|
+
if (!Node.isPropertyAccessExpression(callee)) continue;
|
|
1436
|
+
const method = callee.getName();
|
|
1437
|
+
const receiver = lastSegmentOf(callee.getExpression());
|
|
1438
|
+
let payload;
|
|
1439
|
+
if (RENDERERS.has(receiver) && RENDER_METHODS.has(method)) payload = call.getArguments()[1];
|
|
1440
|
+
else if (receiver === "response" && RESPONSE_METHODS.has(method)) payload = call.getArguments()[0];
|
|
1441
|
+
else if (isPrinter(callee.getExpression(), ctx.body)) {
|
|
1442
|
+
any = true;
|
|
1443
|
+
seen.add(call);
|
|
1444
|
+
for (const argument of call.getArguments()) classify(unwrap$1(argument), "", ctx, deliveries, 0);
|
|
1445
|
+
continue;
|
|
1446
|
+
} else continue;
|
|
1447
|
+
any = true;
|
|
1448
|
+
if (!payload) continue;
|
|
1449
|
+
seen.add(call);
|
|
1450
|
+
classify(unwrap$1(payload), "", ctx, deliveries, 0);
|
|
1451
|
+
}
|
|
1452
|
+
for (const statement of ctx.body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
1453
|
+
if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== ctx.body) continue;
|
|
1454
|
+
const value = unwrap$1(statement.getExpression());
|
|
1455
|
+
if (!value) continue;
|
|
1456
|
+
if (Node.isCallExpression(value)) {
|
|
1457
|
+
if (seen.has(value)) continue;
|
|
1458
|
+
/**
|
|
1459
|
+
* `return response.redirect().toRoute(…)`, `return response.noContent()`,
|
|
1460
|
+
* `return inertia.render(…)` (seen above): a call whose chain is rooted at
|
|
1461
|
+
* the response or a renderer is the response itself, not a value handed
|
|
1462
|
+
* back — nothing to classify.
|
|
1463
|
+
*/
|
|
1464
|
+
const root = chainRootOf(value);
|
|
1465
|
+
if (root && (RENDERERS.has(root) || root === "response")) continue;
|
|
1466
|
+
}
|
|
1467
|
+
anyReturn = true;
|
|
1468
|
+
classify(value, "", ctx, returns, 0);
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
calls: deliveries,
|
|
1472
|
+
anyCall: any,
|
|
1473
|
+
returns,
|
|
1474
|
+
anyReturn
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
/** the identifier a call chain is rooted at: `response.redirect().toRoute(x)` -> 'response' */
|
|
1478
|
+
function chainRootOf(node) {
|
|
1479
|
+
let current = node;
|
|
1480
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1481
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
|
|
1482
|
+
current = current.getExpression();
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current)) {
|
|
1486
|
+
current = current.getExpression();
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
return Node.isIdentifier(current) ? current.getText() : Node.isThisExpression(current) ? "this" : null;
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* `this.ui.table().row(x)`, `this.logger.info(x)`, `console.log(x)`: a chain rooted
|
|
1495
|
+
* at a printer — through a variable too (`const table = this.ui.table(); table.row(x)`).
|
|
1496
|
+
*/
|
|
1497
|
+
function isPrinter(receiver, body, depth = 0) {
|
|
1498
|
+
let current = receiver;
|
|
1499
|
+
for (let steps = 0; current && steps < 20; steps++) {
|
|
1500
|
+
if (Node.isCallExpression(current)) {
|
|
1501
|
+
current = current.getExpression();
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (Node.isPropertyAccessExpression(current)) {
|
|
1505
|
+
const inner = current.getExpression();
|
|
1506
|
+
if (Node.isThisExpression(inner)) return PRINTERS.has(current.getName());
|
|
1507
|
+
current = inner;
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (!Node.isIdentifier(current)) return false;
|
|
1511
|
+
if (current.getText() === "console") return true;
|
|
1512
|
+
const bound = depth < 3 ? bindingOf(current.getText(), body) : null;
|
|
1513
|
+
return !!bound && isPrinter(bound.initializer, body, depth + 1);
|
|
1514
|
+
}
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
/** `ctx.inertia` -> 'inertia', `inertia` -> 'inertia', `this.response` -> 'response' */
|
|
1518
|
+
function lastSegmentOf(node) {
|
|
1519
|
+
if (Node.isPropertyAccessExpression(node)) return node.getName();
|
|
1520
|
+
return node.getText();
|
|
1521
|
+
}
|
|
1522
|
+
function classify(value, path, ctx, out, depth) {
|
|
1523
|
+
if (!value || depth > 6) return;
|
|
1524
|
+
if (value.getKind() === SyntaxKind.NullKeyword) return;
|
|
1525
|
+
if (Node.isIdentifier(value) && value.getText() === "undefined") return;
|
|
1526
|
+
/**
|
|
1527
|
+
* A constant with no key — `this.logger.info('done')`, a table's `head(['Nome'])`
|
|
1528
|
+
* — is a label or a message, not a field: AFP counts no message DET (§6). Keyed,
|
|
1529
|
+
* `{ titulo: 'X' }` is a value the page receives, and stays one.
|
|
1530
|
+
*/
|
|
1531
|
+
if (!path && (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isNumericLiteral(value))) return;
|
|
1532
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
1533
|
+
for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join(path, property.getName()), ctx, out, depth + 1);
|
|
1534
|
+
else if (Node.isPropertyAssignment(property)) {
|
|
1535
|
+
const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
|
|
1536
|
+
classify(unwrap$1(property.getInitializer()), join(path, name), ctx, out, depth + 1);
|
|
1537
|
+
} else if (Node.isSpreadAssignment(property)) classify(unwrap$1(property.getExpression()), path, ctx, out, depth + 1);
|
|
1538
|
+
else out.push({
|
|
1539
|
+
kind: "scalar",
|
|
1540
|
+
path: join(path, property.getName?.() ?? "*")
|
|
1541
|
+
});
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (Node.isConditionalExpression(value)) {
|
|
1545
|
+
classify(unwrap$1(value.getWhenTrue()), path, ctx, out, depth + 1);
|
|
1546
|
+
classify(unwrap$1(value.getWhenFalse()), path, ctx, out, depth + 1);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (Node.isArrayLiteralExpression(value)) {
|
|
1550
|
+
for (const element of value.getElements()) classify(unwrap$1(Node.isSpreadElement(element) ? element.getExpression() : element), path, ctx, out, depth + 1);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (Node.isElementAccessExpression(value)) {
|
|
1554
|
+
classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (Node.isIdentifier(value)) {
|
|
1558
|
+
const name = value.getText();
|
|
1559
|
+
if (ctx.symbols.has(name)) {
|
|
1560
|
+
out.push({
|
|
1561
|
+
kind: "store",
|
|
1562
|
+
store: ctx.symbols.get(name),
|
|
1563
|
+
path
|
|
1564
|
+
});
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const bound = bindingOf(name, ctx.body);
|
|
1568
|
+
if (bound) {
|
|
1569
|
+
if (bound.pick && Node.isCallExpression(bound.initializer)) {
|
|
1570
|
+
classifyCall(bound.initializer, path, ctx, out, depth + 1, bound.pick);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (bound.pick && Node.isObjectLiteralExpression(bound.initializer)) {
|
|
1574
|
+
const picked = bound.initializer.getProperty(bound.pick);
|
|
1575
|
+
if (picked && Node.isPropertyAssignment(picked)) {
|
|
1576
|
+
classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (picked && Node.isShorthandPropertyAssignment(picked)) {
|
|
1580
|
+
classify(picked.getNameNode(), path, ctx, out, depth + 1);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
classify(bound.initializer, path, ctx, out, depth + 1);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (isEchoBinding(name, ctx.body)) {
|
|
1588
|
+
out.push({
|
|
1589
|
+
kind: "echo",
|
|
1590
|
+
path
|
|
1591
|
+
});
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
out.push({
|
|
1595
|
+
kind: "scalar",
|
|
1596
|
+
path
|
|
1597
|
+
});
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (Node.isCallExpression(value)) {
|
|
1601
|
+
classifyCall(value, path, ctx, out, depth);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (Node.isPropertyAccessExpression(value) || Node.isElementAccessExpression(value)) {
|
|
1605
|
+
classifyAccess(value, path, ctx, out, depth);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (Node.isAwaitExpression(value)) {
|
|
1609
|
+
classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (Node.isBinaryExpression(value)) {
|
|
1613
|
+
const operator = value.getOperatorToken().getKind();
|
|
1614
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
1615
|
+
classify(unwrap$1(value.getLeft()), path, ctx, out, depth + 1);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken) {
|
|
1619
|
+
classify(unwrap$1(value.getRight()), path, ctx, out, depth + 1);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (Node.isTemplateExpression(value)) {
|
|
1624
|
+
for (const span of value.getTemplateSpans()) classify(unwrap$1(span.getExpression()), path, ctx, out, depth + 1);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
out.push({
|
|
1628
|
+
kind: "scalar",
|
|
1629
|
+
path
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
function classifyCall(value, path, ctx, out, depth, pick) {
|
|
1633
|
+
{
|
|
1634
|
+
const callee = value.getExpression();
|
|
1635
|
+
const method = Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1636
|
+
if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(method) && isRequestLike(callee.getExpression())) {
|
|
1637
|
+
out.push({
|
|
1638
|
+
kind: "echo",
|
|
1639
|
+
path
|
|
1640
|
+
});
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const mapped = mappedLiteralOf(value);
|
|
1644
|
+
if (mapped) {
|
|
1645
|
+
classify(mapped, path, ctx, out, depth + 1);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (method === "map" && Node.isPropertyAccessExpression(callee)) {
|
|
1649
|
+
classifyMapped(value, callee.getExpression(), path, ctx, out, depth);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH.has(method) && value.getArguments().length === 0) {
|
|
1653
|
+
classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (method === "getMeta" && value.getArguments().length === 0) {
|
|
1657
|
+
if (pick) out.push({
|
|
1658
|
+
kind: "scalar",
|
|
1659
|
+
path
|
|
1660
|
+
});
|
|
1661
|
+
else for (const key of PAGINATOR_META) out.push({
|
|
1662
|
+
kind: "scalar",
|
|
1663
|
+
path: join(path, key)
|
|
1664
|
+
});
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (Node.isPropertyAccessExpression(callee) && SAME_COLLECTION.has(method)) {
|
|
1668
|
+
classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (Node.isPropertyAccessExpression(callee) && lastSegmentOf(callee.getExpression()) === "colors") {
|
|
1672
|
+
const coloured = unwrap$1(value.getArguments()[0]);
|
|
1673
|
+
if (coloured) classify(coloured, path, ctx, out, depth + 1);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (Node.isPropertyAccessExpression(callee) && INERTIA_LAZY.has(method) && RENDERERS.has(lastSegmentOf(callee.getExpression()))) {
|
|
1677
|
+
const body = callbackValueOf(unwrap$1(value.getArguments()[0]));
|
|
1678
|
+
if (body) classify(body, path, ctx, out, depth + 1);
|
|
1679
|
+
else out.push({
|
|
1680
|
+
kind: "scalar",
|
|
1681
|
+
path
|
|
1682
|
+
});
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const args = [];
|
|
1686
|
+
for (const argument of value.getArguments()) classify(unwrap$1(argument), path, ctx, args, depth + 1);
|
|
1687
|
+
/** only what carries rows matters for a fallback: a scalar argument is a parameter, not an output */
|
|
1688
|
+
const carried = args.filter((item) => item.kind === "store" || item.kind === "call");
|
|
1689
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1690
|
+
const root = chainRootOf(callee.getExpression());
|
|
1691
|
+
if (root && SCALAR_SERVICES.has(root)) {
|
|
1692
|
+
out.push({
|
|
1693
|
+
kind: "scalar",
|
|
1694
|
+
path
|
|
1695
|
+
});
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const refs = ctx.followed.get(value);
|
|
1700
|
+
if (refs && refs.length > 0) {
|
|
1701
|
+
out.push({
|
|
1702
|
+
kind: "call",
|
|
1703
|
+
refs,
|
|
1704
|
+
path,
|
|
1705
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60),
|
|
1706
|
+
args: carried,
|
|
1707
|
+
...pick ? { pick } : {}
|
|
1708
|
+
});
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const access = detectAccess(value, ctx.symbols, ctx.relations);
|
|
1712
|
+
if (access) {
|
|
1713
|
+
out.push({
|
|
1714
|
+
kind: "store",
|
|
1715
|
+
store: access.store,
|
|
1716
|
+
path
|
|
1717
|
+
});
|
|
1718
|
+
if (access.viaRelation) out.push({
|
|
1719
|
+
kind: "store",
|
|
1720
|
+
store: access.viaRelation,
|
|
1721
|
+
path
|
|
1722
|
+
});
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (Node.isIdentifier(callee) && NATIVE_GLOBALS.has(callee.getText())) {
|
|
1726
|
+
out.push({
|
|
1727
|
+
kind: "scalar",
|
|
1728
|
+
path
|
|
1729
|
+
});
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (Node.isPropertyAccessExpression(callee) && Node.isIdentifier(callee.getExpression()) && NATIVE_GLOBALS.has(callee.getExpression().getText())) {
|
|
1733
|
+
out.push({
|
|
1734
|
+
kind: "scalar",
|
|
1735
|
+
path
|
|
1736
|
+
});
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* A call nobody followed, over rows: a package's CSV builder, a formatter. The
|
|
1741
|
+
* document it builds carries what was handed into it, so the rows' stores leave.
|
|
1742
|
+
* With nothing flowing in there is nothing to say: one DET, opaque, reported.
|
|
1743
|
+
*/
|
|
1744
|
+
if (carried.length > 0) {
|
|
1745
|
+
out.push(...carried);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* `startDate?.toISOString()`, `categoria.trim()`: a call ON a value the body
|
|
1750
|
+
* holds is that value formatted — an echo stays an echo, a store's field one
|
|
1751
|
+
* field. Only when the root itself resolves to nothing else is the call read
|
|
1752
|
+
* on its own.
|
|
1753
|
+
*/
|
|
1754
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1755
|
+
const root = chainRootOf(callee.getExpression());
|
|
1756
|
+
if (root && ctx.symbols.has(root)) {
|
|
1757
|
+
out.push({
|
|
1758
|
+
kind: "scalar",
|
|
1759
|
+
path
|
|
1760
|
+
});
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (root && root !== "this") {
|
|
1764
|
+
const rootNode = rootIdentifierOf(callee.getExpression());
|
|
1765
|
+
const held = [];
|
|
1766
|
+
if (rootNode) classify(rootNode, path, ctx, held, depth + 1);
|
|
1767
|
+
if (held.length > 0 && held.every((item) => item.kind === "echo")) {
|
|
1768
|
+
out.push({
|
|
1769
|
+
kind: "echo",
|
|
1770
|
+
path
|
|
1771
|
+
});
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* `rows.map(…).join('\n')` in a body whose parameter is `rows`: the whole input
|
|
1778
|
+
* transformed, so the input says what leaves — the caller's arguments decide,
|
|
1779
|
+
* and the body itself says nothing. `noticia.publicadaEm?.toISO()` on that
|
|
1780
|
+
* parameter is one FIELD of it, and stays one value below.
|
|
1781
|
+
*/
|
|
1782
|
+
if (Node.isPropertyAccessExpression(callee) && transformsParameter(callee, ctx.body)) {
|
|
1783
|
+
out.push({
|
|
1784
|
+
kind: "opaque",
|
|
1785
|
+
path,
|
|
1786
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60)
|
|
1787
|
+
});
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1791
|
+
/**
|
|
1792
|
+
* `comunicado.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
|
|
1793
|
+
* the chain, or an expression, then a method — one value. A chain rooted at
|
|
1794
|
+
* `this` (`this.service.find()`) is a call into a service, and not this.
|
|
1795
|
+
*/
|
|
1796
|
+
if (readsField(callee.getExpression())) {
|
|
1797
|
+
out.push({
|
|
1798
|
+
kind: "scalar",
|
|
1799
|
+
path
|
|
1800
|
+
});
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (BOOLEAN_METHODS.has(method) || FORMAT_METHODS.has(method)) {
|
|
1804
|
+
out.push({
|
|
1805
|
+
kind: "scalar",
|
|
1806
|
+
path
|
|
1807
|
+
});
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (returnsPrimitive(value)) {
|
|
1812
|
+
out.push({
|
|
1813
|
+
kind: "scalar",
|
|
1814
|
+
path
|
|
1815
|
+
});
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
out.push({
|
|
1819
|
+
kind: "opaque",
|
|
1820
|
+
path,
|
|
1821
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60)
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* `xs.map(cb)` with no literal in the callback: what the callback RETURNS,
|
|
1827
|
+
* once — a function passed by reference (`rows.map(paraLinha)`) is a call to
|
|
1828
|
+
* that function over the rows; an expression body (`(m) => new T(m).toObject()`)
|
|
1829
|
+
* is classified as if it were the value; anything else is one repeating attribute.
|
|
1830
|
+
*/
|
|
1831
|
+
function classifyMapped(value, receiver, path, ctx, out, depth) {
|
|
1832
|
+
const callback = unwrap$1(value.getArguments()[0]);
|
|
1833
|
+
const refs = ctx.followed.get(value);
|
|
1834
|
+
if (callback && Node.isIdentifier(callback) && refs && refs.length > 0) {
|
|
1835
|
+
const rows = [];
|
|
1836
|
+
classify(unwrap$1(receiver), path, ctx, rows, depth + 1);
|
|
1837
|
+
out.push({
|
|
1838
|
+
kind: "call",
|
|
1839
|
+
refs,
|
|
1840
|
+
path,
|
|
1841
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60),
|
|
1842
|
+
args: rows.filter((item) => item.kind === "store" || item.kind === "call")
|
|
1843
|
+
});
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
const body = callbackValueOf(callback);
|
|
1847
|
+
if (body) {
|
|
1848
|
+
classify(body, path, ctx, out, depth + 1);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
out.push({
|
|
1852
|
+
kind: "scalar",
|
|
1853
|
+
path
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
/** the value a callback hands back: an expression body, or the one `return` of a block */
|
|
1857
|
+
function callbackValueOf(callback) {
|
|
1858
|
+
if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return null;
|
|
1859
|
+
const body = callback.getBody();
|
|
1860
|
+
if (!Node.isBlock(body)) return unwrap$1(body);
|
|
1861
|
+
const returns = body.getStatements().filter(Node.isReturnStatement);
|
|
1862
|
+
return returns.length === 1 ? unwrap$1(returns[0].getExpression()) : null;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Does this chain read a property (not a method) or hold an expression before
|
|
1866
|
+
* the method is applied? `comunicado.enviadoEm.toISO()` does; `rows.map(f).join()`
|
|
1867
|
+
* does not; a chain rooted at `this` is a service, and does not.
|
|
1868
|
+
*/
|
|
1869
|
+
function readsField(node) {
|
|
1870
|
+
let current = node;
|
|
1871
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1872
|
+
if (Node.isCallExpression(current)) {
|
|
1873
|
+
const inner = current.getExpression();
|
|
1874
|
+
current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
if (Node.isAwaitExpression(current) || Node.isNonNullExpression(current)) {
|
|
1878
|
+
current = current.getExpression();
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (Node.isParenthesizedExpression(current)) {
|
|
1882
|
+
const inner = unwrap$1(current.getExpression());
|
|
1883
|
+
if (!inner) return false;
|
|
1884
|
+
if (Node.isBinaryExpression(inner) || Node.isConditionalExpression(inner)) return true;
|
|
1885
|
+
current = inner;
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return chainRootOf(current) !== "this";
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Is this chain a method (or methods) applied to a plain parameter of the body,
|
|
1895
|
+
* with no field read in between? `rows.map(f).join(s)` is; `noticia.capa?.toISO()`
|
|
1896
|
+
* reads a field first and is not.
|
|
1897
|
+
*/
|
|
1898
|
+
function transformsParameter(callee, body) {
|
|
1899
|
+
let current = callee.getExpression();
|
|
1900
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1901
|
+
if (Node.isCallExpression(current)) {
|
|
1902
|
+
const inner = current.getExpression();
|
|
1903
|
+
current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
1907
|
+
current = current.getExpression();
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return false;
|
|
1911
|
+
if (!Node.isIdentifier(current)) return false;
|
|
1912
|
+
const name = current.getText();
|
|
1913
|
+
return Node.isParametered(body) && body.getParameters().some((p) => Node.isIdentifier(p.getNameNode()) && p.getNameNode().getText() === name);
|
|
1914
|
+
}
|
|
1915
|
+
return false;
|
|
1201
1916
|
}
|
|
1202
|
-
/**
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1917
|
+
/** the identifier node a chain is rooted at: `startDate?.toISOString` -> `startDate` */
|
|
1918
|
+
function rootIdentifierOf(node) {
|
|
1919
|
+
let current = node;
|
|
1920
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1921
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
1922
|
+
current = current.getExpression();
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
return Node.isIdentifier(current) ? current : null;
|
|
1926
|
+
}
|
|
1927
|
+
return null;
|
|
1207
1928
|
}
|
|
1208
|
-
/**
|
|
1209
|
-
function
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1929
|
+
/** the call's declared return type, `Promise<…>` unwrapped, is a boolean, string, number or a union of those */
|
|
1930
|
+
function returnsPrimitive(call) {
|
|
1931
|
+
try {
|
|
1932
|
+
let type = call.getReturnType();
|
|
1933
|
+
if (type.getSymbol()?.getName() === "Promise") type = type.getTypeArguments()[0] ?? type;
|
|
1934
|
+
const primitive = (t) => t.isBoolean() || t.isBooleanLiteral() || t.isString() || t.isStringLiteral() || t.isNumber() || t.isNumberLiteral() || t.isEnumLiteral() || t.isNull() || t.isUndefined();
|
|
1935
|
+
if (type.isUnion()) {
|
|
1936
|
+
const members = type.getUnionTypes();
|
|
1937
|
+
return members.length > 0 && members.every(primitive) && !members.every((t) => t.isNull() || t.isUndefined());
|
|
1938
|
+
}
|
|
1939
|
+
return primitive(type);
|
|
1940
|
+
} catch {
|
|
1941
|
+
return false;
|
|
1214
1942
|
}
|
|
1215
|
-
return false;
|
|
1216
1943
|
}
|
|
1217
|
-
|
|
1944
|
+
/**
|
|
1945
|
+
* `x.data`, `resultado.linhas`, `rows[0]`: a part of a value the body holds.
|
|
1946
|
+
*
|
|
1947
|
+
* on a variable bound to a followed call that key of what the call returns
|
|
1948
|
+
* on a store-bound variable the store (`rows[0]`, `.data`) or a field (scalar)
|
|
1949
|
+
* on the validated payload or the request an echo of input — counts on entry
|
|
1950
|
+
* anything else one value
|
|
1951
|
+
*/
|
|
1952
|
+
function classifyAccess(value, path, ctx, out, depth) {
|
|
1953
|
+
const root = rootSymbolOf(value);
|
|
1218
1954
|
/**
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* constructor, which is how a transformer usually receives its lookups.
|
|
1955
|
+
* `x.nome`, `x['nome']`: the key named; `x[papel]`: a key the body computes — one
|
|
1956
|
+
* value, unnamed (`*`); `rows[0]`: one row of the collection, the collection.
|
|
1222
1957
|
*/
|
|
1223
|
-
const property =
|
|
1224
|
-
if (
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1958
|
+
const property = Node.isPropertyAccessExpression(value) ? value.getName() : keyOfElementAccess(value);
|
|
1959
|
+
if (root && ctx.symbols.has(root)) {
|
|
1960
|
+
if (!property || PASSES_THROUGH.has(property)) out.push({
|
|
1961
|
+
kind: "store",
|
|
1962
|
+
store: ctx.symbols.get(root),
|
|
1963
|
+
path
|
|
1964
|
+
});
|
|
1965
|
+
else out.push({
|
|
1966
|
+
kind: "scalar",
|
|
1967
|
+
path: path || property.replace(/^\*$/, "")
|
|
1968
|
+
});
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (root) {
|
|
1972
|
+
const bound = bindingOf(root, ctx.body);
|
|
1973
|
+
if (bound && Node.isCallExpression(bound.initializer)) {
|
|
1974
|
+
if (SCALAR_PROPS.has(property)) {
|
|
1975
|
+
out.push({
|
|
1976
|
+
kind: "scalar",
|
|
1977
|
+
path
|
|
1978
|
+
});
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
|
|
1983
|
+
* destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
|
|
1984
|
+
*/
|
|
1985
|
+
const own = property && !PASSES_THROUGH.has(property) ? property : void 0;
|
|
1986
|
+
const pick = [bound.pick, own].filter(Boolean).join(".") || void 0;
|
|
1987
|
+
if (own && !ctx.followed.has(bound.initializer)) {
|
|
1988
|
+
out.push({
|
|
1989
|
+
kind: "scalar",
|
|
1990
|
+
path
|
|
1991
|
+
});
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
classifyCall(bound.initializer, path, ctx, out, depth + 1, pick);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (bound && Node.isObjectLiteralExpression(bound.initializer) && property) {
|
|
1998
|
+
const picked = bound.initializer.getProperty(property);
|
|
1999
|
+
if (picked && Node.isPropertyAssignment(picked)) {
|
|
2000
|
+
classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (isEchoBinding(root, ctx.body) || root === "request" || root === "params") {
|
|
2005
|
+
out.push({
|
|
2006
|
+
kind: "echo",
|
|
2007
|
+
path
|
|
2008
|
+
});
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
out.push({
|
|
2013
|
+
kind: "scalar",
|
|
2014
|
+
path: path || property.replace(/^\*$/, "")
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
function keyOfElementAccess(value) {
|
|
2018
|
+
const argument = unwrap$1(value.getArgumentExpression());
|
|
2019
|
+
if (!argument || Node.isNumericLiteral(argument)) return "";
|
|
2020
|
+
if (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) return argument.getLiteralValue();
|
|
2021
|
+
return "*";
|
|
2022
|
+
}
|
|
2023
|
+
const join = (prefix, name) => prefix ? `${prefix}.${name}` : name;
|
|
2024
|
+
/** `await x` binds x: the value, not the promise */
|
|
2025
|
+
function unwrapAwait$1(node) {
|
|
2026
|
+
let current = unwrap$1(node);
|
|
2027
|
+
while (current && Node.isAwaitExpression(current)) current = unwrap$1(current.getExpression());
|
|
2028
|
+
return current;
|
|
1230
2029
|
}
|
|
1231
2030
|
/**
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
* implementation is in the package — and says nothing about tracing quality.
|
|
2031
|
+
* How a local name was bound in this body: `const x = …` gives the initializer;
|
|
2032
|
+
* `const { x, y } = …` gives the initializer and the key picked out of it.
|
|
1235
2033
|
*/
|
|
1236
|
-
function
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
2034
|
+
function bindingOf(name, body) {
|
|
2035
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2036
|
+
const nameNode = declaration.getNameNode();
|
|
2037
|
+
if (Node.isIdentifier(nameNode)) {
|
|
2038
|
+
if (nameNode.getText() !== name) continue;
|
|
2039
|
+
const initializer = unwrapAwait$1(declaration.getInitializer());
|
|
2040
|
+
return initializer ? { initializer } : null;
|
|
2041
|
+
}
|
|
2042
|
+
if (Node.isObjectBindingPattern(nameNode)) {
|
|
2043
|
+
const element = nameNode.getElements().find((e) => e.getName() === name);
|
|
2044
|
+
if (!element) continue;
|
|
2045
|
+
const initializer = unwrapAwait$1(declaration.getInitializer());
|
|
2046
|
+
if (!initializer) return null;
|
|
2047
|
+
return {
|
|
2048
|
+
initializer,
|
|
2049
|
+
pick: element.getPropertyNameNode()?.getText() ?? element.getName()
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* A name bound by destructuring the validated payload or the request:
|
|
2057
|
+
* `const { busca } = await request.validateUsing(x)`, `const { id } = params`.
|
|
2058
|
+
*/
|
|
2059
|
+
function isEchoBinding(name, body) {
|
|
2060
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2061
|
+
const binding = declaration.getNameNode();
|
|
2062
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
2063
|
+
if (!binding.getElements().some((element) => element.getName() === name)) continue;
|
|
2064
|
+
const initializer = unwrap$1(declaration.getInitializer());
|
|
2065
|
+
if (!initializer) continue;
|
|
2066
|
+
if (Node.isIdentifier(initializer) && /^(params|request)$/.test(initializer.getText())) return true;
|
|
2067
|
+
if (Node.isCallExpression(initializer)) {
|
|
2068
|
+
const callee = initializer.getExpression();
|
|
2069
|
+
if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(callee.getName())) return true;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return false;
|
|
1240
2073
|
}
|
|
2074
|
+
const isRequestLike = (node) => lastSegmentOf(node) === "request";
|
|
1241
2075
|
//#endregion
|
|
1242
2076
|
//#region src/inventory/graph/call_graph.ts
|
|
1243
2077
|
/**
|
|
@@ -1271,7 +2105,7 @@ function validatorFieldsIn(body, file, app) {
|
|
|
1271
2105
|
* validator and is usually not exported, so looking for it where the call site
|
|
1272
2106
|
* is finds nothing — which is how five fields stayed invisible.
|
|
1273
2107
|
*/
|
|
1274
|
-
const { leaves, opaque: unreadable } = leavesOf(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
|
|
2108
|
+
const { leaves, opaque: unreadable } = leavesOf$1(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
|
|
1275
2109
|
const isOpaque = new Set(unreadable);
|
|
1276
2110
|
for (const leaf of leaves) {
|
|
1277
2111
|
const field = `${name}.${leaf}`;
|
|
@@ -1414,7 +2248,7 @@ function calleeOwner(call) {
|
|
|
1414
2248
|
const owner = callee.getExpression();
|
|
1415
2249
|
return Node.isPropertyAccessExpression(owner) ? owner.getName() : void 0;
|
|
1416
2250
|
}
|
|
1417
|
-
function leavesOf(node, resolveRef) {
|
|
2251
|
+
function leavesOf$1(node, resolveRef) {
|
|
1418
2252
|
const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1419
2253
|
if (!object) return {
|
|
1420
2254
|
leaves: [],
|
|
@@ -1607,9 +2441,10 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1607
2441
|
const followUps = [];
|
|
1608
2442
|
const unresolved = [];
|
|
1609
2443
|
const reads = [];
|
|
1610
|
-
/** calls a strategy claimed: a nested transformer's keys arrive through its body */
|
|
1611
|
-
const followedCalls = /* @__PURE__ */ new
|
|
2444
|
+
/** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
|
|
2445
|
+
const followedCalls = /* @__PURE__ */ new Map();
|
|
1612
2446
|
const validator = validatorFieldsIn(body, file, app);
|
|
2447
|
+
const commandFields = owner && isCommandClass(owner) ? commandFieldsOf(owner) : [];
|
|
1613
2448
|
const request = requestFieldsIn(body);
|
|
1614
2449
|
const context = {
|
|
1615
2450
|
file,
|
|
@@ -1652,10 +2487,15 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1652
2487
|
if (access.mode === "read") {
|
|
1653
2488
|
const chain = chainShapeOf(call);
|
|
1654
2489
|
const shape = chain.aggregate ? "aggregate" : chain.selected.length > 0 ? "select" : "whole";
|
|
2490
|
+
/**
|
|
2491
|
+
* The select list survives an aggregate shape: `select('categoria').count()`
|
|
2492
|
+
* with a GROUP BY leaves the grouped column as well as the count. Dropping
|
|
2493
|
+
* it left a summary at 1 DET.
|
|
2494
|
+
*/
|
|
1655
2495
|
const read = (store, how, via) => reads.push({
|
|
1656
2496
|
store,
|
|
1657
2497
|
shape: how,
|
|
1658
|
-
columns: how === "
|
|
2498
|
+
columns: how === "whole" ? [] : chain.selected,
|
|
1659
2499
|
...via ? { via } : {}
|
|
1660
2500
|
});
|
|
1661
2501
|
if (access.method === "related" && access.viaRelation) read(access.viaRelation, shape);
|
|
@@ -1709,7 +2549,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1709
2549
|
by: resolved.by,
|
|
1710
2550
|
technical
|
|
1711
2551
|
});
|
|
1712
|
-
followedCalls.
|
|
2552
|
+
followedCalls.set(call, resolved.refs);
|
|
1713
2553
|
continue;
|
|
1714
2554
|
}
|
|
1715
2555
|
if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
|
|
@@ -1723,12 +2563,21 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1723
2563
|
* Read after the loop: whether a key holds a nested transformer is known only
|
|
1724
2564
|
* once the strategies have said which calls they follow.
|
|
1725
2565
|
*/
|
|
1726
|
-
const
|
|
2566
|
+
const followed = (c) => followedCalls.has(c);
|
|
2567
|
+
const output = outputFieldsIn(body, owner, storesByName, followed);
|
|
2568
|
+
const deliveries = deliveriesIn({
|
|
2569
|
+
body,
|
|
2570
|
+
file,
|
|
2571
|
+
symbols,
|
|
2572
|
+
relations: relationsByStore,
|
|
2573
|
+
followed: followedCalls
|
|
2574
|
+
});
|
|
1727
2575
|
return {
|
|
1728
2576
|
accesses,
|
|
1729
2577
|
followUps,
|
|
1730
2578
|
unresolved,
|
|
1731
2579
|
validators: validator.fields,
|
|
2580
|
+
commandFields,
|
|
1732
2581
|
opaqueValidators: validator.opaque,
|
|
1733
2582
|
requestFields: request.fields,
|
|
1734
2583
|
opaqueRequest: request.opaque,
|
|
@@ -1736,6 +2585,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1736
2585
|
opaqueOutputs: output.opaqueOutputs,
|
|
1737
2586
|
transformed: output.resource,
|
|
1738
2587
|
reads,
|
|
2588
|
+
deliveries,
|
|
1739
2589
|
bodyHash: hashOf(body)
|
|
1740
2590
|
};
|
|
1741
2591
|
}
|
|
@@ -1835,18 +2685,137 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1835
2685
|
const touches = /* @__PURE__ */ new Set();
|
|
1836
2686
|
const writtenStores = /* @__PURE__ */ new Set();
|
|
1837
2687
|
const inputFields = /* @__PURE__ */ new Set();
|
|
2688
|
+
const commandFields = /* @__PURE__ */ new Set();
|
|
1838
2689
|
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1839
2690
|
const requestFields = /* @__PURE__ */ new Set();
|
|
1840
2691
|
let opaqueRequest = false;
|
|
1841
2692
|
const outputFields = /* @__PURE__ */ new Set();
|
|
1842
2693
|
const opaqueOutputFields = /* @__PURE__ */ new Set();
|
|
1843
2694
|
const transformedStores = /* @__PURE__ */ new Set();
|
|
2695
|
+
const deliveredFields = /* @__PURE__ */ new Set();
|
|
2696
|
+
const deliveredOpaque = /* @__PURE__ */ new Set();
|
|
2697
|
+
const deliveredStores = /* @__PURE__ */ new Set();
|
|
2698
|
+
let anyDelivery = false;
|
|
1844
2699
|
const outputReads = /* @__PURE__ */ new Map();
|
|
1845
2700
|
const trace = [];
|
|
1846
2701
|
const scope = [];
|
|
1847
2702
|
const unresolved = [];
|
|
1848
2703
|
const visited = /* @__PURE__ */ new Set();
|
|
1849
2704
|
let writes = false;
|
|
2705
|
+
/** what a followed body reads, itself and through what it follows — bounded like the walk */
|
|
2706
|
+
const storesReadBy = (ref, depth, seen = /* @__PURE__ */ new Set()) => {
|
|
2707
|
+
const found = /* @__PURE__ */ new Set();
|
|
2708
|
+
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
2709
|
+
if (seen.has(key) || depth > maxDepth) return found;
|
|
2710
|
+
seen.add(key);
|
|
2711
|
+
const facts = factsFor(ref);
|
|
2712
|
+
if (!facts) return found;
|
|
2713
|
+
for (const access of facts.accesses) found.add(access.store);
|
|
2714
|
+
for (const followUp of facts.followUps) for (const store of storesReadBy(followUp.ref, depth + 1, seen)) found.add(store);
|
|
2715
|
+
return found;
|
|
2716
|
+
};
|
|
2717
|
+
/**
|
|
2718
|
+
* A delivered call hands on what its body RETURNS — classified, so a returned
|
|
2719
|
+
* `{ data: rows, meta }` delivers the rows' store and the meta's leaves, not
|
|
2720
|
+
* one DET per key. `pick` keeps one key of it: `const { data } = …`,
|
|
2721
|
+
* `resultado.linhas`. Bounded by depth like the walk, and by a seen set so a
|
|
2722
|
+
* body returning itself cannot loop.
|
|
2723
|
+
*/
|
|
2724
|
+
const deliver = (item, depth, seen = /* @__PURE__ */ new Set()) => {
|
|
2725
|
+
switch (item.kind) {
|
|
2726
|
+
case "store":
|
|
2727
|
+
deliveredStores.add(item.store);
|
|
2728
|
+
return;
|
|
2729
|
+
case "scalar":
|
|
2730
|
+
deliveredFields.add(item.path || "<value>");
|
|
2731
|
+
return;
|
|
2732
|
+
case "echo": return;
|
|
2733
|
+
case "opaque":
|
|
2734
|
+
deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
|
|
2735
|
+
return;
|
|
2736
|
+
case "call": {
|
|
2737
|
+
if (depth > maxDepth + 2) return;
|
|
2738
|
+
let resolved = false;
|
|
2739
|
+
for (const ref of item.refs) {
|
|
2740
|
+
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}#${item.pick ?? ""}#${item.path}`;
|
|
2741
|
+
if (seen.has(key)) continue;
|
|
2742
|
+
seen.add(key);
|
|
2743
|
+
const facts = factsFor(ref);
|
|
2744
|
+
if (!facts) continue;
|
|
2745
|
+
if (facts.outputs.length > 0 || facts.transformed) {
|
|
2746
|
+
resolved = true;
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
|
|
2750
|
+
/**
|
|
2751
|
+
* `egresso.curso` where the body returns the row itself (`return
|
|
2752
|
+
* Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
|
|
2753
|
+
* value — one field of a store is one value; one key of a returned call
|
|
2754
|
+
* is that call picked deeper.
|
|
2755
|
+
*/
|
|
2756
|
+
if (item.pick && returned.length === 0) {
|
|
2757
|
+
const above = facts.deliveries.returns.filter((r) => r.path === "" || item.pick.startsWith(`${r.path}.`));
|
|
2758
|
+
for (const r of above) {
|
|
2759
|
+
const rest = r.path ? item.pick.slice(r.path.length + 1) : item.pick;
|
|
2760
|
+
if (r.kind === "call") deliver({
|
|
2761
|
+
...r,
|
|
2762
|
+
path: item.path,
|
|
2763
|
+
pick: rest
|
|
2764
|
+
}, depth + 1, seen);
|
|
2765
|
+
else if (r.kind === "store" || r.kind === "scalar") deliveredFields.add(item.path || "<value>");
|
|
2766
|
+
}
|
|
2767
|
+
if (above.length > 0) {
|
|
2768
|
+
resolved = true;
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* A return the classifier could not read at all (`rows.map(…).join(…)`)
|
|
2774
|
+
* says nothing about what leaves; what the body read, or what was handed
|
|
2775
|
+
* into it, says more — so it does not count as resolved.
|
|
2776
|
+
*/
|
|
2777
|
+
if (returned.length > 0 && returned.some((r) => r.kind !== "opaque")) {
|
|
2778
|
+
for (const r of returned) {
|
|
2779
|
+
const rest = item.pick ? r.path.slice(item.pick.length).replace(/^\./, "") : r.path;
|
|
2780
|
+
const path = [item.path, rest].filter(Boolean).join(".");
|
|
2781
|
+
deliver({
|
|
2782
|
+
...r,
|
|
2783
|
+
path
|
|
2784
|
+
}, depth + 1, seen);
|
|
2785
|
+
}
|
|
2786
|
+
resolved = true;
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (item.pick) continue;
|
|
2790
|
+
const read = storesReadBy(ref, depth + 1);
|
|
2791
|
+
if (read.size > 0) {
|
|
2792
|
+
for (const store of read) deliveredStores.add(store);
|
|
2793
|
+
resolved = true;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (resolved) return;
|
|
2797
|
+
/**
|
|
2798
|
+
* `const { confidenciais } = await this.contar()` where the body's return
|
|
2799
|
+
* is unreadable: one KEY of it, named — one value, not an opaque floor.
|
|
2800
|
+
* Only a key that carries rows on (`data`, `rows`) stays unreadable.
|
|
2801
|
+
*/
|
|
2802
|
+
if (item.pick && !PASSES_ROWS.has(item.pick.split(".")[0])) {
|
|
2803
|
+
deliveredFields.add(item.path || "<value>");
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* The body returned no literal and read no store — a CSV builder, a
|
|
2808
|
+
* formatter over rows handed in. The document it built carries what it
|
|
2809
|
+
* received, so the rows' stores leave. Nothing handed in: opaque.
|
|
2810
|
+
*/
|
|
2811
|
+
if (item.args.length > 0) {
|
|
2812
|
+
for (const argument of item.args) deliver(argument, depth, seen);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
1850
2819
|
const visit = (ref, depth, technical = false) => {
|
|
1851
2820
|
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
1852
2821
|
if (visited.has(key) || depth > maxDepth) return;
|
|
@@ -1885,12 +2854,20 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1885
2854
|
}
|
|
1886
2855
|
unresolved.push(...facts.unresolved);
|
|
1887
2856
|
for (const field of facts.validators) inputFields.add(field);
|
|
2857
|
+
for (const field of facts.commandFields) commandFields.add(field);
|
|
1888
2858
|
for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
|
|
1889
2859
|
for (const field of facts.requestFields) requestFields.add(field);
|
|
1890
2860
|
if (facts.opaqueRequest) opaqueRequest = true;
|
|
1891
2861
|
for (const field of facts.outputs) outputFields.add(field);
|
|
1892
2862
|
for (const field of facts.opaqueOutputs) opaqueOutputFields.add(field);
|
|
1893
2863
|
if (facts.transformed) transformedStores.add(facts.transformed);
|
|
2864
|
+
/**
|
|
2865
|
+
* Deliveries, resolved here because a delivered CALL leads to a body only
|
|
2866
|
+
* the graph knows: what that body returns is what the value hands on.
|
|
2867
|
+
*/
|
|
2868
|
+
const items = [...facts.deliveries.calls, ...depth === 0 ? facts.deliveries.returns : []];
|
|
2869
|
+
if (facts.deliveries.anyCall || depth === 0 && facts.deliveries.anyReturn) anyDelivery = true;
|
|
2870
|
+
for (const item of items) deliver(item, depth);
|
|
1894
2871
|
for (const { store, shape, columns, via } of facts.reads) {
|
|
1895
2872
|
const known = outputReads.get(store) ?? {
|
|
1896
2873
|
whole: false,
|
|
@@ -1931,12 +2908,19 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1931
2908
|
touches: [...touches].sort(),
|
|
1932
2909
|
writtenStores: [...writtenStores].sort(),
|
|
1933
2910
|
inputFields: [...inputFields].sort(),
|
|
2911
|
+
commandFields: [...commandFields].sort(),
|
|
1934
2912
|
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1935
2913
|
requestFields: [...requestFields].sort(),
|
|
1936
2914
|
opaqueRequest,
|
|
1937
2915
|
outputFields: [...outputFields].sort(),
|
|
1938
2916
|
opaqueOutputFields: [...opaqueOutputFields].sort(),
|
|
1939
2917
|
transformedStores: [...transformedStores].sort(),
|
|
2918
|
+
delivered: {
|
|
2919
|
+
any: anyDelivery,
|
|
2920
|
+
fields: [...deliveredFields].sort(),
|
|
2921
|
+
opaqueFields: [...deliveredOpaque].sort(),
|
|
2922
|
+
stores: [...deliveredStores].sort()
|
|
2923
|
+
},
|
|
1940
2924
|
outputReads: Object.fromEntries([...outputReads.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([store, read]) => [store, {
|
|
1941
2925
|
whole: read.whole,
|
|
1942
2926
|
selected: [...read.selected].sort(),
|
|
@@ -1969,6 +2953,8 @@ function findBody(file, ref) {
|
|
|
1969
2953
|
}
|
|
1970
2954
|
const fn = file.getFunction(ref.member);
|
|
1971
2955
|
if (fn) return fn;
|
|
2956
|
+
const initializer = file.getVariableDeclaration(ref.member)?.getInitializer();
|
|
2957
|
+
if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) return initializer;
|
|
1972
2958
|
return null;
|
|
1973
2959
|
}
|
|
1974
2960
|
for (const cls of file.getClasses()) {
|
|
@@ -1997,6 +2983,27 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
1997
2983
|
if (stores.has(named.getName())) symbols.set(binding, named.getName());
|
|
1998
2984
|
}
|
|
1999
2985
|
}
|
|
2986
|
+
/**
|
|
2987
|
+
* `const { default: Noticia } = await import('#noticias/models/noticia')`: a
|
|
2988
|
+
* model imported INSIDE the body — an ace command does this to keep the app
|
|
2989
|
+
* from booting for `--help`. The store is the same; only the binding moved.
|
|
2990
|
+
*/
|
|
2991
|
+
const storeByFile = /* @__PURE__ */ new Map();
|
|
2992
|
+
for (const [name, store] of stores) storeByFile.set(toPosix(store.provenance.file), name);
|
|
2993
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2994
|
+
const binding = declaration.getNameNode();
|
|
2995
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
2996
|
+
let initializer = declaration.getInitializer();
|
|
2997
|
+
if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
|
|
2998
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
2999
|
+
if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
3000
|
+
const specifier = initializer.getArguments()[0];
|
|
3001
|
+
if (!specifier || !Node.isStringLiteral(specifier)) continue;
|
|
3002
|
+
const target = app.resolveSpecifier(specifier.getLiteralValue());
|
|
3003
|
+
const store = target ? storeByFile.get(toPosix(target)) : void 0;
|
|
3004
|
+
if (!store) continue;
|
|
3005
|
+
for (const element of binding.getElements()) if ((element.getPropertyNameNode()?.getText() ?? element.getName()) === "default") symbols.set(element.getName(), store);
|
|
3006
|
+
}
|
|
2000
3007
|
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2001
3008
|
const initializer = declaration.getInitializer();
|
|
2002
3009
|
const name = declaration.getNameNode();
|
|
@@ -2162,6 +3169,32 @@ function importMapsOf(file, app) {
|
|
|
2162
3169
|
if (alias) exportedAs.set(alias, named.getName());
|
|
2163
3170
|
}
|
|
2164
3171
|
}
|
|
3172
|
+
/**
|
|
3173
|
+
* `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
|
|
3174
|
+
* `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
|
|
3175
|
+
*
|
|
3176
|
+
* A module imported INSIDE a body — the shape ace commands use so `--help` does
|
|
3177
|
+
* not boot the application. The binding moved; the body it names did not, and
|
|
3178
|
+
* a command importing its whole action layer this way reached nothing.
|
|
3179
|
+
*/
|
|
3180
|
+
for (const declaration of file.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
3181
|
+
const binding = declaration.getNameNode();
|
|
3182
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
3183
|
+
let initializer = declaration.getInitializer();
|
|
3184
|
+
if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
|
|
3185
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
3186
|
+
if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
3187
|
+
const specifier = initializer.getArguments()[0];
|
|
3188
|
+
if (!specifier || !Node.isStringLiteral(specifier)) continue;
|
|
3189
|
+
const target = app.resolveSpecifier(specifier.getLiteralValue());
|
|
3190
|
+
if (!target) continue;
|
|
3191
|
+
for (const element of binding.getElements()) {
|
|
3192
|
+
const exported = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
3193
|
+
const local = element.getName();
|
|
3194
|
+
imports.set(local, target);
|
|
3195
|
+
if (exported !== "default" && exported !== local) exportedAs.set(local, exported);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
2165
3198
|
return {
|
|
2166
3199
|
imports,
|
|
2167
3200
|
exportedAs
|
|
@@ -2215,6 +3248,154 @@ function hashOf(body) {
|
|
|
2215
3248
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2216
3249
|
}
|
|
2217
3250
|
//#endregion
|
|
3251
|
+
//#region src/inventory/sources/jobs.ts
|
|
3252
|
+
/** `X.schedule({}).cron(…)`: a scheduler's way of dispatching */
|
|
3253
|
+
const SCHEDULE_METHODS = new Set(["schedule"]);
|
|
3254
|
+
function collectJobs(app) {
|
|
3255
|
+
const project = new Project({
|
|
3256
|
+
skipAddingFilesFromTsConfig: true,
|
|
3257
|
+
skipFileDependencyResolution: true,
|
|
3258
|
+
compilerOptions: { allowJs: false }
|
|
3259
|
+
});
|
|
3260
|
+
const root = toPosix(app.root);
|
|
3261
|
+
for (const dir of app.scanRoots) project.addSourceFilesAtPaths(`${toPosix(dir)}/**/*.ts`);
|
|
3262
|
+
project.addSourceFilesAtPaths(`${root}/start/**/*.ts`);
|
|
3263
|
+
project.addSourceFilesAtPaths(`${root}/commands/**/*.ts`);
|
|
3264
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
3265
|
+
for (const file of project.getSourceFiles()) {
|
|
3266
|
+
const filePath = toPosix(file.getFilePath());
|
|
3267
|
+
if (!isApplicationCode(app.root, filePath) || !inJobsDirectory(filePath)) continue;
|
|
3268
|
+
for (const cls of file.getClasses()) {
|
|
3269
|
+
const name = cls.getName();
|
|
3270
|
+
if (!name || !looksLikeJob(cls)) continue;
|
|
3271
|
+
jobs.set(filePath, {
|
|
3272
|
+
name,
|
|
3273
|
+
file: filePath,
|
|
3274
|
+
dispatchedFrom: [],
|
|
3275
|
+
scheduledFrom: []
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
if (jobs.size === 0) return [];
|
|
3280
|
+
for (const file of project.getSourceFiles()) {
|
|
3281
|
+
const filePath = toPosix(file.getFilePath());
|
|
3282
|
+
if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
|
|
3283
|
+
const { imports } = importMapsOf(file, app);
|
|
3284
|
+
if (imports.size === 0) continue;
|
|
3285
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
3286
|
+
const callee = call.getExpression();
|
|
3287
|
+
if (!Node.isPropertyAccessExpression(callee)) continue;
|
|
3288
|
+
const method = callee.getName();
|
|
3289
|
+
const scheduled = SCHEDULE_METHODS.has(method);
|
|
3290
|
+
if (!scheduled && !DISPATCH_METHODS.has(method)) continue;
|
|
3291
|
+
const receiver = callee.getExpression();
|
|
3292
|
+
if (!Node.isIdentifier(receiver)) continue;
|
|
3293
|
+
const target = imports.get(receiver.getText());
|
|
3294
|
+
const job = target ? jobs.get(toPosix(target)) : void 0;
|
|
3295
|
+
if (!job) continue;
|
|
3296
|
+
const sites = scheduled ? job.scheduledFrom : job.dispatchedFrom;
|
|
3297
|
+
if (!sites.includes(filePath)) sites.push(filePath);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
return [...jobs.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
3301
|
+
}
|
|
3302
|
+
const inJobsDirectory = (filePath) => filePath.split("/").slice(0, -1).some((segment) => segment === "jobs" || segment === "job");
|
|
3303
|
+
/** extends something called `…Job`, or declares the method a queue would run */
|
|
3304
|
+
function looksLikeJob(cls) {
|
|
3305
|
+
const parent = cls.getExtends()?.getExpression().getText() ?? "";
|
|
3306
|
+
if (/Job$/.test(parent)) return true;
|
|
3307
|
+
return EXECUTION_METHODS.some((method) => cls.getMethod(method) !== void 0);
|
|
3308
|
+
}
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region src/inventory/sources/json_schemas.ts
|
|
3311
|
+
function collectJsonSchemas(app) {
|
|
3312
|
+
const project = new Project({
|
|
3313
|
+
skipAddingFilesFromTsConfig: true,
|
|
3314
|
+
skipFileDependencyResolution: true,
|
|
3315
|
+
compilerOptions: { allowJs: false }
|
|
3316
|
+
});
|
|
3317
|
+
/**
|
|
3318
|
+
* Wider than `scanRoots`, and only here.
|
|
3319
|
+
*
|
|
3320
|
+
* `database/` is excluded from the application roots on purpose: test factories
|
|
3321
|
+
* and migrations contain real persistence calls, and scanning them would turn a
|
|
3322
|
+
* test write into a counted function. But `node ace make:seeder` puts seeders in
|
|
3323
|
+
* `database/seeders`, which is where seed data — and therefore a form's schema —
|
|
3324
|
+
* normally lives. Reading a literal counts nothing, so there is no conflict: the
|
|
3325
|
+
* exclusion protects the call graph, not the schema catalogue.
|
|
3326
|
+
*
|
|
3327
|
+
* Without this, `overrides.detFromSchema` naming a schema declared in a seeder
|
|
3328
|
+
* reported "not declared anywhere in the code" and left the DET count at the
|
|
3329
|
+
* floor — the exact case the override exists for.
|
|
3330
|
+
*/
|
|
3331
|
+
for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
3332
|
+
const found = /* @__PURE__ */ new Map();
|
|
3333
|
+
for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
|
|
3334
|
+
const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
3335
|
+
if (!literal || !isJsonSchema(literal)) continue;
|
|
3336
|
+
const leaves = leavesOf(literal);
|
|
3337
|
+
if (leaves.length === 0) continue;
|
|
3338
|
+
found.set(declaration.getName(), {
|
|
3339
|
+
name: declaration.getName(),
|
|
3340
|
+
fields: leaves.length,
|
|
3341
|
+
leaves,
|
|
3342
|
+
provenance: {
|
|
3343
|
+
file: toPosix(file.getFilePath()),
|
|
3344
|
+
line: declaration.getStartLineNumber(),
|
|
3345
|
+
by: "json-schema"
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
return found;
|
|
3350
|
+
}
|
|
3351
|
+
/** `{ … } as const` and `{ … } satisfies X` still hold the literal */
|
|
3352
|
+
function unwrap(node) {
|
|
3353
|
+
if (!node) return void 0;
|
|
3354
|
+
if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) return unwrap(node.getExpression());
|
|
3355
|
+
return node;
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Recognised by shape, never by name.
|
|
3359
|
+
*
|
|
3360
|
+
* A const called `schema` may be anything; an object declaring `type: 'object'`
|
|
3361
|
+
* with a `properties` map is a JSON Schema whatever it is called.
|
|
3362
|
+
*/
|
|
3363
|
+
function isJsonSchema(literal) {
|
|
3364
|
+
return (literal.getProperty("type")?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer()?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() === "object" && literal.getProperty("properties") !== void 0;
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Leaves of a JSON Schema, by the table in counting-decisions §7.
|
|
3368
|
+
*
|
|
3369
|
+
* scalar 1
|
|
3370
|
+
* nested object leaves counted individually
|
|
3371
|
+
* array of scalar 1 (repeating group)
|
|
3372
|
+
* array of object the object's leaves, once
|
|
3373
|
+
* enum / const 1
|
|
3374
|
+
*/
|
|
3375
|
+
function leavesOf(literal, prefix = "") {
|
|
3376
|
+
const properties = literal.getProperty("properties")?.asKind(SyntaxKind.PropertyAssignment);
|
|
3377
|
+
const items = literal.getProperty("items")?.asKind(SyntaxKind.PropertyAssignment);
|
|
3378
|
+
if (properties) {
|
|
3379
|
+
const map = properties.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
3380
|
+
if (!map) return [];
|
|
3381
|
+
return map.getProperties().flatMap((property) => {
|
|
3382
|
+
const assignment = property.asKind(SyntaxKind.PropertyAssignment);
|
|
3383
|
+
if (!assignment) return [];
|
|
3384
|
+
const name = assignment.getName().replace(/['"]/g, "");
|
|
3385
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
3386
|
+
const nested = unwrap(assignment.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
3387
|
+
const inner = nested ? leavesOf(nested, path) : [];
|
|
3388
|
+
return inner.length > 0 ? inner : [path];
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
if (items) {
|
|
3392
|
+
const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
3393
|
+
const inner = element ? leavesOf(element, prefix) : [];
|
|
3394
|
+
return inner.length > 0 ? inner : [prefix];
|
|
3395
|
+
}
|
|
3396
|
+
return [];
|
|
3397
|
+
}
|
|
3398
|
+
//#endregion
|
|
2218
3399
|
//#region src/albrecht/tables.ts
|
|
2219
3400
|
/** shared grid: [ref band][DET band] -> complexity */
|
|
2220
3401
|
const GRID = [
|
|
@@ -2757,7 +3938,7 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2757
3938
|
counted.add(field);
|
|
2758
3939
|
sources.push(source);
|
|
2759
3940
|
};
|
|
2760
|
-
for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
3941
|
+
if (entry.kind === "http") for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
2761
3942
|
/**
|
|
2762
3943
|
* `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
|
|
2763
3944
|
* with a schema's fields, and it used to assume there was exactly one and that
|
|
@@ -2777,6 +3958,11 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2777
3958
|
* paid for twice.
|
|
2778
3959
|
*/
|
|
2779
3960
|
for (const field of behavior.requestFields) add(field, `request:${field}`);
|
|
3961
|
+
for (const field of behavior.commandFields ?? []) {
|
|
3962
|
+
const [kind, ...rest] = field.split(".");
|
|
3963
|
+
const name = rest.join(".");
|
|
3964
|
+
add(name, `${kind === "args" ? "arg" : "flag"}:${name}`);
|
|
3965
|
+
}
|
|
2780
3966
|
if (type === "EO" || type === "EQ") {
|
|
2781
3967
|
/**
|
|
2782
3968
|
* counting-decisions §6, per store, in order of what is visible:
|
|
@@ -2812,8 +3998,34 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2812
3998
|
}
|
|
2813
3999
|
}
|
|
2814
4000
|
for (const field of behavior.outputFields) add(field, `transformer:${field}${opaqueOutputs.has(field) ? " (opaque)" : ""}`);
|
|
4001
|
+
/**
|
|
4002
|
+
* Plan 0.7 §A′ — the delivery is the boundary. When the transaction hands
|
|
4003
|
+
* something to a renderer or a response, THAT is what leaves: derived values
|
|
4004
|
+
* and the leaves of literals a followed body returned count 1 each; a store
|
|
4005
|
+
* leaves only if its rows were handed on, raw or through a query object; a
|
|
4006
|
+
* store read to authorise or to decide contributes nothing to the output. A
|
|
4007
|
+
* field that entered and is echoed back counts once, on entry (§7.3) — the
|
|
4008
|
+
* classifier marks it as an echo. Without a delivery point the rule cannot
|
|
4009
|
+
* apply, and every store read leaves, as before.
|
|
4010
|
+
*/
|
|
4011
|
+
const delivered = behavior.delivered;
|
|
4012
|
+
if (delivered.any) {
|
|
4013
|
+
for (const field of delivered.fields) add(`render.${field}`, `render:${field}`);
|
|
4014
|
+
for (const field of delivered.opaqueFields) add(`render.${field}`, `render:${field} (opaque)`);
|
|
4015
|
+
}
|
|
4016
|
+
const deliveredStores = new Set(delivered.stores);
|
|
4017
|
+
for (let changed = true; changed;) {
|
|
4018
|
+
changed = false;
|
|
4019
|
+
for (const [store, read] of Object.entries(behavior.outputReads)) {
|
|
4020
|
+
if (deliveredStores.has(store) || read.direct) continue;
|
|
4021
|
+
if (!read.via.some((parent) => deliveredStores.has(parent))) continue;
|
|
4022
|
+
deliveredStores.add(store);
|
|
4023
|
+
changed = true;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
2815
4026
|
for (const store of touched) {
|
|
2816
4027
|
const read = behavior.outputReads[store];
|
|
4028
|
+
if (delivered.any && !deliveredStores.has(store) && !covered.has(store)) continue;
|
|
2817
4029
|
if (read?.aggregate) add(`${store}.<aggregate>`, `aggregate:${store} (a count or an existence check: one scalar)`);
|
|
2818
4030
|
if (covered.has(store)) continue;
|
|
2819
4031
|
if (read && read.aggregate && !read.whole && read.selected.length === 0) continue;
|
|
@@ -2926,12 +4138,16 @@ const RULESET = "afp";
|
|
|
2926
4138
|
* column; system timestamps and `serializeAs: null` columns leaving the DETs;
|
|
2927
4139
|
* master-detail folded into one data function; identity by table; token tables
|
|
2928
4140
|
* technical; and opaque declarations reaching every function carrying the origin.
|
|
4141
|
+
* Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
|
|
4142
|
+
* props, the response payload, what a command prints) read back to their origin;
|
|
4143
|
+
* a function of the same file and a `.map(fn)` by reference are followed, so
|
|
4144
|
+
* FTRs move; and ace commands are transactions, with flags as input.
|
|
2929
4145
|
*
|
|
2930
4146
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
2931
4147
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
2932
4148
|
* exists for exactly that, and only this constant arms it.
|
|
2933
4149
|
*/
|
|
2934
|
-
const RULESET_VERSION = "1.
|
|
4150
|
+
const RULESET_VERSION = "1.6.0";
|
|
2935
4151
|
function count(input, options = {}) {
|
|
2936
4152
|
const warnings = [];
|
|
2937
4153
|
const usage = usageOf(input);
|
|
@@ -3013,7 +4229,8 @@ function count(input, options = {}) {
|
|
|
3013
4229
|
const countedRoots = new Set(dataFunctions.map((fn) => fn.name));
|
|
3014
4230
|
const countedStores = new Map(countable.filter((store) => countedRoots.has(grouping.rootOf.get(store.name) ?? store.name)).map((store) => [store.name, store]));
|
|
3015
4231
|
const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
|
|
3016
|
-
const
|
|
4232
|
+
const entryPoints = input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? ""));
|
|
4233
|
+
const transactionalFunctions = countTransactionalFunctions(entryPoints, input.behaviors, {
|
|
3017
4234
|
countedStores,
|
|
3018
4235
|
grouping,
|
|
3019
4236
|
root: input.app.root,
|
|
@@ -3047,6 +4264,9 @@ function count(input, options = {}) {
|
|
|
3047
4264
|
}));
|
|
3048
4265
|
warnings.push(...unreadableInputWarnings(input));
|
|
3049
4266
|
warnings.push(...unreadableOutputWarnings(input));
|
|
4267
|
+
warnings.push(...unreadableDeliveryWarnings(input));
|
|
4268
|
+
warnings.push(...commandWarnings(entryPoints, transactionalFunctions));
|
|
4269
|
+
warnings.push(...undispatchedJobWarnings(input));
|
|
3050
4270
|
warnings.push(...lookAlikeWarnings(functions));
|
|
3051
4271
|
warnings.push(...seededOnlyWarnings(functions, grouping.members, input.seededAnywhere));
|
|
3052
4272
|
return {
|
|
@@ -3058,6 +4278,67 @@ function count(input, options = {}) {
|
|
|
3058
4278
|
};
|
|
3059
4279
|
}
|
|
3060
4280
|
/**
|
|
4281
|
+
* Transactions that deliver something the analysis cannot read.
|
|
4282
|
+
*
|
|
4283
|
+
* A generated document (`response.send(gerarCsv(rows))`), a value built by a
|
|
4284
|
+
* call nobody followed. Each counts 1 DET — a floor — and the transaction's
|
|
4285
|
+
* output is understated by whatever the value carries. Named with the
|
|
4286
|
+
* expression, because the fix is usually in the code: return a literal, or a
|
|
4287
|
+
* transformer, and the fields become readable.
|
|
4288
|
+
*/
|
|
4289
|
+
function unreadableDeliveryWarnings(input) {
|
|
4290
|
+
const blind = input.entryPoints.map((entry) => ({
|
|
4291
|
+
entry,
|
|
4292
|
+
behavior: input.behaviors.get(entry.id)
|
|
4293
|
+
})).filter(({ behavior }) => behavior && !behavior.writes && behavior.delivered.opaqueFields.length > 0);
|
|
4294
|
+
if (blind.length === 0) return [];
|
|
4295
|
+
return [
|
|
4296
|
+
`${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
|
|
4297
|
+
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
|
|
4298
|
+
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
4299
|
+
];
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Jobs no transaction reaches — plan 0.7 §D.
|
|
4303
|
+
*
|
|
4304
|
+
* A job dispatched by a handler is part of that handler's transaction (§9). One
|
|
4305
|
+
* that nothing reachable dispatches is either a scheduled process — an elementary
|
|
4306
|
+
* process nobody is counting — or dead code, and the code cannot say which. It is
|
|
4307
|
+
* reported, never counted: inventing an elementary process is the error this
|
|
4308
|
+
* package exists to avoid.
|
|
4309
|
+
*/
|
|
4310
|
+
function undispatchedJobWarnings(input) {
|
|
4311
|
+
if (!input.jobs?.length) return [];
|
|
4312
|
+
const reached = /* @__PURE__ */ new Set();
|
|
4313
|
+
for (const behavior of input.behaviors.values()) for (const step of behavior.trace) reached.add(toPosix(step.file));
|
|
4314
|
+
const orphans = input.jobs.filter((job) => !reached.has(job.file));
|
|
4315
|
+
if (orphans.length === 0) return [];
|
|
4316
|
+
const where = (file) => relativeTo(input.app.root, file);
|
|
4317
|
+
return [`${orphans.length} job(s) reached by no transaction — a scheduled process nobody counts, or dead code. Reported, not counted: a scheduler is an entry point of its own once one is read (counting-decisions §9):`, ...orphans.map(({ name, file, dispatchedFrom, scheduledFrom }) => {
|
|
4318
|
+
const at = ` ${name} (${where(file)}): `;
|
|
4319
|
+
if (scheduledFrom.length > 0) return `${at}scheduled from ${scheduledFrom.map(where).join(", ")}, outside every transaction`;
|
|
4320
|
+
if (dispatchedFrom.length > 0) return `${at}dispatched from ${dispatchedFrom.map(where).join(", ")}, which no transaction reaches`;
|
|
4321
|
+
return `${at}dispatched by nothing in the application — dead code, or a scheduler this analysis does not read`;
|
|
4322
|
+
})];
|
|
4323
|
+
}
|
|
4324
|
+
/**
|
|
4325
|
+
* Ace commands counted as transactions — plan 0.7 §C.
|
|
4326
|
+
*
|
|
4327
|
+
* A batch process an operator starts is an elementary process, and it is counted.
|
|
4328
|
+
* Half the commands on the validated applications are development tools — a data
|
|
4329
|
+
* generator, a scaffolder — which the CPM does not count and the code cannot tell
|
|
4330
|
+
* from an importer: both write the same table. So each one is listed with the FP
|
|
4331
|
+
* at stake, the collector's hint beside it, and the way out is a declaration.
|
|
4332
|
+
*/
|
|
4333
|
+
function commandWarnings(entryPoints, functions) {
|
|
4334
|
+
const counted = entryPoints.filter((entry) => entry.kind === "command").map((entry) => ({
|
|
4335
|
+
entry,
|
|
4336
|
+
fn: functions.find((f) => f.id === `tx:${entry.identity}`)
|
|
4337
|
+
})).filter(({ fn }) => fn !== void 0);
|
|
4338
|
+
if (counted.length === 0) return [];
|
|
4339
|
+
return [`${counted.length} ace command(s) counted as elementary processes — a batch process an operator starts is a transaction. A development tool (a data generator, a scaffolder) is not the user's: exclude it with \`boundary.ignoreEntryPoints: ['<commandName>']\`:`, ...counted.map(({ entry, fn }) => ` ${entry.identity}: ${fn.type} ${fn.points} FP` + (entry.hints?.length ? ` — ${entry.hints.join("; ")}` : ""))];
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
3061
4342
|
* Transactions that look like the same elementary process.
|
|
3062
4343
|
*
|
|
3063
4344
|
* The CPM counts identical processing logic once. `GET /perfil` and
|
|
@@ -3379,7 +4660,9 @@ var CoverageTooLowError = class extends Error {
|
|
|
3379
4660
|
async function analyze(root, options = {}) {
|
|
3380
4661
|
const app = await discoverApp(root);
|
|
3381
4662
|
const { stores, unresolved: storeProblems } = await collectDataStores(app);
|
|
3382
|
-
const
|
|
4663
|
+
const routes = await collectEntryPoints(app);
|
|
4664
|
+
const routeProblems = routes.unresolved;
|
|
4665
|
+
const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
|
|
3383
4666
|
/** only read when an override names one — but collected once, like everything else */
|
|
3384
4667
|
const jsonSchemas = collectJsonSchemas(app);
|
|
3385
4668
|
const analyzer = createAnalyzer(app, stores, {
|
|
@@ -3461,6 +4744,13 @@ async function analyze(root, options = {}) {
|
|
|
3461
4744
|
by: "request"
|
|
3462
4745
|
}
|
|
3463
4746
|
})),
|
|
4747
|
+
...behavior.commandFields.length ? { commandFields: behavior.commandFields.map((name) => ({
|
|
4748
|
+
name,
|
|
4749
|
+
provenance: {
|
|
4750
|
+
file: emit(app.root),
|
|
4751
|
+
by: "ace-commands"
|
|
4752
|
+
}
|
|
4753
|
+
})) } : {},
|
|
3464
4754
|
opaqueRequest: behavior.opaqueRequest,
|
|
3465
4755
|
outputFields: behavior.outputFields.map((name) => ({
|
|
3466
4756
|
name,
|
|
@@ -3477,6 +4767,7 @@ async function analyze(root, options = {}) {
|
|
|
3477
4767
|
}
|
|
3478
4768
|
})),
|
|
3479
4769
|
transformedStores: behavior.transformedStores,
|
|
4770
|
+
delivered: behavior.delivered,
|
|
3480
4771
|
outputReads: behavior.outputReads,
|
|
3481
4772
|
trace: behavior.trace.map((step) => ({
|
|
3482
4773
|
...step,
|
|
@@ -3505,6 +4796,7 @@ async function analyze(root, options = {}) {
|
|
|
3505
4796
|
entryPoints,
|
|
3506
4797
|
behaviors,
|
|
3507
4798
|
jsonSchemas,
|
|
4799
|
+
jobs: collectJobs(app),
|
|
3508
4800
|
writtenAnywhere: analyzer.writtenAnywhere(),
|
|
3509
4801
|
addressedAnywhere: analyzer.addressedAnywhere(),
|
|
3510
4802
|
seededAnywhere: analyzer.seededAnywhere()
|