graphddb 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdc/index.d.ts +2 -2
- package/dist/{chunk-N5NQM3SO.js → chunk-GFGVDF4W.js} +500 -12
- package/dist/cli.js +155 -6
- package/dist/{from-change-CFzBy7aU.d.ts → from-change-pnURY-cV.d.ts} +1 -1
- package/dist/{index-CtJbTrfB.d.ts → index-Eg94ChE1.d.ts} +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +11 -1
- package/dist/linter/index.d.ts +4 -4
- package/dist/{maintenance-view-adapter-CFeasCKo.d.ts → maintenance-view-adapter-BP2CJDdz.d.ts} +2489 -2245
- package/dist/{registry-DbqmFyab.d.ts → registry-Cv9nl_3i.d.ts} +1 -1
- package/dist/{relation-depth-0TiWr5OW.d.ts → relation-depth-BR0y7Q1i.d.ts} +1 -1
- package/dist/spec/index.d.ts +3 -3
- package/dist/spec/index.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/transform/index.d.ts +167 -0
- package/dist/transform/index.js +719 -0
- package/docs/prepared-statements.md +165 -0
- package/package.json +5 -1
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
// src/transform/prepared-transform.ts
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
var HOIST_SLOT_PREFIX = "__gddbPrepared";
|
|
4
|
+
var SLOT_NAME_RE = /^__gddbPrepared\d+$/;
|
|
5
|
+
function collectModuleBindings(sf) {
|
|
6
|
+
const out = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const stmt of sf.statements) {
|
|
8
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
9
|
+
const clause = stmt.importClause;
|
|
10
|
+
if (clause.name) {
|
|
11
|
+
out.set(clause.name.text, {
|
|
12
|
+
kind: "import",
|
|
13
|
+
importedName: void 0,
|
|
14
|
+
namespace: false
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const bindings = clause.namedBindings;
|
|
18
|
+
if (bindings) {
|
|
19
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
20
|
+
out.set(bindings.name.text, {
|
|
21
|
+
kind: "import",
|
|
22
|
+
importedName: void 0,
|
|
23
|
+
namespace: true
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
for (const spec of bindings.elements) {
|
|
27
|
+
out.set(spec.name.text, {
|
|
28
|
+
kind: "import",
|
|
29
|
+
importedName: (spec.propertyName ?? spec.name).text,
|
|
30
|
+
namespace: false
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
36
|
+
const isConst = (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
37
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
38
|
+
if (ts.isIdentifier(decl.name)) {
|
|
39
|
+
out.set(
|
|
40
|
+
decl.name.text,
|
|
41
|
+
isConst ? { kind: "const", initializer: decl.initializer } : { kind: "mutable" }
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
for (const name of bindingNames(decl.name)) {
|
|
45
|
+
out.set(
|
|
46
|
+
name,
|
|
47
|
+
isConst ? { kind: "const", initializer: void 0 } : { kind: "mutable" }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if (ts.isClassDeclaration(stmt) && stmt.name) {
|
|
53
|
+
out.set(stmt.name.text, { kind: "class", node: stmt });
|
|
54
|
+
} else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
55
|
+
out.set(stmt.name.text, { kind: "function", node: stmt });
|
|
56
|
+
} else if (ts.isEnumDeclaration(stmt)) {
|
|
57
|
+
out.set(stmt.name.text, { kind: "enum" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function bindingNames(name) {
|
|
63
|
+
if (ts.isIdentifier(name)) return [name.text];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const el of name.elements) {
|
|
66
|
+
if (ts.isBindingElement(el)) out.push(...bindingNames(el.name));
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function isDDBModelRef(expr, bindings, seen = /* @__PURE__ */ new Set()) {
|
|
71
|
+
const e = unwrapExpr(expr);
|
|
72
|
+
if (ts.isIdentifier(e)) {
|
|
73
|
+
const name = e.text;
|
|
74
|
+
if (seen.has(name)) return false;
|
|
75
|
+
seen.add(name);
|
|
76
|
+
const b = bindings.get(name);
|
|
77
|
+
if (!b) return false;
|
|
78
|
+
if (b.kind === "import") return b.importedName === "DDBModel";
|
|
79
|
+
if (b.kind === "const" && b.initializer)
|
|
80
|
+
return isDDBModelRef(b.initializer, bindings, seen);
|
|
81
|
+
if (b.kind === "class") {
|
|
82
|
+
const heritage = b.node.heritageClauses?.find(
|
|
83
|
+
(h) => h.token === ts.SyntaxKind.ExtendsKeyword
|
|
84
|
+
);
|
|
85
|
+
const base = heritage?.types[0]?.expression;
|
|
86
|
+
return base !== void 0 && isDDBModelRef(base, bindings, seen);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (ts.isPropertyAccessExpression(e) && e.name.text === "DDBModel") {
|
|
91
|
+
const root = unwrapExpr(e.expression);
|
|
92
|
+
if (ts.isIdentifier(root)) {
|
|
93
|
+
const b = bindings.get(root.text);
|
|
94
|
+
return b?.kind === "import" && b.namespace;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function unwrapExpr(e) {
|
|
100
|
+
let cur = e;
|
|
101
|
+
for (; ; ) {
|
|
102
|
+
if (ts.isParenthesizedExpression(cur)) cur = cur.expression;
|
|
103
|
+
else if (ts.isAsExpression(cur)) cur = cur.expression;
|
|
104
|
+
else if (ts.isSatisfiesExpression(cur)) cur = cur.expression;
|
|
105
|
+
else if (ts.isNonNullExpression(cur)) cur = cur.expression;
|
|
106
|
+
else if (ts.isTypeAssertionExpression(cur)) cur = cur.expression;
|
|
107
|
+
else return cur;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isFunctionLikeScope(n) {
|
|
111
|
+
return ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n);
|
|
112
|
+
}
|
|
113
|
+
function scopeDeclares(scope, name) {
|
|
114
|
+
if (isFunctionLikeScope(scope)) {
|
|
115
|
+
const fn = scope;
|
|
116
|
+
for (const p of fn.parameters) {
|
|
117
|
+
if (bindingNames(p.name).includes(name)) return true;
|
|
118
|
+
}
|
|
119
|
+
if (fn.body && ts.isBlock(fn.body) && blockDeclares(fn.body, name)) return true;
|
|
120
|
+
if ((ts.isFunctionExpression(scope) || ts.isFunctionDeclaration(scope)) && scope.name?.text === name) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (ts.isBlock(scope)) return blockDeclares(scope, name);
|
|
126
|
+
if (ts.isCatchClause(scope)) {
|
|
127
|
+
return scope.variableDeclaration !== void 0 && bindingNames(scope.variableDeclaration.name).includes(name);
|
|
128
|
+
}
|
|
129
|
+
if (ts.isForStatement(scope) || ts.isForOfStatement(scope) || ts.isForInStatement(scope)) {
|
|
130
|
+
const init = scope.initializer;
|
|
131
|
+
if (init && ts.isVariableDeclarationList(init)) {
|
|
132
|
+
for (const d of init.declarations) {
|
|
133
|
+
if (bindingNames(d.name).includes(name)) return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function blockDeclares(block, name) {
|
|
141
|
+
for (const stmt of block.statements) {
|
|
142
|
+
if (ts.isVariableStatement(stmt)) {
|
|
143
|
+
for (const d of stmt.declarationList.declarations) {
|
|
144
|
+
if (bindingNames(d.name).includes(name)) return true;
|
|
145
|
+
}
|
|
146
|
+
} else if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name?.text === name) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function resolveIdentifier(id, bodyFn, bindings) {
|
|
153
|
+
const name = id.text;
|
|
154
|
+
let node = id;
|
|
155
|
+
while (node.parent) {
|
|
156
|
+
const parent = node.parent;
|
|
157
|
+
if (isFunctionLikeScope(parent) || ts.isBlock(parent) || ts.isCatchClause(parent) || ts.isForStatement(parent) || ts.isForOfStatement(parent) || ts.isForInStatement(parent)) {
|
|
158
|
+
if (scopeDeclares(parent, name)) {
|
|
159
|
+
if (bodyFn && isInside(parent, bodyFn)) return { at: "body-local" };
|
|
160
|
+
if (isFunctionLikeScope(parent) || hasFunctionAncestorBelowModule(parent)) {
|
|
161
|
+
return { at: "enclosing-function" };
|
|
162
|
+
}
|
|
163
|
+
return { at: "module", binding: { kind: "const", initializer: void 0 } };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
node = parent;
|
|
167
|
+
}
|
|
168
|
+
const b = bindings.get(name);
|
|
169
|
+
if (b) return { at: "module", binding: b };
|
|
170
|
+
return { at: "unresolved" };
|
|
171
|
+
}
|
|
172
|
+
function isInside(node, ancestor) {
|
|
173
|
+
let cur = node;
|
|
174
|
+
while (cur) {
|
|
175
|
+
if (cur === ancestor) return true;
|
|
176
|
+
cur = cur.parent;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function hasFunctionAncestorBelowModule(node) {
|
|
181
|
+
let cur = node.parent;
|
|
182
|
+
while (cur && !ts.isSourceFile(cur)) {
|
|
183
|
+
if (isFunctionLikeScope(cur)) return true;
|
|
184
|
+
cur = cur.parent;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
function report(ctx, node, code, message, category = "error") {
|
|
189
|
+
const { line, character } = ctx.sf.getLineAndCharacterOfPosition(node.getStart(ctx.sf));
|
|
190
|
+
ctx.diagnostics.push({
|
|
191
|
+
code,
|
|
192
|
+
category,
|
|
193
|
+
message,
|
|
194
|
+
file: ctx.fileName,
|
|
195
|
+
line: line + 1,
|
|
196
|
+
column: character + 1
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function lintPreparedBody(ctx, fn) {
|
|
200
|
+
const before = countErrors(ctx.diagnostics);
|
|
201
|
+
if (fn.parameters.length > 1) {
|
|
202
|
+
report(
|
|
203
|
+
ctx,
|
|
204
|
+
fn.parameters[1],
|
|
205
|
+
"impure-expression",
|
|
206
|
+
"A prepared body takes exactly one parameter (the `$` placeholder)."
|
|
207
|
+
);
|
|
208
|
+
return countErrors(ctx.diagnostics) - before;
|
|
209
|
+
}
|
|
210
|
+
const paramNames = /* @__PURE__ */ new Set();
|
|
211
|
+
const destructured = /* @__PURE__ */ new Set();
|
|
212
|
+
const param = fn.parameters[0];
|
|
213
|
+
if (param) {
|
|
214
|
+
if (ts.isIdentifier(param.name)) {
|
|
215
|
+
paramNames.add(param.name.text);
|
|
216
|
+
} else if (ts.isObjectBindingPattern(param.name)) {
|
|
217
|
+
for (const el of param.name.elements) {
|
|
218
|
+
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name) || el.initializer !== void 0 || el.dotDotDotToken !== void 0 || el.propertyName !== void 0 && !ts.isIdentifier(el.propertyName)) {
|
|
219
|
+
report(
|
|
220
|
+
ctx,
|
|
221
|
+
el,
|
|
222
|
+
"impure-expression",
|
|
223
|
+
"Only a plain, single-level destructuring of the `$` placeholder is supported (no nesting, defaults, or rest)."
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
destructured.add(el.name.text);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
report(
|
|
231
|
+
ctx,
|
|
232
|
+
param,
|
|
233
|
+
"impure-expression",
|
|
234
|
+
"The `$` parameter must be a plain identifier or a single-level object destructuring."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const env = { bodyFn: fn, paramNames, destructuredParamNames: destructured };
|
|
239
|
+
const body = fn.body;
|
|
240
|
+
if (body !== void 0) {
|
|
241
|
+
if (ts.isBlock(body)) lintBodyBlock(ctx, body, env);
|
|
242
|
+
else checkExpr(ctx, body, env);
|
|
243
|
+
}
|
|
244
|
+
return countErrors(ctx.diagnostics) - before;
|
|
245
|
+
}
|
|
246
|
+
function countErrors(diags) {
|
|
247
|
+
return diags.reduce((n, d) => n + (d.category === "error" ? 1 : 0), 0);
|
|
248
|
+
}
|
|
249
|
+
function lintBodyBlock(ctx, block, env) {
|
|
250
|
+
for (const stmt of block.statements) {
|
|
251
|
+
if (ts.isReturnStatement(stmt)) {
|
|
252
|
+
if (stmt.expression) checkExpr(ctx, stmt.expression, env);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
|
|
256
|
+
for (const d of stmt.declarationList.declarations) {
|
|
257
|
+
if (d.initializer) checkExpr(ctx, d.initializer, env);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
report(
|
|
262
|
+
ctx,
|
|
263
|
+
stmt,
|
|
264
|
+
"impure-expression",
|
|
265
|
+
"A prepared body block may contain only `const` declarations and `return` statements \u2014 any other statement is per-call control flow, which breaks compile-time hoisting."
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function checkExpr(ctx, node, env) {
|
|
270
|
+
const e = unwrapExpr(node);
|
|
271
|
+
if (ts.isStringLiteral(e) || ts.isNumericLiteral(e) || ts.isBigIntLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e) || e.kind === ts.SyntaxKind.TrueKeyword || e.kind === ts.SyntaxKind.FalseKeyword || e.kind === ts.SyntaxKind.NullKeyword) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (ts.isPrefixUnaryExpression(e) && (e.operator === ts.SyntaxKind.MinusToken || e.operator === ts.SyntaxKind.PlusToken) && ts.isNumericLiteral(e.operand)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (ts.isObjectLiteralExpression(e)) {
|
|
278
|
+
for (const prop of e.properties) {
|
|
279
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
280
|
+
if (ts.isComputedPropertyName(prop.name)) {
|
|
281
|
+
report(
|
|
282
|
+
ctx,
|
|
283
|
+
prop.name,
|
|
284
|
+
"impure-expression",
|
|
285
|
+
"A computed property name is per-call control flow \u2014 a prepared template needs statically known keys."
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
checkExpr(ctx, prop.initializer, env);
|
|
289
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
290
|
+
checkIdentifierRef(ctx, prop.name, env);
|
|
291
|
+
} else if (ts.isSpreadAssignment(prop)) {
|
|
292
|
+
checkExpr(ctx, prop.expression, env);
|
|
293
|
+
} else {
|
|
294
|
+
report(
|
|
295
|
+
ctx,
|
|
296
|
+
prop,
|
|
297
|
+
"impure-expression",
|
|
298
|
+
"Only plain property assignments are supported in a prepared template."
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (ts.isArrayLiteralExpression(e)) {
|
|
305
|
+
for (const el of e.elements) {
|
|
306
|
+
if (ts.isSpreadElement(el)) checkExpr(ctx, el.expression, env);
|
|
307
|
+
else checkExpr(ctx, el, env);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
|
312
|
+
if (e.parameters.length !== 0) {
|
|
313
|
+
report(
|
|
314
|
+
ctx,
|
|
315
|
+
e,
|
|
316
|
+
"impure-expression",
|
|
317
|
+
"A function inside a prepared template is only valid as a zero-parameter model ref (`() => Model`)."
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const refExpr = thunkResultExpression(e);
|
|
322
|
+
if (refExpr === void 0) {
|
|
323
|
+
report(
|
|
324
|
+
ctx,
|
|
325
|
+
e,
|
|
326
|
+
"impure-expression",
|
|
327
|
+
"A model-ref thunk must be a single expression (or single `return`) referencing a module-static model."
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
checkStaticRefChain(ctx, refExpr, env);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (ts.isIdentifier(e)) {
|
|
335
|
+
checkIdentifierRef(ctx, e, env);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (ts.isPropertyAccessExpression(e)) {
|
|
339
|
+
checkPropertyChain(ctx, e, env);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (ts.isCallExpression(e) || ts.isNewExpression(e)) {
|
|
343
|
+
report(
|
|
344
|
+
ctx,
|
|
345
|
+
e,
|
|
346
|
+
"call-in-body",
|
|
347
|
+
"A call inside a prepared body is an indirect runtime-capture channel (the lint cannot see through it, and even a global like `Date.now()` is a fresh per-call value). Inline the result as a static literal, bind it via `$`, or extract a module-scope `const` template."
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (e.kind === ts.SyntaxKind.ThisKeyword) {
|
|
352
|
+
report(
|
|
353
|
+
ctx,
|
|
354
|
+
e,
|
|
355
|
+
"runtime-capture",
|
|
356
|
+
"`this` inside a prepared body captures the per-call receiver. Bind the value via `$` instead."
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (ts.isTemplateExpression(e)) {
|
|
361
|
+
report(
|
|
362
|
+
ctx,
|
|
363
|
+
e,
|
|
364
|
+
"impure-expression",
|
|
365
|
+
"A template literal with substitutions embeds a runtime value into the template. Bind the value via `$` (the runtime binds whole fields, never string fragments)."
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
report(
|
|
370
|
+
ctx,
|
|
371
|
+
e,
|
|
372
|
+
"impure-expression",
|
|
373
|
+
`Unsupported expression (${ts.SyntaxKind[e.kind]}) in a prepared body \u2014 only object/array literals, static literals, \`$.<name>\` refs, module-static references, and \`() => Model\` thunks are declarative.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
function thunkResultExpression(fn) {
|
|
377
|
+
if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return fn.body;
|
|
378
|
+
const body = fn.body;
|
|
379
|
+
if (body && ts.isBlock(body) && body.statements.length === 1) {
|
|
380
|
+
const only = body.statements[0];
|
|
381
|
+
if (ts.isReturnStatement(only) && only.expression) return only.expression;
|
|
382
|
+
}
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
function checkStaticRefChain(ctx, expr, env) {
|
|
386
|
+
const e = unwrapExpr(expr);
|
|
387
|
+
if (ts.isIdentifier(e)) {
|
|
388
|
+
checkIdentifierRef(
|
|
389
|
+
ctx,
|
|
390
|
+
e,
|
|
391
|
+
env,
|
|
392
|
+
/* refOnly */
|
|
393
|
+
true
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (ts.isPropertyAccessExpression(e)) {
|
|
398
|
+
checkPropertyChain(
|
|
399
|
+
ctx,
|
|
400
|
+
e,
|
|
401
|
+
env,
|
|
402
|
+
/* refOnly */
|
|
403
|
+
true
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
report(
|
|
408
|
+
ctx,
|
|
409
|
+
e,
|
|
410
|
+
"impure-expression",
|
|
411
|
+
"A model ref must be a plain reference to a module-static model (`() => Model` / `() => ns.Model`)."
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
function checkIdentifierRef(ctx, id, env, refOnly = false) {
|
|
415
|
+
const name = id.text;
|
|
416
|
+
if (name === "undefined") {
|
|
417
|
+
report(
|
|
418
|
+
ctx,
|
|
419
|
+
id,
|
|
420
|
+
"non-static-reference",
|
|
421
|
+
"`undefined` is not a bindable template value \u2014 omit the field or bind it via `$`."
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (env.paramNames.has(name)) {
|
|
426
|
+
report(
|
|
427
|
+
ctx,
|
|
428
|
+
id,
|
|
429
|
+
"impure-expression",
|
|
430
|
+
"The `$` placeholder itself is not a value \u2014 bind a specific param via `$.<name>`."
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (env.destructuredParamNames.has(name)) return;
|
|
435
|
+
const res = resolveIdentifier(id, env.bodyFn, ctx.bindings);
|
|
436
|
+
switch (res.at) {
|
|
437
|
+
case "body-local":
|
|
438
|
+
return;
|
|
439
|
+
case "enclosing-function":
|
|
440
|
+
report(
|
|
441
|
+
ctx,
|
|
442
|
+
id,
|
|
443
|
+
"runtime-capture",
|
|
444
|
+
`'${name}' is declared in an enclosing function \u2014 a per-call runtime value captured into the prepared body. Bind it via \`$\` (a per-call param) or move it to a module-scope \`const\`.`
|
|
445
|
+
);
|
|
446
|
+
return;
|
|
447
|
+
case "unresolved":
|
|
448
|
+
report(
|
|
449
|
+
ctx,
|
|
450
|
+
id,
|
|
451
|
+
"non-static-reference",
|
|
452
|
+
`'${name}' does not resolve to \`$\` or a module-static binding in this file. Globals are per-call runtime state; a prepared body may reference only \`$\` and module-static values.`
|
|
453
|
+
);
|
|
454
|
+
return;
|
|
455
|
+
case "module":
|
|
456
|
+
checkModuleBindingUse(ctx, id, name, res.binding, refOnly);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function checkModuleBindingUse(ctx, at, name, binding, refOnly) {
|
|
461
|
+
switch (binding.kind) {
|
|
462
|
+
case "mutable":
|
|
463
|
+
report(
|
|
464
|
+
ctx,
|
|
465
|
+
at,
|
|
466
|
+
"mutable-module-state",
|
|
467
|
+
`'${name}' is module-scope \`let\`/\`var\` \u2014 mutable module state. A hoisted body would freeze its value while an unhoisted one would not; results must not depend on placement. Make it a \`const\` or bind it via \`$\`.`
|
|
468
|
+
);
|
|
469
|
+
return;
|
|
470
|
+
case "import":
|
|
471
|
+
case "class":
|
|
472
|
+
case "enum":
|
|
473
|
+
return;
|
|
474
|
+
// module-static by construction (see the documented limits).
|
|
475
|
+
case "function":
|
|
476
|
+
if (binding.node.parameters.length === 0) {
|
|
477
|
+
const result = binding.node.body ? functionSingleReturn(binding.node) : void 0;
|
|
478
|
+
if (result !== void 0) {
|
|
479
|
+
checkStaticRefChain(
|
|
480
|
+
ctx,
|
|
481
|
+
result,
|
|
482
|
+
// Module context: no `$`, no body scope.
|
|
483
|
+
{ bodyFn: void 0, paramNames: /* @__PURE__ */ new Set(), destructuredParamNames: /* @__PURE__ */ new Set() }
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!refOnly) return;
|
|
489
|
+
report(
|
|
490
|
+
ctx,
|
|
491
|
+
at,
|
|
492
|
+
"impure-expression",
|
|
493
|
+
`'${name}' is not a zero-parameter thunk returning a module-static reference \u2014 a model ref must be statically resolvable.`
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
case "const": {
|
|
497
|
+
if (binding.initializer === void 0) return;
|
|
498
|
+
if (ctx.visitedConsts.has(name)) return;
|
|
499
|
+
ctx.visitedConsts.add(name);
|
|
500
|
+
checkExpr(ctx, binding.initializer, {
|
|
501
|
+
bodyFn: void 0,
|
|
502
|
+
paramNames: /* @__PURE__ */ new Set(),
|
|
503
|
+
destructuredParamNames: /* @__PURE__ */ new Set()
|
|
504
|
+
});
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function functionSingleReturn(fn) {
|
|
510
|
+
const body = fn.body;
|
|
511
|
+
if (body && body.statements.length === 1) {
|
|
512
|
+
const only = body.statements[0];
|
|
513
|
+
if (ts.isReturnStatement(only) && only.expression) return only.expression;
|
|
514
|
+
}
|
|
515
|
+
return void 0;
|
|
516
|
+
}
|
|
517
|
+
function checkPropertyChain(ctx, e, env, refOnly = false) {
|
|
518
|
+
let levels = 0;
|
|
519
|
+
let cur = e;
|
|
520
|
+
while (ts.isPropertyAccessExpression(cur)) {
|
|
521
|
+
levels += 1;
|
|
522
|
+
cur = unwrapExpr(cur.expression);
|
|
523
|
+
}
|
|
524
|
+
if (ts.isIdentifier(cur)) {
|
|
525
|
+
const rootName = cur.text;
|
|
526
|
+
if (env.paramNames.has(rootName)) {
|
|
527
|
+
if (levels > 1) {
|
|
528
|
+
report(
|
|
529
|
+
ctx,
|
|
530
|
+
e,
|
|
531
|
+
"param-deep-access",
|
|
532
|
+
"Only a direct `$.<name>` read is supported \u2014 nested access on a param is a per-call transform the runtime rejects too."
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (env.destructuredParamNames.has(rootName)) {
|
|
538
|
+
report(
|
|
539
|
+
ctx,
|
|
540
|
+
e,
|
|
541
|
+
"param-deep-access",
|
|
542
|
+
"A destructured `$` param is already the bound value \u2014 nested access on it is a per-call transform."
|
|
543
|
+
);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
checkIdentifierRef(ctx, cur, env, refOnly);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (cur.kind === ts.SyntaxKind.ThisKeyword) {
|
|
550
|
+
report(
|
|
551
|
+
ctx,
|
|
552
|
+
e,
|
|
553
|
+
"runtime-capture",
|
|
554
|
+
"`this` inside a prepared body captures the per-call receiver. Bind the value via `$` instead."
|
|
555
|
+
);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
report(
|
|
559
|
+
ctx,
|
|
560
|
+
e,
|
|
561
|
+
"impure-expression",
|
|
562
|
+
"A property chain in a prepared body must be rooted at `$` or a module-static reference."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
function isPerCallSite(call) {
|
|
566
|
+
let cur = call.parent;
|
|
567
|
+
while (cur && !ts.isSourceFile(cur)) {
|
|
568
|
+
if (isFunctionLikeScope(cur)) return true;
|
|
569
|
+
if (ts.isPropertyDeclaration(cur) && !cur.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) {
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
cur = cur.parent;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
function isAlreadyHoisted(call) {
|
|
577
|
+
const parent = call.parent;
|
|
578
|
+
return parent !== void 0 && ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken && ts.isIdentifier(parent.left) && SLOT_NAME_RE.test(parent.left.text) && parent.right === call;
|
|
579
|
+
}
|
|
580
|
+
function transformPreparedSource(fileName, sourceText, options = {}) {
|
|
581
|
+
const mode = options.mode ?? "transform";
|
|
582
|
+
const scriptKind = fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : fileName.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TS;
|
|
583
|
+
const sf = ts.createSourceFile(
|
|
584
|
+
fileName,
|
|
585
|
+
sourceText,
|
|
586
|
+
ts.ScriptTarget.Latest,
|
|
587
|
+
/* setParentNodes */
|
|
588
|
+
true,
|
|
589
|
+
scriptKind
|
|
590
|
+
);
|
|
591
|
+
const bindings = collectModuleBindings(sf);
|
|
592
|
+
const ctx = {
|
|
593
|
+
sf,
|
|
594
|
+
fileName,
|
|
595
|
+
bindings,
|
|
596
|
+
diagnostics: [],
|
|
597
|
+
visitedConsts: /* @__PURE__ */ new Set()
|
|
598
|
+
};
|
|
599
|
+
const calls = [];
|
|
600
|
+
const visit = (node) => {
|
|
601
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "prepare" && isDDBModelRef(node.expression.expression, bindings)) {
|
|
602
|
+
let bodyFn;
|
|
603
|
+
let indirect = false;
|
|
604
|
+
const arg = node.arguments.length === 1 ? unwrapExpr(node.arguments[0]) : void 0;
|
|
605
|
+
if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))) {
|
|
606
|
+
bodyFn = arg;
|
|
607
|
+
} else if (arg && ts.isIdentifier(arg)) {
|
|
608
|
+
const res = resolveIdentifier(arg, void 0, bindings);
|
|
609
|
+
if (res.at === "module") {
|
|
610
|
+
const b = res.binding;
|
|
611
|
+
if (b.kind === "const" && b.initializer) {
|
|
612
|
+
const init = unwrapExpr(b.initializer);
|
|
613
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
614
|
+
bodyFn = init;
|
|
615
|
+
indirect = true;
|
|
616
|
+
}
|
|
617
|
+
} else if (b.kind === "function") {
|
|
618
|
+
bodyFn = b.node;
|
|
619
|
+
indirect = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
calls.push({ call: node, bodyFn, bodyIsIndirect: indirect });
|
|
624
|
+
}
|
|
625
|
+
ts.forEachChild(node, visit);
|
|
626
|
+
};
|
|
627
|
+
visit(sf);
|
|
628
|
+
const edits = [];
|
|
629
|
+
const slotNames = [];
|
|
630
|
+
let hoistedCount = 0;
|
|
631
|
+
let moduleScopeCount = 0;
|
|
632
|
+
let alreadyHoistedCount = 0;
|
|
633
|
+
let slotIndex = 0;
|
|
634
|
+
for (const m of sourceText.matchAll(/__gddbPrepared(\d+)\b/g)) {
|
|
635
|
+
slotIndex = Math.max(slotIndex, Number(m[1]));
|
|
636
|
+
}
|
|
637
|
+
for (const { call, bodyFn } of calls) {
|
|
638
|
+
if (bodyFn === void 0) {
|
|
639
|
+
report(
|
|
640
|
+
ctx,
|
|
641
|
+
call,
|
|
642
|
+
"unverifiable-body",
|
|
643
|
+
"The `prepare` argument is not a statically visible function (an inline arrow/function or a module-scope `const` body). The no-runtime-capture lint cannot verify it \u2014 pass the body inline or as a module-scope const."
|
|
644
|
+
);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const bodyErrors = lintPreparedBody(ctx, bodyFn);
|
|
648
|
+
if (bodyErrors > 0) continue;
|
|
649
|
+
if (isAlreadyHoisted(call)) {
|
|
650
|
+
alreadyHoistedCount += 1;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (!isPerCallSite(call)) {
|
|
654
|
+
moduleScopeCount += 1;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (mode === "lint") {
|
|
658
|
+
report(
|
|
659
|
+
ctx,
|
|
660
|
+
call,
|
|
661
|
+
"hoistable",
|
|
662
|
+
"Inline `DDBModel.prepare` call site \u2014 `graphddb transform prepared --write` normalizes it to a module-scope prepared slot (zero per-op overhead). Untransformed it falls back to the runtime structural-memoization path.",
|
|
663
|
+
"note"
|
|
664
|
+
);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
slotIndex += 1;
|
|
668
|
+
const slot = `${HOIST_SLOT_PREFIX}${slotIndex}`;
|
|
669
|
+
slotNames.push(slot);
|
|
670
|
+
edits.push({ pos: call.getStart(sf), insert: `(${slot} ??= ` });
|
|
671
|
+
edits.push({ pos: call.getEnd(), insert: `)` });
|
|
672
|
+
hoistedCount += 1;
|
|
673
|
+
}
|
|
674
|
+
const errorCount = countErrors(ctx.diagnostics);
|
|
675
|
+
if (errorCount > 0) {
|
|
676
|
+
return {
|
|
677
|
+
fileName,
|
|
678
|
+
text: sourceText,
|
|
679
|
+
changed: false,
|
|
680
|
+
hoistedCount: 0,
|
|
681
|
+
moduleScopeCount,
|
|
682
|
+
alreadyHoistedCount,
|
|
683
|
+
diagnostics: ctx.diagnostics,
|
|
684
|
+
errorCount
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
let text = sourceText;
|
|
688
|
+
if (hoistedCount > 0) {
|
|
689
|
+
let declPos = 0;
|
|
690
|
+
for (const stmt of sf.statements) {
|
|
691
|
+
if (ts.isImportDeclaration(stmt)) declPos = stmt.getEnd();
|
|
692
|
+
}
|
|
693
|
+
const nl = sourceText.includes("\r\n") ? "\r\n" : "\n";
|
|
694
|
+
const header = `${nl}${nl}// graphddb transform prepared (#206): module-scope lazy slots \u2014 each${nl}// prepared statement compiles once per module and every later call is a${nl}// plain \`.execute\` (zero per-op planning / hashing).` + slotNames.map((s) => `${nl}let ${s};`).join("");
|
|
695
|
+
edits.push({ pos: declPos, insert: declPos === 0 ? header.trimStart() + nl : header });
|
|
696
|
+
edits.sort((a, b) => b.pos - a.pos);
|
|
697
|
+
for (const e of edits) {
|
|
698
|
+
text = text.slice(0, e.pos) + e.insert + text.slice(e.pos);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
fileName,
|
|
703
|
+
text,
|
|
704
|
+
changed: text !== sourceText,
|
|
705
|
+
hoistedCount,
|
|
706
|
+
moduleScopeCount,
|
|
707
|
+
alreadyHoistedCount,
|
|
708
|
+
diagnostics: ctx.diagnostics,
|
|
709
|
+
errorCount: 0
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function lintPreparedSource(fileName, sourceText) {
|
|
713
|
+
return transformPreparedSource(fileName, sourceText, { mode: "lint" }).diagnostics;
|
|
714
|
+
}
|
|
715
|
+
export {
|
|
716
|
+
HOIST_SLOT_PREFIX,
|
|
717
|
+
lintPreparedSource,
|
|
718
|
+
transformPreparedSource
|
|
719
|
+
};
|