@filipebraida/adonis-function-points 0.7.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 +70 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-3TxGdS1b.js → fp_calibrate-B_oD9b02.js} +1 -1
- package/build/{fp_count-arGLnVlY.js → fp_count-B_KPNKMO.js} +1 -1
- package/build/{fp_diff-DBBvzq5x.js → fp_diff-B3NhXzrb.js} +1 -1
- package/build/{fp_explain-aNFApwiT.js → fp_explain-C2s6Kpaj.js} +1 -1
- package/build/{fp_inventory-DIjKIC9t.js → fp_inventory-B322MvZT.js} +1 -1
- package/build/{fp_metrics-BpU61waG.js → fp_metrics-DjADb7F6.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-DO2301fV.js → pipeline-C6kHKB9-.js} +1002 -45
- package/build/{resolvers-DaU4uAqT.js → resolvers-DhJO-qvQ.js} +16 -1
- package/build/{runners-Dm7cWGa-.js → runners-BpBJsGMj.js} +2 -2
- package/build/src/albrecht/counter.d.ts +6 -2
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +7 -0
- package/build/src/inventory/graph/deliveries.d.ts +7 -1
- package/build/src/inventory/graph/pages.d.ts +44 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
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-
|
|
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([
|
|
@@ -1288,7 +1288,7 @@ const PRINTERS = new Set([
|
|
|
1288
1288
|
"console"
|
|
1289
1289
|
]);
|
|
1290
1290
|
/** `x.data`, `x.rows` on a paginated / wrapped result hand the collection on */
|
|
1291
|
-
const PASSES_THROUGH = new Set([
|
|
1291
|
+
const PASSES_THROUGH$1 = new Set([
|
|
1292
1292
|
"data",
|
|
1293
1293
|
"rows",
|
|
1294
1294
|
"all",
|
|
@@ -1436,8 +1436,15 @@ function deliveriesIn(ctx) {
|
|
|
1436
1436
|
const method = callee.getName();
|
|
1437
1437
|
const receiver = lastSegmentOf(callee.getExpression());
|
|
1438
1438
|
let payload;
|
|
1439
|
-
|
|
1440
|
-
|
|
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];
|
|
1441
1448
|
else if (isPrinter(callee.getExpression(), ctx.body)) {
|
|
1442
1449
|
any = true;
|
|
1443
1450
|
seen.add(call);
|
|
@@ -1447,7 +1454,9 @@ function deliveriesIn(ctx) {
|
|
|
1447
1454
|
any = true;
|
|
1448
1455
|
if (!payload) continue;
|
|
1449
1456
|
seen.add(call);
|
|
1457
|
+
const before = deliveries.length;
|
|
1450
1458
|
classify(unwrap$1(payload), "", ctx, deliveries, 0);
|
|
1459
|
+
if (via) for (let i = before; i < deliveries.length; i++) deliveries[i].via = via;
|
|
1451
1460
|
}
|
|
1452
1461
|
for (const statement of ctx.body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
|
|
1453
1462
|
if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== ctx.body) continue;
|
|
@@ -1530,14 +1539,14 @@ function classify(value, path, ctx, out, depth) {
|
|
|
1530
1539
|
*/
|
|
1531
1540
|
if (!path && (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isNumericLiteral(value))) return;
|
|
1532
1541
|
if (Node.isObjectLiteralExpression(value)) {
|
|
1533
|
-
for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join(path, property.getName()), ctx, out, depth + 1);
|
|
1542
|
+
for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join$1(path, property.getName()), ctx, out, depth + 1);
|
|
1534
1543
|
else if (Node.isPropertyAssignment(property)) {
|
|
1535
1544
|
const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
|
|
1536
|
-
classify(unwrap$1(property.getInitializer()), join(path, name), ctx, out, depth + 1);
|
|
1545
|
+
classify(unwrap$1(property.getInitializer()), join$1(path, name), ctx, out, depth + 1);
|
|
1537
1546
|
} else if (Node.isSpreadAssignment(property)) classify(unwrap$1(property.getExpression()), path, ctx, out, depth + 1);
|
|
1538
1547
|
else out.push({
|
|
1539
1548
|
kind: "scalar",
|
|
1540
|
-
path: join(path, property.getName?.() ?? "*")
|
|
1549
|
+
path: join$1(path, property.getName?.() ?? "*")
|
|
1541
1550
|
});
|
|
1542
1551
|
return;
|
|
1543
1552
|
}
|
|
@@ -1649,7 +1658,7 @@ function classifyCall(value, path, ctx, out, depth, pick) {
|
|
|
1649
1658
|
classifyMapped(value, callee.getExpression(), path, ctx, out, depth);
|
|
1650
1659
|
return;
|
|
1651
1660
|
}
|
|
1652
|
-
if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH.has(method) && value.getArguments().length === 0) {
|
|
1661
|
+
if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH$1.has(method) && value.getArguments().length === 0) {
|
|
1653
1662
|
classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
|
|
1654
1663
|
return;
|
|
1655
1664
|
}
|
|
@@ -1660,7 +1669,7 @@ function classifyCall(value, path, ctx, out, depth, pick) {
|
|
|
1660
1669
|
});
|
|
1661
1670
|
else for (const key of PAGINATOR_META) out.push({
|
|
1662
1671
|
kind: "scalar",
|
|
1663
|
-
path: join(path, key)
|
|
1672
|
+
path: join$1(path, key)
|
|
1664
1673
|
});
|
|
1665
1674
|
return;
|
|
1666
1675
|
}
|
|
@@ -1754,10 +1763,15 @@ function classifyCall(value, path, ctx, out, depth, pick) {
|
|
|
1754
1763
|
if (Node.isPropertyAccessExpression(callee)) {
|
|
1755
1764
|
const root = chainRootOf(callee.getExpression());
|
|
1756
1765
|
if (root && ctx.symbols.has(root)) {
|
|
1757
|
-
out.push({
|
|
1766
|
+
if (readsField(callee.getExpression())) out.push({
|
|
1758
1767
|
kind: "scalar",
|
|
1759
1768
|
path
|
|
1760
1769
|
});
|
|
1770
|
+
else out.push({
|
|
1771
|
+
kind: "store",
|
|
1772
|
+
store: ctx.symbols.get(root),
|
|
1773
|
+
path
|
|
1774
|
+
});
|
|
1761
1775
|
return;
|
|
1762
1776
|
}
|
|
1763
1777
|
if (root && root !== "this") {
|
|
@@ -1957,7 +1971,7 @@ function classifyAccess(value, path, ctx, out, depth) {
|
|
|
1957
1971
|
*/
|
|
1958
1972
|
const property = Node.isPropertyAccessExpression(value) ? value.getName() : keyOfElementAccess(value);
|
|
1959
1973
|
if (root && ctx.symbols.has(root)) {
|
|
1960
|
-
if (!property || PASSES_THROUGH.has(property)) out.push({
|
|
1974
|
+
if (!property || PASSES_THROUGH$1.has(property)) out.push({
|
|
1961
1975
|
kind: "store",
|
|
1962
1976
|
store: ctx.symbols.get(root),
|
|
1963
1977
|
path
|
|
@@ -1982,7 +1996,7 @@ function classifyAccess(value, path, ctx, out, depth) {
|
|
|
1982
1996
|
* `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
|
|
1983
1997
|
* destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
|
|
1984
1998
|
*/
|
|
1985
|
-
const own = property && !PASSES_THROUGH.has(property) ? property : void 0;
|
|
1999
|
+
const own = property && !PASSES_THROUGH$1.has(property) ? property : void 0;
|
|
1986
2000
|
const pick = [bound.pick, own].filter(Boolean).join(".") || void 0;
|
|
1987
2001
|
if (own && !ctx.followed.has(bound.initializer)) {
|
|
1988
2002
|
out.push({
|
|
@@ -2020,7 +2034,7 @@ function keyOfElementAccess(value) {
|
|
|
2020
2034
|
if (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) return argument.getLiteralValue();
|
|
2021
2035
|
return "*";
|
|
2022
2036
|
}
|
|
2023
|
-
const join = (prefix, name) => prefix ? `${prefix}.${name}` : name;
|
|
2037
|
+
const join$1 = (prefix, name) => prefix ? `${prefix}.${name}` : name;
|
|
2024
2038
|
/** `await x` binds x: the value, not the promise */
|
|
2025
2039
|
function unwrapAwait$1(node) {
|
|
2026
2040
|
let current = unwrap$1(node);
|
|
@@ -2073,6 +2087,514 @@ function isEchoBinding(name, body) {
|
|
|
2073
2087
|
}
|
|
2074
2088
|
const isRequestLike = (node) => lastSegmentOf(node) === "request";
|
|
2075
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
|
|
2076
2598
|
//#region src/inventory/graph/call_graph.ts
|
|
2077
2599
|
/**
|
|
2078
2600
|
* Fields declared by the validators used in this body.
|
|
@@ -2427,16 +2949,188 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2427
2949
|
factsCache.set(key, facts);
|
|
2428
2950
|
return facts;
|
|
2429
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
|
+
}
|
|
2430
3116
|
function computeFacts(ref) {
|
|
2431
3117
|
const file = sourceFile(ref.file);
|
|
2432
3118
|
if (!file) return null;
|
|
2433
3119
|
const body = findBody(file, ref);
|
|
2434
3120
|
if (!body) return null;
|
|
2435
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
|
+
}
|
|
2436
3130
|
/** the class this body belongs to: how `this.something` resolves */
|
|
2437
3131
|
const owner = body.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
2438
3132
|
const injected = injectedFor(owner, file, app);
|
|
2439
|
-
const symbols = storeSymbolsFor(body, file, app, storesByName);
|
|
3133
|
+
const symbols = storeSymbolsFor(body, file, app, storesByName, relationsByStore);
|
|
2440
3134
|
const accesses = [];
|
|
2441
3135
|
const followUps = [];
|
|
2442
3136
|
const unresolved = [];
|
|
@@ -2457,6 +3151,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2457
3151
|
resolveSpecifier: app.resolveSpecifier,
|
|
2458
3152
|
sourceFile
|
|
2459
3153
|
};
|
|
3154
|
+
bindLocals(body, symbols, context, resolvers);
|
|
2460
3155
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2461
3156
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
2462
3157
|
if (access) {
|
|
@@ -2552,6 +3247,21 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2552
3247
|
followedCalls.set(call, resolved.refs);
|
|
2553
3248
|
continue;
|
|
2554
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
|
+
});
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
2555
3265
|
if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
|
|
2556
3266
|
file: ref.file,
|
|
2557
3267
|
line: call.getStartLineNumber(),
|
|
@@ -2631,7 +3341,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2631
3341
|
*/
|
|
2632
3342
|
if (!isApplicationCode(app.root, file.getFilePath())) {
|
|
2633
3343
|
if (isSeeder(app.root, file.getFilePath())) {
|
|
2634
|
-
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
3344
|
+
const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
|
|
2635
3345
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2636
3346
|
const access = symbols.size > 0 ? detectAccess(call, symbols, relationsByStore) : null;
|
|
2637
3347
|
if (access?.mode !== "write") continue;
|
|
@@ -2641,7 +3351,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2641
3351
|
}
|
|
2642
3352
|
continue;
|
|
2643
3353
|
}
|
|
2644
|
-
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
3354
|
+
const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
|
|
2645
3355
|
if (symbols.size === 0) continue;
|
|
2646
3356
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2647
3357
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
@@ -2695,6 +3405,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2695
3405
|
const deliveredFields = /* @__PURE__ */ new Set();
|
|
2696
3406
|
const deliveredOpaque = /* @__PURE__ */ new Set();
|
|
2697
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 = [];
|
|
2698
3410
|
let anyDelivery = false;
|
|
2699
3411
|
const outputReads = /* @__PURE__ */ new Map();
|
|
2700
3412
|
const trace = [];
|
|
@@ -2725,6 +3437,11 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2725
3437
|
switch (item.kind) {
|
|
2726
3438
|
case "store":
|
|
2727
3439
|
deliveredStores.add(item.store);
|
|
3440
|
+
if (item.via) rawDeliveries.push({
|
|
3441
|
+
...item.via,
|
|
3442
|
+
store: item.store,
|
|
3443
|
+
path: item.path
|
|
3444
|
+
});
|
|
2728
3445
|
return;
|
|
2729
3446
|
case "scalar":
|
|
2730
3447
|
deliveredFields.add(item.path || "<value>");
|
|
@@ -2760,7 +3477,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2760
3477
|
if (r.kind === "call") deliver({
|
|
2761
3478
|
...r,
|
|
2762
3479
|
path: item.path,
|
|
2763
|
-
pick: rest
|
|
3480
|
+
pick: rest,
|
|
3481
|
+
via: item.via
|
|
2764
3482
|
}, depth + 1, seen);
|
|
2765
3483
|
else if (r.kind === "store" || r.kind === "scalar") deliveredFields.add(item.path || "<value>");
|
|
2766
3484
|
}
|
|
@@ -2780,7 +3498,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2780
3498
|
const path = [item.path, rest].filter(Boolean).join(".");
|
|
2781
3499
|
deliver({
|
|
2782
3500
|
...r,
|
|
2783
|
-
path
|
|
3501
|
+
path,
|
|
3502
|
+
via: item.via
|
|
2784
3503
|
}, depth + 1, seen);
|
|
2785
3504
|
}
|
|
2786
3505
|
resolved = true;
|
|
@@ -2789,7 +3508,14 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2789
3508
|
if (item.pick) continue;
|
|
2790
3509
|
const read = storesReadBy(ref, depth + 1);
|
|
2791
3510
|
if (read.size > 0) {
|
|
2792
|
-
for (const store of read)
|
|
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
|
+
}
|
|
2793
3519
|
resolved = true;
|
|
2794
3520
|
}
|
|
2795
3521
|
}
|
|
@@ -2809,7 +3535,10 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2809
3535
|
* received, so the rows' stores leave. Nothing handed in: opaque.
|
|
2810
3536
|
*/
|
|
2811
3537
|
if (item.args.length > 0) {
|
|
2812
|
-
for (const argument of item.args) deliver(
|
|
3538
|
+
for (const argument of item.args) deliver({
|
|
3539
|
+
...argument,
|
|
3540
|
+
via: item.via
|
|
3541
|
+
}, depth, seen);
|
|
2813
3542
|
return;
|
|
2814
3543
|
}
|
|
2815
3544
|
deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
|
|
@@ -2903,6 +3632,18 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2903
3632
|
}
|
|
2904
3633
|
};
|
|
2905
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
|
+
});
|
|
2906
3647
|
return {
|
|
2907
3648
|
writes,
|
|
2908
3649
|
touches: [...touches].sort(),
|
|
@@ -2921,6 +3662,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
2921
3662
|
opaqueFields: [...deliveredOpaque].sort(),
|
|
2922
3663
|
stores: [...deliveredStores].sort()
|
|
2923
3664
|
},
|
|
3665
|
+
pageReads: Object.fromEntries([...pages.columns.entries()].map(([store, columns]) => [store, [...columns].sort()])),
|
|
3666
|
+
unreadablePages: Object.fromEntries(pages.unreadable),
|
|
2924
3667
|
outputReads: Object.fromEntries([...outputReads.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([store, read]) => [store, {
|
|
2925
3668
|
whole: read.whole,
|
|
2926
3669
|
selected: [...read.selected].sort(),
|
|
@@ -2972,7 +3715,7 @@ function findBody(file, ref) {
|
|
|
2972
3715
|
* from them: `const invite = await Invite.findOrFail(...)` makes `invite.save()`
|
|
2973
3716
|
* count as a write to `Invite`.
|
|
2974
3717
|
*/
|
|
2975
|
-
function storeSymbolsFor(body, file, app, stores) {
|
|
3718
|
+
function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ new Map()) {
|
|
2976
3719
|
const symbols = /* @__PURE__ */ new Map();
|
|
2977
3720
|
for (const declaration of file.getImportDeclarations()) {
|
|
2978
3721
|
if (!app.resolveSpecifier(declaration.getModuleSpecifierValue())) continue;
|
|
@@ -3007,7 +3750,24 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
3007
3750
|
for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
3008
3751
|
const initializer = declaration.getInitializer();
|
|
3009
3752
|
const name = declaration.getNameNode();
|
|
3010
|
-
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
|
+
}
|
|
3011
3771
|
const root = rootSymbolOf(initializer);
|
|
3012
3772
|
const store = root ? symbols.get(root) : void 0;
|
|
3013
3773
|
if (store) symbols.set(name.getText(), store);
|
|
@@ -3015,8 +3775,8 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
3015
3775
|
if (Node.isMethodDeclaration(body) || Node.isFunctionDeclaration(body)) for (const parameter of body.getParameters()) {
|
|
3016
3776
|
const typeNode = parameter.getTypeNode();
|
|
3017
3777
|
const nameNode = parameter.getNameNode();
|
|
3018
|
-
const typeName = typeNode?.getText();
|
|
3019
|
-
if (typeName &&
|
|
3778
|
+
const typeName = storeNamedBy(typeNode?.getText(), stores);
|
|
3779
|
+
if (typeName && Node.isIdentifier(nameNode)) {
|
|
3020
3780
|
symbols.set(nameNode.getText(), typeName);
|
|
3021
3781
|
continue;
|
|
3022
3782
|
}
|
|
@@ -3027,32 +3787,56 @@ function storeSymbolsFor(body, file, app, stores) {
|
|
|
3027
3787
|
* Registers the PATH `input.invite`, because that is how the write
|
|
3028
3788
|
* appears: `input.invite.save()`.
|
|
3029
3789
|
*/
|
|
3030
|
-
if (typeNode && Node.isIdentifier(nameNode)) {
|
|
3031
|
-
|
|
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);
|
|
3032
3793
|
}
|
|
3033
3794
|
/**
|
|
3034
|
-
* Destructured form: `handle({ invite }: { invite: Invite })
|
|
3035
|
-
*
|
|
3036
|
-
*
|
|
3037
|
-
*
|
|
3038
|
-
*
|
|
3039
|
-
* 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).
|
|
3040
3800
|
*/
|
|
3041
3801
|
const binding = nameNode.asKind(SyntaxKind.ObjectBindingPattern);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
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);
|
|
3049
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;
|
|
3050
3821
|
for (const element of binding.getElements()) {
|
|
3051
3822
|
const property = element.getPropertyNameNode()?.getText() ?? element.getName();
|
|
3052
|
-
const
|
|
3053
|
-
if (
|
|
3823
|
+
const store = symbols.get(`${root}.${property}`);
|
|
3824
|
+
if (store) symbols.set(element.getName(), store);
|
|
3054
3825
|
}
|
|
3055
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
|
+
}
|
|
3056
3840
|
return symbols;
|
|
3057
3841
|
}
|
|
3058
3842
|
/**
|
|
@@ -3116,6 +3900,67 @@ function resolveTypeToFile(typeName, file, app) {
|
|
|
3116
3900
|
* Accepts an inline type literal and a named type declared in this file or
|
|
3117
3901
|
* imported from the application. Anything else yields empty — no guessing.
|
|
3118
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
|
+
}
|
|
3119
3964
|
function membersOfType(typeNode, file, app) {
|
|
3120
3965
|
const members = /* @__PURE__ */ new Map();
|
|
3121
3966
|
const collect = (node) => {
|
|
@@ -3230,6 +4075,77 @@ function isWorthReporting(call, symbols, imports) {
|
|
|
3230
4075
|
if (root === "this") return true;
|
|
3231
4076
|
return imports.has(root);
|
|
3232
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
|
+
}
|
|
3233
4149
|
/** `(await x())` and `x()` are the same receiver for this purpose. */
|
|
3234
4150
|
function unwrapAwait(node) {
|
|
3235
4151
|
let current = node;
|
|
@@ -4051,6 +4967,19 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
4051
4967
|
}
|
|
4052
4968
|
continue;
|
|
4053
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
|
+
}
|
|
4054
4983
|
for (const column of attributes) {
|
|
4055
4984
|
if (excluded.has(column.name)) continue;
|
|
4056
4985
|
add(`${store}.${column.name}`, `output:${store}.${column.name}${mark(column.name)}`);
|
|
@@ -4141,13 +5070,17 @@ const RULESET = "afp";
|
|
|
4141
5070
|
* Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
|
|
4142
5071
|
* props, the response payload, what a command prints) read back to their origin;
|
|
4143
5072
|
* a function of the same file and a `.map(fn)` by reference are followed, so
|
|
4144
|
-
* FTRs move; and ace commands are transactions, with flags as input.
|
|
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.
|
|
4145
5078
|
*
|
|
4146
5079
|
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
4147
5080
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
4148
5081
|
* exists for exactly that, and only this constant arms it.
|
|
4149
5082
|
*/
|
|
4150
|
-
const RULESET_VERSION = "1.
|
|
5083
|
+
const RULESET_VERSION = "1.7.0";
|
|
4151
5084
|
function count(input, options = {}) {
|
|
4152
5085
|
const warnings = [];
|
|
4153
5086
|
const usage = usageOf(input);
|
|
@@ -4267,6 +5200,7 @@ function count(input, options = {}) {
|
|
|
4267
5200
|
warnings.push(...unreadableDeliveryWarnings(input));
|
|
4268
5201
|
warnings.push(...commandWarnings(entryPoints, transactionalFunctions));
|
|
4269
5202
|
warnings.push(...undispatchedJobWarnings(input));
|
|
5203
|
+
warnings.push(...unreadablePageWarnings(input));
|
|
4270
5204
|
warnings.push(...lookAlikeWarnings(functions));
|
|
4271
5205
|
warnings.push(...seededOnlyWarnings(functions, grouping.members, input.seededAnywhere));
|
|
4272
5206
|
return {
|
|
@@ -4299,6 +5233,27 @@ function unreadableDeliveryWarnings(input) {
|
|
|
4299
5233
|
];
|
|
4300
5234
|
}
|
|
4301
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
|
+
/**
|
|
4302
5257
|
* Jobs no transaction reaches — plan 0.7 §D.
|
|
4303
5258
|
*
|
|
4304
5259
|
* A job dispatched by a handler is part of that handler's transaction (§9). One
|
|
@@ -4768,6 +5723,8 @@ async function analyze(root, options = {}) {
|
|
|
4768
5723
|
})),
|
|
4769
5724
|
transformedStores: behavior.transformedStores,
|
|
4770
5725
|
delivered: behavior.delivered,
|
|
5726
|
+
pageReads: behavior.pageReads,
|
|
5727
|
+
unreadablePages: behavior.unreadablePages,
|
|
4771
5728
|
outputReads: behavior.outputReads,
|
|
4772
5729
|
trace: behavior.trace.map((step) => ({
|
|
4773
5730
|
...step,
|