@checkdigit/eslint-plugin 7.18.0-PR.143-b47c → 7.18.0-PR.143-cf9d

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.
@@ -1,9 +1,5 @@
1
1
  // athena/visitor.ts
2
2
 
3
- import debug from 'debug';
4
-
5
- const log = debug('athena:visitor');
6
-
7
3
  import type {
8
4
  AggrFunc,
9
5
  BaseFrom,
@@ -70,20 +66,23 @@ export interface VisitorMap {
70
66
  // nodes without an intermediate cast.
71
67
  // -------------------------------------------------------------------
72
68
 
69
+ function hasNodeType(node: unknown, type: string): boolean {
70
+ return typeof node === 'object' && node !== null && (node as { type?: unknown }).type === type;
71
+ }
72
+
73
73
  export function isUnnestFrom(node: unknown): node is UnnestFrom {
74
- return typeof node === 'object' && node !== null && (node as { type?: unknown }).type === 'unnest';
74
+ return hasNodeType(node, 'unnest');
75
75
  }
76
76
 
77
77
  export function isDual(node: unknown): node is Dual {
78
- return typeof node === 'object' && node !== null && (node as { type?: unknown }).type === 'dual';
78
+ return hasNodeType(node, 'dual');
79
79
  }
80
80
 
81
81
  export function isValuesFrom(node: unknown): node is ValuesFrom {
82
82
  if (typeof node !== 'object' || node === null) {
83
83
  return false;
84
84
  }
85
- const expr = (node as { expr?: unknown }).expr;
86
- return typeof expr === 'object' && expr !== null && (expr as { type?: unknown }).type === 'values';
85
+ return hasNodeType((node as { expr?: unknown }).expr, 'values');
87
86
  }
88
87
 
89
88
  export function isTableExpr(node: unknown): node is TableExpr {
@@ -94,27 +93,16 @@ export function isTableExpr(node: unknown): node is TableExpr {
94
93
  return typeof expr === 'object' && expr !== null && 'ast' in expr;
95
94
  }
96
95
 
96
+ function isRegularFromItem(node: unknown): boolean {
97
+ return typeof node === 'object' && node !== null && !isUnnestFrom(node) && !isDual(node) && !isTableExpr(node);
98
+ }
99
+
97
100
  export function isJoin(node: unknown): node is Join {
98
- return (
99
- !isUnnestFrom(node) &&
100
- !isDual(node) &&
101
- !isTableExpr(node) &&
102
- typeof node === 'object' &&
103
- node !== null &&
104
- 'join' in node
105
- );
101
+ return isRegularFromItem(node) && 'join' in (node as object);
106
102
  }
107
103
 
108
104
  export function isBaseFrom(node: unknown): node is BaseFrom {
109
- return (
110
- !isUnnestFrom(node) &&
111
- !isDual(node) &&
112
- !isTableExpr(node) &&
113
- !isJoin(node) &&
114
- typeof node === 'object' &&
115
- node !== null &&
116
- 'table' in node
117
- );
105
+ return isRegularFromItem(node) && !isJoin(node) && 'table' in (node as object);
118
106
  }
119
107
 
120
108
  export function hasArrayIndex(node: ColumnRefItem): node is ColumnRefWithIndex {
@@ -237,6 +225,17 @@ function walkFrom(node: From, visitor: VisitorMap): void {
237
225
  visitor.visitBaseFrom?.(node);
238
226
  }
239
227
 
228
+ /** Normalise select.from (null / single item / array) to a From[]. */
229
+ export function fromClauseItems(select: Select): From[] {
230
+ if (Array.isArray(select.from)) {
231
+ return select.from;
232
+ }
233
+ if (select.from !== null) {
234
+ return [select.from];
235
+ }
236
+ return [];
237
+ }
238
+
240
239
  // -------------------------------------------------------------------
241
240
  // Core walk — dispatches per node type and recurses into children.
242
241
  // -------------------------------------------------------------------
@@ -254,15 +253,7 @@ export function walk(node: unknown, visitor: VisitorMap): void {
254
253
  visitor.visitWith?.(withItem);
255
254
  walk(withItem.stmt.ast, visitor);
256
255
  }
257
- let fromItems: From[];
258
- if (Array.isArray(sel.from)) {
259
- fromItems = sel.from;
260
- } else if (sel.from !== null) {
261
- fromItems = [sel.from];
262
- } else {
263
- fromItems = [];
264
- }
265
- for (const fromItem of fromItems) {
256
+ for (const fromItem of fromClauseItems(sel)) {
266
257
  walkFrom(fromItem, visitor);
267
258
  }
268
259
  for (const col of sel.columns) {
@@ -307,30 +298,6 @@ export function extractColumnRefs(expr: unknown): ColumnRefItem[] {
307
298
  return refs;
308
299
  }
309
300
 
310
- /** Return the path string from the first json_extract_scalar / json_extract call found. */
311
- export function extractJsonExtractPath(expr: unknown): string | undefined {
312
- let path: string | undefined;
313
- walkExpr(expr, {
314
- visitFunction(node) {
315
- if (path !== undefined) {
316
- return;
317
- }
318
- const fnName = node.name.name[0]?.value;
319
- if (fnName === 'json_extract_scalar' || fnName === 'json_extract') {
320
- const pathArg = node.args?.value[1];
321
- if (pathArg !== undefined) {
322
- const val = (pathArg as unknown as { value?: unknown }).value;
323
- if (typeof val === 'string') {
324
- path = val;
325
- }
326
- }
327
- log('extractJsonExtractPath, function:', fnName, path);
328
- }
329
- },
330
- });
331
- return path;
332
- }
333
-
334
301
  export interface JsonExtractCall {
335
302
  ref: ColumnRefItem;
336
303
  path: string;
@@ -366,6 +333,11 @@ export function extractJsonExtractCalls(expr: unknown): JsonExtractCall[] {
366
333
  return calls;
367
334
  }
368
335
 
336
+ /** Return the path string from the first json_extract_scalar / json_extract call found. */
337
+ export function extractJsonExtractPath(expr: unknown): string | undefined {
338
+ return extractJsonExtractCalls(expr)[0]?.path;
339
+ }
340
+
369
341
  /** Return the JSONPath-style string from a bracket accessor (col['key']), or undefined. */
370
342
  export function extractBracketAccessorPath(expr: unknown): string | undefined {
371
343
  let path: string | undefined;
@@ -397,13 +369,12 @@ export function hasFunctionCalls(expr: unknown): boolean {
397
369
  return found;
398
370
  }
399
371
 
400
- /** Return true when the expression tree contains any CAST / TRY_CAST to ARRAY<…>. */
401
- export function containsCastToArray(expr: unknown): boolean {
372
+ function containsCastToType(expr: unknown, dataType: 'ARRAY' | 'MAP'): boolean {
402
373
  let found = false;
403
374
  walkExpr(expr, {
404
375
  visitCast(node) {
405
376
  const target = (node as unknown as { target?: { dataType?: string }[] }).target?.[0];
406
- if (target?.dataType === 'ARRAY') {
377
+ if (target?.dataType === dataType) {
407
378
  found = true;
408
379
  }
409
380
  },
@@ -411,16 +382,12 @@ export function containsCastToArray(expr: unknown): boolean {
411
382
  return found;
412
383
  }
413
384
 
385
+ /** Return true when the expression tree contains any CAST / TRY_CAST to ARRAY<…>. */
386
+ export function containsCastToArray(expr: unknown): boolean {
387
+ return containsCastToType(expr, 'ARRAY');
388
+ }
389
+
414
390
  /** Return true when the expression tree contains any CAST / TRY_CAST to MAP<…>. */
415
391
  export function containsCastToMap(expr: unknown): boolean {
416
- let found = false;
417
- walkExpr(expr, {
418
- visitCast(node) {
419
- const target = (node as unknown as { target?: { dataType?: string }[] }).target?.[0];
420
- if (target?.dataType === 'MAP') {
421
- found = true;
422
- }
423
- },
424
- });
425
- return found;
392
+ return containsCastToType(expr, 'MAP');
426
393
  }
@@ -86,8 +86,9 @@ function getRequestParametersSchema(
86
86
  return;
87
87
  }
88
88
 
89
- let parameters = getParameters(operation.parameters, document);
90
- parameters = parameters.filter((parameter) => parameter.in === parameterType);
89
+ const parameters = getParameters(operation.parameters, document).filter(
90
+ (parameter) => parameter.in === parameterType,
91
+ );
91
92
  if (parameters.length === 0) {
92
93
  return;
93
94
  }
@@ -95,8 +96,7 @@ function getRequestParametersSchema(
95
96
  const parametersSchema = Object.fromEntries(
96
97
  parameters.map((parameter) => [
97
98
  parameterType === 'header' ? parameter.name.toLowerCase() : parameter.name,
98
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
99
- parameter.schema ?? ({ type: 'string' } as v31.SchemaObject),
99
+ parameter.schema ?? { type: 'string' as const },
100
100
  ]),
101
101
  );
102
102
  const requiredParameterNames = parameters
@@ -113,17 +113,8 @@ function getRequestParametersSchema(
113
113
  }
114
114
 
115
115
  function getBodySchema(contents: Record<string, v31.MediaTypeObject> | undefined): v31.SchemaObject | undefined {
116
- if (contents === undefined) {
117
- return undefined;
118
- }
119
-
120
- const schema = Object.values(contents)[0]?.schema;
121
- if (schema !== undefined && Object.keys(schema).length === 0) {
122
- // empty schema should be treated as undefined
123
- return undefined;
124
- }
125
-
126
- return schema;
116
+ const schema = Object.values(contents ?? {})[0]?.schema;
117
+ return schema !== undefined && Object.keys(schema).length > 0 ? schema : undefined;
127
118
  }
128
119
 
129
120
  function getRequestBodySchema(operation: v31.OperationObject, document: v31.Document) {
@@ -201,15 +192,10 @@ function getResponseHeadersSchema(
201
192
  Object.entries(headers).map(([name, header]) => [name.toLowerCase(), resolve(document, header)]),
202
193
  );
203
194
  const resolvedHeaderSchemas = Object.fromEntries(
204
- Object.entries(resolvedHeaders).map(([name, header]) => [
205
- name,
206
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
207
- header.schema ?? ({ type: 'string' } as v31.SchemaObject),
208
- ]),
195
+ Object.entries(resolvedHeaders).map(([name, header]) => [name, header.schema ?? { type: 'string' as const }]),
209
196
  );
210
197
  const requiredHeaderNames = Object.entries(resolvedHeaders)
211
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
212
- .filter(([_name, header]) => header.required === true)
198
+ .filter(([, header]) => header.required === true)
213
199
  .map(([name]) => name);
214
200
  return {
215
201
  type: 'object',
@@ -282,12 +268,10 @@ function getOperationId(
282
268
 
283
269
  // KISS, we could try to to come up with a better naming convension in case of name collision, but it's probably better to leave it to the service to decide a appropriate operationId
284
270
  let operationIdIndex = 1;
285
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
286
- while (operationIds.has(`${operationId}${operationIdIndex}`)) {
271
+ while (operationIds.has(`${operationId}${operationIdIndex.toString()}`)) {
287
272
  operationIdIndex += 1;
288
273
  }
289
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
290
- return `${operationId}${operationIdIndex}`;
274
+ return `${operationId}${operationIdIndex.toString()}`;
291
275
  }
292
276
 
293
277
  function updateOpenapiSchemaDefinitionsReferences(
@@ -315,6 +299,11 @@ function updateOpenapiSchemaDefinitionsReferences(
315
299
  return value;
316
300
  }
317
301
 
302
+ function getFirehoseLoggedExtension(obj: object): unknown {
303
+ const record = obj as Record<string, unknown>;
304
+ return record['x-firehose-logged'] ?? record['x-firehoseLogged'];
305
+ }
306
+
318
307
  function buildApiSchemaFromDocument(
319
308
  document: v31.Document,
320
309
  organization: string,
@@ -333,24 +322,19 @@ function buildApiSchemaFromDocument(
333
322
  const apiSchemas: Record<string, Record<string, OperationSchemas>> = {};
334
323
  const allSchemas: ApiSchemas = { apis: apiSchemas };
335
324
  const operationIds = new Set<string>();
336
- const documentFirehoseLogged =
337
- (document as Record<string, unknown>)['x-firehose-logged'] ??
338
- (document as Record<string, unknown>)['x-firehoseLogged'];
325
+ const documentFirehoseLogged = getFirehoseLoggedExtension(document);
339
326
  log('document firehose logged value', documentFirehoseLogged);
340
327
 
341
328
  for (const [path, pathItems] of Object.entries(document.paths)) {
342
329
  // convert openapi path to koa router path, e.g. "/user/{userId}" --> "/user/:userId"
343
- // eslint-disable-next-line prefer-named-capture-group
344
- const koaPath = path.replaceAll(/\{([^}]+)\}/gu, ':$1');
330
+ const koaPath = path.replaceAll(/\{(?<param>[^}]+)\}/gu, ':$<param>');
345
331
  const pathSchemas: Record<string, OperationSchemas> = {};
346
332
  apiSchemas[`${serverPathname}${koaPath}`] = pathSchemas;
347
333
 
348
334
  for (const method of ALL_OPERATION_METHODS) {
349
335
  const operation = pathItems?.[method];
350
336
  if (operation !== undefined) {
351
- const operationFirehoseLogged =
352
- (operation as Record<string, unknown>)['x-firehose-logged'] ??
353
- (operation as Record<string, unknown>)['x-firehoseLogged'];
337
+ const operationFirehoseLogged = getFirehoseLoggedExtension(operation);
354
338
  log('operation firehose logged value', operationFirehoseLogged);
355
339
  const effectiveFirehoseLogged = operationFirehoseLogged ?? documentFirehoseLogged;
356
340
  if (effectiveFirehoseLogged !== true) {
@@ -11,6 +11,10 @@ const log = debug('eslint-plugin:athena:service-schema-generator');
11
11
  const GITHUB_ORGANIZATIONS = ['checkdigit', 'rebolt-checkdigit'] as const;
12
12
  const SWAGGER_SCHEMA_DEREF_FILENAME = 'swagger.schema.deref.json';
13
13
 
14
+ function errorMessageFromError(error: unknown): string {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+
14
18
  interface ServiceEndpoint {
15
19
  path: string;
16
20
  yamlContent: string;
@@ -66,8 +70,7 @@ function readServiceConfig(
66
70
  if (apiRoot === undefined || apiEndpoints === undefined || apiEndpoints.length === 0) {
67
71
  return null;
68
72
  }
69
- const pkgOrg = packageJson.name?.slice(1).split('/')[0] ?? org;
70
- const pkgServiceName = packageJson.name?.slice(1).split('/')[1] ?? serviceName;
73
+ const [pkgOrg = org, pkgServiceName = serviceName] = packageJson.name?.slice(1).split('/') ?? [];
71
74
  return { organization: pkgOrg, serviceName: pkgServiceName, apiRoot, endpoints: apiEndpoints };
72
75
  }
73
76
 
@@ -116,9 +119,7 @@ function findServiceInNodeModules(serviceName: string): ServiceSource | null {
116
119
  }
117
120
  log(`[schema-generator] no swagger schema inside node_modules/@${org}/${serviceName}`);
118
121
  } catch (error) {
119
- log(
120
- `[schema-generator] error reading node_modules/@${org}/${serviceName}: ${error instanceof Error ? error.message : String(error)}`,
121
- );
122
+ log(`[schema-generator] error reading node_modules/@${org}/${serviceName}: ${errorMessageFromError(error)}`);
122
123
  }
123
124
  }
124
125
  return null;
@@ -158,9 +159,7 @@ export function generateSchemasForService(
158
159
  log(`[schema-generator] cached schema to ${dir}/${SWAGGER_SCHEMA_DEREF_FILENAME}`);
159
160
  }
160
161
  } catch (error) {
161
- log(
162
- `[schema-generator] error processing endpoint ${endpoint}: ${error instanceof Error ? error.message : String(error)}`,
163
- );
162
+ log(`[schema-generator] error processing endpoint ${endpoint}: ${errorMessageFromError(error)}`);
164
163
  }
165
164
  }
166
165