@facilis.dev/analyze 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/analyze.d.ts +17 -0
- package/dist/analyze.js +99 -0
- package/dist/ast/node.d.ts +29 -0
- package/dist/ast/node.js +85 -0
- package/dist/ast/walk.d.ts +8 -0
- package/dist/ast/walk.js +40 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +12 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.js +51 -0
- package/dist/graph-edges.d.ts +3 -0
- package/dist/graph-edges.js +148 -0
- package/dist/graph-ids.d.ts +6 -0
- package/dist/graph-ids.js +22 -0
- package/dist/graph.d.ts +15 -0
- package/dist/graph.js +158 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/json.d.ts +8 -0
- package/dist/json.js +31 -0
- package/dist/module-graph.d.ts +33 -0
- package/dist/module-graph.js +86 -0
- package/dist/module-info.d.ts +27 -0
- package/dist/module-info.js +143 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +41 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +1 -0
- package/dist/passes/clients.d.ts +22 -0
- package/dist/passes/clients.js +167 -0
- package/dist/passes/config.d.ts +16 -0
- package/dist/passes/config.js +229 -0
- package/dist/passes/file-facts.d.ts +13 -0
- package/dist/passes/file-facts.js +130 -0
- package/dist/passes/i18n.d.ts +14 -0
- package/dist/passes/i18n.js +55 -0
- package/dist/passes/job-relations-diagnostics.d.ts +9 -0
- package/dist/passes/job-relations-diagnostics.js +38 -0
- package/dist/passes/job-relations.d.ts +18 -0
- package/dist/passes/job-relations.js +52 -0
- package/dist/passes/jobs.d.ts +20 -0
- package/dist/passes/jobs.js +133 -0
- package/dist/passes/jsx.d.ts +22 -0
- package/dist/passes/jsx.js +89 -0
- package/dist/passes/path-expression.d.ts +26 -0
- package/dist/passes/path-expression.js +265 -0
- package/dist/passes/route-config.d.ts +24 -0
- package/dist/passes/route-config.js +152 -0
- package/dist/passes/route-description.d.ts +13 -0
- package/dist/passes/route-description.js +23 -0
- package/dist/passes/route-elevation.d.ts +18 -0
- package/dist/passes/route-elevation.js +136 -0
- package/dist/passes/route-handler.d.ts +27 -0
- package/dist/passes/route-handler.js +87 -0
- package/dist/passes/route-location.d.ts +16 -0
- package/dist/passes/route-location.js +58 -0
- package/dist/passes/route-relation-resolve.d.ts +51 -0
- package/dist/passes/route-relation-resolve.js +172 -0
- package/dist/passes/route-relations-diagnostics.d.ts +15 -0
- package/dist/passes/route-relations-diagnostics.js +79 -0
- package/dist/passes/route-relations-follow.d.ts +16 -0
- package/dist/passes/route-relations-follow.js +116 -0
- package/dist/passes/route-relations-usage.d.ts +50 -0
- package/dist/passes/route-relations-usage.js +44 -0
- package/dist/passes/route-relations-walk.d.ts +25 -0
- package/dist/passes/route-relations-walk.js +199 -0
- package/dist/passes/route-relations.d.ts +17 -0
- package/dist/passes/route-relations.js +43 -0
- package/dist/passes/route-table.d.ts +21 -0
- package/dist/passes/route-table.js +104 -0
- package/dist/passes/route-tables.d.ts +10 -0
- package/dist/passes/route-tables.js +42 -0
- package/dist/passes/routes.d.ts +18 -0
- package/dist/passes/routes.js +152 -0
- package/dist/passes/schema-column-types.d.ts +9 -0
- package/dist/passes/schema-column-types.js +55 -0
- package/dist/passes/schema-columns.d.ts +35 -0
- package/dist/passes/schema-columns.js +197 -0
- package/dist/passes/schema-dsl.d.ts +24 -0
- package/dist/passes/schema-dsl.js +117 -0
- package/dist/passes/schema-file-links-ddl.d.ts +18 -0
- package/dist/passes/schema-file-links-ddl.js +106 -0
- package/dist/passes/schema-file-links.d.ts +19 -0
- package/dist/passes/schema-file-links.js +144 -0
- package/dist/passes/schema-foreign-keys.d.ts +21 -0
- package/dist/passes/schema-foreign-keys.js +80 -0
- package/dist/passes/schema-policies.d.ts +15 -0
- package/dist/passes/schema-policies.js +201 -0
- package/dist/passes/schema-re-exports.d.ts +5 -0
- package/dist/passes/schema-re-exports.js +77 -0
- package/dist/passes/schema-relations.d.ts +8 -0
- package/dist/passes/schema-relations.js +182 -0
- package/dist/passes/schema-scope.d.ts +7 -0
- package/dist/passes/schema-scope.js +20 -0
- package/dist/passes/schema-tables.d.ts +13 -0
- package/dist/passes/schema-tables.js +149 -0
- package/dist/passes/schema.d.ts +9 -0
- package/dist/passes/schema.js +55 -0
- package/dist/passes/screen-components.d.ts +22 -0
- package/dist/passes/screen-components.js +143 -0
- package/dist/passes/screen-preview.d.ts +29 -0
- package/dist/passes/screen-preview.js +159 -0
- package/dist/passes/screens.d.ts +12 -0
- package/dist/passes/screens.js +278 -0
- package/dist/passes/static-read.d.ts +47 -0
- package/dist/passes/static-read.js +151 -0
- package/dist/passes/views.d.ts +20 -0
- package/dist/passes/views.js +101 -0
- package/dist/passes/workflow-tables.d.ts +18 -0
- package/dist/passes/workflow-tables.js +71 -0
- package/dist/passes/workflows.d.ts +8 -0
- package/dist/passes/workflows.js +160 -0
- package/dist/path-resolver.d.ts +20 -0
- package/dist/path-resolver.js +134 -0
- package/dist/revision.d.ts +2 -0
- package/dist/revision.js +23 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.js +13 -0
- package/dist/static-eval.d.ts +36 -0
- package/dist/static-eval.js +215 -0
- package/dist/types-graph.d.ts +130 -0
- package/dist/types-graph.js +4 -0
- package/dist/types-jobs.d.ts +28 -0
- package/dist/types-jobs.js +14 -0
- package/dist/types-relations.d.ts +49 -0
- package/dist/types-relations.js +11 -0
- package/dist/types-workflows.d.ts +36 -0
- package/dist/types-workflows.js +3 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.js +1 -0
- package/package.json +49 -4
- package/src/analyze.ts +119 -0
- package/src/ast/node.ts +102 -0
- package/src/ast/walk.ts +37 -0
- package/src/diagnostics.ts +22 -0
- package/src/fs.ts +59 -0
- package/src/graph-edges.ts +167 -0
- package/src/graph-ids.ts +31 -0
- package/src/graph.ts +190 -0
- package/src/index.ts +24 -0
- package/src/json.ts +34 -0
- package/src/module-graph.ts +117 -0
- package/src/module-info.ts +162 -0
- package/src/node.ts +54 -0
- package/src/parser.ts +19 -0
- package/src/passes/clients.ts +233 -0
- package/src/passes/config.ts +278 -0
- package/src/passes/file-facts.ts +142 -0
- package/src/passes/i18n.ts +66 -0
- package/src/passes/job-relations-diagnostics.ts +60 -0
- package/src/passes/job-relations.ts +76 -0
- package/src/passes/jobs.ts +181 -0
- package/src/passes/jsx.ts +92 -0
- package/src/passes/path-expression.ts +297 -0
- package/src/passes/route-config.ts +188 -0
- package/src/passes/route-description.ts +33 -0
- package/src/passes/route-elevation.ts +161 -0
- package/src/passes/route-handler.ts +105 -0
- package/src/passes/route-location.ts +62 -0
- package/src/passes/route-relation-resolve.ts +228 -0
- package/src/passes/route-relations-diagnostics.ts +115 -0
- package/src/passes/route-relations-follow.ts +146 -0
- package/src/passes/route-relations-usage.ts +98 -0
- package/src/passes/route-relations-walk.ts +255 -0
- package/src/passes/route-relations.ts +62 -0
- package/src/passes/route-table.ts +123 -0
- package/src/passes/route-tables.ts +50 -0
- package/src/passes/routes.ts +188 -0
- package/src/passes/schema-column-types.ts +62 -0
- package/src/passes/schema-columns.ts +287 -0
- package/src/passes/schema-dsl.ts +136 -0
- package/src/passes/schema-file-links-ddl.ts +125 -0
- package/src/passes/schema-file-links.ts +213 -0
- package/src/passes/schema-foreign-keys.ts +121 -0
- package/src/passes/schema-policies.ts +270 -0
- package/src/passes/schema-re-exports.ts +95 -0
- package/src/passes/schema-relations.ts +239 -0
- package/src/passes/schema-scope.ts +29 -0
- package/src/passes/schema-tables.ts +216 -0
- package/src/passes/schema.ts +67 -0
- package/src/passes/screen-components.ts +162 -0
- package/src/passes/screen-preview.ts +268 -0
- package/src/passes/screens.ts +291 -0
- package/src/passes/static-read.ts +196 -0
- package/src/passes/views.ts +125 -0
- package/src/passes/workflow-tables.ts +87 -0
- package/src/passes/workflows.ts +213 -0
- package/src/path-resolver.ts +146 -0
- package/src/revision.ts +25 -0
- package/src/source.ts +15 -0
- package/src/static-eval.ts +242 -0
- package/src/types-graph.ts +161 -0
- package/src/types-jobs.ts +40 -0
- package/src/types-relations.ts +64 -0
- package/src/types-workflows.ts +42 -0
- package/src/types.ts +299 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name resolution for the route-relations pass (D-157). Split from route-relations.ts so both
|
|
3
|
+
* files stay under the 300-line limit.
|
|
4
|
+
*
|
|
5
|
+
* Two questions are answered here, without executing project code:
|
|
6
|
+
* - which relation (table or view) an expression denotes, following same-file consts, project
|
|
7
|
+
* imports, re-exports and namespace members;
|
|
8
|
+
* - which db client an expression denotes, so a use can be attributed to `ctx.db` or to one
|
|
9
|
+
* audited elevation callback.
|
|
10
|
+
*
|
|
11
|
+
* Anything outside the static subset is explicit: "opaque" means a package binding we cannot
|
|
12
|
+
* inspect (no error, the runtime guard is the authority), "unresolved" means dynamic project
|
|
13
|
+
* code (an error, D-157: route-table-unresolved).
|
|
14
|
+
*/
|
|
15
|
+
import { callArguments, identifierName, memberName, unwrapExpression, } from "../ast/node.js";
|
|
16
|
+
import { walk } from "../ast/walk.js";
|
|
17
|
+
import { localInitializer } from "../module-info.js";
|
|
18
|
+
import { findDeclaration } from "./static-read.js";
|
|
19
|
+
export const ROUTE_CLIENT = { elevation: null };
|
|
20
|
+
const UNRESOLVED = { kind: "unresolved" };
|
|
21
|
+
const OPAQUE = { kind: "opaque" };
|
|
22
|
+
/** Table and view bindings, keyed by declaring file + binding (D-157). */
|
|
23
|
+
export function createRelationIndex(tables, views) {
|
|
24
|
+
const byBinding = new Map();
|
|
25
|
+
const names = new Set();
|
|
26
|
+
for (const table of tables) {
|
|
27
|
+
names.add(table.name);
|
|
28
|
+
if (table.binding !== null)
|
|
29
|
+
byBinding.set(table.source.file + "#" + table.binding, table.name);
|
|
30
|
+
}
|
|
31
|
+
for (const view of views) {
|
|
32
|
+
names.add(view.name);
|
|
33
|
+
if (view.binding !== null)
|
|
34
|
+
byBinding.set(view.source.file + "#" + view.binding, view.name);
|
|
35
|
+
}
|
|
36
|
+
const lookup = { byBinding, names };
|
|
37
|
+
return {
|
|
38
|
+
names,
|
|
39
|
+
resolve: (graph, file, node, aliases, depth = 0) => resolveRelation(graph, file, node, aliases, lookup, depth),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const MAX_RELATION_DEPTH = 8;
|
|
43
|
+
function resolveRelation(graph, file, node, aliases, lookup, depth) {
|
|
44
|
+
const expression = unwrapExpression(node);
|
|
45
|
+
if (expression === undefined || depth > MAX_RELATION_DEPTH)
|
|
46
|
+
return UNRESOLVED;
|
|
47
|
+
if (expression.type === "Identifier") {
|
|
48
|
+
const local = identifierName(expression);
|
|
49
|
+
if (local === undefined)
|
|
50
|
+
return UNRESOLVED;
|
|
51
|
+
const alias = aliases.get(local);
|
|
52
|
+
if (alias !== undefined)
|
|
53
|
+
return { kind: "resolved", name: alias };
|
|
54
|
+
const initializer = localInitializer(file.program, local);
|
|
55
|
+
if (initializer !== undefined) {
|
|
56
|
+
return resolveRelation(graph, file, initializer, aliases, lookup, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
const binding = graph.resolveBinding(file.path, local);
|
|
59
|
+
if (binding === null) {
|
|
60
|
+
// Framework/package sources are often absent from the file map (the browser IDE's VFS
|
|
61
|
+
// carries project files only): an imported binding we cannot inspect is opaque, so the
|
|
62
|
+
// runtime guard stays the authority instead of a false "unresolved" error (D-157).
|
|
63
|
+
if (file.module.imports.some((entry) => entry.local === local))
|
|
64
|
+
return OPAQUE;
|
|
65
|
+
// A project file map may still omit a framework source: fall back to the SQL name.
|
|
66
|
+
return lookup.names.has(local) ? { kind: "resolved", name: local } : UNRESOLVED;
|
|
67
|
+
}
|
|
68
|
+
const direct = lookup.byBinding.get(binding.file + "#" + binding.name);
|
|
69
|
+
if (direct !== undefined)
|
|
70
|
+
return { kind: "resolved", name: direct };
|
|
71
|
+
return binding.file.startsWith("node_modules/") ? OPAQUE : UNRESOLVED;
|
|
72
|
+
}
|
|
73
|
+
if (expression.type === "MemberExpression") {
|
|
74
|
+
const property = memberName(expression);
|
|
75
|
+
const object = unwrapExpression(expression.object);
|
|
76
|
+
if (property === undefined || object?.type !== "Identifier")
|
|
77
|
+
return UNRESOLVED;
|
|
78
|
+
const namespace = identifierName(object);
|
|
79
|
+
if (namespace === undefined)
|
|
80
|
+
return UNRESOLVED;
|
|
81
|
+
const imported = file.module.imports.find((entry) => entry.local === namespace);
|
|
82
|
+
if (imported === undefined || imported.kind !== "namespace")
|
|
83
|
+
return UNRESOLVED;
|
|
84
|
+
const target = graph.resolve(file.path, imported.specifier);
|
|
85
|
+
if (target === null)
|
|
86
|
+
return OPAQUE;
|
|
87
|
+
const name = lookup.byBinding.get(target + "#" + property);
|
|
88
|
+
return name === undefined ? UNRESOLVED : { kind: "resolved", name };
|
|
89
|
+
}
|
|
90
|
+
if (expression.type === "CallExpression") {
|
|
91
|
+
// alias(todos, "t") and similar wrappers: the relation is the first argument that resolves.
|
|
92
|
+
for (const argument of callArguments(expression)) {
|
|
93
|
+
const resolution = resolveRelation(graph, file, argument, aliases, lookup, depth + 1);
|
|
94
|
+
if (resolution.kind !== "unresolved")
|
|
95
|
+
return resolution;
|
|
96
|
+
}
|
|
97
|
+
return UNRESOLVED;
|
|
98
|
+
}
|
|
99
|
+
return UNRESOLVED;
|
|
100
|
+
}
|
|
101
|
+
/** The db client an expression denotes directly (a local binding or `ctx.db`), if any. */
|
|
102
|
+
export function clientOf(node, env, contextName) {
|
|
103
|
+
const expression = unwrapExpression(node);
|
|
104
|
+
if (expression === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
if (expression.type === "Identifier") {
|
|
107
|
+
const local = identifierName(expression);
|
|
108
|
+
return local === undefined ? null : (env.get(local) ?? null);
|
|
109
|
+
}
|
|
110
|
+
if (expression.type === "MemberExpression" && memberName(expression) === "db") {
|
|
111
|
+
const object = unwrapExpression(expression.object);
|
|
112
|
+
if (object?.type === "Identifier" && identifierName(object) === contextName)
|
|
113
|
+
return ROUTE_CLIENT;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
/** The client at the root of a fluent chain (`db.select()`, `db.query.todos`). */
|
|
118
|
+
export function chainClient(node, env, contextName) {
|
|
119
|
+
const expression = unwrapExpression(node);
|
|
120
|
+
if (expression === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const direct = clientOf(expression, env, contextName);
|
|
123
|
+
if (direct !== null)
|
|
124
|
+
return direct;
|
|
125
|
+
if (expression.type === "CallExpression")
|
|
126
|
+
return chainClient(expression.callee, env, contextName);
|
|
127
|
+
if (expression.type === "MemberExpression")
|
|
128
|
+
return chainClient(expression.object, env, contextName);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/** True when a callee argument carries a db client anywhere (opaque callee detection). */
|
|
132
|
+
export function carriesClient(node, env, contextName) {
|
|
133
|
+
let found = false;
|
|
134
|
+
walk(node, (candidate) => {
|
|
135
|
+
if (!found && clientOf(candidate, env, contextName) !== null)
|
|
136
|
+
found = true;
|
|
137
|
+
});
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
const FUNCTION_TYPES = new Set([
|
|
141
|
+
"FunctionExpression",
|
|
142
|
+
"FunctionDeclaration",
|
|
143
|
+
"ArrowFunctionExpression",
|
|
144
|
+
]);
|
|
145
|
+
function functionNode(node) {
|
|
146
|
+
const candidate = unwrapExpression(node);
|
|
147
|
+
return candidate !== undefined && FUNCTION_TYPES.has(candidate.type) ? candidate : undefined;
|
|
148
|
+
}
|
|
149
|
+
/** A project-local function (never a package one: package calls stay opaque, D-157). */
|
|
150
|
+
export function resolveLocalFunction(graph, file, callee) {
|
|
151
|
+
const expression = unwrapExpression(callee);
|
|
152
|
+
if (expression?.type !== "Identifier")
|
|
153
|
+
return null;
|
|
154
|
+
const name = identifierName(expression);
|
|
155
|
+
if (name === undefined)
|
|
156
|
+
return null;
|
|
157
|
+
const declared = functionNode(localInitializer(file.program, name)) ??
|
|
158
|
+
functionNode(findDeclaration(file.program, name));
|
|
159
|
+
if (declared !== undefined)
|
|
160
|
+
return { key: file.path + "#" + name, file, node: declared };
|
|
161
|
+
const binding = graph.resolveBinding(file.path, name);
|
|
162
|
+
if (binding === null || binding.file.startsWith("node_modules/"))
|
|
163
|
+
return null;
|
|
164
|
+
const target = graph.file(binding.file);
|
|
165
|
+
if (target === undefined)
|
|
166
|
+
return null;
|
|
167
|
+
const found = functionNode(localInitializer(target.program, binding.name)) ??
|
|
168
|
+
functionNode(findDeclaration(target.program, binding.name));
|
|
169
|
+
if (found === undefined)
|
|
170
|
+
return null;
|
|
171
|
+
return { key: target.path + "#" + binding.name, file: target, node: found };
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-157 diagnostics derived from a route's relation-usage fact:
|
|
3
|
+
*
|
|
4
|
+
* - route-table-undeclared (error): used and not declared in dbTables;
|
|
5
|
+
* - route-table-unused (warning): declared and not used — suppressed when a project-external
|
|
6
|
+
* callee received the client or db.execute is present, where the declaration stays
|
|
7
|
+
* authoritative (the runtime guard is the enforcement);
|
|
8
|
+
* - route-table-unresolved (error): dynamic/unresolvable relation expression;
|
|
9
|
+
* - route-raw-sql (error): db.execute without rawSql: true;
|
|
10
|
+
* - route-elevation-undeclared (error): used inside an elevation outside its tables option.
|
|
11
|
+
*/
|
|
12
|
+
import type { RouteRelationsFact } from "../types-relations.js";
|
|
13
|
+
import type { Diagnostic, RouteFact } from "../types.js";
|
|
14
|
+
/** Every diagnostic the relation pass derives from one route's usage fact. */
|
|
15
|
+
export declare function routeRelationDiagnostics(route: RouteFact, usage: RouteRelationsFact): Diagnostic[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-157 diagnostics derived from a route's relation-usage fact:
|
|
3
|
+
*
|
|
4
|
+
* - route-table-undeclared (error): used and not declared in dbTables;
|
|
5
|
+
* - route-table-unused (warning): declared and not used — suppressed when a project-external
|
|
6
|
+
* callee received the client or db.execute is present, where the declaration stays
|
|
7
|
+
* authoritative (the runtime guard is the enforcement);
|
|
8
|
+
* - route-table-unresolved (error): dynamic/unresolvable relation expression;
|
|
9
|
+
* - route-raw-sql (error): db.execute without rawSql: true;
|
|
10
|
+
* - route-elevation-undeclared (error): used inside an elevation outside its tables option.
|
|
11
|
+
*/
|
|
12
|
+
import { error, warning } from "../diagnostics.js";
|
|
13
|
+
function usedNames(usage) {
|
|
14
|
+
const names = new Set(usage.used.map((use) => use.name));
|
|
15
|
+
for (const elevation of usage.elevations) {
|
|
16
|
+
for (const use of elevation.used)
|
|
17
|
+
names.add(use.name);
|
|
18
|
+
}
|
|
19
|
+
return names;
|
|
20
|
+
}
|
|
21
|
+
function unusedSuppressed(usage) {
|
|
22
|
+
return (usage.opaque ||
|
|
23
|
+
usage.rawSql.length > 0 ||
|
|
24
|
+
usage.elevations.some((elevation) => elevation.opaque || elevation.tablesUnknown));
|
|
25
|
+
}
|
|
26
|
+
function undeclared(usage, declarations) {
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
for (const use of usage.used) {
|
|
29
|
+
if (declarations.has(use.name))
|
|
30
|
+
continue;
|
|
31
|
+
diagnostics.push(error("route-table-undeclared", 'relation "' + use.name + '" is used through the route db but not declared in dbTables', use.source));
|
|
32
|
+
}
|
|
33
|
+
for (const elevation of usage.elevations) {
|
|
34
|
+
for (const use of elevation.used) {
|
|
35
|
+
if (declarations.has(use.name))
|
|
36
|
+
continue;
|
|
37
|
+
diagnostics.push(error("route-table-undeclared", 'relation "' + use.name + '" is used inside an elevation but not declared in dbTables', use.source));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return diagnostics;
|
|
41
|
+
}
|
|
42
|
+
/** Every diagnostic the relation pass derives from one route's usage fact. */
|
|
43
|
+
export function routeRelationDiagnostics(route, usage) {
|
|
44
|
+
const diagnostics = [];
|
|
45
|
+
const declared = route.dbTables === null ? null : new Set(route.dbTables);
|
|
46
|
+
if (declared !== null)
|
|
47
|
+
diagnostics.push(...undeclared(usage, declared));
|
|
48
|
+
for (const source of usage.unresolved) {
|
|
49
|
+
diagnostics.push(error("route-table-unresolved", "a relation used through the route db cannot be resolved statically", source));
|
|
50
|
+
}
|
|
51
|
+
for (const source of usage.rawSql) {
|
|
52
|
+
if (route.rawSql === true)
|
|
53
|
+
continue;
|
|
54
|
+
diagnostics.push(error("route-raw-sql", "db.execute() requires rawSql: true on the route config", source));
|
|
55
|
+
}
|
|
56
|
+
for (const elevation of usage.elevations) {
|
|
57
|
+
if (elevation.tablesUnknown || elevation.opaque)
|
|
58
|
+
continue;
|
|
59
|
+
const tables = new Set(elevation.tables);
|
|
60
|
+
for (const use of elevation.used) {
|
|
61
|
+
if (tables.has(use.name))
|
|
62
|
+
continue;
|
|
63
|
+
diagnostics.push(error("route-elevation-undeclared", 'relation "' +
|
|
64
|
+
use.name +
|
|
65
|
+
'" is used inside a "' +
|
|
66
|
+
elevation.kind +
|
|
67
|
+
'" elevation but not declared in its tables option', use.source));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (declared !== null && !unusedSuppressed(usage)) {
|
|
71
|
+
const used = usedNames(usage);
|
|
72
|
+
for (const name of route.dbTables ?? []) {
|
|
73
|
+
if (used.has(name))
|
|
74
|
+
continue;
|
|
75
|
+
diagnostics.push(warning("route-table-unused", 'dbTables declares "' + name + '" but the route never uses it', route.source));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return diagnostics;
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprocedural part of the route-relations pass (D-157): follow a project-local function
|
|
3
|
+
* with argument mapping, and enter an audited elevation callback with its elevated client.
|
|
4
|
+
*
|
|
5
|
+
* Package calls stay opaque: the runtime guard is the authority there. Following is bounded
|
|
6
|
+
* by MAX_DEPTH (walk) and guarded by `scope.stack`, so recursion with a db client terminates.
|
|
7
|
+
*/
|
|
8
|
+
import { type AstNode } from "../ast/node.js";
|
|
9
|
+
import type { ElevationKind } from "../types.js";
|
|
10
|
+
import type { LocalFunction } from "./route-relation-resolve.js";
|
|
11
|
+
import type { RelationScope } from "./route-relations-usage.js";
|
|
12
|
+
export type Visit = (node: AstNode, scope: RelationScope) => void;
|
|
13
|
+
/** Follow a project-local call, mapping db clients, elevation handlers and relation bindings. */
|
|
14
|
+
export declare function followFunction(visit: Visit, node: AstNode, scope: RelationScope, local: LocalFunction): void;
|
|
15
|
+
/** Enter an elevation callback with its elevated client bound to the first parameter. */
|
|
16
|
+
export declare function handleElevation(visit: Visit, node: AstNode, scope: RelationScope, kind: ElevationKind): boolean;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprocedural part of the route-relations pass (D-157): follow a project-local function
|
|
3
|
+
* with argument mapping, and enter an audited elevation callback with its elevated client.
|
|
4
|
+
*
|
|
5
|
+
* Package calls stay opaque: the runtime guard is the authority there. Following is bounded
|
|
6
|
+
* by MAX_DEPTH (walk) and guarded by `scope.stack`, so recursion with a db client terminates.
|
|
7
|
+
*/
|
|
8
|
+
import { asNode, callArguments, identifierName, memberName, nodeArray, unwrapExpression, } from "../ast/node.js";
|
|
9
|
+
import { sourceRef } from "../source.js";
|
|
10
|
+
import { clientOf } from "./route-relation-resolve.js";
|
|
11
|
+
import { readStaticObject } from "./static-read.js";
|
|
12
|
+
function refOf(scope, node) {
|
|
13
|
+
return sourceRef(scope.file.path, scope.file.source, typeof node.start === "number" ? node.start : 0);
|
|
14
|
+
}
|
|
15
|
+
function stringArray(value) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return null;
|
|
18
|
+
return value.every((entry) => typeof entry === "string") ? [...value] : null;
|
|
19
|
+
}
|
|
20
|
+
/** Follow a project-local call, mapping db clients, elevation handlers and relation bindings. */
|
|
21
|
+
export function followFunction(visit, node, scope, local) {
|
|
22
|
+
if (scope.stack.has(local.key))
|
|
23
|
+
return;
|
|
24
|
+
const args = callArguments(node);
|
|
25
|
+
// Usage inside the arguments belongs to the caller (e.g. a query handed to a helper).
|
|
26
|
+
for (const argument of args)
|
|
27
|
+
visit(argument, scope);
|
|
28
|
+
const env = new Map(scope.env);
|
|
29
|
+
const aliases = new Map(scope.aliases);
|
|
30
|
+
const elevationFns = new Map(scope.elevationFns);
|
|
31
|
+
for (const [position, param] of nodeArray(local.node, "params").entries()) {
|
|
32
|
+
const name = identifierName(param);
|
|
33
|
+
const argument = args[position];
|
|
34
|
+
if (name === undefined || argument === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
const client = clientOf(argument, scope.env, scope.contextName);
|
|
37
|
+
if (client !== null) {
|
|
38
|
+
env.set(name, client);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const kind = elevationKindOf(argument, scope);
|
|
42
|
+
if (kind !== null) {
|
|
43
|
+
elevationFns.set(name, kind);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const resolution = scope.index.resolve(scope.graph, scope.file, argument, scope.aliases);
|
|
47
|
+
if (resolution.kind === "resolved")
|
|
48
|
+
aliases.set(name, resolution.name);
|
|
49
|
+
}
|
|
50
|
+
const body = asNode(local.node.body);
|
|
51
|
+
if (body === undefined)
|
|
52
|
+
return;
|
|
53
|
+
visit(body, {
|
|
54
|
+
...scope,
|
|
55
|
+
file: local.file,
|
|
56
|
+
env,
|
|
57
|
+
aliases,
|
|
58
|
+
elevationFns,
|
|
59
|
+
contextName: null,
|
|
60
|
+
depth: scope.depth + 1,
|
|
61
|
+
stack: new Set([...scope.stack, local.key]),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** An argument that denotes `ctx.sudo`/`ctx.system` (destructured or forwarded). */
|
|
65
|
+
function elevationKindOf(argument, scope) {
|
|
66
|
+
const expression = unwrapExpression(argument);
|
|
67
|
+
if (expression?.type === "Identifier") {
|
|
68
|
+
return scope.elevationFns.get(identifierName(expression) ?? "") ?? null;
|
|
69
|
+
}
|
|
70
|
+
if (expression?.type !== "MemberExpression")
|
|
71
|
+
return null;
|
|
72
|
+
const name = memberName(expression);
|
|
73
|
+
const kind = name === "sudo" || name === "system" ? name : null;
|
|
74
|
+
if (kind === null)
|
|
75
|
+
return null;
|
|
76
|
+
const object = unwrapExpression(expression.object);
|
|
77
|
+
return object?.type === "Identifier" && identifierName(object) === scope.contextName
|
|
78
|
+
? kind
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
/** Enter an elevation callback with its elevated client bound to the first parameter. */
|
|
82
|
+
export function handleElevation(visit, node, scope, kind) {
|
|
83
|
+
const options = readStaticObject(callArguments(node)[0], { graph: scope.graph, file: scope.file.path }, { code: "route-relations-static", report: false });
|
|
84
|
+
const field = options.fields.get("tables");
|
|
85
|
+
const read = field !== undefined && field.ok ? stringArray(field.value) : null;
|
|
86
|
+
const tables = read ?? [];
|
|
87
|
+
const tablesUnknown = read === null;
|
|
88
|
+
const elevation = {
|
|
89
|
+
kind,
|
|
90
|
+
source: refOf(scope, node),
|
|
91
|
+
tables,
|
|
92
|
+
tablesUnknown,
|
|
93
|
+
used: new Map(),
|
|
94
|
+
unresolved: [],
|
|
95
|
+
opaque: false,
|
|
96
|
+
};
|
|
97
|
+
const index = scope.usage.elevations.push(elevation) - 1;
|
|
98
|
+
const callback = unwrapExpression(callArguments(node)[1]);
|
|
99
|
+
if (callback === undefined ||
|
|
100
|
+
(callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression")) {
|
|
101
|
+
// A callback we cannot inspect leaves the elevated usage unknown, never guessed.
|
|
102
|
+
elevation.opaque = true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const body = asNode(callback.body);
|
|
106
|
+
if (body === undefined) {
|
|
107
|
+
elevation.opaque = true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const env = new Map(scope.env);
|
|
111
|
+
const param = identifierName(nodeArray(callback, "params")[0]);
|
|
112
|
+
if (param !== undefined)
|
|
113
|
+
env.set(param, { elevation: index });
|
|
114
|
+
visit(body, { ...scope, env, elevation: index, depth: scope.depth + 1 });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared state of the route-relations walk (D-157): the mutable relation-usage accumulators
|
|
3
|
+
* and the per-frame scope. Kept in one module so the walk and the follow helpers agree on
|
|
4
|
+
* their shape without importing each other.
|
|
5
|
+
*/
|
|
6
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.js";
|
|
7
|
+
import type { RouteRelationsFact } from "../types-relations.js";
|
|
8
|
+
import type { ElevationKind, SourceRef } from "../types.js";
|
|
9
|
+
import type { DbClient, RelationIndex } from "./route-relation-resolve.js";
|
|
10
|
+
export interface MutableElevation {
|
|
11
|
+
readonly kind: ElevationKind;
|
|
12
|
+
readonly source: SourceRef;
|
|
13
|
+
readonly tables: readonly string[];
|
|
14
|
+
readonly tablesUnknown: boolean;
|
|
15
|
+
readonly used: Map<string, SourceRef>;
|
|
16
|
+
readonly unresolved: SourceRef[];
|
|
17
|
+
opaque: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface MutableUsage {
|
|
20
|
+
readonly used: Map<string, SourceRef>;
|
|
21
|
+
readonly unresolved: SourceRef[];
|
|
22
|
+
readonly rawSql: SourceRef[];
|
|
23
|
+
opaque: boolean;
|
|
24
|
+
readonly elevations: MutableElevation[];
|
|
25
|
+
}
|
|
26
|
+
/** One handler or project-local helper call frame. */
|
|
27
|
+
export interface RelationScope {
|
|
28
|
+
readonly graph: ModuleGraph;
|
|
29
|
+
readonly index: RelationIndex;
|
|
30
|
+
readonly file: ProjectFile;
|
|
31
|
+
readonly env: Map<string, DbClient>;
|
|
32
|
+
readonly aliases: Map<string, string>;
|
|
33
|
+
readonly elevationFns: Map<string, ElevationKind>;
|
|
34
|
+
readonly contextName: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the scanned body can elevate (`ctx.sudo`/`ctx.system`). Jobs already run
|
|
37
|
+
* system-scoped, so their pass has no elevation dimension (D-157).
|
|
38
|
+
*/
|
|
39
|
+
readonly elevations: boolean;
|
|
40
|
+
/** Elevation bucket of the current callback; null is the route's own ctx.db. */
|
|
41
|
+
readonly elevation: number | null;
|
|
42
|
+
readonly depth: number;
|
|
43
|
+
readonly stack: ReadonlySet<string>;
|
|
44
|
+
readonly usage: MutableUsage;
|
|
45
|
+
}
|
|
46
|
+
export declare function createUsage(): MutableUsage;
|
|
47
|
+
export declare function addUse(scope: RelationScope, name: string, source: SourceRef): void;
|
|
48
|
+
export declare function addUnresolved(scope: RelationScope, source: SourceRef): void;
|
|
49
|
+
export declare function markOpaque(scope: RelationScope): void;
|
|
50
|
+
export declare function freezeUsage(usage: MutableUsage): RouteRelationsFact;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared state of the route-relations walk (D-157): the mutable relation-usage accumulators
|
|
3
|
+
* and the per-frame scope. Kept in one module so the walk and the follow helpers agree on
|
|
4
|
+
* their shape without importing each other.
|
|
5
|
+
*/
|
|
6
|
+
export function createUsage() {
|
|
7
|
+
return { used: new Map(), unresolved: [], rawSql: [], opaque: false, elevations: [] };
|
|
8
|
+
}
|
|
9
|
+
function bucket(scope) {
|
|
10
|
+
const index = scope.elevation;
|
|
11
|
+
return index === null ? scope.usage : (scope.usage.elevations[index] ?? scope.usage);
|
|
12
|
+
}
|
|
13
|
+
export function addUse(scope, name, source) {
|
|
14
|
+
const target = bucket(scope);
|
|
15
|
+
if (!target.used.has(name))
|
|
16
|
+
target.used.set(name, source);
|
|
17
|
+
}
|
|
18
|
+
export function addUnresolved(scope, source) {
|
|
19
|
+
bucket(scope).unresolved.push(source);
|
|
20
|
+
}
|
|
21
|
+
export function markOpaque(scope) {
|
|
22
|
+
bucket(scope).opaque = true;
|
|
23
|
+
}
|
|
24
|
+
function freezeElevation(elevation) {
|
|
25
|
+
const used = [...elevation.used].map(([name, source]) => ({ name, source }));
|
|
26
|
+
return {
|
|
27
|
+
kind: elevation.kind,
|
|
28
|
+
source: elevation.source,
|
|
29
|
+
tables: [...elevation.tables],
|
|
30
|
+
used,
|
|
31
|
+
unresolved: [...elevation.unresolved],
|
|
32
|
+
tablesUnknown: elevation.tablesUnknown,
|
|
33
|
+
opaque: elevation.opaque,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function freezeUsage(usage) {
|
|
37
|
+
return {
|
|
38
|
+
used: [...usage.used].map(([name, source]) => ({ name, source })),
|
|
39
|
+
unresolved: [...usage.unresolved],
|
|
40
|
+
rawSql: [...usage.rawSql],
|
|
41
|
+
opaque: usage.opaque,
|
|
42
|
+
elevations: usage.elevations.map(freezeElevation),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The route-relations walker (D-157): starts at the route handler, tracks db clients and
|
|
3
|
+
* relation bindings through local declarations, records `from/join/insert/update/delete`,
|
|
4
|
+
* `db.query.<relation>` and `db.execute`, and hands the interprocedural cases to
|
|
5
|
+
* route-relations-follow.ts. Everything outside the static subset is explicit (opaque or
|
|
6
|
+
* unresolved), never guessed.
|
|
7
|
+
*/
|
|
8
|
+
import { type AstNode } from "../ast/node.js";
|
|
9
|
+
import type { ModuleGraph, ProjectFile } from "../module-graph.js";
|
|
10
|
+
import type { RouteRelationsFact } from "../types-relations.js";
|
|
11
|
+
import { type RelationIndex } from "./route-relation-resolve.js";
|
|
12
|
+
/**
|
|
13
|
+
* How a scanned function receives its db client. Route handlers are `(event, ctx)` and may
|
|
14
|
+
* elevate; a job's `run` is `(ctx)` and already runs system-scoped, so it has no elevations.
|
|
15
|
+
*/
|
|
16
|
+
export interface UsageSeed {
|
|
17
|
+
/** Index of the parameter that carries the db context (route: 1, job run: 0). */
|
|
18
|
+
readonly contextParam: number;
|
|
19
|
+
/** Route handlers may call ctx.sudo/ctx.system; jobs may not (D-157). */
|
|
20
|
+
readonly elevations: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Scan one route handler or job run function into its relation-usage fact. */
|
|
23
|
+
export declare function scanFunctionUsage(graph: ModuleGraph, file: ProjectFile, fn: AstNode, index: RelationIndex, seed: UsageSeed): RouteRelationsFact;
|
|
24
|
+
/** Scan one valid route handler into its relation-usage fact. */
|
|
25
|
+
export declare function scanHandlerUsage(graph: ModuleGraph, file: ProjectFile, handler: AstNode, index: RelationIndex): RouteRelationsFact;
|