@checkdigit/eslint-plugin 7.18.0-PR.143-a26e → 7.18.0-PR.143-bebb

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-a26e","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":">=24.14.1"},"service":{"api":{"root":"src","endpoints":["api/v1"]}}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-bebb","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":">=24.14.1"},"service":{"api":{"root":"src","endpoints":["api/v1"]}}}
@@ -127,6 +127,23 @@ function getFunctionAliasName(as: { name: { name: { value: string }[] } } | null
127
127
  return as?.name.name[0]?.value;
128
128
  }
129
129
 
130
+ // Extracts a column alias name from an alias-arg node. Two cases arise:
131
+ // 1. column_ref (the normal case): { type: 'column_ref', column: 'dates' } → 'dates'
132
+ // 2. zero-arg keyword function: SQL type keywords like `date` are parsed as DATE() function
133
+ // calls; recover the column name by lowercasing the function name.
134
+ function extractColumnAliasName(arg: unknown): string | undefined {
135
+ const colRef = arg as { column?: unknown };
136
+ if (typeof colRef.column === 'string') {
137
+ return colRef.column;
138
+ }
139
+ const fnNode = arg as { type?: unknown; name?: { name?: { value?: string }[] }; args?: { value?: unknown[] } };
140
+ if (fnNode.type === 'function' && fnNode.args?.value?.length === 0) {
141
+ const functionName = fnNode.name?.name?.[0]?.value;
142
+ return functionName !== undefined ? functionName.toLowerCase() : undefined;
143
+ }
144
+ return undefined;
145
+ }
146
+
130
147
  // UNNEST whose argument is a function call (e.g. SEQUENCE), not a column reference.
131
148
  function isStandaloneUnnest(node: unknown): node is UnnestFrom {
132
149
  return isUnnestFrom(node) && typeof (node.expr as { column?: unknown }).column !== 'string';
@@ -190,7 +207,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
190
207
  if (tableAlias !== undefined) {
191
208
  const columns = new Map(
192
209
  (unknownItem.as?.args.value ?? []).map((columnRef) => {
193
- const colName = (columnRef as { column: string }).column;
210
+ const colName = extractColumnAliasName(columnRef) ?? '';
194
211
  return [colName, [resolvedCol(colName, {})]];
195
212
  }),
196
213
  );
@@ -223,6 +240,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
223
240
  interface UnnestMapping {
224
241
  fromColumn: string;
225
242
  toColumns: string[];
243
+ tableAlias?: string;
226
244
  ast: object;
227
245
  }
228
246
 
@@ -246,13 +264,20 @@ function extractUnnestMappings(select: Select): UnnestMapping[] {
246
264
  const aliasArgs = item.as?.args.value ?? [];
247
265
  const toColumns: string[] = [];
248
266
  for (const col of aliasArgs) {
249
- if (typeof (col as { column?: unknown }).column === 'string') {
250
- toColumns.push((col as { column: string }).column);
267
+ const colName = extractColumnAliasName(col);
268
+ if (colName !== undefined) {
269
+ toColumns.push(colName);
251
270
  }
252
271
  }
253
272
  assert.ok(toColumns.length > 0, 'UNNEST alias must have at least one column name');
254
273
 
255
- mappings.push({ fromColumn, toColumns, ast: item.expr });
274
+ const tableAlias = getFunctionAliasName(item.as);
275
+ mappings.push({
276
+ fromColumn,
277
+ toColumns,
278
+ ...(tableAlias !== undefined ? { tableAlias } : {}),
279
+ ast: item.expr,
280
+ });
256
281
  }
257
282
 
258
283
  return mappings;
@@ -261,11 +286,11 @@ function extractUnnestMappings(select: Select): UnnestMapping[] {
261
286
  function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMapping[] {
262
287
  const deferred: UnnestMapping[] = [];
263
288
 
264
- for (const { fromColumn, toColumns, ast } of mappings) {
289
+ for (const { fromColumn, toColumns, tableAlias, ast } of mappings) {
265
290
  const ownerTable = [...ctx.tables.values()].flat().find((table) => table.columns.has(fromColumn));
266
291
 
267
292
  if (ownerTable === undefined) {
268
- deferred.push({ fromColumn, toColumns, ast });
293
+ deferred.push({ fromColumn, toColumns, ...(tableAlias !== undefined ? { tableAlias } : {}), ast });
269
294
  continue;
270
295
  }
271
296
 
@@ -274,16 +299,17 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
274
299
  const unnestTableName = `${ownerTable.name ?? ANONYMOUS_TABLE}:<unnested>`;
275
300
  const apiOperation = ownerTable.apiOperation !== undefined ? { apiOperation: ownerTable.apiOperation } : {};
276
301
 
302
+ let unnestEntry: ResolvedTable[];
277
303
  if (sourceSchema?.type === 'array') {
278
304
  const [toColumn] = toColumns;
279
305
  assert.ok(toColumn !== undefined);
280
- ctx.tables.set(unnestTableName, [
306
+ unnestEntry = [
281
307
  {
282
308
  name: unnestTableName,
283
309
  ...apiOperation,
284
310
  columns: new Map([[toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceColumns[0]?.ast)]]]),
285
311
  },
286
- ]);
312
+ ];
287
313
  } else if (sourceSchema?.type === 'object') {
288
314
  const [keyColumn, valueColumn] = toColumns;
289
315
  assert.ok(
@@ -293,7 +319,7 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
293
319
  const addlProps = (sourceSchema as SchemaObject)['additionalProperties'] as unknown;
294
320
  const valueSchema: SchemaObject =
295
321
  typeof addlProps === 'object' && addlProps !== null ? addlProps : { type: 'string' };
296
- ctx.tables.set(unnestTableName, [
322
+ unnestEntry = [
297
323
  {
298
324
  name: unnestTableName,
299
325
  ...apiOperation,
@@ -302,13 +328,28 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
302
328
  [valueColumn, [resolvedCol(valueColumn, valueSchema, sourceColumns[0]?.ast)]],
303
329
  ]),
304
330
  },
305
- ]);
306
- } else {
331
+ ];
332
+ } else if (ownerTable.apiOperation !== undefined) {
333
+ // Only raise an error when the column schema was derived from a known API spec — for
334
+ // computed columns (subqueries, CTEs, functions) the inferred schema may be imprecise.
307
335
  throw new AthenaError(
308
336
  ATHENA_ERROR,
309
337
  `UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
310
338
  ast,
311
339
  );
340
+ } else {
341
+ // Non-API source: schema is an estimate; register target columns with unknown schema.
342
+ unnestEntry = [
343
+ {
344
+ name: unnestTableName,
345
+ ...apiOperation,
346
+ columns: new Map(toColumns.map((toColumn) => [toColumn, [resolvedCol(toColumn, {})]])),
347
+ },
348
+ ];
349
+ }
350
+ ctx.tables.set(unnestTableName, unnestEntry);
351
+ if (tableAlias !== undefined) {
352
+ ctx.tables.set(tableAlias, unnestEntry);
312
353
  }
313
354
  }
314
355