@tailor-platform/sdk-codemod 0.3.0-next.2 → 0.3.0-next.4
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 +137 -7
- package/dist/codemods/ast-grep-helpers-D3FXAKNz.js +171 -0
- package/dist/codemods/v2/apply-to-deploy/scripts/transform.js +1 -1
- package/dist/codemods/v2/auth-attributes-rename/scripts/transform.js +213 -0
- package/dist/codemods/v2/auth-connection-token-helper/scripts/transform.js +243 -0
- package/dist/codemods/v2/auth-invoker-call-unwrap/scripts/transform.js +7 -0
- package/dist/codemods/v2/auth-invoker-unwrap/scripts/transform.js +13 -6
- package/dist/codemods/v2/db-type-to-table/scripts/transform.js +383 -0
- package/dist/codemods/v2/define-generators-to-plugins/scripts/transform.js +2 -1
- package/dist/codemods/v2/env-var-rename/scripts/transform.js +88 -0
- package/dist/codemods/v2/principal-unify/scripts/transform.js +428 -5
- package/dist/codemods/v2/rename-bin/scripts/transform.js +1087 -0
- package/dist/codemods/v2/runtime-globals-opt-in/scripts/transform.js +103 -0
- package/dist/codemods/v2/runtime-subpath-namespace/scripts/transform.js +792 -0
- package/dist/codemods/v2/sdk-skills-shim/scripts/transform.js +3 -3
- package/dist/codemods/v2/tailor-output-ignore-dir/scripts/transform.js +14 -0
- package/dist/codemods/v2/wait-point-rename/scripts/transform.js +126 -0
- package/dist/index.js +868 -60
- package/package.json +8 -7
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { a as importSource, c as localDeclarationNames, i as importBindings, o as importSpecNames, r as findImportStatements, s as isTypeOnlyImport, t as buildAddNamedImportEdit } from "../../../ast-grep-helpers-D3FXAKNz.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v2/auth-connection-token-helper/scripts/transform.ts
|
|
4
|
+
const RUNTIME_MODULE = "@tailor-platform/sdk/runtime";
|
|
5
|
+
const AUTHCONNECTION = "authconnection";
|
|
6
|
+
const GET_CONNECTION_TOKEN = "getConnectionToken";
|
|
7
|
+
const JSX_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx"]);
|
|
8
|
+
const JS_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9
|
+
".js",
|
|
10
|
+
".mjs",
|
|
11
|
+
".cjs"
|
|
12
|
+
]);
|
|
13
|
+
function quickFilter(source) {
|
|
14
|
+
return source.includes(GET_CONNECTION_TOKEN);
|
|
15
|
+
}
|
|
16
|
+
function sourceLang(filePath, source) {
|
|
17
|
+
const lower = filePath.toLowerCase();
|
|
18
|
+
const extension = lower.slice(lower.lastIndexOf("."));
|
|
19
|
+
if (JSX_FILE_EXTENSIONS.has(extension)) return Lang.Tsx;
|
|
20
|
+
if (JS_FILE_EXTENSIONS.has(extension) && /<>|<\/>|<[A-Za-z][\w.$:-]/.test(source)) return Lang.Tsx;
|
|
21
|
+
return Lang.TypeScript;
|
|
22
|
+
}
|
|
23
|
+
function parseRoot(source, filePath) {
|
|
24
|
+
if (!quickFilter(source)) return null;
|
|
25
|
+
try {
|
|
26
|
+
return parse(sourceLang(filePath, source), source).root();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isTailorConfigSource(source) {
|
|
32
|
+
return /(^|\/)tailor\.config(?:\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/.test(source);
|
|
33
|
+
}
|
|
34
|
+
function findAuthImports(imports) {
|
|
35
|
+
const authImports = [];
|
|
36
|
+
for (const importStmt of imports) {
|
|
37
|
+
const source = importSource(importStmt);
|
|
38
|
+
if (!source || !isTailorConfigSource(source) || isTypeOnlyImport(importStmt)) continue;
|
|
39
|
+
for (const spec of importStmt.findAll({ rule: { kind: "import_specifier" } })) {
|
|
40
|
+
const names = importSpecNames(spec);
|
|
41
|
+
if (names?.importedName !== "auth" || names.typeOnly) continue;
|
|
42
|
+
authImports.push({
|
|
43
|
+
importStmt,
|
|
44
|
+
localName: names.localName,
|
|
45
|
+
spec
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return authImports;
|
|
50
|
+
}
|
|
51
|
+
function runtimeAuthconnectionReference(imports) {
|
|
52
|
+
for (const importStmt of imports) {
|
|
53
|
+
if (importSource(importStmt) !== RUNTIME_MODULE || isTypeOnlyImport(importStmt)) continue;
|
|
54
|
+
for (const binding of importBindings(importStmt)) if (binding.importedName === AUTHCONNECTION && !binding.typeOnly) return binding.localName;
|
|
55
|
+
const local = (importStmt.children().find((child) => child.kind() === "import_clause")?.children().find((child) => child.kind() === "namespace_import"))?.children().find((child) => child.kind() === "identifier");
|
|
56
|
+
if (local) return `${local.text()}.${AUTHCONNECTION}`;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function hasRuntimeImportCollision(root, imports) {
|
|
61
|
+
if (localDeclarationNames(root).has(AUTHCONNECTION)) return true;
|
|
62
|
+
return imports.some((importStmt) => importSource(importStmt) !== RUNTIME_MODULE && importBindings(importStmt).some((binding) => binding.localName === AUTHCONNECTION && !binding.typeOnly));
|
|
63
|
+
}
|
|
64
|
+
function hasAuthLocalCollision(root, authLocalNames) {
|
|
65
|
+
const localNames = localDeclarationNames(root);
|
|
66
|
+
return Array.from(authLocalNames).some((name) => localNames.has(name));
|
|
67
|
+
}
|
|
68
|
+
function findDirectAuthCalls(root, authLocalNames) {
|
|
69
|
+
const calls = [];
|
|
70
|
+
for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
|
|
71
|
+
const callee = call.field("function");
|
|
72
|
+
if (callee?.kind() !== "member_expression") continue;
|
|
73
|
+
const object = callee.field("object");
|
|
74
|
+
const property = callee.field("property");
|
|
75
|
+
if (object?.kind() !== "identifier" || property?.text() !== GET_CONNECTION_TOKEN || !authLocalNames.has(object.text())) continue;
|
|
76
|
+
const range = object.range();
|
|
77
|
+
calls.push({
|
|
78
|
+
objectNode: object,
|
|
79
|
+
localName: object.text(),
|
|
80
|
+
range: [range.start.index, range.end.index]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return calls;
|
|
84
|
+
}
|
|
85
|
+
function isInsideImportStatement(node) {
|
|
86
|
+
let current = node.parent();
|
|
87
|
+
while (current) {
|
|
88
|
+
if (current.kind() === "import_statement") return true;
|
|
89
|
+
current = current.parent();
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function isInsideScheduledRange(node, ranges) {
|
|
94
|
+
const start = node.range().start.index;
|
|
95
|
+
return ranges.some(([rangeStart, rangeEnd]) => start >= rangeStart && start < rangeEnd);
|
|
96
|
+
}
|
|
97
|
+
function countRemainingRefs(root, localName, scheduledRanges) {
|
|
98
|
+
return root.findAll({ rule: { any: [{ kind: "identifier" }, { kind: "shorthand_property_identifier" }] } }).filter((node) => node.text() === localName).filter((node) => !isInsideImportStatement(node) && !isInsideScheduledRange(node, scheduledRanges)).length;
|
|
99
|
+
}
|
|
100
|
+
function importInsertionIndex(root, imports, source) {
|
|
101
|
+
const lastImport = imports.at(-1);
|
|
102
|
+
if (lastImport) return lastImport.range().end.index;
|
|
103
|
+
if (source.startsWith("#!")) {
|
|
104
|
+
const newlineIndex = source.indexOf("\n");
|
|
105
|
+
return newlineIndex === -1 ? source.length : newlineIndex + 1;
|
|
106
|
+
}
|
|
107
|
+
return root.children().find((child) => child.kind() !== "comment")?.range().start.index ?? 0;
|
|
108
|
+
}
|
|
109
|
+
function buildAddRuntimeImportEdit(root, source, imports) {
|
|
110
|
+
return buildAddNamedImportEdit({
|
|
111
|
+
importName: AUTHCONNECTION,
|
|
112
|
+
imports,
|
|
113
|
+
insertionIndex: importInsertionIndex,
|
|
114
|
+
moduleName: RUNTIME_MODULE,
|
|
115
|
+
root,
|
|
116
|
+
source
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function lineStartIndex(source, index) {
|
|
120
|
+
let pos = index;
|
|
121
|
+
while (pos > 0 && source[pos - 1] !== "\n" && source[pos - 1] !== "\r") pos--;
|
|
122
|
+
return pos;
|
|
123
|
+
}
|
|
124
|
+
function consumeLineBreak(source, index) {
|
|
125
|
+
if (source[index] === "\r") return source[index + 1] === "\n" ? index + 2 : index + 1;
|
|
126
|
+
if (source[index] === "\n") return index + 1;
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
129
|
+
function isHorizontalWhitespace(source, start, end) {
|
|
130
|
+
for (let index = start; index < end; index++) if (source[index] !== " " && source[index] !== " ") return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function buildImportRemovalEdit(source, binding) {
|
|
134
|
+
if (binding.importStmt.findAll({ rule: { kind: "import_specifier" } }).length === 1) {
|
|
135
|
+
const range = binding.importStmt.range();
|
|
136
|
+
let end = range.end.index;
|
|
137
|
+
if (source[end] === "\n") end++;
|
|
138
|
+
return {
|
|
139
|
+
startPos: range.start.index,
|
|
140
|
+
endPos: end,
|
|
141
|
+
insertedText: ""
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const range = binding.spec.range();
|
|
145
|
+
let start = range.start.index;
|
|
146
|
+
let end = range.end.index;
|
|
147
|
+
const specEnd = end;
|
|
148
|
+
while (end < source.length && /[ \t]/.test(source[end])) end++;
|
|
149
|
+
if (source[end] === ",") {
|
|
150
|
+
end++;
|
|
151
|
+
while (end < source.length && /[ \t]/.test(source[end])) end++;
|
|
152
|
+
const nextLine = consumeLineBreak(source, end);
|
|
153
|
+
const lineStart = lineStartIndex(source, start);
|
|
154
|
+
if (nextLine !== end && isHorizontalWhitespace(source, lineStart, start)) return {
|
|
155
|
+
startPos: lineStart,
|
|
156
|
+
endPos: nextLine,
|
|
157
|
+
insertedText: ""
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
startPos: start,
|
|
161
|
+
endPos: end,
|
|
162
|
+
insertedText: ""
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const nextLine = consumeLineBreak(source, end);
|
|
166
|
+
const lineStart = lineStartIndex(source, start);
|
|
167
|
+
if (nextLine !== end && isHorizontalWhitespace(source, lineStart, start)) return {
|
|
168
|
+
startPos: lineStart,
|
|
169
|
+
endPos: nextLine,
|
|
170
|
+
insertedText: ""
|
|
171
|
+
};
|
|
172
|
+
while (start > 0 && /[ \t]/.test(source[start - 1])) start--;
|
|
173
|
+
if (source[start - 1] === ",") start--;
|
|
174
|
+
return {
|
|
175
|
+
startPos: start,
|
|
176
|
+
endPos: specEnd,
|
|
177
|
+
insertedText: ""
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function applyEdits(source, edits) {
|
|
181
|
+
return edits.toSorted((a, b) => b.startPos - a.startPos || b.endPos - a.endPos).reduce((current, edit) => `${current.slice(0, edit.startPos)}${edit.insertedText}${current.slice(edit.endPos)}`, source).replace(/^\n+/, "");
|
|
182
|
+
}
|
|
183
|
+
function transformParsed(source, root) {
|
|
184
|
+
const imports = findImportStatements(root);
|
|
185
|
+
const authImports = findAuthImports(imports);
|
|
186
|
+
if (authImports.length === 0) return null;
|
|
187
|
+
const authLocalNames = new Set(authImports.map((binding) => binding.localName));
|
|
188
|
+
if (hasAuthLocalCollision(root, authLocalNames)) return null;
|
|
189
|
+
const calls = findDirectAuthCalls(root, authLocalNames);
|
|
190
|
+
if (calls.length === 0) return null;
|
|
191
|
+
const existingRuntimeRef = runtimeAuthconnectionReference(imports);
|
|
192
|
+
if (!existingRuntimeRef && hasRuntimeImportCollision(root, imports)) return null;
|
|
193
|
+
const runtimeRef = existingRuntimeRef ?? AUTHCONNECTION;
|
|
194
|
+
const edits = calls.map((call) => call.objectNode.replace(runtimeRef));
|
|
195
|
+
if (!existingRuntimeRef) edits.push(buildAddRuntimeImportEdit(root, source, imports));
|
|
196
|
+
const scheduledRangesByLocalName = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const call of calls) {
|
|
198
|
+
const ranges = scheduledRangesByLocalName.get(call.localName) ?? [];
|
|
199
|
+
ranges.push(call.range);
|
|
200
|
+
scheduledRangesByLocalName.set(call.localName, ranges);
|
|
201
|
+
}
|
|
202
|
+
for (const binding of authImports) {
|
|
203
|
+
if (!scheduledRangesByLocalName.has(binding.localName)) continue;
|
|
204
|
+
if (countRemainingRefs(root, binding.localName, scheduledRangesByLocalName.get(binding.localName) ?? []) > 0) continue;
|
|
205
|
+
const edit = buildImportRemovalEdit(source, binding);
|
|
206
|
+
if (edit) edits.push(edit);
|
|
207
|
+
}
|
|
208
|
+
const result = applyEdits(source, edits);
|
|
209
|
+
return result === source ? null : result;
|
|
210
|
+
}
|
|
211
|
+
function transform(source, filePath) {
|
|
212
|
+
const root = parseRoot(source, filePath);
|
|
213
|
+
return root ? transformParsed(source, root) : null;
|
|
214
|
+
}
|
|
215
|
+
function lineForIndex(source, index) {
|
|
216
|
+
return source.slice(0, index).split(/\r\n|\r|\n/).length;
|
|
217
|
+
}
|
|
218
|
+
function excerptForLine(line) {
|
|
219
|
+
return line.trim();
|
|
220
|
+
}
|
|
221
|
+
function isReviewLine(excerpt) {
|
|
222
|
+
if (!excerpt.includes(GET_CONNECTION_TOKEN)) return false;
|
|
223
|
+
if (excerpt.includes(`${AUTHCONNECTION}.${GET_CONNECTION_TOKEN}`) || excerpt.includes(`tailor.${AUTHCONNECTION}.${GET_CONNECTION_TOKEN}`)) return false;
|
|
224
|
+
return excerpt.includes(`.${GET_CONNECTION_TOKEN}`) || excerpt.includes(`["${GET_CONNECTION_TOKEN}"]`) || excerpt.includes(`['${GET_CONNECTION_TOKEN}']`) || new RegExp(`[,{]\\s*${GET_CONNECTION_TOKEN}\\s*[:}=,]`).test(excerpt);
|
|
225
|
+
}
|
|
226
|
+
function reviewFindings(source, _filePath, relativePath) {
|
|
227
|
+
if (!quickFilter(source)) return [];
|
|
228
|
+
const findings = [];
|
|
229
|
+
let offset = 0;
|
|
230
|
+
for (const line of source.split(/\n/)) {
|
|
231
|
+
const excerpt = excerptForLine(line);
|
|
232
|
+
if (isReviewLine(excerpt)) findings.push({
|
|
233
|
+
file: relativePath,
|
|
234
|
+
line: lineForIndex(source, offset),
|
|
235
|
+
message: "Replace defineAuth auth.getConnectionToken() with runtime authconnection.",
|
|
236
|
+
excerpt
|
|
237
|
+
});
|
|
238
|
+
offset += line.length + 1;
|
|
239
|
+
}
|
|
240
|
+
return findings;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
export { transform as default, reviewFindings };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { transformAuthInvoker } from "../../auth-invoker-unwrap/scripts/transform.js";
|
|
2
|
+
//#region codemods/v2/auth-invoker-call-unwrap/scripts/transform.ts
|
|
3
|
+
function transform(source, filePath) {
|
|
4
|
+
return transformAuthInvoker(source, filePath, { renameOptionKeys: false });
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { transform as default };
|
|
@@ -209,7 +209,7 @@ function renameQuotedKey(node) {
|
|
|
209
209
|
}
|
|
210
210
|
/**
|
|
211
211
|
* Replace `auth.invoker("name")` calls with the bare `"name"` string literal
|
|
212
|
-
* and rename `authInvoker:` option keys to `invoker:`.
|
|
212
|
+
* and optionally rename `authInvoker:` option keys to `invoker:`.
|
|
213
213
|
* If no other `auth` references remain after the rewrite, drop the `auth`
|
|
214
214
|
* specifier (or the entire import line when `auth` was its sole specifier).
|
|
215
215
|
*
|
|
@@ -218,16 +218,20 @@ function renameQuotedKey(node) {
|
|
|
218
218
|
* would otherwise pull config-layer modules into runtime bundles.
|
|
219
219
|
* @param source - File contents
|
|
220
220
|
* @param filePath - Absolute path to the file (kept for the runner signature)
|
|
221
|
+
* @param options - Transform behavior flags
|
|
221
222
|
* @returns Transformed source or null when nothing matched.
|
|
222
223
|
*/
|
|
223
|
-
function
|
|
224
|
+
function transformAuthInvoker(source, _filePath, options = {}) {
|
|
224
225
|
if (!quickFilter(source)) return null;
|
|
226
|
+
const renameOptionKeys = options.renameOptionKeys ?? true;
|
|
225
227
|
const root = parse(source.includes("</") || source.includes("/>") ? Lang.Tsx : Lang.TypeScript, source).root();
|
|
226
228
|
const calls = findInvokerCalls(root).filter((c) => isSupportedInvokerValueCall(c.callNode));
|
|
227
229
|
const edits = calls.map((c) => c.callNode.replace(c.argText));
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
if (renameOptionKeys) {
|
|
231
|
+
edits.push(...findAuthInvokerPropertyKeys(root).map((node) => node.replace("invoker")));
|
|
232
|
+
edits.push(...findQuotedAuthInvokerPropertyKeys(root).map((node) => node.replace(renameQuotedKey(node))));
|
|
233
|
+
edits.push(...findAuthInvokerShorthands(root).map((node) => node.replace("invoker: authInvoker")));
|
|
234
|
+
}
|
|
231
235
|
if (calls.length > 0 && countRemainingAuthRefs(root, calls.map((c) => c.range)) === 0) for (const importStmt of findAuthImports(root)) {
|
|
232
236
|
const edit = buildAuthImportRemovalEdit(source, importStmt);
|
|
233
237
|
if (edit) edits.push(edit);
|
|
@@ -236,5 +240,8 @@ function transform(source, _filePath) {
|
|
|
236
240
|
result = result.replace(/^[\t ]*\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
237
241
|
return result === source ? null : result;
|
|
238
242
|
}
|
|
243
|
+
function transform(source, filePath) {
|
|
244
|
+
return transformAuthInvoker(source, filePath);
|
|
245
|
+
}
|
|
239
246
|
//#endregion
|
|
240
|
-
export { transform as default };
|
|
247
|
+
export { transform as default, transformAuthInvoker };
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { a as importSource, i as importBindings, r as findImportStatements } from "../../../ast-grep-helpers-D3FXAKNz.js";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
//#region codemods/v2/db-type-to-table/scripts/transform.ts
|
|
4
|
+
const SDK_MODULE = "@tailor-platform/sdk";
|
|
5
|
+
function sourceLang(filePath, source) {
|
|
6
|
+
return filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || source.includes("</") ? Lang.Tsx : Lang.TypeScript;
|
|
7
|
+
}
|
|
8
|
+
function namespaceImportNames(importStmt) {
|
|
9
|
+
return importStmt.findAll({ rule: { kind: "namespace_import" } }).flatMap((node) => node.children().filter((child) => child.kind() === "identifier")).map((node) => node.text());
|
|
10
|
+
}
|
|
11
|
+
function isInsideImportStatement(node) {
|
|
12
|
+
let current = node.parent();
|
|
13
|
+
while (current) {
|
|
14
|
+
if (current.kind() === "import_statement") return true;
|
|
15
|
+
current = current.parent();
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function isBindingLeafKind(kind) {
|
|
20
|
+
return kind === "identifier" || kind === "shorthand_property_identifier_pattern";
|
|
21
|
+
}
|
|
22
|
+
function isBindingPatternKind(kind) {
|
|
23
|
+
return isBindingLeafKind(kind) || kind === "object_pattern" || kind === "array_pattern" || kind === "rest_pattern";
|
|
24
|
+
}
|
|
25
|
+
function collectBindingNodes(node, names, result) {
|
|
26
|
+
if (isBindingLeafKind(node.kind())) {
|
|
27
|
+
if (names.has(node.text())) result.push(node);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const child of node.children()) {
|
|
31
|
+
if (child.kind() === "property_identifier") continue;
|
|
32
|
+
if (child.kind() === "=") break;
|
|
33
|
+
collectBindingNodes(child, names, result);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function bindingNodes(node, names) {
|
|
37
|
+
const result = [];
|
|
38
|
+
collectBindingNodes(node, names, result);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function directBindingNodes(node, names) {
|
|
42
|
+
const result = [];
|
|
43
|
+
for (const child of node.children()) {
|
|
44
|
+
if (child.kind() === "=") break;
|
|
45
|
+
if (isBindingPatternKind(child.kind())) collectBindingNodes(child, names, result);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function firstDeclaratorChild(node) {
|
|
50
|
+
return node.children().find((child) => child.kind() !== "=") ?? null;
|
|
51
|
+
}
|
|
52
|
+
function declaratorValue(node) {
|
|
53
|
+
const children = node.children();
|
|
54
|
+
const equalsIndex = children.findIndex((child) => child.kind() === "=");
|
|
55
|
+
if (equalsIndex === -1) return null;
|
|
56
|
+
return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null;
|
|
57
|
+
}
|
|
58
|
+
function assignmentTarget(node) {
|
|
59
|
+
const children = node.children();
|
|
60
|
+
const equalsIndex = children.findIndex((child) => child.kind() === "=");
|
|
61
|
+
if (equalsIndex === -1) return null;
|
|
62
|
+
return children.slice(0, equalsIndex).find((child) => child.kind() !== "comment") ?? null;
|
|
63
|
+
}
|
|
64
|
+
function assignmentValue(node) {
|
|
65
|
+
const children = node.children();
|
|
66
|
+
const equalsIndex = children.findIndex((child) => child.kind() === "=");
|
|
67
|
+
if (equalsIndex === -1) return null;
|
|
68
|
+
return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null;
|
|
69
|
+
}
|
|
70
|
+
function parameterDefaultTarget(node) {
|
|
71
|
+
const children = node.children();
|
|
72
|
+
const equalsIndex = children.findIndex((child) => child.kind() === "=");
|
|
73
|
+
if (equalsIndex === -1) return null;
|
|
74
|
+
return children.slice(0, equalsIndex).find((child) => isBindingPatternKind(child.kind())) ?? null;
|
|
75
|
+
}
|
|
76
|
+
function parameterDefaultValue(node) {
|
|
77
|
+
const children = node.children();
|
|
78
|
+
const equalsIndex = children.findIndex((child) => child.kind() === "=");
|
|
79
|
+
if (equalsIndex === -1) return null;
|
|
80
|
+
return children.slice(equalsIndex + 1).find((child) => child.kind() !== "comment") ?? null;
|
|
81
|
+
}
|
|
82
|
+
function addShadowedRange(shadowedRanges, name, scopeNode) {
|
|
83
|
+
const range = scopeNode.range();
|
|
84
|
+
if (!shadowedRanges.has(name)) shadowedRanges.set(name, []);
|
|
85
|
+
shadowedRanges.get(name).push({
|
|
86
|
+
start: range.start.index,
|
|
87
|
+
end: range.end.index
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function nearestScope(node) {
|
|
91
|
+
let current = node.parent();
|
|
92
|
+
while (current) {
|
|
93
|
+
const kind = current.kind();
|
|
94
|
+
if (kind === "statement_block" || kind === "program" || kind === "switch_body" || kind === "for_statement" || kind === "for_in_statement") return current;
|
|
95
|
+
current = current.parent();
|
|
96
|
+
}
|
|
97
|
+
return node;
|
|
98
|
+
}
|
|
99
|
+
function functionScope(node) {
|
|
100
|
+
let current = node.parent();
|
|
101
|
+
while (current) {
|
|
102
|
+
const kind = current.kind();
|
|
103
|
+
if (kind === "function_declaration" || kind === "function_expression" || kind === "arrow_function" || kind === "method_definition" || kind === "program") return current;
|
|
104
|
+
current = current.parent();
|
|
105
|
+
}
|
|
106
|
+
return node;
|
|
107
|
+
}
|
|
108
|
+
function variableDeclarationScope(node) {
|
|
109
|
+
const declaration = node.parent();
|
|
110
|
+
if (/^var\b/.test(declaration?.text().trimStart() ?? "")) return functionScope(node);
|
|
111
|
+
return nearestScope(node);
|
|
112
|
+
}
|
|
113
|
+
function parameterScope(node) {
|
|
114
|
+
let current = node.parent();
|
|
115
|
+
while (current) {
|
|
116
|
+
const kind = current.kind();
|
|
117
|
+
if (kind === "formal_parameters") {
|
|
118
|
+
current = current.parent();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (kind === "function_declaration" || kind === "function_expression" || kind === "arrow_function" || kind === "method_definition") return current;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return nearestScope(node);
|
|
125
|
+
}
|
|
126
|
+
function buildShadowedRanges(root, names) {
|
|
127
|
+
const shadowedRanges = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) {
|
|
129
|
+
if (isInsideImportStatement(decl)) continue;
|
|
130
|
+
const binding = firstDeclaratorChild(decl);
|
|
131
|
+
if (!binding) continue;
|
|
132
|
+
for (const name of bindingNodes(binding, names)) addShadowedRange(shadowedRanges, name.text(), variableDeclarationScope(decl));
|
|
133
|
+
}
|
|
134
|
+
for (const decl of root.findAll({ rule: { any: [
|
|
135
|
+
{ kind: "function_declaration" },
|
|
136
|
+
{ kind: "class_declaration" },
|
|
137
|
+
{ kind: "enum_declaration" }
|
|
138
|
+
] } })) {
|
|
139
|
+
const name = decl.children().find((child) => child.kind() === "identifier" && names.has(child.text()));
|
|
140
|
+
if (name) addShadowedRange(shadowedRanges, name.text(), nearestScope(decl));
|
|
141
|
+
}
|
|
142
|
+
for (const param of root.findAll({ rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] } })) for (const name of directBindingNodes(param, names)) addShadowedRange(shadowedRanges, name.text(), parameterScope(param));
|
|
143
|
+
for (const arrow of root.findAll({ rule: { kind: "arrow_function" } })) {
|
|
144
|
+
const children = arrow.children();
|
|
145
|
+
const arrowIndex = children.findIndex((child) => child.kind() === "=>");
|
|
146
|
+
if (arrowIndex === -1) continue;
|
|
147
|
+
for (const child of children.slice(0, arrowIndex)) {
|
|
148
|
+
if (child.kind() === "=") break;
|
|
149
|
+
if (!isBindingPatternKind(child.kind())) continue;
|
|
150
|
+
for (const name of bindingNodes(child, names)) addShadowedRange(shadowedRanges, name.text(), arrow);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const catchClause of root.findAll({ rule: { kind: "catch_clause" } })) for (const name of directBindingNodes(catchClause, names)) addShadowedRange(shadowedRanges, name.text(), catchClause);
|
|
154
|
+
for (const loop of root.findAll({ rule: { kind: "for_in_statement" } })) {
|
|
155
|
+
const children = loop.children();
|
|
156
|
+
const keywordIndex = children.findIndex((child) => child.kind() === "in" || child.kind() === "of");
|
|
157
|
+
if (keywordIndex === -1) continue;
|
|
158
|
+
for (const child of children.slice(0, keywordIndex)) for (const name of bindingNodes(child, names)) addShadowedRange(shadowedRanges, name.text(), loop);
|
|
159
|
+
}
|
|
160
|
+
return shadowedRanges;
|
|
161
|
+
}
|
|
162
|
+
function isShadowed(node, shadowedRanges) {
|
|
163
|
+
const ranges = shadowedRanges.get(node.text());
|
|
164
|
+
if (!ranges) return false;
|
|
165
|
+
const position = node.range().start.index;
|
|
166
|
+
return ranges.some((range) => position >= range.start && position < range.end);
|
|
167
|
+
}
|
|
168
|
+
function unwrapExpression(node) {
|
|
169
|
+
let current = node;
|
|
170
|
+
while (current) {
|
|
171
|
+
const kind = current.kind();
|
|
172
|
+
if (kind === "parenthesized_expression") {
|
|
173
|
+
current = current.children().find((child) => child.kind() !== "(" && child.kind() !== ")") ?? null;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (kind === "as_expression" || kind === "satisfies_expression" || kind === "non_null_expression") {
|
|
177
|
+
current = current.children()[0] ?? null;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (kind === "type_assertion") {
|
|
181
|
+
current = current.children().find((child) => child.kind() !== "type_arguments") ?? null;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function isSdkDbMember(object, dbNames, namespaceNames, shadowedRanges) {
|
|
189
|
+
const unwrapped = unwrapExpression(object);
|
|
190
|
+
if (!unwrapped) return false;
|
|
191
|
+
if (unwrapped.kind() === "identifier") return dbNames.has(unwrapped.text()) && !isShadowed(unwrapped, shadowedRanges);
|
|
192
|
+
if (unwrapped.kind() !== "member_expression") return false;
|
|
193
|
+
const base = unwrapExpression(unwrapped.field("object"));
|
|
194
|
+
const property = memberProperty(unwrapped);
|
|
195
|
+
return base?.kind() === "identifier" && namespaceNames.has(base.text()) && !isShadowed(base, shadowedRanges) && property?.text() === "db";
|
|
196
|
+
}
|
|
197
|
+
function memberProperty(member) {
|
|
198
|
+
const children = member.children();
|
|
199
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
200
|
+
const child = children[index];
|
|
201
|
+
if (child.kind() === "property_identifier") return child;
|
|
202
|
+
}
|
|
203
|
+
return member.field("property");
|
|
204
|
+
}
|
|
205
|
+
function typeStringLiteral(node) {
|
|
206
|
+
if (!node) return null;
|
|
207
|
+
const kind = node.kind();
|
|
208
|
+
if (kind !== "string" && kind !== "template_string") return null;
|
|
209
|
+
const fragments = node.children().filter((child) => child.kind() === "string_fragment");
|
|
210
|
+
return fragments.length === 1 && fragments[0].text() === "type" ? node : null;
|
|
211
|
+
}
|
|
212
|
+
function replaceStringLiteralValue(node, value) {
|
|
213
|
+
const text = node.text();
|
|
214
|
+
const quote = text.startsWith("'") ? "'" : text.startsWith("`") ? "`" : "\"";
|
|
215
|
+
return node.replace(`${quote}${value}${quote}`);
|
|
216
|
+
}
|
|
217
|
+
function hasTypeBuilderUse(root, name, afterIndex) {
|
|
218
|
+
for (const member of root.findAll({ rule: { kind: "member_expression" } })) {
|
|
219
|
+
if (member.range().start.index <= afterIndex) continue;
|
|
220
|
+
if (memberProperty(member)?.text() !== "type") continue;
|
|
221
|
+
const object = unwrapExpression(member.field("object"));
|
|
222
|
+
if (object?.kind() === "identifier" && object.text() === name) return true;
|
|
223
|
+
}
|
|
224
|
+
for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) {
|
|
225
|
+
if (subscript.range().start.index <= afterIndex) continue;
|
|
226
|
+
if (!typeStringLiteral(subscript.field("index"))) continue;
|
|
227
|
+
const object = unwrapExpression(subscript.field("object"));
|
|
228
|
+
if (object?.kind() === "identifier" && object.text() === name) return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function transform(source, filePath) {
|
|
233
|
+
if (!source.includes("type") || !source.includes(SDK_MODULE)) return null;
|
|
234
|
+
let root;
|
|
235
|
+
try {
|
|
236
|
+
root = parse(sourceLang(filePath, source), source).root();
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const imports = findImportStatements(root).filter((importStmt) => importSource(importStmt) === SDK_MODULE);
|
|
241
|
+
if (imports.length === 0) return null;
|
|
242
|
+
const dbNames = /* @__PURE__ */ new Set();
|
|
243
|
+
const namespaceNames = /* @__PURE__ */ new Set();
|
|
244
|
+
for (const importStmt of imports) {
|
|
245
|
+
for (const binding of importBindings(importStmt)) if (binding.importedName === "db") dbNames.add(binding.localName);
|
|
246
|
+
for (const name of namespaceImportNames(importStmt)) namespaceNames.add(name);
|
|
247
|
+
}
|
|
248
|
+
if (dbNames.size === 0 && namespaceNames.size === 0) return null;
|
|
249
|
+
const shadowedRanges = buildShadowedRanges(root, /* @__PURE__ */ new Set([...dbNames, ...namespaceNames]));
|
|
250
|
+
const edits = [];
|
|
251
|
+
for (const member of root.findAll({ rule: { kind: "member_expression" } })) {
|
|
252
|
+
const property = memberProperty(member);
|
|
253
|
+
if (property?.text() !== "type") continue;
|
|
254
|
+
if (!isSdkDbMember(member.field("object"), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
255
|
+
edits.push(property.replace("table"));
|
|
256
|
+
}
|
|
257
|
+
for (const subscript of root.findAll({ rule: { kind: "subscript_expression" } })) {
|
|
258
|
+
const index = typeStringLiteral(subscript.field("index"));
|
|
259
|
+
if (!index) continue;
|
|
260
|
+
if (!isSdkDbMember(subscript.field("object"), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
261
|
+
edits.push(replaceStringLiteralValue(index, "table"));
|
|
262
|
+
}
|
|
263
|
+
return edits.length > 0 ? root.commitEdits(edits) : null;
|
|
264
|
+
}
|
|
265
|
+
function lineForIndex(source, index) {
|
|
266
|
+
return source.slice(0, index).split(/\r\n|\r|\n/).length;
|
|
267
|
+
}
|
|
268
|
+
function excerptAtIndex(source, index) {
|
|
269
|
+
const lineStart = Math.max(source.lastIndexOf("\n", index - 1) + 1, 0);
|
|
270
|
+
const lineEnd = source.indexOf("\n", index);
|
|
271
|
+
return source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd).trim();
|
|
272
|
+
}
|
|
273
|
+
function objectPatternHasTypeProperty(pattern) {
|
|
274
|
+
return pattern.findAll({ rule: { any: [{
|
|
275
|
+
kind: "property_identifier",
|
|
276
|
+
regex: "^type$"
|
|
277
|
+
}, {
|
|
278
|
+
kind: "shorthand_property_identifier_pattern",
|
|
279
|
+
regex: "^type$"
|
|
280
|
+
}] } }).some((node) => node.text() === "type");
|
|
281
|
+
}
|
|
282
|
+
function namespaceDbAliasBindings(pattern) {
|
|
283
|
+
const aliases = [];
|
|
284
|
+
for (const child of pattern.children()) {
|
|
285
|
+
if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "db") {
|
|
286
|
+
aliases.push(child);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (child.kind() !== "pair_pattern") continue;
|
|
290
|
+
if (child.field("key")?.text() !== "db") continue;
|
|
291
|
+
const value = child.field("value");
|
|
292
|
+
if (value?.kind() === "identifier") aliases.push(value);
|
|
293
|
+
}
|
|
294
|
+
return aliases;
|
|
295
|
+
}
|
|
296
|
+
function isSdkNamespaceMember(node, namespaceNames, shadowedRanges) {
|
|
297
|
+
const unwrapped = unwrapExpression(node);
|
|
298
|
+
return unwrapped?.kind() === "identifier" && namespaceNames.has(unwrapped.text()) && !isShadowed(unwrapped, shadowedRanges);
|
|
299
|
+
}
|
|
300
|
+
function reviewFindings(source, filePath, relativePath) {
|
|
301
|
+
if (!source.includes("type") || !source.includes(SDK_MODULE)) return [];
|
|
302
|
+
let root;
|
|
303
|
+
try {
|
|
304
|
+
root = parse(sourceLang(filePath, source), source).root();
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
const imports = findImportStatements(root).filter((importStmt) => importSource(importStmt) === SDK_MODULE);
|
|
309
|
+
if (imports.length === 0) return [];
|
|
310
|
+
const dbNames = /* @__PURE__ */ new Set();
|
|
311
|
+
const namespaceNames = /* @__PURE__ */ new Set();
|
|
312
|
+
for (const importStmt of imports) {
|
|
313
|
+
for (const binding of importBindings(importStmt)) if (binding.importedName === "db") dbNames.add(binding.localName);
|
|
314
|
+
for (const name of namespaceImportNames(importStmt)) namespaceNames.add(name);
|
|
315
|
+
}
|
|
316
|
+
if (dbNames.size === 0 && namespaceNames.size === 0) return [];
|
|
317
|
+
const shadowedRanges = buildShadowedRanges(root, /* @__PURE__ */ new Set([...dbNames, ...namespaceNames]));
|
|
318
|
+
const findings = [];
|
|
319
|
+
for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) {
|
|
320
|
+
const binding = firstDeclaratorChild(decl);
|
|
321
|
+
if (binding?.kind() !== "object_pattern" || !objectPatternHasTypeProperty(binding)) continue;
|
|
322
|
+
if (!isSdkDbMember(declaratorValue(decl), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
323
|
+
findings.push({
|
|
324
|
+
file: relativePath,
|
|
325
|
+
line: lineForIndex(source, binding.range().start.index),
|
|
326
|
+
message: "Review destructured db.type builder usage and migrate it to db.table.",
|
|
327
|
+
excerpt: excerptAtIndex(source, binding.range().start.index)
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) {
|
|
331
|
+
const binding = firstDeclaratorChild(decl);
|
|
332
|
+
if (binding?.kind() !== "object_pattern") continue;
|
|
333
|
+
if (!isSdkNamespaceMember(declaratorValue(decl), namespaceNames, shadowedRanges)) continue;
|
|
334
|
+
for (const alias of namespaceDbAliasBindings(binding)) {
|
|
335
|
+
if (!hasTypeBuilderUse(root, alias.text(), decl.range().end.index)) continue;
|
|
336
|
+
findings.push({
|
|
337
|
+
file: relativePath,
|
|
338
|
+
line: lineForIndex(source, binding.range().start.index),
|
|
339
|
+
message: "Review SDK db alias usage and migrate db.type builder calls to db.table.",
|
|
340
|
+
excerpt: excerptAtIndex(source, binding.range().start.index)
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const decl of root.findAll({ rule: { kind: "variable_declarator" } })) {
|
|
345
|
+
const binding = firstDeclaratorChild(decl);
|
|
346
|
+
if (binding?.kind() !== "identifier") continue;
|
|
347
|
+
if (!isSdkDbMember(declaratorValue(decl), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
348
|
+
if (!hasTypeBuilderUse(root, binding.text(), decl.range().end.index)) continue;
|
|
349
|
+
findings.push({
|
|
350
|
+
file: relativePath,
|
|
351
|
+
line: lineForIndex(source, binding.range().start.index),
|
|
352
|
+
message: "Review SDK db alias usage and migrate db.type builder calls to db.table.",
|
|
353
|
+
excerpt: excerptAtIndex(source, binding.range().start.index)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
for (const assignment of root.findAll({ rule: { kind: "assignment_expression" } })) {
|
|
357
|
+
const target = assignmentTarget(assignment);
|
|
358
|
+
if (target?.kind() !== "identifier") continue;
|
|
359
|
+
if (!isSdkDbMember(assignmentValue(assignment), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
360
|
+
if (!hasTypeBuilderUse(root, target.text(), assignment.range().end.index)) continue;
|
|
361
|
+
findings.push({
|
|
362
|
+
file: relativePath,
|
|
363
|
+
line: lineForIndex(source, assignment.range().start.index),
|
|
364
|
+
message: "Review SDK db alias usage and migrate db.type builder calls to db.table.",
|
|
365
|
+
excerpt: excerptAtIndex(source, assignment.range().start.index)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
for (const param of root.findAll({ rule: { any: [{ kind: "required_parameter" }, { kind: "optional_parameter" }] } })) {
|
|
369
|
+
const target = parameterDefaultTarget(param);
|
|
370
|
+
if (target?.kind() !== "identifier") continue;
|
|
371
|
+
if (!isSdkDbMember(parameterDefaultValue(param), dbNames, namespaceNames, shadowedRanges)) continue;
|
|
372
|
+
if (!hasTypeBuilderUse(root, target.text(), param.range().end.index)) continue;
|
|
373
|
+
findings.push({
|
|
374
|
+
file: relativePath,
|
|
375
|
+
line: lineForIndex(source, param.range().start.index),
|
|
376
|
+
message: "Review SDK db alias usage and migrate db.type builder calls to db.table.",
|
|
377
|
+
excerpt: excerptAtIndex(source, param.range().start.index)
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return findings;
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
export { transform as default, reviewFindings };
|