@514labs/moose-lsp 1.4.0 → 1.4.1
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/codeActions.d.ts +7 -2
- package/dist/codeActions.js +57 -10
- package/dist/codeActions.js.map +1 -1
- package/dist/codeActions.test.js +64 -2
- package/dist/codeActions.test.js.map +1 -1
- package/dist/diagnostics.d.ts +9 -1
- package/dist/diagnostics.js +24 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/diagnostics.test.js +56 -5
- package/dist/diagnostics.test.js.map +1 -1
- package/dist/node_modules/@514labs/moose-sql-validator-wasm/package.json +1 -1
- package/dist/node_modules/@514labs/moose-sql-validator-wasm/pkg/sql_validator_bg.wasm +0 -0
- package/dist/pythonSqlExtractor.js +5 -1
- package/dist/pythonSqlExtractor.js.map +1 -1
- package/dist/server.integration.test.js +222 -26
- package/dist/server.integration.test.js.map +1 -1
- package/dist/server.js +209 -87
- package/dist/server.js.map +1 -1
- package/dist/serverLogic.js +9 -0
- package/dist/serverLogic.js.map +1 -1
- package/dist/serverLogic.test.js +127 -10
- package/dist/serverLogic.test.js.map +1 -1
- package/dist/sqlExtractor.js +43 -12
- package/dist/sqlExtractor.js.map +1 -1
- package/dist/sqlExtractor.test.js +106 -4
- package/dist/sqlExtractor.test.js.map +1 -1
- package/dist/sqlLocations.d.ts +9 -1
- package/dist/sqlLocations.js +9 -1
- package/dist/sqlLocations.js.map +1 -1
- package/dist/sqlLocations.test.js +49 -1
- package/dist/sqlLocations.test.js.map +1 -1
- package/package.json +1 -1
package/dist/serverLogic.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(serverLogic_exports, {
|
|
|
24
24
|
validateSqlLocations: () => validateSqlLocations
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(serverLogic_exports);
|
|
27
|
+
var import_diagnostics = require("./diagnostics");
|
|
27
28
|
var import_sqlLocations = require("./sqlLocations");
|
|
28
29
|
function shouldValidateFile(filePath, mooseProjectRoot) {
|
|
29
30
|
if (!mooseProjectRoot) return false;
|
|
@@ -39,6 +40,14 @@ function isPythonFile(filePath) {
|
|
|
39
40
|
function validateSqlLocations(sqlLocations, validateSql, createDiagnostic) {
|
|
40
41
|
const diagnosticsMap = /* @__PURE__ */ new Map();
|
|
41
42
|
for (const location of sqlLocations) {
|
|
43
|
+
if (location.tagKind === "bare") {
|
|
44
|
+
const { uri, diagnostic } = (0, import_diagnostics.createDeprecationDiagnostic)(location);
|
|
45
|
+
if (!diagnosticsMap.has(uri)) {
|
|
46
|
+
diagnosticsMap.set(uri, []);
|
|
47
|
+
}
|
|
48
|
+
diagnosticsMap.get(uri)?.push(diagnostic);
|
|
49
|
+
}
|
|
50
|
+
if (location.tagKind === "fragment") continue;
|
|
42
51
|
const preparedSql = (0, import_sqlLocations.prepareSqlForValidation)(location.templateText);
|
|
43
52
|
const result = validateSql(preparedSql);
|
|
44
53
|
if (!result.valid && result.error) {
|
package/dist/serverLogic.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serverLogic.ts"],"sourcesContent":["import type { ValidationResult } from '@514labs/moose-sql-validator-wasm';\nimport type { Diagnostic } from 'vscode-languageserver/node';\nimport { prepareSqlForValidation, type SqlLocation } from './sqlLocations';\n\n/**\n * Function type for SQL validation\n */\nexport type ValidateSqlFn = (sql: string) => ValidationResult;\n\n/**\n * Function type for creating diagnostics from SqlLocation\n */\nexport type CreateLocationDiagnosticFn = (\n location: SqlLocation,\n error: NonNullable<ValidationResult['error']>,\n) => { uri: string; diagnostic: Diagnostic };\n\n/**\n * Determines if a file should be validated based on path and project root\n * @param filePath - The absolute path to the file\n * @param mooseProjectRoot - The root of the Moose project (or null if not detected)\n * @returns true if the file should be validated\n */\nexport function shouldValidateFile(\n filePath: string,\n mooseProjectRoot: string | null,\n): boolean {\n if (!mooseProjectRoot) return false;\n if (!filePath.startsWith(mooseProjectRoot)) return false;\n return filePath.endsWith('.ts') || filePath.endsWith('.py');\n}\n\n/**\n * Determines if a file is a TypeScript file\n */\nexport function isTypeScriptFile(filePath: string): boolean {\n return filePath.endsWith('.ts') || filePath.endsWith('.tsx');\n}\n\n/**\n * Determines if a file is a Python file\n */\nexport function isPythonFile(filePath: string): boolean {\n return filePath.endsWith('.py');\n}\n\n/**\n * Validates SQL from template locations and returns a map of URI -> diagnostics\n * @param sqlLocations - Array of SQL template locations\n * @param validateSql - Function to validate SQL strings\n * @param createDiagnostic - Function to create LSP diagnostics from validation errors\n * @returns Map of file URIs to their diagnostics\n */\nexport function validateSqlLocations(\n sqlLocations: SqlLocation[],\n validateSql: ValidateSqlFn,\n createDiagnostic: CreateLocationDiagnosticFn,\n): Map<string, Diagnostic[]> {\n const diagnosticsMap = new Map<string, Diagnostic[]>();\n\n for (const location of sqlLocations) {\n // Replace ${...} placeholders with valid SQL identifiers before validation\n const preparedSql = prepareSqlForValidation(location.templateText);\n const result = validateSql(preparedSql);\n\n if (!result.valid && result.error) {\n const { uri, diagnostic } = createDiagnostic(location, result.error);\n\n if (!diagnosticsMap.has(uri)) {\n diagnosticsMap.set(uri, []);\n }\n diagnosticsMap.get(uri)?.push(diagnostic);\n }\n }\n\n return diagnosticsMap;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,0BAA0D;AAqBnD,SAAS,mBACd,UACA,kBACS;AACT,MAAI,CAAC,iBAAkB,QAAO;AAC9B,MAAI,CAAC,SAAS,WAAW,gBAAgB,EAAG,QAAO;AACnD,SAAO,SAAS,SAAS,KAAK,KAAK,SAAS,SAAS,KAAK;AAC5D;AAKO,SAAS,iBAAiB,UAA2B;AAC1D,SAAO,SAAS,SAAS,KAAK,KAAK,SAAS,SAAS,MAAM;AAC7D;AAKO,SAAS,aAAa,UAA2B;AACtD,SAAO,SAAS,SAAS,KAAK;AAChC;AASO,SAAS,qBACd,cACA,aACA,kBAC2B;AAC3B,QAAM,iBAAiB,oBAAI,IAA0B;AAErD,aAAW,YAAY,cAAc;AAEnC,UAAM,kBAAc,6CAAwB,SAAS,YAAY;AACjE,UAAM,SAAS,YAAY,WAAW;AAEtC,QAAI,CAAC,OAAO,SAAS,OAAO,OAAO;AACjC,YAAM,EAAE,KAAK,WAAW,IAAI,iBAAiB,UAAU,OAAO,KAAK;AAEnE,UAAI,CAAC,eAAe,IAAI,GAAG,GAAG;AAC5B,uBAAe,IAAI,KAAK,CAAC,CAAC;AAAA,MAC5B;AACA,qBAAe,IAAI,GAAG,GAAG,KAAK,UAAU;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/serverLogic.ts"],"sourcesContent":["import type { ValidationResult } from '@514labs/moose-sql-validator-wasm';\nimport type { Diagnostic } from 'vscode-languageserver/node';\nimport { createDeprecationDiagnostic } from './diagnostics';\nimport { prepareSqlForValidation, type SqlLocation } from './sqlLocations';\n\n/**\n * Function type for SQL validation\n */\nexport type ValidateSqlFn = (sql: string) => ValidationResult;\n\n/**\n * Function type for creating diagnostics from SqlLocation\n */\nexport type CreateLocationDiagnosticFn = (\n location: SqlLocation,\n error: NonNullable<ValidationResult['error']>,\n) => { uri: string; diagnostic: Diagnostic };\n\n/**\n * Determines if a file should be validated based on path and project root\n * @param filePath - The absolute path to the file\n * @param mooseProjectRoot - The root of the Moose project (or null if not detected)\n * @returns true if the file should be validated\n */\nexport function shouldValidateFile(\n filePath: string,\n mooseProjectRoot: string | null,\n): boolean {\n if (!mooseProjectRoot) return false;\n if (!filePath.startsWith(mooseProjectRoot)) return false;\n return filePath.endsWith('.ts') || filePath.endsWith('.py');\n}\n\n/**\n * Determines if a file is a TypeScript file\n */\nexport function isTypeScriptFile(filePath: string): boolean {\n return filePath.endsWith('.ts') || filePath.endsWith('.tsx');\n}\n\n/**\n * Determines if a file is a Python file\n */\nexport function isPythonFile(filePath: string): boolean {\n return filePath.endsWith('.py');\n}\n\n/**\n * Validates SQL from template locations and returns a map of URI -> diagnostics\n * @param sqlLocations - Array of SQL template locations\n * @param validateSql - Function to validate SQL strings\n * @param createDiagnostic - Function to create LSP diagnostics from validation errors\n * @returns Map of file URIs to their diagnostics\n */\nexport function validateSqlLocations(\n sqlLocations: SqlLocation[],\n validateSql: ValidateSqlFn,\n createDiagnostic: CreateLocationDiagnosticFn,\n): Map<string, Diagnostic[]> {\n const diagnosticsMap = new Map<string, Diagnostic[]>();\n\n for (const location of sqlLocations) {\n // Emit deprecation hint for bare sql tags\n if (location.tagKind === 'bare') {\n const { uri, diagnostic } = createDeprecationDiagnostic(location);\n if (!diagnosticsMap.has(uri)) {\n diagnosticsMap.set(uri, []);\n }\n diagnosticsMap.get(uri)?.push(diagnostic);\n }\n\n // Skip validation for fragments — they're intentionally partial SQL\n if (location.tagKind === 'fragment') continue;\n\n // Replace ${...} placeholders with valid SQL identifiers before validation\n const preparedSql = prepareSqlForValidation(location.templateText);\n const result = validateSql(preparedSql);\n\n if (!result.valid && result.error) {\n const { uri, diagnostic } = createDiagnostic(location, result.error);\n\n if (!diagnosticsMap.has(uri)) {\n diagnosticsMap.set(uri, []);\n }\n diagnosticsMap.get(uri)?.push(diagnostic);\n }\n }\n\n return diagnosticsMap;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,yBAA4C;AAC5C,0BAA0D;AAqBnD,SAAS,mBACd,UACA,kBACS;AACT,MAAI,CAAC,iBAAkB,QAAO;AAC9B,MAAI,CAAC,SAAS,WAAW,gBAAgB,EAAG,QAAO;AACnD,SAAO,SAAS,SAAS,KAAK,KAAK,SAAS,SAAS,KAAK;AAC5D;AAKO,SAAS,iBAAiB,UAA2B;AAC1D,SAAO,SAAS,SAAS,KAAK,KAAK,SAAS,SAAS,MAAM;AAC7D;AAKO,SAAS,aAAa,UAA2B;AACtD,SAAO,SAAS,SAAS,KAAK;AAChC;AASO,SAAS,qBACd,cACA,aACA,kBAC2B;AAC3B,QAAM,iBAAiB,oBAAI,IAA0B;AAErD,aAAW,YAAY,cAAc;AAEnC,QAAI,SAAS,YAAY,QAAQ;AAC/B,YAAM,EAAE,KAAK,WAAW,QAAI,gDAA4B,QAAQ;AAChE,UAAI,CAAC,eAAe,IAAI,GAAG,GAAG;AAC5B,uBAAe,IAAI,KAAK,CAAC,CAAC;AAAA,MAC5B;AACA,qBAAe,IAAI,GAAG,GAAG,KAAK,UAAU;AAAA,IAC1C;AAGA,QAAI,SAAS,YAAY,WAAY;AAGrC,UAAM,kBAAc,6CAAwB,SAAS,YAAY;AACjE,UAAM,SAAS,YAAY,WAAW;AAEtC,QAAI,CAAC,OAAO,SAAS,OAAO,OAAO;AACjC,YAAM,EAAE,KAAK,WAAW,IAAI,iBAAiB,UAAU,OAAO,KAAK;AAEnE,UAAI,CAAC,eAAe,IAAI,GAAG,GAAG;AAC5B,uBAAe,IAAI,KAAK,CAAC,CAAC;AAAA,MAC5B;AACA,qBAAe,IAAI,GAAG,GAAG,KAAK,UAAU;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
package/dist/serverLogic.test.js
CHANGED
|
@@ -106,8 +106,12 @@ function createMockDiagnostic(message) {
|
|
|
106
106
|
column: 22,
|
|
107
107
|
endLine: 61,
|
|
108
108
|
endColumn: 6,
|
|
109
|
-
templateText: "SLECT ${...} FROM ${...}"
|
|
109
|
+
templateText: "SLECT ${...} FROM ${...}",
|
|
110
110
|
// typo
|
|
111
|
+
tagKind: "bare",
|
|
112
|
+
tagLine: 1,
|
|
113
|
+
tagColumn: 1,
|
|
114
|
+
tagEndColumn: 4
|
|
111
115
|
}
|
|
112
116
|
];
|
|
113
117
|
const mockValidateSql = () => ({
|
|
@@ -126,8 +130,10 @@ function createMockDiagnostic(message) {
|
|
|
126
130
|
import_node_assert.default.strictEqual(result.size, 1);
|
|
127
131
|
const diagnostics = result.get("file:///project/app/apis/bar.ts");
|
|
128
132
|
import_node_assert.default.ok(diagnostics);
|
|
129
|
-
import_node_assert.default.strictEqual(diagnostics.length,
|
|
130
|
-
|
|
133
|
+
import_node_assert.default.strictEqual(diagnostics.length, 2);
|
|
134
|
+
const errors = diagnostics.filter((d) => d.severity === 1);
|
|
135
|
+
import_node_assert.default.strictEqual(errors.length, 1);
|
|
136
|
+
import_node_assert.default.strictEqual(errors[0].message, "Expected SELECT, found SLECT");
|
|
131
137
|
});
|
|
132
138
|
await t.test("skips valid SQL", () => {
|
|
133
139
|
const locations = [
|
|
@@ -138,7 +144,11 @@ function createMockDiagnostic(message) {
|
|
|
138
144
|
column: 22,
|
|
139
145
|
endLine: 61,
|
|
140
146
|
endColumn: 6,
|
|
141
|
-
templateText: "SELECT ${...} FROM ${...}"
|
|
147
|
+
templateText: "SELECT ${...} FROM ${...}",
|
|
148
|
+
tagKind: "statement",
|
|
149
|
+
tagLine: 1,
|
|
150
|
+
tagColumn: 1,
|
|
151
|
+
tagEndColumn: 14
|
|
142
152
|
}
|
|
143
153
|
];
|
|
144
154
|
const mockValidateSql = () => ({ valid: true });
|
|
@@ -162,7 +172,11 @@ function createMockDiagnostic(message) {
|
|
|
162
172
|
column: 22,
|
|
163
173
|
endLine: 61,
|
|
164
174
|
endColumn: 6,
|
|
165
|
-
templateText: "SLECT ${...}"
|
|
175
|
+
templateText: "SLECT ${...}",
|
|
176
|
+
tagKind: "bare",
|
|
177
|
+
tagLine: 1,
|
|
178
|
+
tagColumn: 1,
|
|
179
|
+
tagEndColumn: 4
|
|
166
180
|
},
|
|
167
181
|
{
|
|
168
182
|
id: "app/apis/bar.ts:100:22",
|
|
@@ -171,7 +185,11 @@ function createMockDiagnostic(message) {
|
|
|
171
185
|
column: 22,
|
|
172
186
|
endLine: 105,
|
|
173
187
|
endColumn: 6,
|
|
174
|
-
templateText: "SELCT ${...}"
|
|
188
|
+
templateText: "SELCT ${...}",
|
|
189
|
+
tagKind: "bare",
|
|
190
|
+
tagLine: 1,
|
|
191
|
+
tagColumn: 1,
|
|
192
|
+
tagEndColumn: 4
|
|
175
193
|
}
|
|
176
194
|
];
|
|
177
195
|
const mockValidateSql = () => ({
|
|
@@ -190,7 +208,7 @@ function createMockDiagnostic(message) {
|
|
|
190
208
|
import_node_assert.default.strictEqual(result.size, 1);
|
|
191
209
|
const diagnostics = result.get("file:///project/app/apis/bar.ts");
|
|
192
210
|
import_node_assert.default.ok(diagnostics);
|
|
193
|
-
import_node_assert.default.strictEqual(diagnostics.length,
|
|
211
|
+
import_node_assert.default.strictEqual(diagnostics.length, 4);
|
|
194
212
|
});
|
|
195
213
|
await t.test("handles multiple files", () => {
|
|
196
214
|
const locations = [
|
|
@@ -201,7 +219,11 @@ function createMockDiagnostic(message) {
|
|
|
201
219
|
column: 5,
|
|
202
220
|
endLine: 15,
|
|
203
221
|
endColumn: 6,
|
|
204
|
-
templateText: "SLECT ${...}"
|
|
222
|
+
templateText: "SLECT ${...}",
|
|
223
|
+
tagKind: "bare",
|
|
224
|
+
tagLine: 1,
|
|
225
|
+
tagColumn: 1,
|
|
226
|
+
tagEndColumn: 4
|
|
205
227
|
},
|
|
206
228
|
{
|
|
207
229
|
id: "app/apis/bar.ts:54:22",
|
|
@@ -210,7 +232,11 @@ function createMockDiagnostic(message) {
|
|
|
210
232
|
column: 22,
|
|
211
233
|
endLine: 61,
|
|
212
234
|
endColumn: 6,
|
|
213
|
-
templateText: "SELCT ${...}"
|
|
235
|
+
templateText: "SELCT ${...}",
|
|
236
|
+
tagKind: "bare",
|
|
237
|
+
tagLine: 1,
|
|
238
|
+
tagColumn: 1,
|
|
239
|
+
tagEndColumn: 4
|
|
214
240
|
}
|
|
215
241
|
];
|
|
216
242
|
const mockValidateSql = () => ({
|
|
@@ -230,6 +256,62 @@ function createMockDiagnostic(message) {
|
|
|
230
256
|
import_node_assert.default.ok(result.has("file:///project/app/apis/foo.ts"));
|
|
231
257
|
import_node_assert.default.ok(result.has("file:///project/app/apis/bar.ts"));
|
|
232
258
|
});
|
|
259
|
+
await t.test("skips validation for fragment tagKind", () => {
|
|
260
|
+
const validatedSqls = [];
|
|
261
|
+
const locations = [
|
|
262
|
+
{
|
|
263
|
+
id: "test.ts:1:1",
|
|
264
|
+
file: "/project/test.ts",
|
|
265
|
+
line: 1,
|
|
266
|
+
column: 1,
|
|
267
|
+
endLine: 1,
|
|
268
|
+
endColumn: 50,
|
|
269
|
+
templateText: "SELECT * FROM users",
|
|
270
|
+
tagKind: "statement",
|
|
271
|
+
tagLine: 1,
|
|
272
|
+
tagColumn: 1,
|
|
273
|
+
tagEndColumn: 14
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "test.ts:5:1",
|
|
277
|
+
file: "/project/test.ts",
|
|
278
|
+
line: 5,
|
|
279
|
+
column: 1,
|
|
280
|
+
endLine: 5,
|
|
281
|
+
endColumn: 30,
|
|
282
|
+
templateText: "status = 'active'",
|
|
283
|
+
tagKind: "fragment",
|
|
284
|
+
tagLine: 5,
|
|
285
|
+
tagColumn: 1,
|
|
286
|
+
tagEndColumn: 13
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "test.ts:10:1",
|
|
290
|
+
file: "/project/test.ts",
|
|
291
|
+
line: 10,
|
|
292
|
+
column: 1,
|
|
293
|
+
endLine: 10,
|
|
294
|
+
endColumn: 50,
|
|
295
|
+
templateText: "SELECT 1",
|
|
296
|
+
tagKind: "bare",
|
|
297
|
+
tagLine: 1,
|
|
298
|
+
tagColumn: 1,
|
|
299
|
+
tagEndColumn: 4
|
|
300
|
+
}
|
|
301
|
+
];
|
|
302
|
+
const mockValidateSql = (sql) => {
|
|
303
|
+
validatedSqls.push(sql);
|
|
304
|
+
return { valid: true };
|
|
305
|
+
};
|
|
306
|
+
const mockCreateDiagnostic = () => ({
|
|
307
|
+
uri: "",
|
|
308
|
+
diagnostic: createMockDiagnostic("")
|
|
309
|
+
});
|
|
310
|
+
(0, import_serverLogic.validateSqlLocations)(locations, mockValidateSql, mockCreateDiagnostic);
|
|
311
|
+
import_node_assert.default.strictEqual(validatedSqls.length, 2);
|
|
312
|
+
import_node_assert.default.ok(validatedSqls[0].includes("SELECT"));
|
|
313
|
+
import_node_assert.default.ok(validatedSqls[1].includes("SELECT"));
|
|
314
|
+
});
|
|
233
315
|
await t.test(
|
|
234
316
|
"prepares SQL by replacing ${...} placeholders before validation",
|
|
235
317
|
() => {
|
|
@@ -241,7 +323,11 @@ function createMockDiagnostic(message) {
|
|
|
241
323
|
column: 1,
|
|
242
324
|
endLine: 1,
|
|
243
325
|
endColumn: 50,
|
|
244
|
-
templateText: "SELECT ${...} FROM ${...}"
|
|
326
|
+
templateText: "SELECT ${...} FROM ${...}",
|
|
327
|
+
tagKind: "bare",
|
|
328
|
+
tagLine: 1,
|
|
329
|
+
tagColumn: 1,
|
|
330
|
+
tagEndColumn: 4
|
|
245
331
|
}
|
|
246
332
|
];
|
|
247
333
|
let validatedSql = "";
|
|
@@ -259,5 +345,36 @@ function createMockDiagnostic(message) {
|
|
|
259
345
|
import_node_assert.default.ok(validatedSql.includes("FROM"));
|
|
260
346
|
}
|
|
261
347
|
);
|
|
348
|
+
await t.test("emits deprecation diagnostic for bare tagKind", () => {
|
|
349
|
+
const locations = [
|
|
350
|
+
{
|
|
351
|
+
id: "test.ts:1:15",
|
|
352
|
+
file: "/project/test.ts",
|
|
353
|
+
line: 1,
|
|
354
|
+
column: 15,
|
|
355
|
+
endLine: 1,
|
|
356
|
+
endColumn: 50,
|
|
357
|
+
templateText: "SELECT * FROM users",
|
|
358
|
+
tagKind: "bare",
|
|
359
|
+
tagLine: 1,
|
|
360
|
+
tagColumn: 11,
|
|
361
|
+
tagEndColumn: 14
|
|
362
|
+
}
|
|
363
|
+
];
|
|
364
|
+
const mockValidateSql = () => ({ valid: true });
|
|
365
|
+
const mockCreateDiagnostic = () => ({
|
|
366
|
+
uri: "",
|
|
367
|
+
diagnostic: createMockDiagnostic("")
|
|
368
|
+
});
|
|
369
|
+
const result = (0, import_serverLogic.validateSqlLocations)(
|
|
370
|
+
locations,
|
|
371
|
+
mockValidateSql,
|
|
372
|
+
mockCreateDiagnostic
|
|
373
|
+
);
|
|
374
|
+
const diagnostics = result.get("file:///project/test.ts");
|
|
375
|
+
import_node_assert.default.ok(diagnostics);
|
|
376
|
+
import_node_assert.default.strictEqual(diagnostics.length, 1);
|
|
377
|
+
import_node_assert.default.ok(diagnostics[0].message.includes("deprecated"));
|
|
378
|
+
});
|
|
262
379
|
});
|
|
263
380
|
//# sourceMappingURL=serverLogic.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serverLogic.test.ts"],"sourcesContent":["import assert from 'node:assert';\nimport { test } from 'node:test';\nimport type { Diagnostic, Range } from 'vscode-languageserver/node';\nimport {\n type CreateLocationDiagnosticFn,\n shouldValidateFile,\n type ValidateSqlFn,\n validateSqlLocations,\n} from './serverLogic';\nimport type { SqlLocation } from './sqlLocations';\n\n// Helper to create a mock diagnostic\nfunction createMockDiagnostic(message: string): Diagnostic {\n const range: Range = {\n start: { line: 0, character: 0 },\n end: { line: 0, character: 10 },\n };\n return {\n range,\n message,\n severity: 1, // Error\n source: 'moose-sql',\n };\n}\n\ntest('shouldValidateFile Tests', async (t) => {\n await t.test('returns false when mooseProjectRoot is null', () => {\n assert.strictEqual(shouldValidateFile('/some/path/file.ts', null), false);\n });\n\n await t.test('returns false for non-TypeScript files', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/project/src/index.js', projectRoot),\n false,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/package.json', projectRoot),\n false,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/README.md', projectRoot),\n false,\n );\n });\n\n await t.test('returns false for files outside project', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/other-project/src/index.ts', projectRoot),\n false,\n );\n assert.strictEqual(shouldValidateFile('/tmp/file.ts', projectRoot), false);\n });\n\n await t.test('returns true for .ts files in project', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/project/src/index.ts', projectRoot),\n true,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/app/models/user.ts', projectRoot),\n true,\n );\n });\n\n await t.test('handles edge case where file path equals project root', () => {\n const projectRoot = '/home/user/project';\n\n // A file can't be the same as the project root and end in .ts\n assert.strictEqual(\n shouldValidateFile('/home/user/project', projectRoot),\n false,\n );\n });\n});\n\ntest('validateSqlLocations Tests', async (t) => {\n await t.test('returns empty map for empty locations', () => {\n const mockValidateSql: ValidateSqlFn = () => ({ valid: true });\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n const result = validateSqlLocations(\n [],\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 0);\n });\n\n await t.test('returns diagnostics for invalid SQL in template', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SLECT ${...} FROM ${...}', // typo\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Expected SELECT, found SLECT' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 1);\n const diagnostics = result.get('file:///project/app/apis/bar.ts');\n assert.ok(diagnostics);\n assert.strictEqual(diagnostics.length, 1);\n assert.strictEqual(diagnostics[0].message, 'Expected SELECT, found SLECT');\n });\n\n await t.test('skips valid SQL', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SELECT ${...} FROM ${...}',\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({ valid: true });\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: 'file:///project/app/apis/bar.ts',\n diagnostic: createMockDiagnostic('Should not be called'),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 0);\n });\n\n await t.test('groups diagnostics by file URI', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SLECT ${...}',\n },\n {\n id: 'app/apis/bar.ts:100:22',\n file: '/project/app/apis/bar.ts',\n line: 100,\n column: 22,\n endLine: 105,\n endColumn: 6,\n templateText: 'SELCT ${...}',\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Syntax error' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 1);\n const diagnostics = result.get('file:///project/app/apis/bar.ts');\n assert.ok(diagnostics);\n assert.strictEqual(diagnostics.length, 2);\n });\n\n await t.test('handles multiple files', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/foo.ts:10:5',\n file: '/project/app/apis/foo.ts',\n line: 10,\n column: 5,\n endLine: 15,\n endColumn: 6,\n templateText: 'SLECT ${...}',\n },\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SELCT ${...}',\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Syntax error' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 2);\n assert.ok(result.has('file:///project/app/apis/foo.ts'));\n assert.ok(result.has('file:///project/app/apis/bar.ts'));\n });\n\n await t.test(\n 'prepares SQL by replacing ${...} placeholders before validation',\n () => {\n const locations: SqlLocation[] = [\n {\n id: 'test.ts:1:1',\n file: '/project/test.ts',\n line: 1,\n column: 1,\n endLine: 1,\n endColumn: 50,\n templateText: 'SELECT ${...} FROM ${...}',\n },\n ];\n\n let validatedSql = '';\n const mockValidateSql: ValidateSqlFn = (sql) => {\n validatedSql = sql;\n return { valid: true };\n };\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n validateSqlLocations(locations, mockValidateSql, mockCreateDiagnostic);\n\n // Should have replaced ${...} with placeholders\n assert.ok(!validatedSql.includes('${...}'));\n assert.ok(validatedSql.includes('SELECT'));\n assert.ok(validatedSql.includes('FROM'));\n },\n );\n});\n"],"mappings":"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","names":["assert"]}
|
|
1
|
+
{"version":3,"sources":["../src/serverLogic.test.ts"],"sourcesContent":["import assert from 'node:assert';\nimport { test } from 'node:test';\nimport type { Diagnostic, Range } from 'vscode-languageserver/node';\nimport {\n type CreateLocationDiagnosticFn,\n shouldValidateFile,\n type ValidateSqlFn,\n validateSqlLocations,\n} from './serverLogic';\nimport type { SqlLocation } from './sqlLocations';\n\n// Helper to create a mock diagnostic\nfunction createMockDiagnostic(message: string): Diagnostic {\n const range: Range = {\n start: { line: 0, character: 0 },\n end: { line: 0, character: 10 },\n };\n return {\n range,\n message,\n severity: 1, // Error\n source: 'moose-sql',\n };\n}\n\ntest('shouldValidateFile Tests', async (t) => {\n await t.test('returns false when mooseProjectRoot is null', () => {\n assert.strictEqual(shouldValidateFile('/some/path/file.ts', null), false);\n });\n\n await t.test('returns false for non-TypeScript files', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/project/src/index.js', projectRoot),\n false,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/package.json', projectRoot),\n false,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/README.md', projectRoot),\n false,\n );\n });\n\n await t.test('returns false for files outside project', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/other-project/src/index.ts', projectRoot),\n false,\n );\n assert.strictEqual(shouldValidateFile('/tmp/file.ts', projectRoot), false);\n });\n\n await t.test('returns true for .ts files in project', () => {\n const projectRoot = '/home/user/project';\n\n assert.strictEqual(\n shouldValidateFile('/home/user/project/src/index.ts', projectRoot),\n true,\n );\n assert.strictEqual(\n shouldValidateFile('/home/user/project/app/models/user.ts', projectRoot),\n true,\n );\n });\n\n await t.test('handles edge case where file path equals project root', () => {\n const projectRoot = '/home/user/project';\n\n // A file can't be the same as the project root and end in .ts\n assert.strictEqual(\n shouldValidateFile('/home/user/project', projectRoot),\n false,\n );\n });\n});\n\ntest('validateSqlLocations Tests', async (t) => {\n await t.test('returns empty map for empty locations', () => {\n const mockValidateSql: ValidateSqlFn = () => ({ valid: true });\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n const result = validateSqlLocations(\n [],\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 0);\n });\n\n await t.test('returns diagnostics for invalid SQL in template', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SLECT ${...} FROM ${...}', // typo\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Expected SELECT, found SLECT' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 1);\n const diagnostics = result.get('file:///project/app/apis/bar.ts');\n assert.ok(diagnostics);\n // 1 deprecation hint + 1 error diagnostic\n assert.strictEqual(diagnostics.length, 2);\n const errors = diagnostics.filter((d) => d.severity === 1);\n assert.strictEqual(errors.length, 1);\n assert.strictEqual(errors[0].message, 'Expected SELECT, found SLECT');\n });\n\n await t.test('skips valid SQL', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SELECT ${...} FROM ${...}',\n tagKind: 'statement',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 14,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({ valid: true });\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: 'file:///project/app/apis/bar.ts',\n diagnostic: createMockDiagnostic('Should not be called'),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 0);\n });\n\n await t.test('groups diagnostics by file URI', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SLECT ${...}',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n {\n id: 'app/apis/bar.ts:100:22',\n file: '/project/app/apis/bar.ts',\n line: 100,\n column: 22,\n endLine: 105,\n endColumn: 6,\n templateText: 'SELCT ${...}',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Syntax error' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 1);\n const diagnostics = result.get('file:///project/app/apis/bar.ts');\n assert.ok(diagnostics);\n // 2 error diagnostics + 2 deprecation hints (one per bare tag)\n assert.strictEqual(diagnostics.length, 4);\n });\n\n await t.test('handles multiple files', () => {\n const locations: SqlLocation[] = [\n {\n id: 'app/apis/foo.ts:10:5',\n file: '/project/app/apis/foo.ts',\n line: 10,\n column: 5,\n endLine: 15,\n endColumn: 6,\n templateText: 'SLECT ${...}',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n {\n id: 'app/apis/bar.ts:54:22',\n file: '/project/app/apis/bar.ts',\n line: 54,\n column: 22,\n endLine: 61,\n endColumn: 6,\n templateText: 'SELCT ${...}',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({\n valid: false,\n error: { message: 'Syntax error' },\n });\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = (\n location,\n error,\n ) => ({\n uri: `file://${location.file}`,\n diagnostic: createMockDiagnostic(error.message),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n assert.strictEqual(result.size, 2);\n assert.ok(result.has('file:///project/app/apis/foo.ts'));\n assert.ok(result.has('file:///project/app/apis/bar.ts'));\n });\n\n await t.test('skips validation for fragment tagKind', () => {\n const validatedSqls: string[] = [];\n const locations: SqlLocation[] = [\n {\n id: 'test.ts:1:1',\n file: '/project/test.ts',\n line: 1,\n column: 1,\n endLine: 1,\n endColumn: 50,\n templateText: 'SELECT * FROM users',\n tagKind: 'statement',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 14,\n },\n {\n id: 'test.ts:5:1',\n file: '/project/test.ts',\n line: 5,\n column: 1,\n endLine: 5,\n endColumn: 30,\n templateText: \"status = 'active'\",\n tagKind: 'fragment',\n tagLine: 5,\n tagColumn: 1,\n tagEndColumn: 13,\n },\n {\n id: 'test.ts:10:1',\n file: '/project/test.ts',\n line: 10,\n column: 1,\n endLine: 10,\n endColumn: 50,\n templateText: 'SELECT 1',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = (sql) => {\n validatedSqls.push(sql);\n return { valid: true };\n };\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n validateSqlLocations(locations, mockValidateSql, mockCreateDiagnostic);\n\n // Fragment should be skipped, statement and bare should be validated\n assert.strictEqual(validatedSqls.length, 2);\n assert.ok(validatedSqls[0].includes('SELECT'));\n assert.ok(validatedSqls[1].includes('SELECT'));\n });\n\n await t.test(\n 'prepares SQL by replacing ${...} placeholders before validation',\n () => {\n const locations: SqlLocation[] = [\n {\n id: 'test.ts:1:1',\n file: '/project/test.ts',\n line: 1,\n column: 1,\n endLine: 1,\n endColumn: 50,\n templateText: 'SELECT ${...} FROM ${...}',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 1,\n tagEndColumn: 4,\n },\n ];\n\n let validatedSql = '';\n const mockValidateSql: ValidateSqlFn = (sql) => {\n validatedSql = sql;\n return { valid: true };\n };\n\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n validateSqlLocations(locations, mockValidateSql, mockCreateDiagnostic);\n\n // Should have replaced ${...} with placeholders\n assert.ok(!validatedSql.includes('${...}'));\n assert.ok(validatedSql.includes('SELECT'));\n assert.ok(validatedSql.includes('FROM'));\n },\n );\n\n await t.test('emits deprecation diagnostic for bare tagKind', () => {\n const locations: SqlLocation[] = [\n {\n id: 'test.ts:1:15',\n file: '/project/test.ts',\n line: 1,\n column: 15,\n endLine: 1,\n endColumn: 50,\n templateText: 'SELECT * FROM users',\n tagKind: 'bare',\n tagLine: 1,\n tagColumn: 11,\n tagEndColumn: 14,\n },\n ];\n\n const mockValidateSql: ValidateSqlFn = () => ({ valid: true });\n const mockCreateDiagnostic: CreateLocationDiagnosticFn = () => ({\n uri: '',\n diagnostic: createMockDiagnostic(''),\n });\n\n const result = validateSqlLocations(\n locations,\n mockValidateSql,\n mockCreateDiagnostic,\n );\n\n // Even though SQL is valid, should have a deprecation hint\n const diagnostics = result.get('file:///project/test.ts');\n assert.ok(diagnostics);\n assert.strictEqual(diagnostics.length, 1);\n assert.ok(diagnostics[0].message.includes('deprecated'));\n });\n});\n"],"mappings":"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","names":["assert"]}
|
package/dist/sqlExtractor.js
CHANGED
|
@@ -33,23 +33,43 @@ __export(sqlExtractor_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(sqlExtractor_exports);
|
|
35
35
|
var import_typescript = __toESM(require("typescript"));
|
|
36
|
-
function
|
|
36
|
+
function getMooseSqlTagKind(node, typeChecker) {
|
|
37
37
|
const tag = node.tag;
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
let sqlIdentifier;
|
|
39
|
+
let tagKind;
|
|
40
|
+
if (import_typescript.default.isIdentifier(tag) && tag.text === "sql") {
|
|
41
|
+
sqlIdentifier = tag;
|
|
42
|
+
tagKind = "bare";
|
|
43
|
+
} else if (import_typescript.default.isPropertyAccessExpression(tag) && import_typescript.default.isIdentifier(tag.expression) && tag.expression.text === "sql") {
|
|
44
|
+
const propName = tag.name.text;
|
|
45
|
+
if (propName === "statement") {
|
|
46
|
+
tagKind = "statement";
|
|
47
|
+
} else if (propName === "fragment") {
|
|
48
|
+
tagKind = "fragment";
|
|
49
|
+
} else {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
sqlIdentifier = tag.expression;
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const symbol = typeChecker.getSymbolAtLocation(sqlIdentifier);
|
|
57
|
+
if (!symbol) {
|
|
58
|
+
return tagKind;
|
|
40
59
|
}
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const isFromMooseLib =
|
|
60
|
+
const resolvedSymbol = symbol.flags & import_typescript.default.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(symbol) : symbol;
|
|
61
|
+
if (resolvedSymbol?.declarations?.length) {
|
|
62
|
+
const isFromMooseLib = resolvedSymbol.declarations.some((decl) => {
|
|
44
63
|
const sourceFile = decl.getSourceFile();
|
|
45
64
|
const fileName = sourceFile.fileName;
|
|
46
65
|
return fileName.includes("moose-lib") || fileName.includes("@514labs/moose-lib") || fileName.includes("514labs/moose-lib") || fileName.includes("sqlHelpers");
|
|
47
66
|
});
|
|
48
67
|
if (isFromMooseLib) {
|
|
49
|
-
return
|
|
68
|
+
return tagKind;
|
|
50
69
|
}
|
|
70
|
+
return null;
|
|
51
71
|
}
|
|
52
|
-
return
|
|
72
|
+
return tagKind;
|
|
53
73
|
}
|
|
54
74
|
function extractTemplateText(template) {
|
|
55
75
|
if (import_typescript.default.isNoSubstitutionTemplateLiteral(template)) {
|
|
@@ -61,11 +81,15 @@ function extractTemplateText(template) {
|
|
|
61
81
|
}
|
|
62
82
|
return text;
|
|
63
83
|
}
|
|
64
|
-
function extractSqlLocation(node, sourceFile) {
|
|
84
|
+
function extractSqlLocation(node, sourceFile, tagKind) {
|
|
65
85
|
const start = sourceFile.getLineAndCharacterOfPosition(
|
|
66
86
|
node.template.getStart()
|
|
67
87
|
);
|
|
68
88
|
const end = sourceFile.getLineAndCharacterOfPosition(node.template.getEnd());
|
|
89
|
+
const tagStart = sourceFile.getLineAndCharacterOfPosition(
|
|
90
|
+
node.tag.getStart()
|
|
91
|
+
);
|
|
92
|
+
const tagEnd = sourceFile.getLineAndCharacterOfPosition(node.tag.getEnd());
|
|
69
93
|
return {
|
|
70
94
|
id: `${sourceFile.fileName}:${start.line + 1}:${start.character + 1}`,
|
|
71
95
|
file: sourceFile.fileName,
|
|
@@ -75,14 +99,21 @@ function extractSqlLocation(node, sourceFile) {
|
|
|
75
99
|
// 1-based
|
|
76
100
|
endLine: end.line + 1,
|
|
77
101
|
endColumn: end.character + 1,
|
|
78
|
-
templateText: extractTemplateText(node.template)
|
|
102
|
+
templateText: extractTemplateText(node.template),
|
|
103
|
+
tagKind,
|
|
104
|
+
tagLine: tagStart.line + 1,
|
|
105
|
+
tagColumn: tagStart.character + 1,
|
|
106
|
+
tagEndColumn: tagEnd.character + 1
|
|
79
107
|
};
|
|
80
108
|
}
|
|
81
109
|
function extractSqlLocations(sourceFile, typeChecker) {
|
|
82
110
|
const locations = [];
|
|
83
111
|
function visit(node) {
|
|
84
|
-
if (import_typescript.default.isTaggedTemplateExpression(node)
|
|
85
|
-
|
|
112
|
+
if (import_typescript.default.isTaggedTemplateExpression(node)) {
|
|
113
|
+
const tagKind = getMooseSqlTagKind(node, typeChecker);
|
|
114
|
+
if (tagKind !== null) {
|
|
115
|
+
locations.push(extractSqlLocation(node, sourceFile, tagKind));
|
|
116
|
+
}
|
|
86
117
|
}
|
|
87
118
|
import_typescript.default.forEachChild(node, visit);
|
|
88
119
|
}
|
package/dist/sqlExtractor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sqlExtractor.ts"],"sourcesContent":["import ts from 'typescript';\nimport type { SqlLocation } from './sqlLocations';\n\n/**\n * Check if the
|
|
1
|
+
{"version":3,"sources":["../src/sqlExtractor.ts"],"sourcesContent":["import ts from 'typescript';\nimport type { SqlLocation, SqlTagKind } from './sqlLocations';\n\n/**\n * Check if the tag is a moose-lib sql tag and determine its kind.\n * Returns the tag kind, or null if it's not a moose-lib sql tag.\n *\n * Handles:\n * - sql`...` -> 'bare'\n * - sql.statement`...` -> 'statement'\n * - sql.fragment`...` -> 'fragment'\n */\nfunction getMooseSqlTagKind(\n node: ts.TaggedTemplateExpression,\n typeChecker: ts.TypeChecker,\n): SqlTagKind | null {\n const tag = node.tag;\n\n let sqlIdentifier: ts.Identifier;\n let tagKind: SqlTagKind;\n\n if (ts.isIdentifier(tag) && tag.text === 'sql') {\n // Bare sql`...`\n sqlIdentifier = tag;\n tagKind = 'bare';\n } else if (\n ts.isPropertyAccessExpression(tag) &&\n ts.isIdentifier(tag.expression) &&\n tag.expression.text === 'sql'\n ) {\n // sql.statement`...` or sql.fragment`...`\n const propName = tag.name.text;\n if (propName === 'statement') {\n tagKind = 'statement';\n } else if (propName === 'fragment') {\n tagKind = 'fragment';\n } else {\n return null;\n }\n sqlIdentifier = tag.expression;\n } else {\n return null;\n }\n\n // Verify the sql identifier comes from moose-lib\n const symbol = typeChecker.getSymbolAtLocation(sqlIdentifier);\n if (!symbol) {\n // Can't resolve the symbol at all — assume it's our sql tag\n return tagKind;\n }\n\n // Follow import aliases to the original declaration\n const resolvedSymbol =\n symbol.flags & ts.SymbolFlags.Alias\n ? typeChecker.getAliasedSymbol(symbol)\n : symbol;\n\n if (resolvedSymbol?.declarations?.length) {\n const isFromMooseLib = resolvedSymbol.declarations.some((decl) => {\n const sourceFile = decl.getSourceFile();\n const fileName = sourceFile.fileName;\n return (\n fileName.includes('moose-lib') ||\n fileName.includes('@514labs/moose-lib') ||\n fileName.includes('514labs/moose-lib') ||\n fileName.includes('sqlHelpers')\n );\n });\n if (isFromMooseLib) {\n return tagKind;\n }\n // Symbol resolved to a non-moose-lib declaration — not our tag\n return null;\n }\n\n // Fallback: symbol exists but has no declarations — assume it's our sql tag\n return tagKind;\n}\n\n/**\n * Extract template text with ${...} placeholders.\n * Converts template literals like `SELECT ${col} FROM ${table}`\n * into \"SELECT ${...} FROM ${...}\" for SQL validation.\n */\nfunction extractTemplateText(template: ts.TemplateLiteral): string {\n if (ts.isNoSubstitutionTemplateLiteral(template)) {\n return template.text;\n }\n\n // Template with substitutions: `head ${expr} middle ${expr} tail`\n let text = template.head.text;\n for (const span of template.templateSpans) {\n text += `\\${...}${span.literal.text}`;\n }\n return text;\n}\n\n/**\n * Extract SQL location from a tagged template expression\n */\nfunction extractSqlLocation(\n node: ts.TaggedTemplateExpression,\n sourceFile: ts.SourceFile,\n tagKind: SqlTagKind,\n): SqlLocation {\n const start = sourceFile.getLineAndCharacterOfPosition(\n node.template.getStart(),\n );\n const end = sourceFile.getLineAndCharacterOfPosition(node.template.getEnd());\n\n // Tag position: covers `sql`, `sql.statement`, or `sql.fragment`\n const tagStart = sourceFile.getLineAndCharacterOfPosition(\n node.tag.getStart(),\n );\n const tagEnd = sourceFile.getLineAndCharacterOfPosition(node.tag.getEnd());\n\n return {\n id: `${sourceFile.fileName}:${start.line + 1}:${start.character + 1}`,\n file: sourceFile.fileName,\n line: start.line + 1, // 1-based\n column: start.character + 1, // 1-based\n endLine: end.line + 1,\n endColumn: end.character + 1,\n templateText: extractTemplateText(node.template),\n tagKind,\n tagLine: tagStart.line + 1,\n tagColumn: tagStart.character + 1,\n tagEndColumn: tagEnd.character + 1,\n };\n}\n\n/**\n * Extract all SQL locations from a single source file.\n * Uses the TypeChecker to verify that the `sql` tag comes from moose-lib.\n */\nexport function extractSqlLocations(\n sourceFile: ts.SourceFile,\n typeChecker: ts.TypeChecker,\n): SqlLocation[] {\n const locations: SqlLocation[] = [];\n\n function visit(node: ts.Node): void {\n if (ts.isTaggedTemplateExpression(node)) {\n const tagKind = getMooseSqlTagKind(node, typeChecker);\n if (tagKind !== null) {\n locations.push(extractSqlLocation(node, sourceFile, tagKind));\n }\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return locations;\n}\n\n/**\n * Extract SQL locations from all source files (for initial scan).\n * Iterates through all source files in the program and collects SQL templates.\n */\nexport function extractAllSqlLocations(\n sourceFiles: readonly ts.SourceFile[],\n typeChecker: ts.TypeChecker,\n): SqlLocation[] {\n const allLocations: SqlLocation[] = [];\n\n for (const sourceFile of sourceFiles) {\n const locations = extractSqlLocations(sourceFile, typeChecker);\n allLocations.push(...locations);\n }\n\n return allLocations;\n}\n"],"mappings":"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","names":["ts"]}
|
|
@@ -39,11 +39,23 @@ function createTestProgram(files) {
|
|
|
39
39
|
fs.mkdirSync(mooseLibDir, { recursive: true });
|
|
40
40
|
fs.writeFileSync(
|
|
41
41
|
path.join(mooseLibDir, "index.d.ts"),
|
|
42
|
-
`
|
|
42
|
+
`
|
|
43
|
+
interface SqlTemplateTag {
|
|
44
|
+
(strings: TemplateStringsArray, ...values: any[]): string;
|
|
45
|
+
statement(strings: TemplateStringsArray, ...values: any[]): string;
|
|
46
|
+
fragment(strings: TemplateStringsArray, ...values: any[]): string;
|
|
47
|
+
}
|
|
48
|
+
export declare const sql: SqlTemplateTag;
|
|
49
|
+
`
|
|
43
50
|
);
|
|
44
51
|
fs.writeFileSync(
|
|
45
52
|
path.join(mooseLibDir, "index.js"),
|
|
46
|
-
`
|
|
53
|
+
`
|
|
54
|
+
const handler = function(strings, ...values) { return strings.join(''); };
|
|
55
|
+
handler.statement = handler;
|
|
56
|
+
handler.fragment = handler;
|
|
57
|
+
module.exports.sql = handler;
|
|
58
|
+
`
|
|
47
59
|
);
|
|
48
60
|
fs.writeFileSync(
|
|
49
61
|
path.join(mooseLibDir, "package.json"),
|
|
@@ -187,7 +199,7 @@ console.log(x + y);
|
|
|
187
199
|
cleanupTestProject(tmpDir);
|
|
188
200
|
}
|
|
189
201
|
});
|
|
190
|
-
(0, import_node_test.it)("
|
|
202
|
+
(0, import_node_test.it)("ignores sql tag when symbol resolves to non-moose-lib declaration", () => {
|
|
191
203
|
const { program, tmpDir } = createTestProgram({
|
|
192
204
|
"src/index.ts": `
|
|
193
205
|
// Define our own sql function (not from moose-lib)
|
|
@@ -205,7 +217,7 @@ const query = sql\`SELECT * FROM users\`;
|
|
|
205
217
|
import_node_assert.default.ok(sourceFile);
|
|
206
218
|
const typeChecker = program.getTypeChecker();
|
|
207
219
|
const locations = (0, import_sqlExtractor.extractSqlLocations)(sourceFile, typeChecker);
|
|
208
|
-
import_node_assert.default.strictEqual(locations.length,
|
|
220
|
+
import_node_assert.default.strictEqual(locations.length, 0);
|
|
209
221
|
} finally {
|
|
210
222
|
cleanupTestProject(tmpDir);
|
|
211
223
|
}
|
|
@@ -231,6 +243,96 @@ const query = sql\`SELECT * FROM users\`;
|
|
|
231
243
|
cleanupTestProject(tmpDir);
|
|
232
244
|
}
|
|
233
245
|
});
|
|
246
|
+
(0, import_node_test.it)('extracts sql.statement with tagKind "statement"', () => {
|
|
247
|
+
const { program, tmpDir } = createTestProgram({
|
|
248
|
+
"src/index.ts": `
|
|
249
|
+
import { sql } from '@514labs/moose-lib';
|
|
250
|
+
|
|
251
|
+
const query = sql.statement\`SELECT * FROM users\`;
|
|
252
|
+
`
|
|
253
|
+
});
|
|
254
|
+
try {
|
|
255
|
+
const sourceFile = program.getSourceFile(
|
|
256
|
+
path.join(tmpDir, "src/index.ts")
|
|
257
|
+
);
|
|
258
|
+
import_node_assert.default.ok(sourceFile);
|
|
259
|
+
const typeChecker = program.getTypeChecker();
|
|
260
|
+
const locations = (0, import_sqlExtractor.extractSqlLocations)(sourceFile, typeChecker);
|
|
261
|
+
import_node_assert.default.strictEqual(locations.length, 1);
|
|
262
|
+
import_node_assert.default.strictEqual(locations[0].templateText, "SELECT * FROM users");
|
|
263
|
+
import_node_assert.default.strictEqual(locations[0].tagKind, "statement");
|
|
264
|
+
} finally {
|
|
265
|
+
cleanupTestProject(tmpDir);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
(0, import_node_test.it)('extracts sql.fragment with tagKind "fragment"', () => {
|
|
269
|
+
const { program, tmpDir } = createTestProgram({
|
|
270
|
+
"src/index.ts": `
|
|
271
|
+
import { sql } from '@514labs/moose-lib';
|
|
272
|
+
|
|
273
|
+
const condition = sql.fragment\`status = 'active'\`;
|
|
274
|
+
`
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
const sourceFile = program.getSourceFile(
|
|
278
|
+
path.join(tmpDir, "src/index.ts")
|
|
279
|
+
);
|
|
280
|
+
import_node_assert.default.ok(sourceFile);
|
|
281
|
+
const typeChecker = program.getTypeChecker();
|
|
282
|
+
const locations = (0, import_sqlExtractor.extractSqlLocations)(sourceFile, typeChecker);
|
|
283
|
+
import_node_assert.default.strictEqual(locations.length, 1);
|
|
284
|
+
import_node_assert.default.strictEqual(locations[0].templateText, "status = 'active'");
|
|
285
|
+
import_node_assert.default.strictEqual(locations[0].tagKind, "fragment");
|
|
286
|
+
} finally {
|
|
287
|
+
cleanupTestProject(tmpDir);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
(0, import_node_test.it)('extracts bare sql with tagKind "bare"', () => {
|
|
291
|
+
const { program, tmpDir } = createTestProgram({
|
|
292
|
+
"src/index.ts": `
|
|
293
|
+
import { sql } from '@514labs/moose-lib';
|
|
294
|
+
|
|
295
|
+
const query = sql\`SELECT * FROM users\`;
|
|
296
|
+
`
|
|
297
|
+
});
|
|
298
|
+
try {
|
|
299
|
+
const sourceFile = program.getSourceFile(
|
|
300
|
+
path.join(tmpDir, "src/index.ts")
|
|
301
|
+
);
|
|
302
|
+
import_node_assert.default.ok(sourceFile);
|
|
303
|
+
const typeChecker = program.getTypeChecker();
|
|
304
|
+
const locations = (0, import_sqlExtractor.extractSqlLocations)(sourceFile, typeChecker);
|
|
305
|
+
import_node_assert.default.strictEqual(locations.length, 1);
|
|
306
|
+
import_node_assert.default.strictEqual(locations[0].tagKind, "bare");
|
|
307
|
+
} finally {
|
|
308
|
+
cleanupTestProject(tmpDir);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
(0, import_node_test.it)("extracts mixed tag kinds from same file", () => {
|
|
312
|
+
const { program, tmpDir } = createTestProgram({
|
|
313
|
+
"src/index.ts": `
|
|
314
|
+
import { sql } from '@514labs/moose-lib';
|
|
315
|
+
|
|
316
|
+
const a = sql\`SELECT 1\`;
|
|
317
|
+
const b = sql.statement\`SELECT 2\`;
|
|
318
|
+
const c = sql.fragment\`col = 1\`;
|
|
319
|
+
`
|
|
320
|
+
});
|
|
321
|
+
try {
|
|
322
|
+
const sourceFile = program.getSourceFile(
|
|
323
|
+
path.join(tmpDir, "src/index.ts")
|
|
324
|
+
);
|
|
325
|
+
import_node_assert.default.ok(sourceFile);
|
|
326
|
+
const typeChecker = program.getTypeChecker();
|
|
327
|
+
const locations = (0, import_sqlExtractor.extractSqlLocations)(sourceFile, typeChecker);
|
|
328
|
+
import_node_assert.default.strictEqual(locations.length, 3);
|
|
329
|
+
import_node_assert.default.strictEqual(locations[0].tagKind, "bare");
|
|
330
|
+
import_node_assert.default.strictEqual(locations[1].tagKind, "statement");
|
|
331
|
+
import_node_assert.default.strictEqual(locations[2].tagKind, "fragment");
|
|
332
|
+
} finally {
|
|
333
|
+
cleanupTestProject(tmpDir);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
234
336
|
});
|
|
235
337
|
(0, import_node_test.describe)("extractAllSqlLocations", () => {
|
|
236
338
|
(0, import_node_test.it)("extracts sql from multiple source files", () => {
|