@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/dist-mjs/athena/athena.mjs +62 -15
- package/dist-mjs/peggy/athena-peggy.mjs +10 -6
- package/package.json +1 -1
- package/src/athena/athena.ts +66 -18
- package/src/peggy/athena-peggy.ts +5 -1
- package/src/peggy/athena.peggy +6 -2
- package/src/peggy/athena-chat.peggy +0 -608
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-
|
|
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"}}}}
|
package/src/athena/athena.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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) {
|
package/src/peggy/athena.peggy
CHANGED
|
@@ -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
|
|