@filipebraida/adonis-function-points 0.6.0 → 0.8.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 +138 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-EAuAtdbq.js → fp_calibrate-B_oD9b02.js} +1 -1
- package/build/{fp_count-CZ0cUUBQ.js → fp_count-B_KPNKMO.js} +1 -1
- package/build/{fp_diff-BTg_LX0r.js → fp_diff-B3NhXzrb.js} +1 -1
- package/build/{fp_explain-D6QvDLKQ.js → fp_explain-C2s6Kpaj.js} +1 -1
- package/build/{fp_inventory-C43fU39x.js → fp_inventory-B322MvZT.js} +1 -1
- package/build/{fp_metrics-DEMPk4xC.js → fp_metrics-DjADb7F6.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-Cq4dNTNE.js → pipeline-C6kHKB9-.js} +2363 -114
- package/build/{resolvers-DlKJOZnk.js → resolvers-DhJO-qvQ.js} +339 -151
- package/build/{runners-FYmPIPub.js → runners-BpBJsGMj.js} +2 -2
- package/build/src/albrecht/counter.d.ts +12 -1
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +22 -0
- package/build/src/inventory/graph/deliveries.d.ts +94 -0
- package/build/src/inventory/graph/output_fields.d.ts +45 -1
- package/build/src/inventory/graph/pages.d.ts +44 -0
- 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 +23 -0
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
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-DhJO-qvQ.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
3
|
+
import path, { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
7
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
8
|
//#region src/inventory/app_context.ts
|
|
9
9
|
/** folders naming an artefact TYPE, in either layout */
|
|
10
10
|
const ARTIFACT_KINDS = new Set([
|
|
@@ -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) {
|
|
@@ -1239,6 +1265,1336 @@ function isNoiseMember(file, member) {
|
|
|
1239
1265
|
return base !== void 0 && FRAMEWORK_SERVICES.has(base);
|
|
1240
1266
|
}
|
|
1241
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$1 = 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
|
+
let via;
|
|
1440
|
+
if (RENDERERS.has(receiver) && RENDER_METHODS.has(method)) {
|
|
1441
|
+
payload = call.getArguments()[1];
|
|
1442
|
+
const name = unwrap$1(call.getArguments()[0]);
|
|
1443
|
+
if (name && Node.isStringLiteral(name)) via = {
|
|
1444
|
+
engine: receiver === "view" ? "edge" : "inertia",
|
|
1445
|
+
page: name.getLiteralValue()
|
|
1446
|
+
};
|
|
1447
|
+
} else if (receiver === "response" && RESPONSE_METHODS.has(method)) payload = call.getArguments()[0];
|
|
1448
|
+
else if (isPrinter(callee.getExpression(), ctx.body)) {
|
|
1449
|
+
any = true;
|
|
1450
|
+
seen.add(call);
|
|
1451
|
+
for (const argument of call.getArguments()) classify(unwrap$1(argument), "", ctx, deliveries, 0);
|
|
1452
|
+
continue;
|
|
1453
|
+
} else continue;
|
|
1454
|
+
any = true;
|
|
1455
|
+
if (!payload) continue;
|
|
1456
|
+
seen.add(call);
|
|
1457
|
+
const before = deliveries.length;
|
|
1458
|
+
classify(unwrap$1(payload), "", ctx, deliveries, 0);
|
|
1459
|
+
if (via) for (let i = before; i < deliveries.length; i++) deliveries[i].via = via;
|
|
1460
|
+
}
|
|
1461
|
+
for (const statement of ctx.body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
1462
|
+
if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== ctx.body) continue;
|
|
1463
|
+
const value = unwrap$1(statement.getExpression());
|
|
1464
|
+
if (!value) continue;
|
|
1465
|
+
if (Node.isCallExpression(value)) {
|
|
1466
|
+
if (seen.has(value)) continue;
|
|
1467
|
+
/**
|
|
1468
|
+
* `return response.redirect().toRoute(…)`, `return response.noContent()`,
|
|
1469
|
+
* `return inertia.render(…)` (seen above): a call whose chain is rooted at
|
|
1470
|
+
* the response or a renderer is the response itself, not a value handed
|
|
1471
|
+
* back — nothing to classify.
|
|
1472
|
+
*/
|
|
1473
|
+
const root = chainRootOf(value);
|
|
1474
|
+
if (root && (RENDERERS.has(root) || root === "response")) continue;
|
|
1475
|
+
}
|
|
1476
|
+
anyReturn = true;
|
|
1477
|
+
classify(value, "", ctx, returns, 0);
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
calls: deliveries,
|
|
1481
|
+
anyCall: any,
|
|
1482
|
+
returns,
|
|
1483
|
+
anyReturn
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/** the identifier a call chain is rooted at: `response.redirect().toRoute(x)` -> 'response' */
|
|
1487
|
+
function chainRootOf(node) {
|
|
1488
|
+
let current = node;
|
|
1489
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1490
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
|
|
1491
|
+
current = current.getExpression();
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current)) {
|
|
1495
|
+
current = current.getExpression();
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
return Node.isIdentifier(current) ? current.getText() : Node.isThisExpression(current) ? "this" : null;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* `this.ui.table().row(x)`, `this.logger.info(x)`, `console.log(x)`: a chain rooted
|
|
1504
|
+
* at a printer — through a variable too (`const table = this.ui.table(); table.row(x)`).
|
|
1505
|
+
*/
|
|
1506
|
+
function isPrinter(receiver, body, depth = 0) {
|
|
1507
|
+
let current = receiver;
|
|
1508
|
+
for (let steps = 0; current && steps < 20; steps++) {
|
|
1509
|
+
if (Node.isCallExpression(current)) {
|
|
1510
|
+
current = current.getExpression();
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (Node.isPropertyAccessExpression(current)) {
|
|
1514
|
+
const inner = current.getExpression();
|
|
1515
|
+
if (Node.isThisExpression(inner)) return PRINTERS.has(current.getName());
|
|
1516
|
+
current = inner;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (!Node.isIdentifier(current)) return false;
|
|
1520
|
+
if (current.getText() === "console") return true;
|
|
1521
|
+
const bound = depth < 3 ? bindingOf(current.getText(), body) : null;
|
|
1522
|
+
return !!bound && isPrinter(bound.initializer, body, depth + 1);
|
|
1523
|
+
}
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
/** `ctx.inertia` -> 'inertia', `inertia` -> 'inertia', `this.response` -> 'response' */
|
|
1527
|
+
function lastSegmentOf(node) {
|
|
1528
|
+
if (Node.isPropertyAccessExpression(node)) return node.getName();
|
|
1529
|
+
return node.getText();
|
|
1530
|
+
}
|
|
1531
|
+
function classify(value, path, ctx, out, depth) {
|
|
1532
|
+
if (!value || depth > 6) return;
|
|
1533
|
+
if (value.getKind() === SyntaxKind.NullKeyword) return;
|
|
1534
|
+
if (Node.isIdentifier(value) && value.getText() === "undefined") return;
|
|
1535
|
+
/**
|
|
1536
|
+
* A constant with no key — `this.logger.info('done')`, a table's `head(['Nome'])`
|
|
1537
|
+
* — is a label or a message, not a field: AFP counts no message DET (§6). Keyed,
|
|
1538
|
+
* `{ titulo: 'X' }` is a value the page receives, and stays one.
|
|
1539
|
+
*/
|
|
1540
|
+
if (!path && (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isNumericLiteral(value))) return;
|
|
1541
|
+
if (Node.isObjectLiteralExpression(value)) {
|
|
1542
|
+
for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join$1(path, property.getName()), ctx, out, depth + 1);
|
|
1543
|
+
else if (Node.isPropertyAssignment(property)) {
|
|
1544
|
+
const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
|
|
1545
|
+
classify(unwrap$1(property.getInitializer()), join$1(path, name), ctx, out, depth + 1);
|
|
1546
|
+
} else if (Node.isSpreadAssignment(property)) classify(unwrap$1(property.getExpression()), path, ctx, out, depth + 1);
|
|
1547
|
+
else out.push({
|
|
1548
|
+
kind: "scalar",
|
|
1549
|
+
path: join$1(path, property.getName?.() ?? "*")
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (Node.isConditionalExpression(value)) {
|
|
1554
|
+
classify(unwrap$1(value.getWhenTrue()), path, ctx, out, depth + 1);
|
|
1555
|
+
classify(unwrap$1(value.getWhenFalse()), path, ctx, out, depth + 1);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (Node.isArrayLiteralExpression(value)) {
|
|
1559
|
+
for (const element of value.getElements()) classify(unwrap$1(Node.isSpreadElement(element) ? element.getExpression() : element), path, ctx, out, depth + 1);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (Node.isElementAccessExpression(value)) {
|
|
1563
|
+
classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (Node.isIdentifier(value)) {
|
|
1567
|
+
const name = value.getText();
|
|
1568
|
+
if (ctx.symbols.has(name)) {
|
|
1569
|
+
out.push({
|
|
1570
|
+
kind: "store",
|
|
1571
|
+
store: ctx.symbols.get(name),
|
|
1572
|
+
path
|
|
1573
|
+
});
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const bound = bindingOf(name, ctx.body);
|
|
1577
|
+
if (bound) {
|
|
1578
|
+
if (bound.pick && Node.isCallExpression(bound.initializer)) {
|
|
1579
|
+
classifyCall(bound.initializer, path, ctx, out, depth + 1, bound.pick);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (bound.pick && Node.isObjectLiteralExpression(bound.initializer)) {
|
|
1583
|
+
const picked = bound.initializer.getProperty(bound.pick);
|
|
1584
|
+
if (picked && Node.isPropertyAssignment(picked)) {
|
|
1585
|
+
classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (picked && Node.isShorthandPropertyAssignment(picked)) {
|
|
1589
|
+
classify(picked.getNameNode(), path, ctx, out, depth + 1);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
classify(bound.initializer, path, ctx, out, depth + 1);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (isEchoBinding(name, ctx.body)) {
|
|
1597
|
+
out.push({
|
|
1598
|
+
kind: "echo",
|
|
1599
|
+
path
|
|
1600
|
+
});
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
out.push({
|
|
1604
|
+
kind: "scalar",
|
|
1605
|
+
path
|
|
1606
|
+
});
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (Node.isCallExpression(value)) {
|
|
1610
|
+
classifyCall(value, path, ctx, out, depth);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (Node.isPropertyAccessExpression(value) || Node.isElementAccessExpression(value)) {
|
|
1614
|
+
classifyAccess(value, path, ctx, out, depth);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
if (Node.isAwaitExpression(value)) {
|
|
1618
|
+
classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (Node.isBinaryExpression(value)) {
|
|
1622
|
+
const operator = value.getOperatorToken().getKind();
|
|
1623
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
|
|
1624
|
+
classify(unwrap$1(value.getLeft()), path, ctx, out, depth + 1);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken) {
|
|
1628
|
+
classify(unwrap$1(value.getRight()), path, ctx, out, depth + 1);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (Node.isTemplateExpression(value)) {
|
|
1633
|
+
for (const span of value.getTemplateSpans()) classify(unwrap$1(span.getExpression()), path, ctx, out, depth + 1);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
out.push({
|
|
1637
|
+
kind: "scalar",
|
|
1638
|
+
path
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
function classifyCall(value, path, ctx, out, depth, pick) {
|
|
1642
|
+
{
|
|
1643
|
+
const callee = value.getExpression();
|
|
1644
|
+
const method = Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
|
|
1645
|
+
if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(method) && isRequestLike(callee.getExpression())) {
|
|
1646
|
+
out.push({
|
|
1647
|
+
kind: "echo",
|
|
1648
|
+
path
|
|
1649
|
+
});
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const mapped = mappedLiteralOf(value);
|
|
1653
|
+
if (mapped) {
|
|
1654
|
+
classify(mapped, path, ctx, out, depth + 1);
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (method === "map" && Node.isPropertyAccessExpression(callee)) {
|
|
1658
|
+
classifyMapped(value, callee.getExpression(), path, ctx, out, depth);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH$1.has(method) && value.getArguments().length === 0) {
|
|
1662
|
+
classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (method === "getMeta" && value.getArguments().length === 0) {
|
|
1666
|
+
if (pick) out.push({
|
|
1667
|
+
kind: "scalar",
|
|
1668
|
+
path
|
|
1669
|
+
});
|
|
1670
|
+
else for (const key of PAGINATOR_META) out.push({
|
|
1671
|
+
kind: "scalar",
|
|
1672
|
+
path: join$1(path, key)
|
|
1673
|
+
});
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (Node.isPropertyAccessExpression(callee) && SAME_COLLECTION.has(method)) {
|
|
1677
|
+
classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (Node.isPropertyAccessExpression(callee) && lastSegmentOf(callee.getExpression()) === "colors") {
|
|
1681
|
+
const coloured = unwrap$1(value.getArguments()[0]);
|
|
1682
|
+
if (coloured) classify(coloured, path, ctx, out, depth + 1);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (Node.isPropertyAccessExpression(callee) && INERTIA_LAZY.has(method) && RENDERERS.has(lastSegmentOf(callee.getExpression()))) {
|
|
1686
|
+
const body = callbackValueOf(unwrap$1(value.getArguments()[0]));
|
|
1687
|
+
if (body) classify(body, path, ctx, out, depth + 1);
|
|
1688
|
+
else out.push({
|
|
1689
|
+
kind: "scalar",
|
|
1690
|
+
path
|
|
1691
|
+
});
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const args = [];
|
|
1695
|
+
for (const argument of value.getArguments()) classify(unwrap$1(argument), path, ctx, args, depth + 1);
|
|
1696
|
+
/** only what carries rows matters for a fallback: a scalar argument is a parameter, not an output */
|
|
1697
|
+
const carried = args.filter((item) => item.kind === "store" || item.kind === "call");
|
|
1698
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1699
|
+
const root = chainRootOf(callee.getExpression());
|
|
1700
|
+
if (root && SCALAR_SERVICES.has(root)) {
|
|
1701
|
+
out.push({
|
|
1702
|
+
kind: "scalar",
|
|
1703
|
+
path
|
|
1704
|
+
});
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const refs = ctx.followed.get(value);
|
|
1709
|
+
if (refs && refs.length > 0) {
|
|
1710
|
+
out.push({
|
|
1711
|
+
kind: "call",
|
|
1712
|
+
refs,
|
|
1713
|
+
path,
|
|
1714
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60),
|
|
1715
|
+
args: carried,
|
|
1716
|
+
...pick ? { pick } : {}
|
|
1717
|
+
});
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
const access = detectAccess(value, ctx.symbols, ctx.relations);
|
|
1721
|
+
if (access) {
|
|
1722
|
+
out.push({
|
|
1723
|
+
kind: "store",
|
|
1724
|
+
store: access.store,
|
|
1725
|
+
path
|
|
1726
|
+
});
|
|
1727
|
+
if (access.viaRelation) out.push({
|
|
1728
|
+
kind: "store",
|
|
1729
|
+
store: access.viaRelation,
|
|
1730
|
+
path
|
|
1731
|
+
});
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (Node.isIdentifier(callee) && NATIVE_GLOBALS.has(callee.getText())) {
|
|
1735
|
+
out.push({
|
|
1736
|
+
kind: "scalar",
|
|
1737
|
+
path
|
|
1738
|
+
});
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (Node.isPropertyAccessExpression(callee) && Node.isIdentifier(callee.getExpression()) && NATIVE_GLOBALS.has(callee.getExpression().getText())) {
|
|
1742
|
+
out.push({
|
|
1743
|
+
kind: "scalar",
|
|
1744
|
+
path
|
|
1745
|
+
});
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* A call nobody followed, over rows: a package's CSV builder, a formatter. The
|
|
1750
|
+
* document it builds carries what was handed into it, so the rows' stores leave.
|
|
1751
|
+
* With nothing flowing in there is nothing to say: one DET, opaque, reported.
|
|
1752
|
+
*/
|
|
1753
|
+
if (carried.length > 0) {
|
|
1754
|
+
out.push(...carried);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* `startDate?.toISOString()`, `categoria.trim()`: a call ON a value the body
|
|
1759
|
+
* holds is that value formatted — an echo stays an echo, a store's field one
|
|
1760
|
+
* field. Only when the root itself resolves to nothing else is the call read
|
|
1761
|
+
* on its own.
|
|
1762
|
+
*/
|
|
1763
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1764
|
+
const root = chainRootOf(callee.getExpression());
|
|
1765
|
+
if (root && ctx.symbols.has(root)) {
|
|
1766
|
+
if (readsField(callee.getExpression())) out.push({
|
|
1767
|
+
kind: "scalar",
|
|
1768
|
+
path
|
|
1769
|
+
});
|
|
1770
|
+
else out.push({
|
|
1771
|
+
kind: "store",
|
|
1772
|
+
store: ctx.symbols.get(root),
|
|
1773
|
+
path
|
|
1774
|
+
});
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (root && root !== "this") {
|
|
1778
|
+
const rootNode = rootIdentifierOf(callee.getExpression());
|
|
1779
|
+
const held = [];
|
|
1780
|
+
if (rootNode) classify(rootNode, path, ctx, held, depth + 1);
|
|
1781
|
+
if (held.length > 0 && held.every((item) => item.kind === "echo")) {
|
|
1782
|
+
out.push({
|
|
1783
|
+
kind: "echo",
|
|
1784
|
+
path
|
|
1785
|
+
});
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* `rows.map(…).join('\n')` in a body whose parameter is `rows`: the whole input
|
|
1792
|
+
* transformed, so the input says what leaves — the caller's arguments decide,
|
|
1793
|
+
* and the body itself says nothing. `noticia.publicadaEm?.toISO()` on that
|
|
1794
|
+
* parameter is one FIELD of it, and stays one value below.
|
|
1795
|
+
*/
|
|
1796
|
+
if (Node.isPropertyAccessExpression(callee) && transformsParameter(callee, ctx.body)) {
|
|
1797
|
+
out.push({
|
|
1798
|
+
kind: "opaque",
|
|
1799
|
+
path,
|
|
1800
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60)
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
1805
|
+
/**
|
|
1806
|
+
* `comunicado.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
|
|
1807
|
+
* the chain, or an expression, then a method — one value. A chain rooted at
|
|
1808
|
+
* `this` (`this.service.find()`) is a call into a service, and not this.
|
|
1809
|
+
*/
|
|
1810
|
+
if (readsField(callee.getExpression())) {
|
|
1811
|
+
out.push({
|
|
1812
|
+
kind: "scalar",
|
|
1813
|
+
path
|
|
1814
|
+
});
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (BOOLEAN_METHODS.has(method) || FORMAT_METHODS.has(method)) {
|
|
1818
|
+
out.push({
|
|
1819
|
+
kind: "scalar",
|
|
1820
|
+
path
|
|
1821
|
+
});
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (returnsPrimitive(value)) {
|
|
1826
|
+
out.push({
|
|
1827
|
+
kind: "scalar",
|
|
1828
|
+
path
|
|
1829
|
+
});
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
out.push({
|
|
1833
|
+
kind: "opaque",
|
|
1834
|
+
path,
|
|
1835
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60)
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* `xs.map(cb)` with no literal in the callback: what the callback RETURNS,
|
|
1841
|
+
* once — a function passed by reference (`rows.map(paraLinha)`) is a call to
|
|
1842
|
+
* that function over the rows; an expression body (`(m) => new T(m).toObject()`)
|
|
1843
|
+
* is classified as if it were the value; anything else is one repeating attribute.
|
|
1844
|
+
*/
|
|
1845
|
+
function classifyMapped(value, receiver, path, ctx, out, depth) {
|
|
1846
|
+
const callback = unwrap$1(value.getArguments()[0]);
|
|
1847
|
+
const refs = ctx.followed.get(value);
|
|
1848
|
+
if (callback && Node.isIdentifier(callback) && refs && refs.length > 0) {
|
|
1849
|
+
const rows = [];
|
|
1850
|
+
classify(unwrap$1(receiver), path, ctx, rows, depth + 1);
|
|
1851
|
+
out.push({
|
|
1852
|
+
kind: "call",
|
|
1853
|
+
refs,
|
|
1854
|
+
path,
|
|
1855
|
+
expression: value.getText().replace(/\s+/g, "").slice(0, 60),
|
|
1856
|
+
args: rows.filter((item) => item.kind === "store" || item.kind === "call")
|
|
1857
|
+
});
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const body = callbackValueOf(callback);
|
|
1861
|
+
if (body) {
|
|
1862
|
+
classify(body, path, ctx, out, depth + 1);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
out.push({
|
|
1866
|
+
kind: "scalar",
|
|
1867
|
+
path
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
/** the value a callback hands back: an expression body, or the one `return` of a block */
|
|
1871
|
+
function callbackValueOf(callback) {
|
|
1872
|
+
if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return null;
|
|
1873
|
+
const body = callback.getBody();
|
|
1874
|
+
if (!Node.isBlock(body)) return unwrap$1(body);
|
|
1875
|
+
const returns = body.getStatements().filter(Node.isReturnStatement);
|
|
1876
|
+
return returns.length === 1 ? unwrap$1(returns[0].getExpression()) : null;
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Does this chain read a property (not a method) or hold an expression before
|
|
1880
|
+
* the method is applied? `comunicado.enviadoEm.toISO()` does; `rows.map(f).join()`
|
|
1881
|
+
* does not; a chain rooted at `this` is a service, and does not.
|
|
1882
|
+
*/
|
|
1883
|
+
function readsField(node) {
|
|
1884
|
+
let current = node;
|
|
1885
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1886
|
+
if (Node.isCallExpression(current)) {
|
|
1887
|
+
const inner = current.getExpression();
|
|
1888
|
+
current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (Node.isAwaitExpression(current) || Node.isNonNullExpression(current)) {
|
|
1892
|
+
current = current.getExpression();
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
if (Node.isParenthesizedExpression(current)) {
|
|
1896
|
+
const inner = unwrap$1(current.getExpression());
|
|
1897
|
+
if (!inner) return false;
|
|
1898
|
+
if (Node.isBinaryExpression(inner) || Node.isConditionalExpression(inner)) return true;
|
|
1899
|
+
current = inner;
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return chainRootOf(current) !== "this";
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Is this chain a method (or methods) applied to a plain parameter of the body,
|
|
1909
|
+
* with no field read in between? `rows.map(f).join(s)` is; `noticia.capa?.toISO()`
|
|
1910
|
+
* reads a field first and is not.
|
|
1911
|
+
*/
|
|
1912
|
+
function transformsParameter(callee, body) {
|
|
1913
|
+
let current = callee.getExpression();
|
|
1914
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1915
|
+
if (Node.isCallExpression(current)) {
|
|
1916
|
+
const inner = current.getExpression();
|
|
1917
|
+
current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
1921
|
+
current = current.getExpression();
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return false;
|
|
1925
|
+
if (!Node.isIdentifier(current)) return false;
|
|
1926
|
+
const name = current.getText();
|
|
1927
|
+
return Node.isParametered(body) && body.getParameters().some((p) => Node.isIdentifier(p.getNameNode()) && p.getNameNode().getText() === name);
|
|
1928
|
+
}
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
/** the identifier node a chain is rooted at: `startDate?.toISOString` -> `startDate` */
|
|
1932
|
+
function rootIdentifierOf(node) {
|
|
1933
|
+
let current = node;
|
|
1934
|
+
for (let depth = 0; current && depth < 40; depth++) {
|
|
1935
|
+
if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
|
|
1936
|
+
current = current.getExpression();
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
return Node.isIdentifier(current) ? current : null;
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
/** the call's declared return type, `Promise<…>` unwrapped, is a boolean, string, number or a union of those */
|
|
1944
|
+
function returnsPrimitive(call) {
|
|
1945
|
+
try {
|
|
1946
|
+
let type = call.getReturnType();
|
|
1947
|
+
if (type.getSymbol()?.getName() === "Promise") type = type.getTypeArguments()[0] ?? type;
|
|
1948
|
+
const primitive = (t) => t.isBoolean() || t.isBooleanLiteral() || t.isString() || t.isStringLiteral() || t.isNumber() || t.isNumberLiteral() || t.isEnumLiteral() || t.isNull() || t.isUndefined();
|
|
1949
|
+
if (type.isUnion()) {
|
|
1950
|
+
const members = type.getUnionTypes();
|
|
1951
|
+
return members.length > 0 && members.every(primitive) && !members.every((t) => t.isNull() || t.isUndefined());
|
|
1952
|
+
}
|
|
1953
|
+
return primitive(type);
|
|
1954
|
+
} catch {
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* `x.data`, `resultado.linhas`, `rows[0]`: a part of a value the body holds.
|
|
1960
|
+
*
|
|
1961
|
+
* on a variable bound to a followed call that key of what the call returns
|
|
1962
|
+
* on a store-bound variable the store (`rows[0]`, `.data`) or a field (scalar)
|
|
1963
|
+
* on the validated payload or the request an echo of input — counts on entry
|
|
1964
|
+
* anything else one value
|
|
1965
|
+
*/
|
|
1966
|
+
function classifyAccess(value, path, ctx, out, depth) {
|
|
1967
|
+
const root = rootSymbolOf(value);
|
|
1968
|
+
/**
|
|
1969
|
+
* `x.nome`, `x['nome']`: the key named; `x[papel]`: a key the body computes — one
|
|
1970
|
+
* value, unnamed (`*`); `rows[0]`: one row of the collection, the collection.
|
|
1971
|
+
*/
|
|
1972
|
+
const property = Node.isPropertyAccessExpression(value) ? value.getName() : keyOfElementAccess(value);
|
|
1973
|
+
if (root && ctx.symbols.has(root)) {
|
|
1974
|
+
if (!property || PASSES_THROUGH$1.has(property)) out.push({
|
|
1975
|
+
kind: "store",
|
|
1976
|
+
store: ctx.symbols.get(root),
|
|
1977
|
+
path
|
|
1978
|
+
});
|
|
1979
|
+
else out.push({
|
|
1980
|
+
kind: "scalar",
|
|
1981
|
+
path: path || property.replace(/^\*$/, "")
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (root) {
|
|
1986
|
+
const bound = bindingOf(root, ctx.body);
|
|
1987
|
+
if (bound && Node.isCallExpression(bound.initializer)) {
|
|
1988
|
+
if (SCALAR_PROPS.has(property)) {
|
|
1989
|
+
out.push({
|
|
1990
|
+
kind: "scalar",
|
|
1991
|
+
path
|
|
1992
|
+
});
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
|
|
1997
|
+
* destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
|
|
1998
|
+
*/
|
|
1999
|
+
const own = property && !PASSES_THROUGH$1.has(property) ? property : void 0;
|
|
2000
|
+
const pick = [bound.pick, own].filter(Boolean).join(".") || void 0;
|
|
2001
|
+
if (own && !ctx.followed.has(bound.initializer)) {
|
|
2002
|
+
out.push({
|
|
2003
|
+
kind: "scalar",
|
|
2004
|
+
path
|
|
2005
|
+
});
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
classifyCall(bound.initializer, path, ctx, out, depth + 1, pick);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (bound && Node.isObjectLiteralExpression(bound.initializer) && property) {
|
|
2012
|
+
const picked = bound.initializer.getProperty(property);
|
|
2013
|
+
if (picked && Node.isPropertyAssignment(picked)) {
|
|
2014
|
+
classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (isEchoBinding(root, ctx.body) || root === "request" || root === "params") {
|
|
2019
|
+
out.push({
|
|
2020
|
+
kind: "echo",
|
|
2021
|
+
path
|
|
2022
|
+
});
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
out.push({
|
|
2027
|
+
kind: "scalar",
|
|
2028
|
+
path: path || property.replace(/^\*$/, "")
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
function keyOfElementAccess(value) {
|
|
2032
|
+
const argument = unwrap$1(value.getArgumentExpression());
|
|
2033
|
+
if (!argument || Node.isNumericLiteral(argument)) return "";
|
|
2034
|
+
if (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) return argument.getLiteralValue();
|
|
2035
|
+
return "*";
|
|
2036
|
+
}
|
|
2037
|
+
const join$1 = (prefix, name) => prefix ? `${prefix}.${name}` : name;
|
|
2038
|
+
/** `await x` binds x: the value, not the promise */
|
|
2039
|
+
function unwrapAwait$1(node) {
|
|
2040
|
+
let current = unwrap$1(node);
|
|
2041
|
+
while (current && Node.isAwaitExpression(current)) current = unwrap$1(current.getExpression());
|
|
2042
|
+
return current;
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* How a local name was bound in this body: `const x = …` gives the initializer;
|
|
2046
|
+
* `const { x, y } = …` gives the initializer and the key picked out of it.
|
|
2047
|
+
*/
|
|
2048
|
+
function bindingOf(name, body) {
|
|
2049
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2050
|
+
const nameNode = declaration.getNameNode();
|
|
2051
|
+
if (Node.isIdentifier(nameNode)) {
|
|
2052
|
+
if (nameNode.getText() !== name) continue;
|
|
2053
|
+
const initializer = unwrapAwait$1(declaration.getInitializer());
|
|
2054
|
+
return initializer ? { initializer } : null;
|
|
2055
|
+
}
|
|
2056
|
+
if (Node.isObjectBindingPattern(nameNode)) {
|
|
2057
|
+
const element = nameNode.getElements().find((e) => e.getName() === name);
|
|
2058
|
+
if (!element) continue;
|
|
2059
|
+
const initializer = unwrapAwait$1(declaration.getInitializer());
|
|
2060
|
+
if (!initializer) return null;
|
|
2061
|
+
return {
|
|
2062
|
+
initializer,
|
|
2063
|
+
pick: element.getPropertyNameNode()?.getText() ?? element.getName()
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* A name bound by destructuring the validated payload or the request:
|
|
2071
|
+
* `const { busca } = await request.validateUsing(x)`, `const { id } = params`.
|
|
2072
|
+
*/
|
|
2073
|
+
function isEchoBinding(name, body) {
|
|
2074
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2075
|
+
const binding = declaration.getNameNode();
|
|
2076
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
2077
|
+
if (!binding.getElements().some((element) => element.getName() === name)) continue;
|
|
2078
|
+
const initializer = unwrap$1(declaration.getInitializer());
|
|
2079
|
+
if (!initializer) continue;
|
|
2080
|
+
if (Node.isIdentifier(initializer) && /^(params|request)$/.test(initializer.getText())) return true;
|
|
2081
|
+
if (Node.isCallExpression(initializer)) {
|
|
2082
|
+
const callee = initializer.getExpression();
|
|
2083
|
+
if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(callee.getName())) return true;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
const isRequestLike = (node) => lastSegmentOf(node) === "request";
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/inventory/graph/pages.ts
|
|
2091
|
+
/** array methods whose callback receives one row */
|
|
2092
|
+
const ITERATES_ROWS$1 = new Set([
|
|
2093
|
+
"map",
|
|
2094
|
+
"forEach",
|
|
2095
|
+
"filter",
|
|
2096
|
+
"find",
|
|
2097
|
+
"findLast",
|
|
2098
|
+
"some",
|
|
2099
|
+
"every",
|
|
2100
|
+
"flatMap"
|
|
2101
|
+
]);
|
|
2102
|
+
/** array methods that hand back one row, or the same rows */
|
|
2103
|
+
const SAME_ROWS = new Set([
|
|
2104
|
+
"filter",
|
|
2105
|
+
"slice",
|
|
2106
|
+
"sort",
|
|
2107
|
+
"toSorted",
|
|
2108
|
+
"reverse",
|
|
2109
|
+
"concat",
|
|
2110
|
+
"flat",
|
|
2111
|
+
"find",
|
|
2112
|
+
"findLast",
|
|
2113
|
+
"at"
|
|
2114
|
+
]);
|
|
2115
|
+
/** wrappers whose property hands the rows on */
|
|
2116
|
+
const PASSES_THROUGH = new Set([
|
|
2117
|
+
"data",
|
|
2118
|
+
"rows",
|
|
2119
|
+
"all"
|
|
2120
|
+
]);
|
|
2121
|
+
/** how deep the reader follows a row into child components */
|
|
2122
|
+
const MAX_COMPONENT_DEPTH = 1;
|
|
2123
|
+
function readPages(deliveries, env) {
|
|
2124
|
+
const reading = {
|
|
2125
|
+
columns: /* @__PURE__ */ new Map(),
|
|
2126
|
+
unreadable: /* @__PURE__ */ new Map(),
|
|
2127
|
+
unknownMembers: /* @__PURE__ */ new Map()
|
|
2128
|
+
};
|
|
2129
|
+
if (deliveries.length === 0) return reading;
|
|
2130
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
2131
|
+
for (const delivery of deliveries) {
|
|
2132
|
+
const key = `${delivery.engine}:${delivery.page}`;
|
|
2133
|
+
byPage.set(key, [...byPage.get(key) ?? [], delivery]);
|
|
2134
|
+
}
|
|
2135
|
+
for (const group of byPage.values()) {
|
|
2136
|
+
const { engine, page } = group[0];
|
|
2137
|
+
const candidates = engine === "inertia" ? inertiaPageFiles(env.root, page) : edgeFiles(env.root, page);
|
|
2138
|
+
if (candidates.length !== 1) {
|
|
2139
|
+
const reason = candidates.length === 0 ? `page "${page}" not found under the ${engine === "inertia" ? "pages" : "views"} conventions` : `two files answer to the page "${page}": ${candidates.map((c) => toPosix(c).replace(`${toPosix(env.root)}/`, "")).join(", ")}`;
|
|
2140
|
+
for (const delivery of group) markUnreadable(reading, delivery.store, reason);
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* One prop carrying several stores — a query object whose unreadable result
|
|
2145
|
+
* made every store it read leave under `atuacao` — cannot be told apart by the
|
|
2146
|
+
* page: `atuacao.total` belongs to none of them. Whole, all of them, and said so.
|
|
2147
|
+
*/
|
|
2148
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2149
|
+
for (const d of group) byPath.set(d.path, new Set([...byPath.get(d.path) ?? [], d.store]));
|
|
2150
|
+
const readable = group.filter((d) => {
|
|
2151
|
+
const stores = byPath.get(d.path);
|
|
2152
|
+
if (stores.size === 1) return true;
|
|
2153
|
+
markUnreadable(reading, d.store, `page "${page}": prop \`${d.path || "(props)"}\` carries ${stores.size} stores (${[...stores].join(", ")}) — what the page reads off it belongs to no one of them`);
|
|
2154
|
+
return false;
|
|
2155
|
+
});
|
|
2156
|
+
if (readable.length === 0) continue;
|
|
2157
|
+
const reader = new PageReader(env, reading);
|
|
2158
|
+
if (engine === "inertia") reader.readTsx(candidates[0], readable);
|
|
2159
|
+
else reader.readEdge(candidates[0], readable);
|
|
2160
|
+
/**
|
|
2161
|
+
* The page was read and no column of the store came out of it: either the page
|
|
2162
|
+
* never touches the rows, or it reads members that are not columns — a row the
|
|
2163
|
+
* controller serialised on the way, or computed getters. Whole, and said so.
|
|
2164
|
+
*/
|
|
2165
|
+
for (const { store } of readable) {
|
|
2166
|
+
if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
|
|
2167
|
+
const members = [...reading.unknownMembers.get(store) ?? []];
|
|
2168
|
+
markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}" never reads ${store}`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return reading;
|
|
2172
|
+
}
|
|
2173
|
+
function markUnreadable(reading, store, reason) {
|
|
2174
|
+
if (!reading.unreadable.has(store)) reading.unreadable.set(store, reason);
|
|
2175
|
+
}
|
|
2176
|
+
const SKIPPED_DIRS = new Set([
|
|
2177
|
+
"node_modules",
|
|
2178
|
+
"dist",
|
|
2179
|
+
"build",
|
|
2180
|
+
".git",
|
|
2181
|
+
"coverage",
|
|
2182
|
+
"tmp",
|
|
2183
|
+
".adonisjs"
|
|
2184
|
+
]);
|
|
2185
|
+
/**
|
|
2186
|
+
* `inertia.render('livros/index')` → `inertia/pages/livros/index.tsx` (the default),
|
|
2187
|
+
* `app/<first>/ui/pages/<rest>.tsx` (a domain-module layout), or any `pages/` directory
|
|
2188
|
+
* under the root holding that path. The `resolve` function of the front-end is not
|
|
2189
|
+
* run — a convention is read, a function is not.
|
|
2190
|
+
*/
|
|
2191
|
+
function inertiaPageFiles(root, page) {
|
|
2192
|
+
const found = /* @__PURE__ */ new Set();
|
|
2193
|
+
const [first, ...rest] = page.split("/");
|
|
2194
|
+
if (rest.length > 0) for (const extension of [
|
|
2195
|
+
".tsx",
|
|
2196
|
+
".jsx",
|
|
2197
|
+
".vue",
|
|
2198
|
+
".svelte"
|
|
2199
|
+
]) {
|
|
2200
|
+
const file = join(root, "app", first, "ui", "pages", `${rest.join("/")}${extension}`);
|
|
2201
|
+
if (existsSync(file)) found.add(file);
|
|
2202
|
+
}
|
|
2203
|
+
for (const dir of pagesDirectories(root)) for (const extension of [
|
|
2204
|
+
".tsx",
|
|
2205
|
+
".jsx",
|
|
2206
|
+
".vue",
|
|
2207
|
+
".svelte"
|
|
2208
|
+
]) {
|
|
2209
|
+
const file = join(dir, `${page}${extension}`);
|
|
2210
|
+
if (existsSync(file)) found.add(file);
|
|
2211
|
+
const index = join(dir, page, `index${extension}`);
|
|
2212
|
+
if (existsSync(index)) found.add(index);
|
|
2213
|
+
}
|
|
2214
|
+
return [...found].sort();
|
|
2215
|
+
}
|
|
2216
|
+
let pagesDirsCache = /* @__PURE__ */ new Map();
|
|
2217
|
+
function pagesDirectories(root) {
|
|
2218
|
+
const cached = pagesDirsCache.get(root);
|
|
2219
|
+
if (cached) return cached;
|
|
2220
|
+
const dirs = [];
|
|
2221
|
+
const walk = (dir, depth) => {
|
|
2222
|
+
if (depth > 6) return;
|
|
2223
|
+
let entries;
|
|
2224
|
+
try {
|
|
2225
|
+
entries = readdirSync(dir);
|
|
2226
|
+
} catch {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
for (const entry of entries) {
|
|
2230
|
+
if (SKIPPED_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
2231
|
+
const full = join(dir, entry);
|
|
2232
|
+
let isDirectory = false;
|
|
2233
|
+
try {
|
|
2234
|
+
isDirectory = statSync(full).isDirectory();
|
|
2235
|
+
} catch {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (!isDirectory) continue;
|
|
2239
|
+
if (entry === "pages") dirs.push(full);
|
|
2240
|
+
else walk(full, depth + 1);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
walk(root, 0);
|
|
2244
|
+
pagesDirsCache.set(root, dirs);
|
|
2245
|
+
return dirs;
|
|
2246
|
+
}
|
|
2247
|
+
/** `view.render('catalogo')` → `resources/views/catalogo.edge` */
|
|
2248
|
+
function edgeFiles(root, page) {
|
|
2249
|
+
const file = join(root, "resources", "views", `${page.replace(/\./g, "/")}.edge`);
|
|
2250
|
+
return existsSync(file) ? [file] : [];
|
|
2251
|
+
}
|
|
2252
|
+
/** tsconfig `paths` of the application, for the page's own imports (`~/components/x`) */
|
|
2253
|
+
function aliasesOf(root) {
|
|
2254
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2255
|
+
for (const config of [join(root, "tsconfig.json"), join(root, "inertia", "tsconfig.json")]) {
|
|
2256
|
+
if (!existsSync(config)) continue;
|
|
2257
|
+
try {
|
|
2258
|
+
const text = readFileSync(config, "utf8").replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, "");
|
|
2259
|
+
const paths = JSON.parse(text)?.compilerOptions?.paths;
|
|
2260
|
+
for (const [alias, targets] of Object.entries(paths ?? {})) {
|
|
2261
|
+
const target = targets[0];
|
|
2262
|
+
if (!target) continue;
|
|
2263
|
+
aliases.set(alias.replace(/\*$/, ""), resolve(dirname(config), target.replace(/\*$/, "")));
|
|
2264
|
+
}
|
|
2265
|
+
} catch {}
|
|
2266
|
+
}
|
|
2267
|
+
if (!aliases.has("~/")) aliases.set("~/", join(root, "inertia"));
|
|
2268
|
+
return aliases;
|
|
2269
|
+
}
|
|
2270
|
+
var PageReader = class {
|
|
2271
|
+
aliases;
|
|
2272
|
+
constructor(env, reading) {
|
|
2273
|
+
this.env = env;
|
|
2274
|
+
this.reading = reading;
|
|
2275
|
+
this.aliases = aliasesOf(env.root);
|
|
2276
|
+
}
|
|
2277
|
+
readTsx(file, deliveries) {
|
|
2278
|
+
const source = this.env.project.addSourceFileAtPathIfExists(file);
|
|
2279
|
+
const component = source ? defaultComponentOf(source) : null;
|
|
2280
|
+
if (!source || !component) {
|
|
2281
|
+
for (const d of deliveries) this.fail(d.store, `page "${deliveries[0].page}" has no default-exported component the reader can open`);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
const scope = this.bindProps(component, deliveries.map((d) => ({
|
|
2285
|
+
name: d.path.split(".")[0],
|
|
2286
|
+
entity: {
|
|
2287
|
+
store: d.store,
|
|
2288
|
+
rest: d.path.split(".").slice(1)
|
|
2289
|
+
}
|
|
2290
|
+
})));
|
|
2291
|
+
this.readComponent(component, scope, 0, deliveries[0].page);
|
|
2292
|
+
}
|
|
2293
|
+
/** the component's first parameter: `{ livros }`, `{ livros: rows }`, or `props` */
|
|
2294
|
+
bindProps(component, props) {
|
|
2295
|
+
const scope = /* @__PURE__ */ new Map();
|
|
2296
|
+
if (!Node.isParametered(component)) return scope;
|
|
2297
|
+
const parameter = component.getParameters()[0];
|
|
2298
|
+
if (!parameter) return scope;
|
|
2299
|
+
const nameNode = parameter.getNameNode();
|
|
2300
|
+
if (Node.isObjectBindingPattern(nameNode)) for (const element of nameNode.getElements()) {
|
|
2301
|
+
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
2302
|
+
const prop = props.find((p) => p.name === property);
|
|
2303
|
+
if (prop) scope.set(element.getName(), prop.entity);
|
|
2304
|
+
}
|
|
2305
|
+
else if (Node.isIdentifier(nameNode)) for (const prop of props) scope.set(`${nameNode.getText()}.${prop.name}`, prop.entity);
|
|
2306
|
+
return scope;
|
|
2307
|
+
}
|
|
2308
|
+
readComponent(component, scope, depth, page) {
|
|
2309
|
+
const body = Node.isParametered(component) && "getBody" in component ? component.getBody() ?? component : component;
|
|
2310
|
+
const file = component.getSourceFile();
|
|
2311
|
+
/** the entity an expression holds, recording a column when the expression IS one */
|
|
2312
|
+
const entityOf = (node, visiting = 0) => {
|
|
2313
|
+
if (!node || visiting > 12) return null;
|
|
2314
|
+
let current = node;
|
|
2315
|
+
while (Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current) || Node.isAsExpression(current) || Node.isAwaitExpression(current)) current = current.getExpression();
|
|
2316
|
+
if (Node.isIdentifier(current)) return scope.get(current.getText()) ?? null;
|
|
2317
|
+
if (Node.isPropertyAccessExpression(current)) {
|
|
2318
|
+
const receiver = current.getExpression();
|
|
2319
|
+
if (Node.isIdentifier(receiver)) {
|
|
2320
|
+
const asPath = scope.get(`${receiver.getText()}.${current.getName()}`);
|
|
2321
|
+
if (asPath) return asPath;
|
|
2322
|
+
}
|
|
2323
|
+
const holder = entityOf(receiver, visiting + 1);
|
|
2324
|
+
if (!holder) return null;
|
|
2325
|
+
return this.member(holder, current.getName());
|
|
2326
|
+
}
|
|
2327
|
+
if (Node.isElementAccessExpression(current)) {
|
|
2328
|
+
const holder = entityOf(current.getExpression(), visiting + 1);
|
|
2329
|
+
if (!holder) return null;
|
|
2330
|
+
const argument = current.getArgumentExpression();
|
|
2331
|
+
if (argument && Node.isStringLiteral(argument)) return this.member(holder, argument.getLiteralValue());
|
|
2332
|
+
return holder;
|
|
2333
|
+
}
|
|
2334
|
+
if (Node.isCallExpression(current)) {
|
|
2335
|
+
const callee = current.getExpression();
|
|
2336
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
2337
|
+
const holder = entityOf(callee.getExpression(), visiting + 1);
|
|
2338
|
+
if (holder && holder.rest.length === 0 && SAME_ROWS.has(callee.getName())) return holder;
|
|
2339
|
+
if (holder && holder.rest.length === 0 && ITERATES_ROWS$1.has(callee.getName())) return null;
|
|
2340
|
+
if (holder && holder.rest.length === 0 && callee.getName() === "length") return null;
|
|
2341
|
+
if (holder) return null;
|
|
2342
|
+
}
|
|
2343
|
+
return null;
|
|
2344
|
+
}
|
|
2345
|
+
if (Node.isConditionalExpression(current)) {
|
|
2346
|
+
entityOf(current.getCondition(), visiting + 1);
|
|
2347
|
+
return entityOf(current.getWhenTrue(), visiting + 1) ?? entityOf(current.getWhenFalse(), visiting + 1);
|
|
2348
|
+
}
|
|
2349
|
+
if (Node.isBinaryExpression(current)) {
|
|
2350
|
+
const operator = current.getOperatorToken().getKind();
|
|
2351
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken) {
|
|
2352
|
+
entityOf(current.getLeft(), visiting + 1);
|
|
2353
|
+
return entityOf(current.getRight(), visiting + 1);
|
|
2354
|
+
}
|
|
2355
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) return entityOf(current.getLeft(), visiting + 1) ?? entityOf(current.getRight(), visiting + 1);
|
|
2356
|
+
entityOf(current.getLeft(), visiting + 1);
|
|
2357
|
+
entityOf(current.getRight(), visiting + 1);
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
if (Node.isTemplateExpression(current)) {
|
|
2361
|
+
for (const span of current.getTemplateSpans()) entityOf(span.getExpression(), visiting + 1);
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
return null;
|
|
2365
|
+
};
|
|
2366
|
+
const escape = (entity, reason) => {
|
|
2367
|
+
const store = entity.rest.length === 0 ? entity.store : entity.store;
|
|
2368
|
+
this.fail(store, `page "${page}": ${reason}`);
|
|
2369
|
+
};
|
|
2370
|
+
body.forEachDescendant((node) => {
|
|
2371
|
+
if (Node.isVariableDeclaration(node)) {
|
|
2372
|
+
const initializer = node.getInitializer();
|
|
2373
|
+
const nameNode = node.getNameNode();
|
|
2374
|
+
if (!initializer) return;
|
|
2375
|
+
const entity = entityOf(initializer);
|
|
2376
|
+
if (!entity) return;
|
|
2377
|
+
if (Node.isIdentifier(nameNode)) scope.set(nameNode.getText(), entity);
|
|
2378
|
+
else if (Node.isObjectBindingPattern(nameNode)) for (const element of nameNode.getElements()) {
|
|
2379
|
+
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
2380
|
+
const member = this.member(entity, property);
|
|
2381
|
+
if (member) scope.set(element.getName(), member);
|
|
2382
|
+
}
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (Node.isCallExpression(node)) {
|
|
2386
|
+
const callee = node.getExpression();
|
|
2387
|
+
if (Node.isPropertyAccessExpression(callee) && ITERATES_ROWS$1.has(callee.getName())) {
|
|
2388
|
+
const holder = entityOf(callee.getExpression());
|
|
2389
|
+
const callback = node.getArguments()[0];
|
|
2390
|
+
if (holder && holder.rest.length === 0 && callback && (Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) {
|
|
2391
|
+
const parameter = callback.getParameters()[0]?.getNameNode();
|
|
2392
|
+
if (parameter && Node.isIdentifier(parameter)) scope.set(parameter.getText(), holder);
|
|
2393
|
+
}
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
for (const argument of node.getArguments()) {
|
|
2397
|
+
const entity = entityOf(argument);
|
|
2398
|
+
if (entity && !Node.isPropertyAccessExpression(unwrapParens(argument))) escape(entity, `\`${node.getExpression().getText()}(…)\` receives the rows and the reader cannot follow a function`);
|
|
2399
|
+
else if (entity) escape(entity, `\`${node.getExpression().getText()}(…)\` receives the rows and the reader cannot follow a function`);
|
|
2400
|
+
}
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (Node.isForOfStatement(node)) {
|
|
2404
|
+
const declared = node.getInitializer();
|
|
2405
|
+
const entity = entityOf(node.getExpression());
|
|
2406
|
+
if (entity && Node.isVariableDeclarationList(declared)) {
|
|
2407
|
+
const nameNode = declared.getDeclarations()[0]?.getNameNode();
|
|
2408
|
+
if (nameNode && Node.isIdentifier(nameNode)) scope.set(nameNode.getText(), entity);
|
|
2409
|
+
}
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (Node.isSpreadAssignment(node) || Node.isJsxSpreadAttribute(node) || Node.isSpreadElement(node)) {
|
|
2413
|
+
const entity = entityOf(node.getExpression());
|
|
2414
|
+
if (entity) escape(entity, "a spread hands the whole row on");
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (Node.isJsxExpression(node)) {
|
|
2418
|
+
const expression = node.getExpression();
|
|
2419
|
+
const parent = node.getParent();
|
|
2420
|
+
if (!expression || Node.isJsxAttribute(parent)) return;
|
|
2421
|
+
const entity = entityOf(expression);
|
|
2422
|
+
if (entity) escape(entity, `\`{${expression.getText().replace(/\s+/g, " ").slice(0, 60)}}\` renders the row itself`);
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
if (Node.isJsxAttribute(node)) {
|
|
2426
|
+
const initializer = node.getInitializer();
|
|
2427
|
+
const entity = entityOf(initializer && Node.isJsxExpression(initializer) ? initializer.getExpression() : void 0);
|
|
2428
|
+
if (!entity) return;
|
|
2429
|
+
const element = node.getFirstAncestor((n) => Node.isJsxOpeningElement(n) || Node.isJsxSelfClosingElement(n));
|
|
2430
|
+
const tag = element && (Node.isJsxOpeningElement(element) || Node.isJsxSelfClosingElement(element)) ? element.getTagNameNode().getText() : "";
|
|
2431
|
+
if (!/^[A-Z]/.test(tag)) return;
|
|
2432
|
+
if (depth >= MAX_COMPONENT_DEPTH) {
|
|
2433
|
+
escape(entity, `\`<${tag} ${node.getNameNode().getText()}={…} />\` is a second level of components; the reader follows one`);
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const child = this.componentOf(tag, file);
|
|
2437
|
+
if (!child) {
|
|
2438
|
+
escape(entity, `\`<${tag} />\` is not a component of the application the reader can open`);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
const childScope = this.bindProps(child, [{
|
|
2442
|
+
name: node.getNameNode().getText(),
|
|
2443
|
+
entity
|
|
2444
|
+
}]);
|
|
2445
|
+
this.readComponent(child, childScope, depth + 1, page);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (Node.isReturnStatement(node) && depth === 0) {
|
|
2449
|
+
const expression = node.getExpression();
|
|
2450
|
+
if (expression && !Node.isJsxElement(expression) && !Node.isJsxFragment(expression) && !Node.isJsxSelfClosingElement(expression) && !Node.isParenthesizedExpression(expression)) {
|
|
2451
|
+
const entity = entityOf(expression);
|
|
2452
|
+
if (entity) escape(entity, `\`return ${expression.getText().replace(/\s+/g, " ").slice(0, 60)}\` hands the rows back as they came`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
/** `entity.name`: one column (recorded), a relation (the target), a wrapper key, or nothing */
|
|
2458
|
+
member(entity, name) {
|
|
2459
|
+
if (entity.rest.length > 0) return entity.rest[0] === name ? {
|
|
2460
|
+
store: entity.store,
|
|
2461
|
+
rest: entity.rest.slice(1)
|
|
2462
|
+
} : null;
|
|
2463
|
+
if (PASSES_THROUGH.has(name)) return entity;
|
|
2464
|
+
if (this.env.stores.get(entity.store)?.attributes.some((a) => a.name === name)) {
|
|
2465
|
+
this.column(entity.store, name);
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
const target = this.env.relations.get(entity.store)?.[name];
|
|
2469
|
+
if (target) return {
|
|
2470
|
+
store: target,
|
|
2471
|
+
rest: []
|
|
2472
|
+
};
|
|
2473
|
+
if (![
|
|
2474
|
+
"length",
|
|
2475
|
+
"id",
|
|
2476
|
+
"map",
|
|
2477
|
+
"filter",
|
|
2478
|
+
"find",
|
|
2479
|
+
"some",
|
|
2480
|
+
"every",
|
|
2481
|
+
"forEach",
|
|
2482
|
+
"slice",
|
|
2483
|
+
"sort"
|
|
2484
|
+
].includes(name)) {
|
|
2485
|
+
const members = this.reading.unknownMembers.get(entity.store) ?? /* @__PURE__ */ new Set();
|
|
2486
|
+
members.add(name);
|
|
2487
|
+
this.reading.unknownMembers.set(entity.store, members);
|
|
2488
|
+
}
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
/** the component a JSX tag names, in this file or through its imports (tsconfig paths, relative) */
|
|
2492
|
+
componentOf(tag, file) {
|
|
2493
|
+
const local = file.getFunction(tag) ?? functionVariable(file, tag);
|
|
2494
|
+
if (local) return local;
|
|
2495
|
+
for (const declaration of file.getImportDeclarations()) {
|
|
2496
|
+
if (![declaration.getDefaultImport()?.getText(), ...declaration.getNamedImports().map((n) => n.getAliasNode()?.getText() ?? n.getName())].includes(tag)) continue;
|
|
2497
|
+
const target = this.resolveImport(declaration.getModuleSpecifierValue(), file.getFilePath());
|
|
2498
|
+
if (!target) return null;
|
|
2499
|
+
const source = this.env.project.addSourceFileAtPathIfExists(target);
|
|
2500
|
+
if (!source) return null;
|
|
2501
|
+
const exported = declaration.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === tag)?.getName() ?? tag;
|
|
2502
|
+
return source.getFunction(exported) ?? functionVariable(source, exported) ?? (declaration.getDefaultImport()?.getText() === tag ? defaultComponentOf(source) : null);
|
|
2503
|
+
}
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
resolveImport(specifier, from) {
|
|
2507
|
+
let base = this.env.resolveSpecifier(specifier);
|
|
2508
|
+
if (base) base = base.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
2509
|
+
if (!base && specifier.startsWith(".")) base = resolve(dirname(from), specifier);
|
|
2510
|
+
else for (const [alias, dir] of this.aliases) if (specifier.startsWith(alias)) base = join(dir, specifier.slice(alias.length));
|
|
2511
|
+
if (!base) return null;
|
|
2512
|
+
for (const candidate of [
|
|
2513
|
+
base,
|
|
2514
|
+
`${base}.tsx`,
|
|
2515
|
+
`${base}.ts`,
|
|
2516
|
+
`${base}.jsx`,
|
|
2517
|
+
join(base, "index.tsx"),
|
|
2518
|
+
join(base, "index.ts")
|
|
2519
|
+
]) if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
readEdge(file, deliveries, depth = 0, scope) {
|
|
2523
|
+
const text = readFileSync(file, "utf8");
|
|
2524
|
+
const page = deliveries[0].page;
|
|
2525
|
+
const bound = scope ?? /* @__PURE__ */ new Map();
|
|
2526
|
+
if (!scope) for (const d of deliveries) bound.set(d.path.split(".")[0], {
|
|
2527
|
+
store: d.store,
|
|
2528
|
+
rest: d.path.split(".").slice(1)
|
|
2529
|
+
});
|
|
2530
|
+
const entityOf = (expression) => {
|
|
2531
|
+
const parts = expression.trim().replace(/\?\./g, ".").split(".");
|
|
2532
|
+
let entity = bound.get(parts[0]) ?? null;
|
|
2533
|
+
for (const part of parts.slice(1)) {
|
|
2534
|
+
if (!entity) return null;
|
|
2535
|
+
entity = this.member(entity, part.replace(/\(.*$/, ""));
|
|
2536
|
+
}
|
|
2537
|
+
return entity;
|
|
2538
|
+
};
|
|
2539
|
+
const read = (expression) => {
|
|
2540
|
+
for (const chain of expression.match(/[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+/g) ?? []) {
|
|
2541
|
+
const entity = entityOf(chain);
|
|
2542
|
+
if (entity) this.fail(entity.store, `page "${page}": \`${chain}\` is a row handed on, not a column`);
|
|
2543
|
+
}
|
|
2544
|
+
const bare = expression.trim();
|
|
2545
|
+
if (/^[A-Za-z_$][\w$]*$/.test(bare) && bound.has(bare)) this.fail(bound.get(bare).store, `page "${page}": \`{{ ${bare} }}\` renders the rows themselves`);
|
|
2546
|
+
};
|
|
2547
|
+
for (const match of text.matchAll(/@each\(\s*\(?\s*([A-Za-z_$][\w$]*)(?:\s*,\s*[A-Za-z_$][\w$]*)?\s*\)?\s+in\s+([^)]+)\)/g)) {
|
|
2548
|
+
const entity = entityOf(match[2]);
|
|
2549
|
+
if (entity) bound.set(match[1], entity);
|
|
2550
|
+
}
|
|
2551
|
+
for (const match of text.matchAll(/\{\{\{?\s*([\s\S]*?)\s*\}?\}\}/g)) read(match[1]);
|
|
2552
|
+
for (const match of text.matchAll(/@(?:if|elseif|unless)\(([^)]*)\)/g)) read(match[1]);
|
|
2553
|
+
for (const match of text.matchAll(/@!?(?:include|component|layout)\(\s*['"]([^'"]+)['"]/g)) {
|
|
2554
|
+
const included = join(this.env.root, "resources", "views", `${match[1].replace(/\./g, "/")}.edge`);
|
|
2555
|
+
if (!existsSync(included)) continue;
|
|
2556
|
+
if (depth >= MAX_COMPONENT_DEPTH) {
|
|
2557
|
+
for (const entity of bound.values()) this.fail(entity.store, `page "${page}": \`@${match[0].slice(1).split("(")[0]}('${match[1]}')\` is a second level; the reader follows one`);
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
this.readEdge(included, deliveries, depth + 1, new Map(bound));
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
column(store, name) {
|
|
2564
|
+
const columns = this.reading.columns.get(store) ?? /* @__PURE__ */ new Set();
|
|
2565
|
+
columns.add(name);
|
|
2566
|
+
this.reading.columns.set(store, columns);
|
|
2567
|
+
}
|
|
2568
|
+
fail(store, reason) {
|
|
2569
|
+
markUnreadable(this.reading, store, reason);
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
function unwrapParens(node) {
|
|
2573
|
+
let current = node;
|
|
2574
|
+
while (Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) current = current.getExpression();
|
|
2575
|
+
return current;
|
|
2576
|
+
}
|
|
2577
|
+
/** `export default function Page(…)`, or `export default Page` naming a function or an arrow */
|
|
2578
|
+
function defaultComponentOf(file) {
|
|
2579
|
+
const declared = file.getFunctions().find((f) => f.isDefaultExport());
|
|
2580
|
+
if (declared) return declared;
|
|
2581
|
+
for (const assignment of file.getExportAssignments()) {
|
|
2582
|
+
const expression = assignment.getExpression();
|
|
2583
|
+
if (Node.isIdentifier(expression)) return file.getFunction(expression.getText()) ?? functionVariable(file, expression.getText());
|
|
2584
|
+
if (Node.isArrowFunction(expression) || Node.isFunctionExpression(expression)) return expression;
|
|
2585
|
+
if (Node.isCallExpression(expression)) {
|
|
2586
|
+
const inner = expression.getArguments()[0];
|
|
2587
|
+
if (inner && Node.isIdentifier(inner)) return file.getFunction(inner.getText()) ?? functionVariable(file, inner.getText());
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
/** `const Page = (props) => …` / `function (…) {}` */
|
|
2593
|
+
function functionVariable(file, name) {
|
|
2594
|
+
const initializer = file.getVariableDeclaration(name)?.getInitializer();
|
|
2595
|
+
return initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) ? initializer : null;
|
|
2596
|
+
}
|
|
2597
|
+
//#endregion
|
|
1242
2598
|
//#region src/inventory/graph/call_graph.ts
|
|
1243
2599
|
/**
|
|
1244
2600
|
* Fields declared by the validators used in this body.
|
|
@@ -1271,7 +2627,7 @@ function validatorFieldsIn(body, file, app) {
|
|
|
1271
2627
|
* validator and is usually not exported, so looking for it where the call site
|
|
1272
2628
|
* is finds nothing — which is how five fields stayed invisible.
|
|
1273
2629
|
*/
|
|
1274
|
-
const { leaves, opaque: unreadable } = leavesOf(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
|
|
2630
|
+
const { leaves, opaque: unreadable } = leavesOf$1(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
|
|
1275
2631
|
const isOpaque = new Set(unreadable);
|
|
1276
2632
|
for (const leaf of leaves) {
|
|
1277
2633
|
const field = `${name}.${leaf}`;
|
|
@@ -1414,7 +2770,7 @@ function calleeOwner(call) {
|
|
|
1414
2770
|
const owner = callee.getExpression();
|
|
1415
2771
|
return Node.isPropertyAccessExpression(owner) ? owner.getName() : void 0;
|
|
1416
2772
|
}
|
|
1417
|
-
function leavesOf(node, resolveRef) {
|
|
2773
|
+
function leavesOf$1(node, resolveRef) {
|
|
1418
2774
|
const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1419
2775
|
if (!object) return {
|
|
1420
2776
|
leaves: [],
|
|
@@ -1593,23 +2949,196 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1593
2949
|
factsCache.set(key, facts);
|
|
1594
2950
|
return facts;
|
|
1595
2951
|
};
|
|
2952
|
+
/** store name by the posix path of the model that declares it */
|
|
2953
|
+
const storeByFile = /* @__PURE__ */ new Map();
|
|
2954
|
+
for (const [name, store] of storesByName) storeByFile.set(toPosix(store.provenance.file), name);
|
|
2955
|
+
/**
|
|
2956
|
+
* The guard's user: `auth.user`, `auth.getUserOrFail()` are rows of the model
|
|
2957
|
+
* `config/auth.ts` names in its provider (`model: () => import('#models/user')`).
|
|
2958
|
+
* Read, never assumed — an application with no such config binds nothing.
|
|
2959
|
+
*/
|
|
2960
|
+
const authUserStore = (() => {
|
|
2961
|
+
const config = project.addSourceFileAtPathIfExists(`${toPosix(app.root)}/config/auth.ts`);
|
|
2962
|
+
if (!config) return null;
|
|
2963
|
+
for (const call of config.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2964
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
2965
|
+
if (call.getFirstAncestorByKind(SyntaxKind.PropertyAssignment)?.getName() !== "model") continue;
|
|
2966
|
+
const specifier = call.getArguments()[0];
|
|
2967
|
+
if (!specifier || !Node.isStringLiteral(specifier)) continue;
|
|
2968
|
+
const target = app.resolveSpecifier(specifier.getLiteralValue());
|
|
2969
|
+
const store = target ? storeByFile.get(toPosix(target)) : void 0;
|
|
2970
|
+
if (store) return store;
|
|
2971
|
+
}
|
|
2972
|
+
return null;
|
|
2973
|
+
})();
|
|
2974
|
+
/**
|
|
2975
|
+
* What a body's locals ARE — plan 0.8 §A. `storeSymbolsFor` binds what the
|
|
2976
|
+
* parameters and the static imports say; this pass, run in source order before
|
|
2977
|
+
* the accesses are detected, binds what the body's own statements say:
|
|
2978
|
+
*
|
|
2979
|
+
* const s = await this.sessoes.ativa(d) the followed body's return type,
|
|
2980
|
+
* or its returns when unannotated
|
|
2981
|
+
* const n = id ? await N.find(id) : new N() a conditional, both branches N
|
|
2982
|
+
* const p = (await P.first()) ?? new P() a default, both operands P
|
|
2983
|
+
* const u = auth.getUserOrFail() the guard's model, per config/auth.ts
|
|
2984
|
+
* const pasta = documento.pasta a relation read off a loaded row
|
|
2985
|
+
* for (const d of pasta.documentos) a row of the relation's target
|
|
2986
|
+
* rows.map(async (d) => …) the callback's parameter, a row
|
|
2987
|
+
*
|
|
2988
|
+
* Every one is a reading of what the code declares; none is a guess. A `save()`
|
|
2989
|
+
* on a local none of this can type is reported as unresolved — not dropped.
|
|
2990
|
+
*/
|
|
2991
|
+
function bindLocals(body, symbols, context, strategies) {
|
|
2992
|
+
const returnedStoreOf = (call, from = context, depth = 0) => {
|
|
2993
|
+
if (depth > 2) return null;
|
|
2994
|
+
const resolved = resolveCall(call, from, strategies);
|
|
2995
|
+
if (!resolved || resolved.refs.length === 0) return null;
|
|
2996
|
+
const found = /* @__PURE__ */ new Set();
|
|
2997
|
+
for (const target of resolved.refs) {
|
|
2998
|
+
const source = sourceFile(target.file);
|
|
2999
|
+
const resolvedBody = source ? findBody(source, target) : null;
|
|
3000
|
+
if (!resolvedBody) continue;
|
|
3001
|
+
const annotation = Node.isReturnTyped(resolvedBody) ? resolvedBody.getReturnTypeNode()?.getText() : void 0;
|
|
3002
|
+
if (annotation) {
|
|
3003
|
+
const store = storeNamedBy(annotation, storesByName);
|
|
3004
|
+
if (store) found.add(store);
|
|
3005
|
+
else return null;
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
const own = storeSymbolsFor(resolvedBody, source, app, storesByName, relationsByStore);
|
|
3009
|
+
const returns = resolvedBody.getDescendantsOfKind(SyntaxKind.ReturnStatement).filter((r) => r.getFirstAncestor((n) => Node.isMethodDeclaration(n) || Node.isFunctionDeclaration(n) || Node.isArrowFunction(n) || Node.isFunctionExpression(n)) === resolvedBody);
|
|
3010
|
+
if (returns.length === 0) return null;
|
|
3011
|
+
for (const statement of returns) {
|
|
3012
|
+
const value = statement.getExpression();
|
|
3013
|
+
if (!value) return null;
|
|
3014
|
+
const unwrapped = unwrapAwait(value);
|
|
3015
|
+
if (unwrapped.getKind() === SyntaxKind.NullKeyword) continue;
|
|
3016
|
+
if (Node.isCallExpression(unwrapped) && !detectAccess(unwrapped, own, relationsByStore)) {
|
|
3017
|
+
const store = returnedStoreOf(unwrapped, {
|
|
3018
|
+
...context,
|
|
3019
|
+
file: source,
|
|
3020
|
+
...importsFor(source),
|
|
3021
|
+
injected: injectedFor(resolvedBody.getFirstAncestorByKind(SyntaxKind.ClassDeclaration), source, app)
|
|
3022
|
+
}, depth + 1);
|
|
3023
|
+
if (!store) return null;
|
|
3024
|
+
found.add(store);
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
const store = Node.isNewExpression(unwrapped) ? storesByName.has(unwrapped.getExpression().getText()) ? unwrapped.getExpression().getText() : null : own.get(rootSymbolOf(unwrapped) ?? "") ?? (storesByName.has(rootSymbolOf(unwrapped) ?? "") ? rootSymbolOf(unwrapped) : null);
|
|
3028
|
+
if (!store) return null;
|
|
3029
|
+
found.add(store);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return found.size === 1 ? [...found][0] : null;
|
|
3033
|
+
};
|
|
3034
|
+
const isAuthUser = (node) => {
|
|
3035
|
+
if (!authUserStore) return false;
|
|
3036
|
+
const chain = Node.isCallExpression(node) ? node.getExpression() : node;
|
|
3037
|
+
if (!Node.isPropertyAccessExpression(chain)) return false;
|
|
3038
|
+
if (rootSymbolOf(chain) !== "auth" && lastSegmentText(chain.getExpression()) !== "auth") return false;
|
|
3039
|
+
const last = chain.getName();
|
|
3040
|
+
return last === "user" || last === "getUserOrFail" || last === "authenticate";
|
|
3041
|
+
};
|
|
3042
|
+
/** the store a value is rows (or one row) of, by what the code says — or nothing */
|
|
3043
|
+
const storeOfValue = (value, depth = 0) => {
|
|
3044
|
+
if (!value || depth > 6) return null;
|
|
3045
|
+
const node = unwrapAwait(value);
|
|
3046
|
+
if (node.getKind() === SyntaxKind.NullKeyword) return null;
|
|
3047
|
+
if (Node.isIdentifier(node)) {
|
|
3048
|
+
if (node.getText() === "undefined") return null;
|
|
3049
|
+
return symbols.get(node.getText()) ?? null;
|
|
3050
|
+
}
|
|
3051
|
+
if (Node.isNewExpression(node)) {
|
|
3052
|
+
const name = node.getExpression().getText();
|
|
3053
|
+
return storesByName.has(name) ? name : null;
|
|
3054
|
+
}
|
|
3055
|
+
if (Node.isConditionalExpression(node)) return sameStore(storeOfValue(node.getWhenTrue(), depth + 1), storeOfValue(node.getWhenFalse(), depth + 1), node.getWhenTrue(), node.getWhenFalse());
|
|
3056
|
+
if (Node.isBinaryExpression(node)) {
|
|
3057
|
+
const operator = node.getOperatorToken().getKind();
|
|
3058
|
+
if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) return sameStore(storeOfValue(node.getLeft(), depth + 1), storeOfValue(node.getRight(), depth + 1), node.getLeft(), node.getRight());
|
|
3059
|
+
return null;
|
|
3060
|
+
}
|
|
3061
|
+
if (Node.isElementAccessExpression(node)) return storeOfValue(node.getExpression(), depth + 1);
|
|
3062
|
+
if (isAuthUser(node)) return authUserStore;
|
|
3063
|
+
if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
|
|
3064
|
+
if (Node.isCallExpression(node)) {
|
|
3065
|
+
const access = detectAccess(node, symbols, relationsByStore);
|
|
3066
|
+
if (access) return access.method === "related" && access.viaRelation ? access.viaRelation : access.store;
|
|
3067
|
+
const callee = node.getExpression();
|
|
3068
|
+
if (Node.isPropertyAccessExpression(callee) && ONE_OF_ROWS.has(callee.getName())) return storeOfValue(callee.getExpression(), depth + 1);
|
|
3069
|
+
return returnedStoreOf(node);
|
|
3070
|
+
}
|
|
3071
|
+
return null;
|
|
3072
|
+
};
|
|
3073
|
+
const isNullish = (node) => {
|
|
3074
|
+
const inner = unwrapAwait(node);
|
|
3075
|
+
return inner.getKind() === SyntaxKind.NullKeyword || Node.isIdentifier(inner) && inner.getText() === "undefined";
|
|
3076
|
+
};
|
|
3077
|
+
const sameStore = (a, b, left, right) => {
|
|
3078
|
+
if (a && b) return a === b ? a : null;
|
|
3079
|
+
if (a && isNullish(right)) return a;
|
|
3080
|
+
if (b && isNullish(left)) return b;
|
|
3081
|
+
return null;
|
|
3082
|
+
};
|
|
3083
|
+
body.forEachDescendant((node) => {
|
|
3084
|
+
if (Node.isVariableDeclaration(node)) {
|
|
3085
|
+
const initializer = node.getInitializer();
|
|
3086
|
+
const nameNode = node.getNameNode();
|
|
3087
|
+
if (!initializer) return;
|
|
3088
|
+
if (Node.isIdentifier(nameNode)) {
|
|
3089
|
+
if (symbols.has(nameNode.getText())) return;
|
|
3090
|
+
const store = storeOfValue(initializer);
|
|
3091
|
+
if (store) symbols.set(nameNode.getText(), store);
|
|
3092
|
+
}
|
|
3093
|
+
return;
|
|
3094
|
+
}
|
|
3095
|
+
if (Node.isForOfStatement(node)) {
|
|
3096
|
+
const declared = node.getInitializer();
|
|
3097
|
+
if (!Node.isVariableDeclarationList(declared)) return;
|
|
3098
|
+
const nameNode = declared.getDeclarations()[0]?.getNameNode();
|
|
3099
|
+
if (!nameNode || !Node.isIdentifier(nameNode) || symbols.has(nameNode.getText())) return;
|
|
3100
|
+
const store = storeOfValue(node.getExpression());
|
|
3101
|
+
if (store) symbols.set(nameNode.getText(), store);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (Node.isCallExpression(node)) {
|
|
3105
|
+
const callee = node.getExpression();
|
|
3106
|
+
if (!Node.isPropertyAccessExpression(callee) || !ITERATES_ROWS.has(callee.getName())) return;
|
|
3107
|
+
const callback = node.getArguments()[0];
|
|
3108
|
+
if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return;
|
|
3109
|
+
const parameter = callback.getParameters()[0]?.getNameNode();
|
|
3110
|
+
if (!parameter || !Node.isIdentifier(parameter) || symbols.has(parameter.getText())) return;
|
|
3111
|
+
const store = storeOfValue(callee.getExpression());
|
|
3112
|
+
if (store) symbols.set(parameter.getText(), store);
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
1596
3116
|
function computeFacts(ref) {
|
|
1597
3117
|
const file = sourceFile(ref.file);
|
|
1598
3118
|
if (!file) return null;
|
|
1599
3119
|
const body = findBody(file, ref);
|
|
1600
3120
|
if (!body) return null;
|
|
1601
3121
|
const { imports, exportedAs } = importsFor(file);
|
|
3122
|
+
/** locals imported from packages, not from the application: their objects are not stores */
|
|
3123
|
+
const packageImports = /* @__PURE__ */ new Set();
|
|
3124
|
+
for (const declaration of file.getImportDeclarations()) {
|
|
3125
|
+
if (app.resolveSpecifier(declaration.getModuleSpecifierValue())) continue;
|
|
3126
|
+
const defaultImport = declaration.getDefaultImport()?.getText();
|
|
3127
|
+
if (defaultImport) packageImports.add(defaultImport);
|
|
3128
|
+
for (const named of declaration.getNamedImports()) packageImports.add(named.getAliasNode()?.getText() ?? named.getName());
|
|
3129
|
+
}
|
|
1602
3130
|
/** the class this body belongs to: how `this.something` resolves */
|
|
1603
3131
|
const owner = body.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
1604
3132
|
const injected = injectedFor(owner, file, app);
|
|
1605
|
-
const symbols = storeSymbolsFor(body, file, app, storesByName);
|
|
3133
|
+
const symbols = storeSymbolsFor(body, file, app, storesByName, relationsByStore);
|
|
1606
3134
|
const accesses = [];
|
|
1607
3135
|
const followUps = [];
|
|
1608
3136
|
const unresolved = [];
|
|
1609
3137
|
const reads = [];
|
|
1610
|
-
/** calls a strategy claimed: a nested transformer's keys arrive through its body */
|
|
1611
|
-
const followedCalls = /* @__PURE__ */ new
|
|
3138
|
+
/** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
|
|
3139
|
+
const followedCalls = /* @__PURE__ */ new Map();
|
|
1612
3140
|
const validator = validatorFieldsIn(body, file, app);
|
|
3141
|
+
const commandFields = owner && isCommandClass(owner) ? commandFieldsOf(owner) : [];
|
|
1613
3142
|
const request = requestFieldsIn(body);
|
|
1614
3143
|
const context = {
|
|
1615
3144
|
file,
|
|
@@ -1622,6 +3151,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1622
3151
|
resolveSpecifier: app.resolveSpecifier,
|
|
1623
3152
|
sourceFile
|
|
1624
3153
|
};
|
|
3154
|
+
bindLocals(body, symbols, context, resolvers);
|
|
1625
3155
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1626
3156
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
1627
3157
|
if (access) {
|
|
@@ -1652,10 +3182,15 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1652
3182
|
if (access.mode === "read") {
|
|
1653
3183
|
const chain = chainShapeOf(call);
|
|
1654
3184
|
const shape = chain.aggregate ? "aggregate" : chain.selected.length > 0 ? "select" : "whole";
|
|
3185
|
+
/**
|
|
3186
|
+
* The select list survives an aggregate shape: `select('categoria').count()`
|
|
3187
|
+
* with a GROUP BY leaves the grouped column as well as the count. Dropping
|
|
3188
|
+
* it left a summary at 1 DET.
|
|
3189
|
+
*/
|
|
1655
3190
|
const read = (store, how, via) => reads.push({
|
|
1656
3191
|
store,
|
|
1657
3192
|
shape: how,
|
|
1658
|
-
columns: how === "
|
|
3193
|
+
columns: how === "whole" ? [] : chain.selected,
|
|
1659
3194
|
...via ? { via } : {}
|
|
1660
3195
|
});
|
|
1661
3196
|
if (access.method === "related" && access.viaRelation) read(access.viaRelation, shape);
|
|
@@ -1709,7 +3244,22 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1709
3244
|
by: resolved.by,
|
|
1710
3245
|
technical
|
|
1711
3246
|
});
|
|
1712
|
-
followedCalls.
|
|
3247
|
+
followedCalls.set(call, resolved.refs);
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* `alvo.save()` on a receiver nobody could type. Not a write the count can
|
|
3252
|
+
* attribute — and not silence either: the reviewed application had six EIs
|
|
3253
|
+
* counted as EOs under a coverage of 99.5%, because a write on an unknown
|
|
3254
|
+
* local was dropped without a word. It lowers coverage and is named.
|
|
3255
|
+
*/
|
|
3256
|
+
if (isUnreadableWrite(call, symbols, imports, packageImports, body) && !isNoise(call, owner)) {
|
|
3257
|
+
unresolved.push({
|
|
3258
|
+
file: ref.file,
|
|
3259
|
+
line: call.getStartLineNumber(),
|
|
3260
|
+
expression: call.getExpression().getText().replace(/\s+/g, ""),
|
|
3261
|
+
reason: "write on a receiver whose type the analysis cannot read"
|
|
3262
|
+
});
|
|
1713
3263
|
continue;
|
|
1714
3264
|
}
|
|
1715
3265
|
if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
|
|
@@ -1723,12 +3273,21 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1723
3273
|
* Read after the loop: whether a key holds a nested transformer is known only
|
|
1724
3274
|
* once the strategies have said which calls they follow.
|
|
1725
3275
|
*/
|
|
1726
|
-
const
|
|
3276
|
+
const followed = (c) => followedCalls.has(c);
|
|
3277
|
+
const output = outputFieldsIn(body, owner, storesByName, followed);
|
|
3278
|
+
const deliveries = deliveriesIn({
|
|
3279
|
+
body,
|
|
3280
|
+
file,
|
|
3281
|
+
symbols,
|
|
3282
|
+
relations: relationsByStore,
|
|
3283
|
+
followed: followedCalls
|
|
3284
|
+
});
|
|
1727
3285
|
return {
|
|
1728
3286
|
accesses,
|
|
1729
3287
|
followUps,
|
|
1730
3288
|
unresolved,
|
|
1731
3289
|
validators: validator.fields,
|
|
3290
|
+
commandFields,
|
|
1732
3291
|
opaqueValidators: validator.opaque,
|
|
1733
3292
|
requestFields: request.fields,
|
|
1734
3293
|
opaqueRequest: request.opaque,
|
|
@@ -1736,6 +3295,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1736
3295
|
opaqueOutputs: output.opaqueOutputs,
|
|
1737
3296
|
transformed: output.resource,
|
|
1738
3297
|
reads,
|
|
3298
|
+
deliveries,
|
|
1739
3299
|
bodyHash: hashOf(body)
|
|
1740
3300
|
};
|
|
1741
3301
|
}
|
|
@@ -1781,7 +3341,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1781
3341
|
*/
|
|
1782
3342
|
if (!isApplicationCode(app.root, file.getFilePath())) {
|
|
1783
3343
|
if (isSeeder(app.root, file.getFilePath())) {
|
|
1784
|
-
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
3344
|
+
const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
|
|
1785
3345
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1786
3346
|
const access = symbols.size > 0 ? detectAccess(call, symbols, relationsByStore) : null;
|
|
1787
3347
|
if (access?.mode !== "write") continue;
|
|
@@ -1791,7 +3351,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1791
3351
|
}
|
|
1792
3352
|
continue;
|
|
1793
3353
|
}
|
|
1794
|
-
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
3354
|
+
const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
|
|
1795
3355
|
if (symbols.size === 0) continue;
|
|
1796
3356
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1797
3357
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
@@ -1835,18 +3395,156 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1835
3395
|
const touches = /* @__PURE__ */ new Set();
|
|
1836
3396
|
const writtenStores = /* @__PURE__ */ new Set();
|
|
1837
3397
|
const inputFields = /* @__PURE__ */ new Set();
|
|
3398
|
+
const commandFields = /* @__PURE__ */ new Set();
|
|
1838
3399
|
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1839
3400
|
const requestFields = /* @__PURE__ */ new Set();
|
|
1840
3401
|
let opaqueRequest = false;
|
|
1841
3402
|
const outputFields = /* @__PURE__ */ new Set();
|
|
1842
3403
|
const opaqueOutputFields = /* @__PURE__ */ new Set();
|
|
1843
3404
|
const transformedStores = /* @__PURE__ */ new Set();
|
|
3405
|
+
const deliveredFields = /* @__PURE__ */ new Set();
|
|
3406
|
+
const deliveredOpaque = /* @__PURE__ */ new Set();
|
|
3407
|
+
const deliveredStores = /* @__PURE__ */ new Set();
|
|
3408
|
+
/** stores handed raw to a page: what the page shows of them is read after the walk */
|
|
3409
|
+
const rawDeliveries = [];
|
|
3410
|
+
let anyDelivery = false;
|
|
1844
3411
|
const outputReads = /* @__PURE__ */ new Map();
|
|
1845
3412
|
const trace = [];
|
|
1846
3413
|
const scope = [];
|
|
1847
3414
|
const unresolved = [];
|
|
1848
3415
|
const visited = /* @__PURE__ */ new Set();
|
|
1849
3416
|
let writes = false;
|
|
3417
|
+
/** what a followed body reads, itself and through what it follows — bounded like the walk */
|
|
3418
|
+
const storesReadBy = (ref, depth, seen = /* @__PURE__ */ new Set()) => {
|
|
3419
|
+
const found = /* @__PURE__ */ new Set();
|
|
3420
|
+
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
3421
|
+
if (seen.has(key) || depth > maxDepth) return found;
|
|
3422
|
+
seen.add(key);
|
|
3423
|
+
const facts = factsFor(ref);
|
|
3424
|
+
if (!facts) return found;
|
|
3425
|
+
for (const access of facts.accesses) found.add(access.store);
|
|
3426
|
+
for (const followUp of facts.followUps) for (const store of storesReadBy(followUp.ref, depth + 1, seen)) found.add(store);
|
|
3427
|
+
return found;
|
|
3428
|
+
};
|
|
3429
|
+
/**
|
|
3430
|
+
* A delivered call hands on what its body RETURNS — classified, so a returned
|
|
3431
|
+
* `{ data: rows, meta }` delivers the rows' store and the meta's leaves, not
|
|
3432
|
+
* one DET per key. `pick` keeps one key of it: `const { data } = …`,
|
|
3433
|
+
* `resultado.linhas`. Bounded by depth like the walk, and by a seen set so a
|
|
3434
|
+
* body returning itself cannot loop.
|
|
3435
|
+
*/
|
|
3436
|
+
const deliver = (item, depth, seen = /* @__PURE__ */ new Set()) => {
|
|
3437
|
+
switch (item.kind) {
|
|
3438
|
+
case "store":
|
|
3439
|
+
deliveredStores.add(item.store);
|
|
3440
|
+
if (item.via) rawDeliveries.push({
|
|
3441
|
+
...item.via,
|
|
3442
|
+
store: item.store,
|
|
3443
|
+
path: item.path
|
|
3444
|
+
});
|
|
3445
|
+
return;
|
|
3446
|
+
case "scalar":
|
|
3447
|
+
deliveredFields.add(item.path || "<value>");
|
|
3448
|
+
return;
|
|
3449
|
+
case "echo": return;
|
|
3450
|
+
case "opaque":
|
|
3451
|
+
deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
|
|
3452
|
+
return;
|
|
3453
|
+
case "call": {
|
|
3454
|
+
if (depth > maxDepth + 2) return;
|
|
3455
|
+
let resolved = false;
|
|
3456
|
+
for (const ref of item.refs) {
|
|
3457
|
+
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}#${item.pick ?? ""}#${item.path}`;
|
|
3458
|
+
if (seen.has(key)) continue;
|
|
3459
|
+
seen.add(key);
|
|
3460
|
+
const facts = factsFor(ref);
|
|
3461
|
+
if (!facts) continue;
|
|
3462
|
+
if (facts.outputs.length > 0 || facts.transformed) {
|
|
3463
|
+
resolved = true;
|
|
3464
|
+
continue;
|
|
3465
|
+
}
|
|
3466
|
+
const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
|
|
3467
|
+
/**
|
|
3468
|
+
* `egresso.curso` where the body returns the row itself (`return
|
|
3469
|
+
* Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
|
|
3470
|
+
* value — one field of a store is one value; one key of a returned call
|
|
3471
|
+
* is that call picked deeper.
|
|
3472
|
+
*/
|
|
3473
|
+
if (item.pick && returned.length === 0) {
|
|
3474
|
+
const above = facts.deliveries.returns.filter((r) => r.path === "" || item.pick.startsWith(`${r.path}.`));
|
|
3475
|
+
for (const r of above) {
|
|
3476
|
+
const rest = r.path ? item.pick.slice(r.path.length + 1) : item.pick;
|
|
3477
|
+
if (r.kind === "call") deliver({
|
|
3478
|
+
...r,
|
|
3479
|
+
path: item.path,
|
|
3480
|
+
pick: rest,
|
|
3481
|
+
via: item.via
|
|
3482
|
+
}, depth + 1, seen);
|
|
3483
|
+
else if (r.kind === "store" || r.kind === "scalar") deliveredFields.add(item.path || "<value>");
|
|
3484
|
+
}
|
|
3485
|
+
if (above.length > 0) {
|
|
3486
|
+
resolved = true;
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
/**
|
|
3491
|
+
* A return the classifier could not read at all (`rows.map(…).join(…)`)
|
|
3492
|
+
* says nothing about what leaves; what the body read, or what was handed
|
|
3493
|
+
* into it, says more — so it does not count as resolved.
|
|
3494
|
+
*/
|
|
3495
|
+
if (returned.length > 0 && returned.some((r) => r.kind !== "opaque")) {
|
|
3496
|
+
for (const r of returned) {
|
|
3497
|
+
const rest = item.pick ? r.path.slice(item.pick.length).replace(/^\./, "") : r.path;
|
|
3498
|
+
const path = [item.path, rest].filter(Boolean).join(".");
|
|
3499
|
+
deliver({
|
|
3500
|
+
...r,
|
|
3501
|
+
path,
|
|
3502
|
+
via: item.via
|
|
3503
|
+
}, depth + 1, seen);
|
|
3504
|
+
}
|
|
3505
|
+
resolved = true;
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
if (item.pick) continue;
|
|
3509
|
+
const read = storesReadBy(ref, depth + 1);
|
|
3510
|
+
if (read.size > 0) {
|
|
3511
|
+
for (const store of read) {
|
|
3512
|
+
deliveredStores.add(store);
|
|
3513
|
+
if (item.via) rawDeliveries.push({
|
|
3514
|
+
...item.via,
|
|
3515
|
+
store,
|
|
3516
|
+
path: item.path
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
resolved = true;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (resolved) return;
|
|
3523
|
+
/**
|
|
3524
|
+
* `const { confidenciais } = await this.contar()` where the body's return
|
|
3525
|
+
* is unreadable: one KEY of it, named — one value, not an opaque floor.
|
|
3526
|
+
* Only a key that carries rows on (`data`, `rows`) stays unreadable.
|
|
3527
|
+
*/
|
|
3528
|
+
if (item.pick && !PASSES_ROWS.has(item.pick.split(".")[0])) {
|
|
3529
|
+
deliveredFields.add(item.path || "<value>");
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* The body returned no literal and read no store — a CSV builder, a
|
|
3534
|
+
* formatter over rows handed in. The document it built carries what it
|
|
3535
|
+
* received, so the rows' stores leave. Nothing handed in: opaque.
|
|
3536
|
+
*/
|
|
3537
|
+
if (item.args.length > 0) {
|
|
3538
|
+
for (const argument of item.args) deliver({
|
|
3539
|
+
...argument,
|
|
3540
|
+
via: item.via
|
|
3541
|
+
}, depth, seen);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
};
|
|
1850
3548
|
const visit = (ref, depth, technical = false) => {
|
|
1851
3549
|
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
1852
3550
|
if (visited.has(key) || depth > maxDepth) return;
|
|
@@ -1885,12 +3583,20 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1885
3583
|
}
|
|
1886
3584
|
unresolved.push(...facts.unresolved);
|
|
1887
3585
|
for (const field of facts.validators) inputFields.add(field);
|
|
3586
|
+
for (const field of facts.commandFields) commandFields.add(field);
|
|
1888
3587
|
for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
|
|
1889
3588
|
for (const field of facts.requestFields) requestFields.add(field);
|
|
1890
3589
|
if (facts.opaqueRequest) opaqueRequest = true;
|
|
1891
3590
|
for (const field of facts.outputs) outputFields.add(field);
|
|
1892
3591
|
for (const field of facts.opaqueOutputs) opaqueOutputFields.add(field);
|
|
1893
3592
|
if (facts.transformed) transformedStores.add(facts.transformed);
|
|
3593
|
+
/**
|
|
3594
|
+
* Deliveries, resolved here because a delivered CALL leads to a body only
|
|
3595
|
+
* the graph knows: what that body returns is what the value hands on.
|
|
3596
|
+
*/
|
|
3597
|
+
const items = [...facts.deliveries.calls, ...depth === 0 ? facts.deliveries.returns : []];
|
|
3598
|
+
if (facts.deliveries.anyCall || depth === 0 && facts.deliveries.anyReturn) anyDelivery = true;
|
|
3599
|
+
for (const item of items) deliver(item, depth);
|
|
1894
3600
|
for (const { store, shape, columns, via } of facts.reads) {
|
|
1895
3601
|
const known = outputReads.get(store) ?? {
|
|
1896
3602
|
whole: false,
|
|
@@ -1926,17 +3632,38 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1926
3632
|
}
|
|
1927
3633
|
};
|
|
1928
3634
|
visit(handler, 0);
|
|
3635
|
+
/**
|
|
3636
|
+
* What each page shows of the stores handed to it raw — read once the walk is
|
|
3637
|
+
* done, because a query object's rows reach the page through the delivery of a
|
|
3638
|
+
* call, not of a variable (plan 0.8 §D).
|
|
3639
|
+
*/
|
|
3640
|
+
const pages = readPages(rawDeliveries.filter((d) => !transformedStores.has(d.store)), {
|
|
3641
|
+
root: app.root,
|
|
3642
|
+
project,
|
|
3643
|
+
stores: storesByName,
|
|
3644
|
+
relations: relationsByStore,
|
|
3645
|
+
resolveSpecifier: app.resolveSpecifier
|
|
3646
|
+
});
|
|
1929
3647
|
return {
|
|
1930
3648
|
writes,
|
|
1931
3649
|
touches: [...touches].sort(),
|
|
1932
3650
|
writtenStores: [...writtenStores].sort(),
|
|
1933
3651
|
inputFields: [...inputFields].sort(),
|
|
3652
|
+
commandFields: [...commandFields].sort(),
|
|
1934
3653
|
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1935
3654
|
requestFields: [...requestFields].sort(),
|
|
1936
3655
|
opaqueRequest,
|
|
1937
3656
|
outputFields: [...outputFields].sort(),
|
|
1938
3657
|
opaqueOutputFields: [...opaqueOutputFields].sort(),
|
|
1939
3658
|
transformedStores: [...transformedStores].sort(),
|
|
3659
|
+
delivered: {
|
|
3660
|
+
any: anyDelivery,
|
|
3661
|
+
fields: [...deliveredFields].sort(),
|
|
3662
|
+
opaqueFields: [...deliveredOpaque].sort(),
|
|
3663
|
+
stores: [...deliveredStores].sort()
|
|
3664
|
+
},
|
|
3665
|
+
pageReads: Object.fromEntries([...pages.columns.entries()].map(([store, columns]) => [store, [...columns].sort()])),
|
|
3666
|
+
unreadablePages: Object.fromEntries(pages.unreadable),
|
|
1940
3667
|
outputReads: Object.fromEntries([...outputReads.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([store, read]) => [store, {
|
|
1941
3668
|
whole: read.whole,
|
|
1942
3669
|
selected: [...read.selected].sort(),
|
|
@@ -1969,6 +3696,8 @@ function findBody(file, ref) {
|
|
|
1969
3696
|
}
|
|
1970
3697
|
const fn = file.getFunction(ref.member);
|
|
1971
3698
|
if (fn) return fn;
|
|
3699
|
+
const initializer = file.getVariableDeclaration(ref.member)?.getInitializer();
|
|
3700
|
+
if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) return initializer;
|
|
1972
3701
|
return null;
|
|
1973
3702
|
}
|
|
1974
3703
|
for (const cls of file.getClasses()) {
|
|
@@ -1986,7 +3715,7 @@ function findBody(file, ref) {
|
|
|
1986
3715
|
* from them: `const invite = await Invite.findOrFail(...)` makes `invite.save()`
|
|
1987
3716
|
* count as a write to `Invite`.
|
|
1988
3717
|
*/
|
|
1989
|
-
function storeSymbolsFor(body, file, app, stores) {
|
|
3718
|
+
function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ new Map()) {
|
|
1990
3719
|
const symbols = /* @__PURE__ */ new Map();
|
|
1991
3720
|
for (const declaration of file.getImportDeclarations()) {
|
|
1992
3721
|
if (!app.resolveSpecifier(declaration.getModuleSpecifierValue())) continue;
|
|
@@ -1997,10 +3726,48 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
1997
3726
|
if (stores.has(named.getName())) symbols.set(binding, named.getName());
|
|
1998
3727
|
}
|
|
1999
3728
|
}
|
|
3729
|
+
/**
|
|
3730
|
+
* `const { default: Noticia } = await import('#noticias/models/noticia')`: a
|
|
3731
|
+
* model imported INSIDE the body — an ace command does this to keep the app
|
|
3732
|
+
* from booting for `--help`. The store is the same; only the binding moved.
|
|
3733
|
+
*/
|
|
3734
|
+
const storeByFile = /* @__PURE__ */ new Map();
|
|
3735
|
+
for (const [name, store] of stores) storeByFile.set(toPosix(store.provenance.file), name);
|
|
3736
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
3737
|
+
const binding = declaration.getNameNode();
|
|
3738
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
3739
|
+
let initializer = declaration.getInitializer();
|
|
3740
|
+
if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
|
|
3741
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
3742
|
+
if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
3743
|
+
const specifier = initializer.getArguments()[0];
|
|
3744
|
+
if (!specifier || !Node.isStringLiteral(specifier)) continue;
|
|
3745
|
+
const target = app.resolveSpecifier(specifier.getLiteralValue());
|
|
3746
|
+
const store = target ? storeByFile.get(toPosix(target)) : void 0;
|
|
3747
|
+
if (!store) continue;
|
|
3748
|
+
for (const element of binding.getElements()) if ((element.getPropertyNameNode()?.getText() ?? element.getName()) === "default") symbols.set(element.getName(), store);
|
|
3749
|
+
}
|
|
2000
3750
|
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
2001
3751
|
const initializer = declaration.getInitializer();
|
|
2002
3752
|
const name = declaration.getNameNode();
|
|
2003
|
-
if (!
|
|
3753
|
+
if (!Node.isIdentifier(name)) continue;
|
|
3754
|
+
const declared = storeNamedBy(declaration.getTypeNode()?.getText(), stores);
|
|
3755
|
+
if (declared) {
|
|
3756
|
+
symbols.set(name.getText(), declared);
|
|
3757
|
+
continue;
|
|
3758
|
+
}
|
|
3759
|
+
if (!initializer) continue;
|
|
3760
|
+
/**
|
|
3761
|
+
* `const pasta = documento.pasta`: a RELATION read off a row is the relation's
|
|
3762
|
+
* target, not the row's store — `pasta.save()` had been billed to `Documento`.
|
|
3763
|
+
* A plain field (`documento.nome`) is no store at all.
|
|
3764
|
+
*/
|
|
3765
|
+
const plain = unwrapAwait(initializer);
|
|
3766
|
+
if (Node.isPropertyAccessExpression(plain)) {
|
|
3767
|
+
const target = storeOfExpression(plain, symbols, relations);
|
|
3768
|
+
if (target) symbols.set(name.getText(), target);
|
|
3769
|
+
continue;
|
|
3770
|
+
}
|
|
2004
3771
|
const root = rootSymbolOf(initializer);
|
|
2005
3772
|
const store = root ? symbols.get(root) : void 0;
|
|
2006
3773
|
if (store) symbols.set(name.getText(), store);
|
|
@@ -2008,8 +3775,8 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
2008
3775
|
if (Node.isMethodDeclaration(body) || Node.isFunctionDeclaration(body)) for (const parameter of body.getParameters()) {
|
|
2009
3776
|
const typeNode = parameter.getTypeNode();
|
|
2010
3777
|
const nameNode = parameter.getNameNode();
|
|
2011
|
-
const typeName = typeNode?.getText();
|
|
2012
|
-
if (typeName &&
|
|
3778
|
+
const typeName = storeNamedBy(typeNode?.getText(), stores);
|
|
3779
|
+
if (typeName && Node.isIdentifier(nameNode)) {
|
|
2013
3780
|
symbols.set(nameNode.getText(), typeName);
|
|
2014
3781
|
continue;
|
|
2015
3782
|
}
|
|
@@ -2020,32 +3787,56 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
2020
3787
|
* Registers the PATH `input.invite`, because that is how the write
|
|
2021
3788
|
* appears: `input.invite.save()`.
|
|
2022
3789
|
*/
|
|
2023
|
-
if (typeNode && Node.isIdentifier(nameNode)) {
|
|
2024
|
-
|
|
3790
|
+
if (typeNode && Node.isIdentifier(nameNode)) for (const [property, propertyType] of membersOfType(typeNode, file, app)) {
|
|
3791
|
+
const named = storeNamedBy(propertyType, stores);
|
|
3792
|
+
if (named) symbols.set(`${nameNode.getText()}.${property}`, named);
|
|
2025
3793
|
}
|
|
2026
3794
|
/**
|
|
2027
|
-
* Destructured form: `handle({ invite }: { invite: Invite })
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
* instead of an EI.
|
|
3795
|
+
* Destructured form: `handle({ invite }: { invite: Invite })`, and
|
|
3796
|
+
* `handle({ document, name }: RenameDocumentInput)` with the interface named
|
|
3797
|
+
* — in this file or imported. The second is the dominant shape on a reviewed
|
|
3798
|
+
* application, and only the inline literal was read: every `document.save()`
|
|
3799
|
+
* behind it was invisible, and the transaction an EO (plan 0.8 §A).
|
|
2033
3800
|
*/
|
|
2034
3801
|
const binding = nameNode.asKind(SyntaxKind.ObjectBindingPattern);
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
if (memberType) propertyTypes.set(member.getName(), memberType);
|
|
3802
|
+
if (!binding || !typeNode) continue;
|
|
3803
|
+
const propertyTypes = membersOfType(typeNode, file, app);
|
|
3804
|
+
for (const element of binding.getElements()) {
|
|
3805
|
+
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
3806
|
+
const resolved = storeNamedBy(propertyTypes.get(property), stores);
|
|
3807
|
+
if (resolved) symbols.set(element.getName(), resolved);
|
|
2042
3808
|
}
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* `const { preIntake } = input` where `input.preIntake` is a registered path:
|
|
3812
|
+
* each element inherits the store of its path. Read AFTER the parameters, which
|
|
3813
|
+
* is where the paths come from.
|
|
3814
|
+
*/
|
|
3815
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
3816
|
+
const binding = declaration.getNameNode();
|
|
3817
|
+
const initializer = declaration.getInitializer();
|
|
3818
|
+
if (!Node.isObjectBindingPattern(binding) || !initializer) continue;
|
|
3819
|
+
const root = Node.isIdentifier(initializer) ? initializer.getText() : null;
|
|
3820
|
+
if (!root) continue;
|
|
2043
3821
|
for (const element of binding.getElements()) {
|
|
2044
3822
|
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
2045
|
-
const
|
|
2046
|
-
if (
|
|
3823
|
+
const store = symbols.get(`${root}.${property}`);
|
|
3824
|
+
if (store) symbols.set(element.getName(), store);
|
|
2047
3825
|
}
|
|
2048
3826
|
}
|
|
3827
|
+
/**
|
|
3828
|
+
* `for (const documento of pasta.documentos)`, `for (const row of rows)`: the
|
|
3829
|
+
* loop variable is a row of the relation's target, or of the rows' store. A
|
|
3830
|
+
* `delete()` inside such a loop was invisible; read last, after the parameters that name the parent — plan 0.8 §A.
|
|
3831
|
+
*/
|
|
3832
|
+
for (const loop of body.getDescendantsOfKind(SyntaxKind.ForOfStatement)) {
|
|
3833
|
+
const declared = loop.getInitializer();
|
|
3834
|
+
if (!Node.isVariableDeclarationList(declared)) continue;
|
|
3835
|
+
const nameNode = declared.getDeclarations()[0]?.getNameNode();
|
|
3836
|
+
if (!nameNode || !Node.isIdentifier(nameNode)) continue;
|
|
3837
|
+
const store = storeOfExpression(unwrapAwait(loop.getExpression()), symbols, relations);
|
|
3838
|
+
if (store) symbols.set(nameNode.getText(), store);
|
|
3839
|
+
}
|
|
2049
3840
|
return symbols;
|
|
2050
3841
|
}
|
|
2051
3842
|
/**
|
|
@@ -2109,6 +3900,67 @@ function resolveTypeToFile(typeName, file, app) {
|
|
|
2109
3900
|
* Accepts an inline type literal and a named type declared in this file or
|
|
2110
3901
|
* imported from the application. Anything else yields empty — no guessing.
|
|
2111
3902
|
*/
|
|
3903
|
+
/**
|
|
3904
|
+
* The store a type annotation names, or nothing: `Sessao`, `Sessao | null`,
|
|
3905
|
+
* `Promise<Sessao | null>`, `Sessao[]`, `Promise<Sessao[]>`. Two different stores
|
|
3906
|
+
* in one union name nothing — a guess is not a binding.
|
|
3907
|
+
*/
|
|
3908
|
+
function storeNamedBy(typeText, stores) {
|
|
3909
|
+
if (!typeText) return null;
|
|
3910
|
+
let text = typeText.trim();
|
|
3911
|
+
const promise = text.match(/^Promise<([\s\S]*)>$/);
|
|
3912
|
+
if (promise) text = promise[1];
|
|
3913
|
+
const named = new Set(text.split("|").map((part) => part.trim().replace(/\[\]$/, "").replace(/^Array<(.*)>$/, "$1").trim()).filter((part) => part && part !== "null" && part !== "undefined"));
|
|
3914
|
+
if (named.size !== 1) return null;
|
|
3915
|
+
const [only] = named;
|
|
3916
|
+
return stores.has(only) ? only : null;
|
|
3917
|
+
}
|
|
3918
|
+
/** array methods that hand back one row, or the same rows, of the receiver */
|
|
3919
|
+
const ONE_OF_ROWS = new Set([
|
|
3920
|
+
"find",
|
|
3921
|
+
"findLast",
|
|
3922
|
+
"at",
|
|
3923
|
+
"filter",
|
|
3924
|
+
"slice",
|
|
3925
|
+
"sort",
|
|
3926
|
+
"toSorted",
|
|
3927
|
+
"reverse",
|
|
3928
|
+
"toReversed",
|
|
3929
|
+
"concat",
|
|
3930
|
+
"flat",
|
|
3931
|
+
"first",
|
|
3932
|
+
"last"
|
|
3933
|
+
]);
|
|
3934
|
+
/** array methods whose callback receives one row of the receiver */
|
|
3935
|
+
const ITERATES_ROWS = new Set([
|
|
3936
|
+
"map",
|
|
3937
|
+
"forEach",
|
|
3938
|
+
"filter",
|
|
3939
|
+
"find",
|
|
3940
|
+
"findLast",
|
|
3941
|
+
"some",
|
|
3942
|
+
"every",
|
|
3943
|
+
"flatMap",
|
|
3944
|
+
"reduce"
|
|
3945
|
+
]);
|
|
3946
|
+
const lastSegmentText = (node) => Node.isPropertyAccessExpression(node) ? node.getName() : node.getText();
|
|
3947
|
+
/** the store an expression is rows of: a store symbol, or `parent.relation` with the relation declared */
|
|
3948
|
+
function storeOfExpression(expression, symbols, relations) {
|
|
3949
|
+
if (!expression) return null;
|
|
3950
|
+
if (Node.isIdentifier(expression)) return symbols.get(expression.getText()) ?? null;
|
|
3951
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
3952
|
+
const receiver = expression.getExpression();
|
|
3953
|
+
if (Node.isIdentifier(receiver)) {
|
|
3954
|
+
const byPath = symbols.get(`${receiver.getText()}.${expression.getName()}`);
|
|
3955
|
+
if (byPath) return byPath;
|
|
3956
|
+
}
|
|
3957
|
+
const parent = symbols.get(rootSymbolOf(receiver) ?? "");
|
|
3958
|
+
if (!parent) return null;
|
|
3959
|
+
return relations.get(parent)?.[expression.getName()] ?? null;
|
|
3960
|
+
}
|
|
3961
|
+
const root = rootSymbolOf(expression);
|
|
3962
|
+
return root ? symbols.get(root) ?? null : null;
|
|
3963
|
+
}
|
|
2112
3964
|
function membersOfType(typeNode, file, app) {
|
|
2113
3965
|
const members = /* @__PURE__ */ new Map();
|
|
2114
3966
|
const collect = (node) => {
|
|
@@ -2162,6 +4014,32 @@ function importMapsOf(file, app) {
|
|
|
2162
4014
|
if (alias) exportedAs.set(alias, named.getName());
|
|
2163
4015
|
}
|
|
2164
4016
|
}
|
|
4017
|
+
/**
|
|
4018
|
+
* `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
|
|
4019
|
+
* `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
|
|
4020
|
+
*
|
|
4021
|
+
* A module imported INSIDE a body — the shape ace commands use so `--help` does
|
|
4022
|
+
* not boot the application. The binding moved; the body it names did not, and
|
|
4023
|
+
* a command importing its whole action layer this way reached nothing.
|
|
4024
|
+
*/
|
|
4025
|
+
for (const declaration of file.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
4026
|
+
const binding = declaration.getNameNode();
|
|
4027
|
+
if (!Node.isObjectBindingPattern(binding)) continue;
|
|
4028
|
+
let initializer = declaration.getInitializer();
|
|
4029
|
+
if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
|
|
4030
|
+
if (!initializer || !Node.isCallExpression(initializer)) continue;
|
|
4031
|
+
if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
4032
|
+
const specifier = initializer.getArguments()[0];
|
|
4033
|
+
if (!specifier || !Node.isStringLiteral(specifier)) continue;
|
|
4034
|
+
const target = app.resolveSpecifier(specifier.getLiteralValue());
|
|
4035
|
+
if (!target) continue;
|
|
4036
|
+
for (const element of binding.getElements()) {
|
|
4037
|
+
const exported = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
4038
|
+
const local = element.getName();
|
|
4039
|
+
imports.set(local, target);
|
|
4040
|
+
if (exported !== "default" && exported !== local) exportedAs.set(local, exported);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
2165
4043
|
return {
|
|
2166
4044
|
imports,
|
|
2167
4045
|
exportedAs
|
|
@@ -2197,6 +4075,77 @@ function isWorthReporting(call, symbols, imports) {
|
|
|
2197
4075
|
if (root === "this") return true;
|
|
2198
4076
|
return imports.has(root);
|
|
2199
4077
|
}
|
|
4078
|
+
/** Lucid's persistence on an instance, with no arguments: `x.save()`, `x.delete()` — a `Map#delete(key)` has one */
|
|
4079
|
+
const INSTANCE_WRITES = new Set([
|
|
4080
|
+
"save",
|
|
4081
|
+
"delete",
|
|
4082
|
+
"forceDelete"
|
|
4083
|
+
]);
|
|
4084
|
+
/** persistence through a relation or a merge: `x.related('y').create(…)`, `x.merge(p).save()` */
|
|
4085
|
+
const CHAINED_WRITES = new Set([
|
|
4086
|
+
"create",
|
|
4087
|
+
"createMany",
|
|
4088
|
+
"save",
|
|
4089
|
+
"saveMany",
|
|
4090
|
+
"attach",
|
|
4091
|
+
"detach",
|
|
4092
|
+
"sync",
|
|
4093
|
+
"updateOrCreate",
|
|
4094
|
+
"firstOrCreate",
|
|
4095
|
+
"delete"
|
|
4096
|
+
]);
|
|
4097
|
+
const WRITE_CHAINS = new Set([
|
|
4098
|
+
"related",
|
|
4099
|
+
"merge",
|
|
4100
|
+
"fill",
|
|
4101
|
+
"useTransaction"
|
|
4102
|
+
]);
|
|
4103
|
+
/**
|
|
4104
|
+
* A persistence call on a receiver the body cannot type: a local that is neither a
|
|
4105
|
+
* store, nor an import, nor `this`, nor the result of a call on one of those.
|
|
4106
|
+
* Reported as unresolved — the count must not stay silent where an EI may hide.
|
|
4107
|
+
*/
|
|
4108
|
+
function isUnreadableWrite(call, symbols, imports, packageImports, body) {
|
|
4109
|
+
const expression = call.getExpression();
|
|
4110
|
+
if (!Node.isPropertyAccessExpression(expression)) return false;
|
|
4111
|
+
const method = expression.getName();
|
|
4112
|
+
const receiver = unwrapAwait(expression.getExpression());
|
|
4113
|
+
if (Node.isCallExpression(receiver)) {
|
|
4114
|
+
const inner = receiver.getExpression();
|
|
4115
|
+
if (!Node.isPropertyAccessExpression(inner) || !WRITE_CHAINS.has(inner.getName())) return false;
|
|
4116
|
+
if (!CHAINED_WRITES.has(method)) return false;
|
|
4117
|
+
} else if (!INSTANCE_WRITES.has(method) || call.getArguments().length > 0) return false;
|
|
4118
|
+
const root = rootSymbolOf(expression.getExpression());
|
|
4119
|
+
if (!root || root === "this") return false;
|
|
4120
|
+
if (symbols.has(root) || imports.has(root) || packageImports.has(root)) return false;
|
|
4121
|
+
if (!Node.isIdentifier(unwrapAwait(rootNodeOf(expression.getExpression())))) return false;
|
|
4122
|
+
/**
|
|
4123
|
+
* `const pdfDoc = await PDFDocument.create()` from `pdf-lib`, then `pdfDoc.save()`:
|
|
4124
|
+
* a package's object with a method called `save`. Its declaration says where it
|
|
4125
|
+
* came from, and it is not a store — nothing to report.
|
|
4126
|
+
*/
|
|
4127
|
+
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
4128
|
+
const nameNode = declaration.getNameNode();
|
|
4129
|
+
if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
|
|
4130
|
+
const initializer = declaration.getInitializer();
|
|
4131
|
+
const origin = initializer ? rootSymbolOf(unwrapAwait(initializer)) : null;
|
|
4132
|
+
if (origin && packageImports.has(origin)) return false;
|
|
4133
|
+
}
|
|
4134
|
+
return true;
|
|
4135
|
+
}
|
|
4136
|
+
/** the leftmost node of a chain */
|
|
4137
|
+
function rootNodeOf(node) {
|
|
4138
|
+
let current = node;
|
|
4139
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
4140
|
+
const next = unwrapAwait(current);
|
|
4141
|
+
if (Node.isPropertyAccessExpression(next) || Node.isCallExpression(next) || Node.isElementAccessExpression(next)) {
|
|
4142
|
+
current = next.getExpression();
|
|
4143
|
+
continue;
|
|
4144
|
+
}
|
|
4145
|
+
return next;
|
|
4146
|
+
}
|
|
4147
|
+
return current;
|
|
4148
|
+
}
|
|
2200
4149
|
/** `(await x())` and `x()` are the same receiver for this purpose. */
|
|
2201
4150
|
function unwrapAwait(node) {
|
|
2202
4151
|
let current = node;
|
|
@@ -2215,6 +4164,154 @@ function hashOf(body) {
|
|
|
2215
4164
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2216
4165
|
}
|
|
2217
4166
|
//#endregion
|
|
4167
|
+
//#region src/inventory/sources/jobs.ts
|
|
4168
|
+
/** `X.schedule({}).cron(…)`: a scheduler's way of dispatching */
|
|
4169
|
+
const SCHEDULE_METHODS = new Set(["schedule"]);
|
|
4170
|
+
function collectJobs(app) {
|
|
4171
|
+
const project = new Project({
|
|
4172
|
+
skipAddingFilesFromTsConfig: true,
|
|
4173
|
+
skipFileDependencyResolution: true,
|
|
4174
|
+
compilerOptions: { allowJs: false }
|
|
4175
|
+
});
|
|
4176
|
+
const root = toPosix(app.root);
|
|
4177
|
+
for (const dir of app.scanRoots) project.addSourceFilesAtPaths(`${toPosix(dir)}/**/*.ts`);
|
|
4178
|
+
project.addSourceFilesAtPaths(`${root}/start/**/*.ts`);
|
|
4179
|
+
project.addSourceFilesAtPaths(`${root}/commands/**/*.ts`);
|
|
4180
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
4181
|
+
for (const file of project.getSourceFiles()) {
|
|
4182
|
+
const filePath = toPosix(file.getFilePath());
|
|
4183
|
+
if (!isApplicationCode(app.root, filePath) || !inJobsDirectory(filePath)) continue;
|
|
4184
|
+
for (const cls of file.getClasses()) {
|
|
4185
|
+
const name = cls.getName();
|
|
4186
|
+
if (!name || !looksLikeJob(cls)) continue;
|
|
4187
|
+
jobs.set(filePath, {
|
|
4188
|
+
name,
|
|
4189
|
+
file: filePath,
|
|
4190
|
+
dispatchedFrom: [],
|
|
4191
|
+
scheduledFrom: []
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
if (jobs.size === 0) return [];
|
|
4196
|
+
for (const file of project.getSourceFiles()) {
|
|
4197
|
+
const filePath = toPosix(file.getFilePath());
|
|
4198
|
+
if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
|
|
4199
|
+
const { imports } = importMapsOf(file, app);
|
|
4200
|
+
if (imports.size === 0) continue;
|
|
4201
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
4202
|
+
const callee = call.getExpression();
|
|
4203
|
+
if (!Node.isPropertyAccessExpression(callee)) continue;
|
|
4204
|
+
const method = callee.getName();
|
|
4205
|
+
const scheduled = SCHEDULE_METHODS.has(method);
|
|
4206
|
+
if (!scheduled && !DISPATCH_METHODS.has(method)) continue;
|
|
4207
|
+
const receiver = callee.getExpression();
|
|
4208
|
+
if (!Node.isIdentifier(receiver)) continue;
|
|
4209
|
+
const target = imports.get(receiver.getText());
|
|
4210
|
+
const job = target ? jobs.get(toPosix(target)) : void 0;
|
|
4211
|
+
if (!job) continue;
|
|
4212
|
+
const sites = scheduled ? job.scheduledFrom : job.dispatchedFrom;
|
|
4213
|
+
if (!sites.includes(filePath)) sites.push(filePath);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
return [...jobs.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
4217
|
+
}
|
|
4218
|
+
const inJobsDirectory = (filePath) => filePath.split("/").slice(0, -1).some((segment) => segment === "jobs" || segment === "job");
|
|
4219
|
+
/** extends something called `…Job`, or declares the method a queue would run */
|
|
4220
|
+
function looksLikeJob(cls) {
|
|
4221
|
+
const parent = cls.getExtends()?.getExpression().getText() ?? "";
|
|
4222
|
+
if (/Job$/.test(parent)) return true;
|
|
4223
|
+
return EXECUTION_METHODS.some((method) => cls.getMethod(method) !== void 0);
|
|
4224
|
+
}
|
|
4225
|
+
//#endregion
|
|
4226
|
+
//#region src/inventory/sources/json_schemas.ts
|
|
4227
|
+
function collectJsonSchemas(app) {
|
|
4228
|
+
const project = new Project({
|
|
4229
|
+
skipAddingFilesFromTsConfig: true,
|
|
4230
|
+
skipFileDependencyResolution: true,
|
|
4231
|
+
compilerOptions: { allowJs: false }
|
|
4232
|
+
});
|
|
4233
|
+
/**
|
|
4234
|
+
* Wider than `scanRoots`, and only here.
|
|
4235
|
+
*
|
|
4236
|
+
* `database/` is excluded from the application roots on purpose: test factories
|
|
4237
|
+
* and migrations contain real persistence calls, and scanning them would turn a
|
|
4238
|
+
* test write into a counted function. But `node ace make:seeder` puts seeders in
|
|
4239
|
+
* `database/seeders`, which is where seed data — and therefore a form's schema —
|
|
4240
|
+
* normally lives. Reading a literal counts nothing, so there is no conflict: the
|
|
4241
|
+
* exclusion protects the call graph, not the schema catalogue.
|
|
4242
|
+
*
|
|
4243
|
+
* Without this, `overrides.detFromSchema` naming a schema declared in a seeder
|
|
4244
|
+
* reported "not declared anywhere in the code" and left the DET count at the
|
|
4245
|
+
* floor — the exact case the override exists for.
|
|
4246
|
+
*/
|
|
4247
|
+
for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
4248
|
+
const found = /* @__PURE__ */ new Map();
|
|
4249
|
+
for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
|
|
4250
|
+
const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
4251
|
+
if (!literal || !isJsonSchema(literal)) continue;
|
|
4252
|
+
const leaves = leavesOf(literal);
|
|
4253
|
+
if (leaves.length === 0) continue;
|
|
4254
|
+
found.set(declaration.getName(), {
|
|
4255
|
+
name: declaration.getName(),
|
|
4256
|
+
fields: leaves.length,
|
|
4257
|
+
leaves,
|
|
4258
|
+
provenance: {
|
|
4259
|
+
file: toPosix(file.getFilePath()),
|
|
4260
|
+
line: declaration.getStartLineNumber(),
|
|
4261
|
+
by: "json-schema"
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
return found;
|
|
4266
|
+
}
|
|
4267
|
+
/** `{ … } as const` and `{ … } satisfies X` still hold the literal */
|
|
4268
|
+
function unwrap(node) {
|
|
4269
|
+
if (!node) return void 0;
|
|
4270
|
+
if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) return unwrap(node.getExpression());
|
|
4271
|
+
return node;
|
|
4272
|
+
}
|
|
4273
|
+
/**
|
|
4274
|
+
* Recognised by shape, never by name.
|
|
4275
|
+
*
|
|
4276
|
+
* A const called `schema` may be anything; an object declaring `type: 'object'`
|
|
4277
|
+
* with a `properties` map is a JSON Schema whatever it is called.
|
|
4278
|
+
*/
|
|
4279
|
+
function isJsonSchema(literal) {
|
|
4280
|
+
return (literal.getProperty("type")?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer()?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() === "object" && literal.getProperty("properties") !== void 0;
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Leaves of a JSON Schema, by the table in counting-decisions §7.
|
|
4284
|
+
*
|
|
4285
|
+
* scalar 1
|
|
4286
|
+
* nested object leaves counted individually
|
|
4287
|
+
* array of scalar 1 (repeating group)
|
|
4288
|
+
* array of object the object's leaves, once
|
|
4289
|
+
* enum / const 1
|
|
4290
|
+
*/
|
|
4291
|
+
function leavesOf(literal, prefix = "") {
|
|
4292
|
+
const properties = literal.getProperty("properties")?.asKind(SyntaxKind.PropertyAssignment);
|
|
4293
|
+
const items = literal.getProperty("items")?.asKind(SyntaxKind.PropertyAssignment);
|
|
4294
|
+
if (properties) {
|
|
4295
|
+
const map = properties.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
4296
|
+
if (!map) return [];
|
|
4297
|
+
return map.getProperties().flatMap((property) => {
|
|
4298
|
+
const assignment = property.asKind(SyntaxKind.PropertyAssignment);
|
|
4299
|
+
if (!assignment) return [];
|
|
4300
|
+
const name = assignment.getName().replace(/['"]/g, "");
|
|
4301
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
4302
|
+
const nested = unwrap(assignment.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
4303
|
+
const inner = nested ? leavesOf(nested, path) : [];
|
|
4304
|
+
return inner.length > 0 ? inner : [path];
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
4307
|
+
if (items) {
|
|
4308
|
+
const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
4309
|
+
const inner = element ? leavesOf(element, prefix) : [];
|
|
4310
|
+
return inner.length > 0 ? inner : [prefix];
|
|
4311
|
+
}
|
|
4312
|
+
return [];
|
|
4313
|
+
}
|
|
4314
|
+
//#endregion
|
|
2218
4315
|
//#region src/albrecht/tables.ts
|
|
2219
4316
|
/** shared grid: [ref band][DET band] -> complexity */
|
|
2220
4317
|
const GRID = [
|
|
@@ -2757,7 +4854,7 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2757
4854
|
counted.add(field);
|
|
2758
4855
|
sources.push(source);
|
|
2759
4856
|
};
|
|
2760
|
-
for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
4857
|
+
if (entry.kind === "http") for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
2761
4858
|
/**
|
|
2762
4859
|
* `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
|
|
2763
4860
|
* with a schema's fields, and it used to assume there was exactly one and that
|
|
@@ -2777,6 +4874,11 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2777
4874
|
* paid for twice.
|
|
2778
4875
|
*/
|
|
2779
4876
|
for (const field of behavior.requestFields) add(field, `request:${field}`);
|
|
4877
|
+
for (const field of behavior.commandFields ?? []) {
|
|
4878
|
+
const [kind, ...rest] = field.split(".");
|
|
4879
|
+
const name = rest.join(".");
|
|
4880
|
+
add(name, `${kind === "args" ? "arg" : "flag"}:${name}`);
|
|
4881
|
+
}
|
|
2780
4882
|
if (type === "EO" || type === "EQ") {
|
|
2781
4883
|
/**
|
|
2782
4884
|
* counting-decisions §6, per store, in order of what is visible:
|
|
@@ -2812,8 +4914,34 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2812
4914
|
}
|
|
2813
4915
|
}
|
|
2814
4916
|
for (const field of behavior.outputFields) add(field, `transformer:${field}${opaqueOutputs.has(field) ? " (opaque)" : ""}`);
|
|
4917
|
+
/**
|
|
4918
|
+
* Plan 0.7 §A′ — the delivery is the boundary. When the transaction hands
|
|
4919
|
+
* something to a renderer or a response, THAT is what leaves: derived values
|
|
4920
|
+
* and the leaves of literals a followed body returned count 1 each; a store
|
|
4921
|
+
* leaves only if its rows were handed on, raw or through a query object; a
|
|
4922
|
+
* store read to authorise or to decide contributes nothing to the output. A
|
|
4923
|
+
* field that entered and is echoed back counts once, on entry (§7.3) — the
|
|
4924
|
+
* classifier marks it as an echo. Without a delivery point the rule cannot
|
|
4925
|
+
* apply, and every store read leaves, as before.
|
|
4926
|
+
*/
|
|
4927
|
+
const delivered = behavior.delivered;
|
|
4928
|
+
if (delivered.any) {
|
|
4929
|
+
for (const field of delivered.fields) add(`render.${field}`, `render:${field}`);
|
|
4930
|
+
for (const field of delivered.opaqueFields) add(`render.${field}`, `render:${field} (opaque)`);
|
|
4931
|
+
}
|
|
4932
|
+
const deliveredStores = new Set(delivered.stores);
|
|
4933
|
+
for (let changed = true; changed;) {
|
|
4934
|
+
changed = false;
|
|
4935
|
+
for (const [store, read] of Object.entries(behavior.outputReads)) {
|
|
4936
|
+
if (deliveredStores.has(store) || read.direct) continue;
|
|
4937
|
+
if (!read.via.some((parent) => deliveredStores.has(parent))) continue;
|
|
4938
|
+
deliveredStores.add(store);
|
|
4939
|
+
changed = true;
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
2815
4942
|
for (const store of touched) {
|
|
2816
4943
|
const read = behavior.outputReads[store];
|
|
4944
|
+
if (delivered.any && !deliveredStores.has(store) && !covered.has(store)) continue;
|
|
2817
4945
|
if (read?.aggregate) add(`${store}.<aggregate>`, `aggregate:${store} (a count or an existence check: one scalar)`);
|
|
2818
4946
|
if (covered.has(store)) continue;
|
|
2819
4947
|
if (read && read.aggregate && !read.whole && read.selected.length === 0) continue;
|
|
@@ -2839,6 +4967,19 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2839
4967
|
}
|
|
2840
4968
|
continue;
|
|
2841
4969
|
}
|
|
4970
|
+
/**
|
|
4971
|
+
* Handed raw to a page the reader could open: the columns the page reads off
|
|
4972
|
+
* the rows are what the user sees (§6, plan 0.8 §D). A page it could not read
|
|
4973
|
+
* leaves the store whole below, and the count says why.
|
|
4974
|
+
*/
|
|
4975
|
+
const shown = behavior.pageReads?.[store];
|
|
4976
|
+
if (shown && !behavior.unreadablePages?.[store]) {
|
|
4977
|
+
for (const column of shown) {
|
|
4978
|
+
if (excluded.has(column)) continue;
|
|
4979
|
+
add(`${store}.${column}`, `page:${store}.${column}${mark(column)}`);
|
|
4980
|
+
}
|
|
4981
|
+
continue;
|
|
4982
|
+
}
|
|
2842
4983
|
for (const column of attributes) {
|
|
2843
4984
|
if (excluded.has(column.name)) continue;
|
|
2844
4985
|
add(`${store}.${column.name}`, `output:${store}.${column.name}${mark(column.name)}`);
|
|
@@ -2926,12 +5067,20 @@ const RULESET = "afp";
|
|
|
2926
5067
|
* column; system timestamps and `serializeAs: null` columns leaving the DETs;
|
|
2927
5068
|
* master-detail folded into one data function; identity by table; token tables
|
|
2928
5069
|
* technical; and opaque declarations reaching every function carrying the origin.
|
|
5070
|
+
* Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
|
|
5071
|
+
* props, the response payload, what a command prints) read back to their origin;
|
|
5072
|
+
* a function of the same file and a `.map(fn)` by reference are followed, so
|
|
5073
|
+
* FTRs move; and ace commands are transactions, with flags as input. One in
|
|
5074
|
+
* 1.7.0, wide: a write binds to what the variable IS — a destructured named
|
|
5075
|
+
* interface, a followed method's return, a relation off a row, a loop over rows,
|
|
5076
|
+
* the guard's user, a service the container made — and a write nobody can type is
|
|
5077
|
+
* an unresolved call instead of silence.
|
|
2929
5078
|
*
|
|
2930
5079
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
2931
5080
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
2932
5081
|
* exists for exactly that, and only this constant arms it.
|
|
2933
5082
|
*/
|
|
2934
|
-
const RULESET_VERSION = "1.
|
|
5083
|
+
const RULESET_VERSION = "1.7.0";
|
|
2935
5084
|
function count(input, options = {}) {
|
|
2936
5085
|
const warnings = [];
|
|
2937
5086
|
const usage = usageOf(input);
|
|
@@ -3013,7 +5162,8 @@ function count(input, options = {}) {
|
|
|
3013
5162
|
const countedRoots = new Set(dataFunctions.map((fn) => fn.name));
|
|
3014
5163
|
const countedStores = new Map(countable.filter((store) => countedRoots.has(grouping.rootOf.get(store.name) ?? store.name)).map((store) => [store.name, store]));
|
|
3015
5164
|
const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
|
|
3016
|
-
const
|
|
5165
|
+
const entryPoints = input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? ""));
|
|
5166
|
+
const transactionalFunctions = countTransactionalFunctions(entryPoints, input.behaviors, {
|
|
3017
5167
|
countedStores,
|
|
3018
5168
|
grouping,
|
|
3019
5169
|
root: input.app.root,
|
|
@@ -3047,6 +5197,10 @@ function count(input, options = {}) {
|
|
|
3047
5197
|
}));
|
|
3048
5198
|
warnings.push(...unreadableInputWarnings(input));
|
|
3049
5199
|
warnings.push(...unreadableOutputWarnings(input));
|
|
5200
|
+
warnings.push(...unreadableDeliveryWarnings(input));
|
|
5201
|
+
warnings.push(...commandWarnings(entryPoints, transactionalFunctions));
|
|
5202
|
+
warnings.push(...undispatchedJobWarnings(input));
|
|
5203
|
+
warnings.push(...unreadablePageWarnings(input));
|
|
3050
5204
|
warnings.push(...lookAlikeWarnings(functions));
|
|
3051
5205
|
warnings.push(...seededOnlyWarnings(functions, grouping.members, input.seededAnywhere));
|
|
3052
5206
|
return {
|
|
@@ -3058,6 +5212,88 @@ function count(input, options = {}) {
|
|
|
3058
5212
|
};
|
|
3059
5213
|
}
|
|
3060
5214
|
/**
|
|
5215
|
+
* Transactions that deliver something the analysis cannot read.
|
|
5216
|
+
*
|
|
5217
|
+
* A generated document (`response.send(gerarCsv(rows))`), a value built by a
|
|
5218
|
+
* call nobody followed. Each counts 1 DET — a floor — and the transaction's
|
|
5219
|
+
* output is understated by whatever the value carries. Named with the
|
|
5220
|
+
* expression, because the fix is usually in the code: return a literal, or a
|
|
5221
|
+
* transformer, and the fields become readable.
|
|
5222
|
+
*/
|
|
5223
|
+
function unreadableDeliveryWarnings(input) {
|
|
5224
|
+
const blind = input.entryPoints.map((entry) => ({
|
|
5225
|
+
entry,
|
|
5226
|
+
behavior: input.behaviors.get(entry.id)
|
|
5227
|
+
})).filter(({ behavior }) => behavior && !behavior.writes && behavior.delivered.opaqueFields.length > 0);
|
|
5228
|
+
if (blind.length === 0) return [];
|
|
5229
|
+
return [
|
|
5230
|
+
`${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:`,
|
|
5231
|
+
...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
|
|
5232
|
+
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
5233
|
+
];
|
|
5234
|
+
}
|
|
5235
|
+
/**
|
|
5236
|
+
* Pages the reader could not open for a store handed to them raw — plan 0.8 §D.
|
|
5237
|
+
*
|
|
5238
|
+
* The store leaves whole, as it always did; this says which page, and why: a
|
|
5239
|
+
* second level of components, a spread, a package's component, two files answering
|
|
5240
|
+
* to one name. Overestimating in the open — the fix is usually in the page.
|
|
5241
|
+
*/
|
|
5242
|
+
function unreadablePageWarnings(input) {
|
|
5243
|
+
const lines = [];
|
|
5244
|
+
for (const entry of input.entryPoints) {
|
|
5245
|
+
const behavior = input.behaviors.get(entry.id);
|
|
5246
|
+
if (!behavior || behavior.writes) continue;
|
|
5247
|
+
for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.trigger} ${entry.signature}: ${store} leaves whole — ${reason}`);
|
|
5248
|
+
}
|
|
5249
|
+
if (lines.length === 0) return [];
|
|
5250
|
+
return [
|
|
5251
|
+
`${lines.length} store(s) handed raw to a page the analysis could not read: every column counted. What the page shows would be less:`,
|
|
5252
|
+
...lines.slice(0, 12),
|
|
5253
|
+
...lines.length > 12 ? [` … and ${lines.length - 12} more`] : []
|
|
5254
|
+
];
|
|
5255
|
+
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Jobs no transaction reaches — plan 0.7 §D.
|
|
5258
|
+
*
|
|
5259
|
+
* A job dispatched by a handler is part of that handler's transaction (§9). One
|
|
5260
|
+
* that nothing reachable dispatches is either a scheduled process — an elementary
|
|
5261
|
+
* process nobody is counting — or dead code, and the code cannot say which. It is
|
|
5262
|
+
* reported, never counted: inventing an elementary process is the error this
|
|
5263
|
+
* package exists to avoid.
|
|
5264
|
+
*/
|
|
5265
|
+
function undispatchedJobWarnings(input) {
|
|
5266
|
+
if (!input.jobs?.length) return [];
|
|
5267
|
+
const reached = /* @__PURE__ */ new Set();
|
|
5268
|
+
for (const behavior of input.behaviors.values()) for (const step of behavior.trace) reached.add(toPosix(step.file));
|
|
5269
|
+
const orphans = input.jobs.filter((job) => !reached.has(job.file));
|
|
5270
|
+
if (orphans.length === 0) return [];
|
|
5271
|
+
const where = (file) => relativeTo(input.app.root, file);
|
|
5272
|
+
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 }) => {
|
|
5273
|
+
const at = ` ${name} (${where(file)}): `;
|
|
5274
|
+
if (scheduledFrom.length > 0) return `${at}scheduled from ${scheduledFrom.map(where).join(", ")}, outside every transaction`;
|
|
5275
|
+
if (dispatchedFrom.length > 0) return `${at}dispatched from ${dispatchedFrom.map(where).join(", ")}, which no transaction reaches`;
|
|
5276
|
+
return `${at}dispatched by nothing in the application — dead code, or a scheduler this analysis does not read`;
|
|
5277
|
+
})];
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* Ace commands counted as transactions — plan 0.7 §C.
|
|
5281
|
+
*
|
|
5282
|
+
* A batch process an operator starts is an elementary process, and it is counted.
|
|
5283
|
+
* Half the commands on the validated applications are development tools — a data
|
|
5284
|
+
* generator, a scaffolder — which the CPM does not count and the code cannot tell
|
|
5285
|
+
* from an importer: both write the same table. So each one is listed with the FP
|
|
5286
|
+
* at stake, the collector's hint beside it, and the way out is a declaration.
|
|
5287
|
+
*/
|
|
5288
|
+
function commandWarnings(entryPoints, functions) {
|
|
5289
|
+
const counted = entryPoints.filter((entry) => entry.kind === "command").map((entry) => ({
|
|
5290
|
+
entry,
|
|
5291
|
+
fn: functions.find((f) => f.id === `tx:${entry.identity}`)
|
|
5292
|
+
})).filter(({ fn }) => fn !== void 0);
|
|
5293
|
+
if (counted.length === 0) return [];
|
|
5294
|
+
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("; ")}` : ""))];
|
|
5295
|
+
}
|
|
5296
|
+
/**
|
|
3061
5297
|
* Transactions that look like the same elementary process.
|
|
3062
5298
|
*
|
|
3063
5299
|
* The CPM counts identical processing logic once. `GET /perfil` and
|
|
@@ -3379,7 +5615,9 @@ var CoverageTooLowError = class extends Error {
|
|
|
3379
5615
|
async function analyze(root, options = {}) {
|
|
3380
5616
|
const app = await discoverApp(root);
|
|
3381
5617
|
const { stores, unresolved: storeProblems } = await collectDataStores(app);
|
|
3382
|
-
const
|
|
5618
|
+
const routes = await collectEntryPoints(app);
|
|
5619
|
+
const routeProblems = routes.unresolved;
|
|
5620
|
+
const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
|
|
3383
5621
|
/** only read when an override names one — but collected once, like everything else */
|
|
3384
5622
|
const jsonSchemas = collectJsonSchemas(app);
|
|
3385
5623
|
const analyzer = createAnalyzer(app, stores, {
|
|
@@ -3461,6 +5699,13 @@ async function analyze(root, options = {}) {
|
|
|
3461
5699
|
by: "request"
|
|
3462
5700
|
}
|
|
3463
5701
|
})),
|
|
5702
|
+
...behavior.commandFields.length ? { commandFields: behavior.commandFields.map((name) => ({
|
|
5703
|
+
name,
|
|
5704
|
+
provenance: {
|
|
5705
|
+
file: emit(app.root),
|
|
5706
|
+
by: "ace-commands"
|
|
5707
|
+
}
|
|
5708
|
+
})) } : {},
|
|
3464
5709
|
opaqueRequest: behavior.opaqueRequest,
|
|
3465
5710
|
outputFields: behavior.outputFields.map((name) => ({
|
|
3466
5711
|
name,
|
|
@@ -3477,6 +5722,9 @@ async function analyze(root, options = {}) {
|
|
|
3477
5722
|
}
|
|
3478
5723
|
})),
|
|
3479
5724
|
transformedStores: behavior.transformedStores,
|
|
5725
|
+
delivered: behavior.delivered,
|
|
5726
|
+
pageReads: behavior.pageReads,
|
|
5727
|
+
unreadablePages: behavior.unreadablePages,
|
|
3480
5728
|
outputReads: behavior.outputReads,
|
|
3481
5729
|
trace: behavior.trace.map((step) => ({
|
|
3482
5730
|
...step,
|
|
@@ -3505,6 +5753,7 @@ async function analyze(root, options = {}) {
|
|
|
3505
5753
|
entryPoints,
|
|
3506
5754
|
behaviors,
|
|
3507
5755
|
jsonSchemas,
|
|
5756
|
+
jobs: collectJobs(app),
|
|
3508
5757
|
writtenAnywhere: analyzer.writtenAnywhere(),
|
|
3509
5758
|
addressedAnywhere: analyzer.addressedAnywhere(),
|
|
3510
5759
|
seededAnywhere: analyzer.seededAnywhere()
|