@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.
@@ -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) {
@@ -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":[]}
@@ -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, 1);
130
- import_node_assert.default.strictEqual(diagnostics[0].message, "Expected SELECT, found SLECT");
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, 2);
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"]}
@@ -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 isMooseLibSqlTag(node, typeChecker) {
36
+ function getMooseSqlTagKind(node, typeChecker) {
37
37
  const tag = node.tag;
38
- if (!import_typescript.default.isIdentifier(tag) || tag.text !== "sql") {
39
- return false;
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 symbol = typeChecker.getSymbolAtLocation(tag);
42
- if (symbol?.declarations?.length) {
43
- const isFromMooseLib = symbol.declarations.some((decl) => {
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 true;
68
+ return tagKind;
50
69
  }
70
+ return null;
51
71
  }
52
- return true;
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) && isMooseLibSqlTag(node, typeChecker)) {
85
- locations.push(extractSqlLocation(node, sourceFile));
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
  }
@@ -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 sql tag comes from @514labs/moose-lib.\n * Falls back to returning true if symbol can't be resolved\n * (better to have false positives than miss real sql queries).\n */\nfunction isMooseLibSqlTag(\n node: ts.TaggedTemplateExpression,\n typeChecker: ts.TypeChecker,\n): boolean {\n const tag = node.tag;\n\n // Must be a simple identifier `sql`\n if (!ts.isIdentifier(tag) || tag.text !== 'sql') {\n return false;\n }\n\n const symbol = typeChecker.getSymbolAtLocation(tag);\n if (symbol?.declarations?.length) {\n // Check if any declaration originates from moose-lib\n const isFromMooseLib = symbol.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 true;\n }\n }\n\n // Fallback: if we can't resolve the symbol, assume it's our sql tag\n // (better to have false positives than miss real sql queries)\n return true;\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): SqlLocation {\n const start = sourceFile.getLineAndCharacterOfPosition(\n node.template.getStart(),\n );\n const end = sourceFile.getLineAndCharacterOfPosition(node.template.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 };\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 (\n ts.isTaggedTemplateExpression(node) &&\n isMooseLibSqlTag(node, typeChecker)\n ) {\n locations.push(extractSqlLocation(node, sourceFile));\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAe;AAQf,SAAS,iBACP,MACA,aACS;AACT,QAAM,MAAM,KAAK;AAGjB,MAAI,CAAC,kBAAAA,QAAG,aAAa,GAAG,KAAK,IAAI,SAAS,OAAO;AAC/C,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,YAAY,oBAAoB,GAAG;AAClD,MAAI,QAAQ,cAAc,QAAQ;AAEhC,UAAM,iBAAiB,OAAO,aAAa,KAAK,CAAC,SAAS;AACxD,YAAM,aAAa,KAAK,cAAc;AACtC,YAAM,WAAW,WAAW;AAC5B,aACE,SAAS,SAAS,WAAW,KAC7B,SAAS,SAAS,oBAAoB,KACtC,SAAS,SAAS,mBAAmB,KACrC,SAAS,SAAS,YAAY;AAAA,IAElC,CAAC;AACD,QAAI,gBAAgB;AAClB,aAAO;AAAA,IACT;AAAA,EACF;AAIA,SAAO;AACT;AAOA,SAAS,oBAAoB,UAAsC;AACjE,MAAI,kBAAAA,QAAG,gCAAgC,QAAQ,GAAG;AAChD,WAAO,SAAS;AAAA,EAClB;AAGA,MAAI,OAAO,SAAS,KAAK;AACzB,aAAW,QAAQ,SAAS,eAAe;AACzC,YAAQ,UAAU,KAAK,QAAQ,IAAI;AAAA,EACrC;AACA,SAAO;AACT;AAKA,SAAS,mBACP,MACA,YACa;AACb,QAAM,QAAQ,WAAW;AAAA,IACvB,KAAK,SAAS,SAAS;AAAA,EACzB;AACA,QAAM,MAAM,WAAW,8BAA8B,KAAK,SAAS,OAAO,CAAC;AAE3E,SAAO;AAAA,IACL,IAAI,GAAG,WAAW,QAAQ,IAAI,MAAM,OAAO,CAAC,IAAI,MAAM,YAAY,CAAC;AAAA,IACnE,MAAM,WAAW;AAAA,IACjB,MAAM,MAAM,OAAO;AAAA;AAAA,IACnB,QAAQ,MAAM,YAAY;AAAA;AAAA,IAC1B,SAAS,IAAI,OAAO;AAAA,IACpB,WAAW,IAAI,YAAY;AAAA,IAC3B,cAAc,oBAAoB,KAAK,QAAQ;AAAA,EACjD;AACF;AAMO,SAAS,oBACd,YACA,aACe;AACf,QAAM,YAA2B,CAAC;AAElC,WAAS,MAAM,MAAqB;AAClC,QACE,kBAAAA,QAAG,2BAA2B,IAAI,KAClC,iBAAiB,MAAM,WAAW,GAClC;AACA,gBAAU,KAAK,mBAAmB,MAAM,UAAU,CAAC;AAAA,IACrD;AACA,sBAAAA,QAAG,aAAa,MAAM,KAAK;AAAA,EAC7B;AAEA,QAAM,UAAU;AAChB,SAAO;AACT;AAMO,SAAS,uBACd,aACA,aACe;AACf,QAAM,eAA8B,CAAC;AAErC,aAAW,cAAc,aAAa;AACpC,UAAM,YAAY,oBAAoB,YAAY,WAAW;AAC7D,iBAAa,KAAK,GAAG,SAAS;AAAA,EAChC;AAEA,SAAO;AACT;","names":["ts"]}
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
- `export declare function sql(strings: TemplateStringsArray, ...values: any[]): string;`
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
- `module.exports.sql = function sql(strings, ...values) { return strings.join(''); };`
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)("extracts sql tag even when symbol cannot be resolved (fallback behavior)", () => {
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, 1);
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", () => {