@checkdigit/eslint-plugin 7.18.0-PR.143-3aac → 7.18.0-PR.143-5adf

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/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-3aac","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 .","lint:fix":"eslint --max-warnings 0 --fix .","peggy":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format es --output \"${file%.peggy}-peggy.ts\"; done","peggy-watch":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format=es --watch --output=\"${file%.peggy}-peggy.ts\"; done","prepare":"","prepublishOnly":"npm run build:dist-types && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"dependencies":{"@apidevtools/json-schema-ref-parser":"^15.3.5","@typescript-eslint/type-utils":"^8.60.1","@typescript-eslint/utils":"^8.60.1","ajv":"^8.20.0","debug":"^4.4.3","glob":"^13.0.6","http-status-codes":"^2.3.0","js-yaml":"^4.2.0","json-pointer":"^0.6.2","jsonpath-plus":"^10.4.0","ts-api-utils":"^2.5.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^6.1.0","@checkdigit/typescript-config":"10.0.0","@eslint/js":"^9.37.0","@types/debug":"^4.1.13","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@types/js-yaml":"^4.0.9","@types/json-pointer":"^1.0.34","@typescript-eslint/parser":"^8.60.1","@typescript-eslint/rule-tester":"^8.60.1","eslint":"^9.37.0","eslint-config-prettier":"^10.1.8","eslint-import-resolver-typescript":"^4.4.5","eslint-plugin-eslint-plugin":"^6.4.0","eslint-plugin-import":"^2.32.0","eslint-plugin-no-only-tests":"^3.4.0","eslint-plugin-no-secrets":"^2.3.3","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"^1.0.4","openapi-types":"^12.1.3","peggy":"^4.2.0","rimraf":"^6.1.3","typescript-eslint":"^8.60.1"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=22.18"},"service":{"api":{"root":"src","endpoints":["api/v1"]}},"wallaby":{"env":{"params":{"runner":"--experimental-vm-modules"}}}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-5adf","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 .","lint:fix":"eslint --max-warnings 0 --fix .","peggy":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format es --output \"${file%.peggy}-peggy.ts\"; done","peggy-watch":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format=es --watch --output=\"${file%.peggy}-peggy.ts\"; done","prepare":"","prepublishOnly":"npm run build:dist-types && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"dependencies":{"@apidevtools/json-schema-ref-parser":"^15.3.5","@typescript-eslint/type-utils":"^8.60.1","@typescript-eslint/utils":"^8.60.1","ajv":"^8.20.0","debug":"^4.4.3","glob":"^13.0.6","http-status-codes":"^2.3.0","js-yaml":"^4.2.0","json-pointer":"^0.6.2","jsonpath-plus":"^10.4.0","ts-api-utils":"^2.5.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^6.1.0","@checkdigit/typescript-config":"10.0.0","@eslint/js":"^9.37.0","@types/debug":"^4.1.13","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@types/js-yaml":"^4.0.9","@types/json-pointer":"^1.0.34","@typescript-eslint/parser":"^8.60.1","@typescript-eslint/rule-tester":"^8.60.1","eslint":"^9.37.0","eslint-config-prettier":"^10.1.8","eslint-import-resolver-typescript":"^4.4.5","eslint-plugin-eslint-plugin":"^6.4.0","eslint-plugin-import":"^2.32.0","eslint-plugin-no-only-tests":"^3.4.0","eslint-plugin-no-secrets":"^2.3.3","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"^1.0.4","openapi-types":"^12.1.3","peggy":"^4.2.0","rimraf":"^6.1.3","typescript-eslint":"^8.60.1"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=22.18"},"service":{"api":{"root":"src","endpoints":["api/v1"]}},"wallaby":{"env":{"params":{"runner":"--experimental-vm-modules"}}}}
@@ -14,7 +14,7 @@ import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/u
14
14
  import type { SchemaObject } from 'ajv/dist/2020';
15
15
 
16
16
  import { parse } from '../peggy/athena-peggy.ts';
17
- import type { AST, From, Select, With } from './types';
17
+ import type { AST, BaseFrom, From, Select, With } from './types';
18
18
  import { matchApi } from './api-matcher.ts';
19
19
  import { locateApi } from './api-locator.ts';
20
20
  import {
@@ -141,6 +141,17 @@ function fromClauseItems(select: Select): From[] {
141
141
  // Pass 1 — Resolve FROM clause: service tables → ctx.tables + ctx.aliases
142
142
  // ---------------------------------------------------------------------------
143
143
 
144
+ function resolveServiceTable(select: Select, item: BaseFrom, ctx: VisitContext): void {
145
+ const { table: tableName } = item;
146
+ try {
147
+ const apiSchemas = getApiSchemas(tableName, ctx);
148
+ const operations = matchApi(select, item, apiSchemas) ?? [];
149
+ ctx.tables.set(tableName, buildServiceTables(tableName, operations));
150
+ } catch (error) {
151
+ throw new AthenaError(ATHENA_ERROR, error instanceof Error ? error.message : String(error), item);
152
+ }
153
+ }
154
+
144
155
  function resolveFromClause(select: Select, ctx: VisitContext): void {
145
156
  for (const item of fromClauseItems(select)) {
146
157
  if (isUnnestFrom(item)) {
@@ -153,9 +164,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
153
164
  ctx.aliases.set(alias, tableName);
154
165
  }
155
166
  if (!ctx.tables.has(tableName)) {
156
- const apiSchemas = getApiSchemas(tableName, ctx);
157
- const operations = matchApi(select, item, apiSchemas) ?? [];
158
- ctx.tables.set(tableName, buildServiceTables(tableName, operations));
167
+ resolveServiceTable(select, item, ctx);
159
168
  }
160
169
  continue;
161
170
  }
@@ -174,11 +183,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
174
183
  continue; // already resolved (CTE or duplicate)
175
184
  }
176
185
 
177
- // Service table: locate + match API schemas from disk.
178
- // matchApi throws when no operation matches, so operations is always defined here.
179
- const apiSchemas = getApiSchemas(tableName, ctx);
180
- const operations = matchApi(select, item, apiSchemas) ?? [];
181
- ctx.tables.set(tableName, buildServiceTables(tableName, operations));
186
+ resolveServiceTable(select, item, ctx);
182
187
  }
183
188
  }
184
189
 
@@ -278,6 +283,28 @@ function expandWildcard(referencedTables: ResolvedTable[], columns: Map<string,
278
283
  }
279
284
  }
280
285
 
286
+ // If every resolved column shares the same object schema, return a hint listing its top-level
287
+ // properties. Returns '' when schemas differ across matched operations or the schema isn't an object.
288
+ function schemaPropertyHint(resolvedColumns: ResolvedColumn[]): string {
289
+ if (resolvedColumns.length === 0) {
290
+ return '';
291
+ }
292
+ const first = JSON.stringify(resolvedColumns[0]?.schema);
293
+ if (!resolvedColumns.every((col) => JSON.stringify(col.schema) === first)) {
294
+ return '';
295
+ }
296
+ const schema = resolvedColumns[0]?.schema;
297
+ if (schema?.type !== 'object') {
298
+ return '';
299
+ }
300
+ const properties = schema.properties;
301
+ if (properties === undefined) {
302
+ return '';
303
+ }
304
+ const propNames = Object.keys(properties);
305
+ return propNames.length > 0 ? `; available properties: ${propNames.join(', ')}` : '';
306
+ }
307
+
281
308
  function resolveSchemaAtPath(
282
309
  colRef: string,
283
310
  propertyAccessor: string,
@@ -295,7 +322,11 @@ function resolveSchemaAtPath(
295
322
  log('extracted schemas', extractedSchemas);
296
323
 
297
324
  if (extractedSchemas.length === 0) {
298
- throw new AthenaError(ATHENA_ERROR, `property not found ${colRef} - ${propertyAccessor}`, ast);
325
+ throw new AthenaError(
326
+ ATHENA_ERROR,
327
+ `property not found ${colRef} - ${propertyAccessor}${schemaPropertyHint(resolvedColumns)}`,
328
+ ast,
329
+ );
299
330
  }
300
331
  return extractedSchemas;
301
332
  }
@@ -365,7 +396,12 @@ function resolveSingleColumnRef(
365
396
 
366
397
  if (resolvedColumns.length === 0) {
367
398
  const tableNames = [...ctx.tables.keys()].join(', ');
368
- throw new AthenaError(ATHENA_ERROR, `can't found column ${colRef} in tables: ${tableNames}`, columnAST as object);
399
+ const availableCols = [...new Set(referencedTables.flatMap((table) => [...table.columns.keys()]))].join(', ');
400
+ throw new AthenaError(
401
+ ATHENA_ERROR,
402
+ `can't found column ${colRef} in tables: ${tableNames}; available columns: ${availableCols}`,
403
+ ref,
404
+ );
369
405
  }
370
406
 
371
407
  const propertyAccessor = extractJsonExtractPath(columnAST) ?? extractBracketAccessorPath(columnAST);
@@ -499,21 +535,33 @@ const rule: ESLintUtils.RuleModule<typeof SYNTEXT_ERROR | typeof ATHENA_ERROR> =
499
535
  return;
500
536
  }
501
537
 
538
+ const sqlMapping = buildSqlMapping(sqlNode);
502
539
  let ast: AST;
503
540
  try {
504
541
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
505
542
  ({ ast } = parse(sql, { includeLocations: true }));
506
543
  } catch (error) {
507
544
  log('error checking Athena AST', { error, sql });
508
- context.report({
509
- node: sqlNode,
510
- messageId: SYNTEXT_ERROR,
511
- data: { errorMessage: JSON.stringify(error, undefined, 2), sql },
512
- });
545
+ const pegLoc = (error as { location?: { start: { offset: number }; end: { offset: number } } }).location;
546
+ if (pegLoc !== undefined) {
547
+ const sourceText = context.sourceCode.getText();
548
+ context.report({
549
+ loc: {
550
+ start: offsetToLoc(sourceText, sqlOffsetToSource(pegLoc.start.offset, sqlMapping)),
551
+ end: offsetToLoc(sourceText, sqlOffsetToSource(pegLoc.end.offset, sqlMapping)),
552
+ },
553
+ messageId: SYNTEXT_ERROR,
554
+ data: { errorMessage: JSON.stringify(error, undefined, 2), sql },
555
+ });
556
+ } else {
557
+ context.report({
558
+ node: sqlNode,
559
+ messageId: SYNTEXT_ERROR,
560
+ data: { errorMessage: JSON.stringify(error, undefined, 2), sql },
561
+ });
562
+ }
513
563
  return;
514
564
  }
515
-
516
- const sqlMapping = buildSqlMapping(sqlNode);
517
565
  const athenaCtx = createRootContext();
518
566
  try {
519
567
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
@@ -1553,12 +1553,13 @@ function peg$parse(input: any, options: any): any {
1553
1553
  var peg$f107 = function (t, alias) {
1554
1554
  if (t.type === 'var') {
1555
1555
  t.as = alias;
1556
- return t;
1556
+ return { ...t, ...getLocationObject() };
1557
1557
  } else {
1558
1558
  return {
1559
1559
  db: t.db,
1560
1560
  table: t.table,
1561
1561
  as: alias,
1562
+ ...getLocationObject(),
1562
1563
  };
1563
1564
  }
1564
1565
  };
@@ -2035,6 +2036,7 @@ function peg$parse(input: any, options: any): any {
2035
2036
  table: tbl[3],
2036
2037
  column: col[3],
2037
2038
  collate: ce && ce[1],
2039
+ ...getLocationObject(),
2038
2040
  };
2039
2041
  };
2040
2042
  var peg$f187 = function (tbl, col, ce) {
@@ -2044,6 +2046,7 @@ function peg$parse(input: any, options: any): any {
2044
2046
  table: tbl,
2045
2047
  column: col,
2046
2048
  collate: ce && ce[1],
2049
+ ...getLocationObject(),
2047
2050
  };
2048
2051
  };
2049
2052
  var peg$f188 = function (col, ce) {
@@ -2053,6 +2056,7 @@ function peg$parse(input: any, options: any): any {
2053
2056
  table: null,
2054
2057
  column: col,
2055
2058
  collate: ce && ce[1],
2059
+ ...getLocationObject(),
2056
2060
  };
2057
2061
  };
2058
2062
  var peg$f189 = function (head, tail) {
@@ -1239,12 +1239,13 @@ table_base
1239
1239
  / t:table_name __ alias:alias_clause? {
1240
1240
  if (t.type === 'var') {
1241
1241
  t.as = alias;
1242
- return t;
1242
+ return { ...t, ...getLocationObject() };
1243
1243
  } else {
1244
1244
  return {
1245
1245
  db: t.db,
1246
1246
  table: t.table,
1247
- as: alias
1247
+ as: alias,
1248
+ ...getLocationObject(),
1248
1249
  };
1249
1250
  }
1250
1251
  }
@@ -1906,6 +1907,7 @@ column_ref
1906
1907
  table: tbl[3],
1907
1908
  column: col[3],
1908
1909
  collate: ce && ce[1],
1910
+ ...getLocationObject(),
1909
1911
  };
1910
1912
  }
1911
1913
  / tbl:ident __ DOT __ col:column ce:(__ collate_expr)? {
@@ -1915,6 +1917,7 @@ column_ref
1915
1917
  table: tbl,
1916
1918
  column: col,
1917
1919
  collate: ce && ce[1],
1920
+ ...getLocationObject(),
1918
1921
  };
1919
1922
  }
1920
1923
  / col:column ce:(__ collate_expr)? {
@@ -1924,6 +1927,7 @@ column_ref
1924
1927
  table: null,
1925
1928
  column: col,
1926
1929
  collate: ce && ce[1],
1930
+ ...getLocationObject(),
1927
1931
  };
1928
1932
  }
1929
1933