@checkdigit/eslint-plugin 7.6.0-PR.75-5da1 → 7.6.0-PR.75-04b9

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.
@@ -0,0 +1,500 @@
1
+ // agent/no-supertest.ts
2
+
3
+ /*
4
+ * Copyright (c) 2021-2024 Check Digit, LLC
5
+ *
6
+ * This code is licensed under the MIT license (see LICENSE.txt for details).
7
+ */
8
+
9
+ import { strict as assert } from 'node:assert';
10
+
11
+ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';
12
+ import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager';
13
+ import type { SourceCode } from '@typescript-eslint/utils/ts-eslint';
14
+
15
+ import {
16
+ getEnclosingFunction,
17
+ getEnclosingScopeNode,
18
+ getEnclosingStatement,
19
+ getParent,
20
+ isUsedInArrayOrAsArgument,
21
+ } from '../library/ts-tree';
22
+ import getDocumentationUrl from '../get-documentation-url';
23
+ import { getIndentation } from '../library/format';
24
+ import { analyzeResponseReferences } from './response-reference';
25
+ import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText } from './fetch';
26
+
27
+ export const ruleId = 'no-supertest';
28
+
29
+ interface FixtureCallInformation {
30
+ rootNode:
31
+ | TSESTree.AwaitExpression
32
+ | TSESTree.ReturnStatement
33
+ | TSESTree.VariableDeclaration
34
+ | TSESTree.CallExpression
35
+ | TSESTree.ExpressionStatement;
36
+ fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression;
37
+ variableDeclaration?: TSESTree.VariableDeclaration;
38
+ variableAssignment?: TSESTree.ExpressionStatement;
39
+ requestBody?: TSESTree.Expression;
40
+ requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[];
41
+ requestHeadersObjectLiteral?: TSESTree.ObjectExpression;
42
+ assertions?: TSESTree.Expression[][];
43
+ inlineStatementNode?: TSESTree.Node;
44
+ inlineBodyReference?: TSESTree.MemberExpression;
45
+ inlineHeadersReference?: TSESTree.MemberExpression;
46
+ }
47
+
48
+ // recursively analyze the fixture/supertest call chain to collect information of request/response
49
+ // eslint-disable-next-line sonarjs/cognitive-complexity
50
+ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
51
+ const parent = getParent(call);
52
+ assert.ok(parent, 'parent should exist for fixture/supertest call node');
53
+
54
+ let nextCall;
55
+ if (parent.type === AST_NODE_TYPES.ReturnStatement) {
56
+ // direct return, no variable declaration or await
57
+ results.fixtureNode = call;
58
+ results.rootNode = parent;
59
+ } else if (
60
+ parent.type === AST_NODE_TYPES.ArrayExpression ||
61
+ parent.type === AST_NODE_TYPES.CallExpression ||
62
+ parent.type === AST_NODE_TYPES.ArrowFunctionExpression
63
+ ) {
64
+ // direct return, no variable declaration or await
65
+ results.fixtureNode = call;
66
+ results.rootNode = call;
67
+ } else if (parent.type === AST_NODE_TYPES.AwaitExpression) {
68
+ results.fixtureNode = call;
69
+ const enclosingStatement = getEnclosingStatement(parent);
70
+ assert.ok(enclosingStatement);
71
+ const awaitParent = getParent(parent);
72
+ if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) {
73
+ results.rootNode = parent;
74
+ results.inlineStatementNode = enclosingStatement;
75
+ if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') {
76
+ results.inlineBodyReference = awaitParent;
77
+ }
78
+ if (
79
+ awaitParent.property.type === AST_NODE_TYPES.Identifier &&
80
+ (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers')
81
+ ) {
82
+ results.inlineHeadersReference = awaitParent;
83
+ }
84
+ } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) {
85
+ results.variableDeclaration = enclosingStatement;
86
+ results.rootNode = enclosingStatement;
87
+ } else if (
88
+ enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement &&
89
+ enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression
90
+ ) {
91
+ results.variableAssignment = enclosingStatement;
92
+ results.rootNode = enclosingStatement;
93
+ } else {
94
+ results.rootNode = parent;
95
+ }
96
+ } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) {
97
+ if (parent.property.name === 'expect') {
98
+ // supertest assertions
99
+ const assertionCall = getParent(parent);
100
+ assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression);
101
+ results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]];
102
+ nextCall = assertionCall;
103
+ } else if (parent.property.name === 'send') {
104
+ // request body
105
+ const sendRequestBodyCall = getParent(parent);
106
+ assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression);
107
+ results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression;
108
+ nextCall = sendRequestBodyCall;
109
+ } else if (parent.property.name === 'set') {
110
+ // request headers
111
+ const setRequestHeaderCall = getParent(parent);
112
+ assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression);
113
+ const [arg1, arg2] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression];
114
+ if (arg1.type === AST_NODE_TYPES.ObjectExpression) {
115
+ results.requestHeadersObjectLiteral = arg1;
116
+ } else {
117
+ results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }];
118
+ }
119
+ nextCall = setRequestHeaderCall;
120
+ }
121
+ } else {
122
+ throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
123
+ }
124
+ if (nextCall) {
125
+ analyzeFixtureCall(nextCall, results, sourceCode);
126
+ }
127
+ }
128
+
129
+ // eslint-disable-next-line sonarjs/cognitive-complexity
130
+ function createResponseAssertions(
131
+ fixtureCallInformation: FixtureCallInformation,
132
+ sourceCode: SourceCode,
133
+ responseVariableName: string,
134
+ destructuringResponseHeadersVariable: Variable | undefined,
135
+ ) {
136
+ let statusAssertion: string | undefined;
137
+ const nonStatusAssertions: string[] = [];
138
+ for (const expectArguments of fixtureCallInformation.assertions ?? []) {
139
+ if (expectArguments.length === 1) {
140
+ const [assertionArgument] = expectArguments;
141
+ assert.ok(assertionArgument);
142
+ if (
143
+ (assertionArgument.type === AST_NODE_TYPES.MemberExpression &&
144
+ assertionArgument.object.type === AST_NODE_TYPES.Identifier &&
145
+ assertionArgument.object.name === 'StatusCodes') ||
146
+ assertionArgument.type === AST_NODE_TYPES.Literal ||
147
+ sourceCode.getText(assertionArgument).includes('StatusCodes.')
148
+ ) {
149
+ // status code assertion
150
+ statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
151
+ } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) {
152
+ // callback assertion using arrow function
153
+ let functionBody = sourceCode.getText(assertionArgument.body);
154
+
155
+ const [originalResponseArgument] = assertionArgument.params;
156
+ assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier);
157
+ const originalResponseArgumentName = originalResponseArgument.name;
158
+ if (originalResponseArgumentName !== responseVariableName) {
159
+ functionBody = functionBody.replace(
160
+ new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
161
+ responseVariableName,
162
+ );
163
+ }
164
+ nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
165
+ } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) {
166
+ // callback assertion using function reference
167
+ nonStatusAssertions.push(
168
+ `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
169
+ );
170
+ } else if (
171
+ assertionArgument.type === AST_NODE_TYPES.ObjectExpression ||
172
+ assertionArgument.type === AST_NODE_TYPES.CallExpression
173
+ ) {
174
+ // body deep equal assertion
175
+ nonStatusAssertions.push(
176
+ `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
177
+ );
178
+ } else {
179
+ throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
180
+ }
181
+ } else if (expectArguments.length === 2) {
182
+ // header assertion
183
+ const [headerName, headerValue] = expectArguments;
184
+ assert.ok(headerName && headerValue);
185
+ const headersReference =
186
+ destructuringResponseHeadersVariable !== undefined
187
+ ? destructuringResponseHeadersVariable.name
188
+ : `${responseVariableName}.headers`;
189
+ if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) {
190
+ nonStatusAssertions.push(
191
+ `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
192
+ );
193
+ } else {
194
+ nonStatusAssertions.push(
195
+ `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
196
+ );
197
+ }
198
+ }
199
+ }
200
+ return {
201
+ statusAssertion,
202
+ nonStatusAssertions,
203
+ };
204
+ }
205
+
206
+ function getResponseVariableNameToUse(
207
+ scopeManager: ScopeManager,
208
+ fixtureCallInformation: FixtureCallInformation,
209
+ scopeVariablesMap: Map<Scope, string[]>,
210
+ ) {
211
+ if (fixtureCallInformation.variableAssignment) {
212
+ assert.ok(
213
+ fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression &&
214
+ fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier,
215
+ );
216
+ return fixtureCallInformation.variableAssignment.expression.left.name;
217
+ }
218
+
219
+ if (fixtureCallInformation.variableDeclaration) {
220
+ const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0];
221
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
222
+ if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) {
223
+ return firstDeclaration.id.name;
224
+ }
225
+ }
226
+
227
+ const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode);
228
+ scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode);
229
+ assert.ok(enclosingScopeNode);
230
+ const scope = scopeManager.acquire(enclosingScopeNode);
231
+ assert.ok(scope !== null);
232
+ let scopeVariables = scopeVariablesMap.get(scope);
233
+ if (!scopeVariables) {
234
+ scopeVariables = [...scope.set.keys()];
235
+ scopeVariablesMap.set(scope, scopeVariables);
236
+ }
237
+
238
+ let responseVariableCounter = 0;
239
+ let responseVariableNameToUse;
240
+ while (responseVariableNameToUse === undefined) {
241
+ responseVariableCounter++;
242
+ responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`;
243
+ if (scopeVariables.includes(responseVariableNameToUse)) {
244
+ responseVariableNameToUse = undefined;
245
+ }
246
+ }
247
+ scopeVariables.push(responseVariableNameToUse);
248
+ return responseVariableNameToUse;
249
+ }
250
+
251
+ function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean {
252
+ const parent = getParent(responseBodyReference);
253
+ return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier;
254
+ }
255
+
256
+ const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
257
+
258
+ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({
259
+ name: ruleId,
260
+ meta: {
261
+ type: 'suggestion',
262
+ docs: {
263
+ description: 'Transform supertest assersions to regular node assertions.',
264
+ url: getDocumentationUrl(ruleId),
265
+ },
266
+ messages: {
267
+ preferNativeFetch: 'Transform supertest assersions to regular node assertions.',
268
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
269
+ },
270
+ fixable: 'code',
271
+ schema: [],
272
+ },
273
+ defaultOptions: [],
274
+ // eslint-disable-next-line max-lines-per-function
275
+ create(context) {
276
+ const sourceCode = context.sourceCode;
277
+ const scopeManager = sourceCode.scopeManager;
278
+ assert.ok(scopeManager !== null);
279
+ const scopeVariablesMap = new Map<Scope, string[]>();
280
+
281
+ return {
282
+ // eslint-disable-next-line max-lines-per-function
283
+ 'CallExpression[callee.property.name="expect"]': (
284
+ supertestCall: TSESTree.CallExpression,
285
+ // eslint-disable-next-line sonarjs/cognitive-complexity
286
+ ) => {
287
+ assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression);
288
+ if (
289
+ supertestCall.callee.object.type === AST_NODE_TYPES.CallExpression &&
290
+ supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression &&
291
+ supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier &&
292
+ supertestCall.callee.object.callee.property.name === 'expect'
293
+ ) {
294
+ // skip nested expect calls, only focus on the top level
295
+ return;
296
+ }
297
+ try {
298
+ if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) {
299
+ // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here
300
+ return;
301
+ }
302
+
303
+ const fixtureFunction = supertestCall.callee.object;
304
+ if (fixtureFunction.type !== AST_NODE_TYPES.CallExpression) {
305
+ return;
306
+ }
307
+
308
+ const indentation = getIndentation(supertestCall, sourceCode);
309
+
310
+ const fixtureCallInformation = {} as FixtureCallInformation;
311
+ analyzeFixtureCall(fixtureFunction, fixtureCallInformation, sourceCode);
312
+ sourceCode.getText(fixtureCallInformation.fixtureNode);
313
+ sourceCode.getText(fixtureCallInformation.rootNode);
314
+ fixtureCallInformation.assertions?.flat().map((ass) => sourceCode.getText(ass));
315
+
316
+ const {
317
+ variable: responseVariable,
318
+ bodyReferences: responseBodyReferences,
319
+ headersReferences: responseHeadersReferences,
320
+ statusReferences: responseStatusReferences,
321
+ destructuringBodyVariable: destructuringResponseBodyVariable,
322
+ destructuringHeadersVariable: destructuringResponseHeadersVariable,
323
+ } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager);
324
+
325
+ const responseVariableNameToUse = getResponseVariableNameToUse(
326
+ scopeManager,
327
+ fixtureCallInformation,
328
+ scopeVariablesMap,
329
+ );
330
+
331
+ const isResponseBodyVariableRedefinitionNeeded =
332
+ destructuringResponseBodyVariable !== undefined ||
333
+ fixtureCallInformation.inlineBodyReference !== undefined ||
334
+ (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
335
+ const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
336
+
337
+ const isResponseHeadersVariableRedefinitionNeeded =
338
+ (destructuringResponseHeadersVariable !== undefined &&
339
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
340
+ (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern) ||
341
+ fixtureCallInformation.inlineHeadersReference !== undefined;
342
+ const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`;
343
+
344
+ const isResponseVariableRedefinitionNeeded =
345
+ (fixtureCallInformation.variableAssignment === undefined &&
346
+ responseVariable === undefined &&
347
+ fixtureCallInformation.assertions !== undefined) ||
348
+ isResponseBodyVariableRedefinitionNeeded ||
349
+ isResponseHeadersVariableRedefinitionNeeded;
350
+
351
+ const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
352
+ ? [
353
+ // eslint-disable-next-line no-nested-ternary
354
+ ...(destructuringResponseBodyVariable
355
+ ? [
356
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
357
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
358
+ ]
359
+ : isResponseBodyVariableRedefinitionNeeded
360
+ ? [
361
+ `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
362
+ ]
363
+ : []),
364
+ // eslint-disable-next-line no-nested-ternary
365
+ ...(destructuringResponseHeadersVariable
366
+ ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
367
+ (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type ===
368
+ AST_NODE_TYPES.ObjectPattern
369
+ ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => {
370
+ assert.ok(property.type === AST_NODE_TYPES.Property);
371
+ assert.ok(property.value.type === AST_NODE_TYPES.Identifier);
372
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
373
+ return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`;
374
+ })
375
+ : [
376
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
377
+ ]
378
+ : isResponseHeadersVariableRedefinitionNeeded
379
+ ? [
380
+ `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
381
+ ]
382
+ : []),
383
+ ]
384
+ : [];
385
+
386
+ const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
387
+ fixtureCallInformation,
388
+ sourceCode,
389
+ responseVariableNameToUse,
390
+ destructuringResponseHeadersVariable as Variable | undefined,
391
+ );
392
+
393
+ // add variable declaration if needed
394
+ const fetchCallText = sourceCode.getText(fixtureFunction);
395
+ const fetchStatementText = !isResponseVariableRedefinitionNeeded
396
+ ? fetchCallText
397
+ : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`;
398
+
399
+ const nodeToReplace = isResponseVariableRedefinitionNeeded
400
+ ? fixtureCallInformation.rootNode
401
+ : fixtureCallInformation.fixtureNode;
402
+ const appendingAssignmentAndAssertionText = [
403
+ '',
404
+ ...(statusAssertion !== undefined ? [statusAssertion] : []),
405
+ ...responseBodyHeadersVariableRedefineLines,
406
+ ...nonStatusAssertions,
407
+ ].join(`;\n${indentation}`);
408
+
409
+ context.report({
410
+ node: supertestCall,
411
+ messageId: 'preferNativeFetch',
412
+
413
+ *fix(fixer) {
414
+ if (fixtureCallInformation.inlineStatementNode) {
415
+ const preInlineDeclaration = [
416
+ fetchStatementText,
417
+ `${appendingAssignmentAndAssertionText};\n${indentation}`,
418
+ ].join(``);
419
+ yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration);
420
+ } else {
421
+ yield fixer.replaceText(nodeToReplace, fetchStatementText);
422
+
423
+ const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
424
+ yield fixer.insertTextAfter(
425
+ nodeToReplace,
426
+ needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText,
427
+ );
428
+ }
429
+
430
+ // handle response body references
431
+ for (const responseBodyReference of responseBodyReferences) {
432
+ yield fixer.replaceText(
433
+ responseBodyReference,
434
+ isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference)
435
+ ? redefineResponseBodyVariableName
436
+ : getResponseBodyRetrievalText(responseVariableNameToUse),
437
+ );
438
+ }
439
+ if (fixtureCallInformation.inlineBodyReference) {
440
+ yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName);
441
+ }
442
+
443
+ // handle response headers references
444
+ for (const responseHeadersReference of responseHeadersReferences) {
445
+ const parent = getParent(responseHeadersReference);
446
+ assert.ok(parent);
447
+ let headerName;
448
+ if (parent.type === AST_NODE_TYPES.MemberExpression) {
449
+ const headerNameNode = parent.property;
450
+ headerName = parent.computed
451
+ ? sourceCode.getText(headerNameNode)
452
+ : `'${sourceCode.getText(headerNameNode)}'`;
453
+ } else if (parent.type === AST_NODE_TYPES.CallExpression) {
454
+ const headerNameNode = parent.arguments[0];
455
+ headerName = sourceCode.getText(headerNameNode);
456
+ }
457
+ assert.ok(headerName !== undefined);
458
+ yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`);
459
+ }
460
+
461
+ // convert response.statusCode to response.status
462
+ for (const responseStatusReference of responseStatusReferences) {
463
+ if (
464
+ responseStatusReference.property.type === AST_NODE_TYPES.Identifier &&
465
+ responseStatusReference.property.name === 'statusCode'
466
+ ) {
467
+ yield fixer.replaceText(responseStatusReference.property, `status`);
468
+ }
469
+ }
470
+
471
+ // handle direct return statement without await, e.g. "return fixture.api.get(...);"
472
+ if (
473
+ fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement &&
474
+ fixtureCallInformation.assertions !== undefined
475
+ ) {
476
+ yield fixer.insertTextAfter(
477
+ fixtureCallInformation.rootNode,
478
+ `\n${indentation}return ${responseVariableNameToUse};`,
479
+ );
480
+ }
481
+ },
482
+ });
483
+ } catch (error) {
484
+ // eslint-disable-next-line no-console
485
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
486
+ context.report({
487
+ node: supertestCall,
488
+ messageId: 'unknownError',
489
+ data: {
490
+ fileName: context.filename,
491
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
492
+ },
493
+ });
494
+ }
495
+ },
496
+ };
497
+ },
498
+ });
499
+
500
+ export default rule;
@@ -7,11 +7,12 @@
7
7
  */
8
8
 
9
9
  import { strict as assert } from 'node:assert';
10
- import type { MemberExpression, ObjectPattern, VariableDeclaration } from 'estree';
11
- import { type Scope } from 'eslint';
10
+
12
11
  import debug from 'debug';
13
12
 
14
- import { getParent } from '../library/tree';
13
+ import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
14
+ import type { ScopeManager, Variable } from '@typescript-eslint/scope-manager';
15
+ import { getParent } from '../library/ts-tree';
15
16
 
16
17
  const log = debug('eslint-plugin:response-reference');
17
18
 
@@ -21,25 +22,25 @@ const log = debug('eslint-plugin:response-reference');
21
22
  * @param variableDeclaration - variable declaration node
22
23
  */
23
24
  export function analyzeResponseReferences(
24
- variableDeclaration: VariableDeclaration | undefined,
25
- scopeManager: Scope.ScopeManager,
25
+ variableDeclaration: TSESTree.VariableDeclaration | undefined,
26
+ scopeManager: ScopeManager,
26
27
  ): {
27
- variable?: Scope.Variable;
28
- bodyReferences: MemberExpression[];
29
- headersReferences: MemberExpression[];
30
- statusReferences: MemberExpression[];
31
- destructuringBodyVariable?: Scope.Variable | ObjectPattern;
32
- destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
33
- destructuringHeadersReferences?: MemberExpression[] | undefined;
28
+ variable?: Variable;
29
+ bodyReferences: TSESTree.MemberExpression[];
30
+ headersReferences: TSESTree.MemberExpression[];
31
+ statusReferences: TSESTree.MemberExpression[];
32
+ destructuringBodyVariable?: Variable | TSESTree.ObjectPattern;
33
+ destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern;
34
+ destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined;
34
35
  } {
35
36
  const results: {
36
- variable?: Scope.Variable;
37
- bodyReferences: MemberExpression[];
38
- headersReferences: MemberExpression[];
39
- statusReferences: MemberExpression[];
40
- destructuringBodyVariable?: Scope.Variable | ObjectPattern;
41
- destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
42
- destructuringHeadersReferences?: MemberExpression[] | undefined;
37
+ variable?: Variable;
38
+ bodyReferences: TSESTree.MemberExpression[];
39
+ headersReferences: TSESTree.MemberExpression[];
40
+ statusReferences: TSESTree.MemberExpression[];
41
+ destructuringBodyVariable?: Variable | TSESTree.ObjectPattern;
42
+ destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern;
43
+ destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined;
43
44
  } = {
44
45
  bodyReferences: [],
45
46
  headersReferences: [],
@@ -55,7 +56,7 @@ export function analyzeResponseReferences(
55
56
  assert.ok(identifier);
56
57
  const identifierParent = getParent(identifier);
57
58
  assert.ok(identifierParent);
58
- if (identifierParent.type === 'VariableDeclarator') {
59
+ if (identifierParent.type === AST_NODE_TYPES.VariableDeclarator) {
59
60
  // e.g. const response = ...
60
61
  results.variable = responseVariable;
61
62
  const responseReferences = responseVariable.references.map((responseReference) =>
@@ -63,34 +64,36 @@ export function analyzeResponseReferences(
63
64
  );
64
65
  // e.g. response.body
65
66
  results.bodyReferences = responseReferences.filter(
66
- (node): node is MemberExpression =>
67
- node?.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'body',
67
+ (node): node is TSESTree.MemberExpression =>
68
+ node?.type === AST_NODE_TYPES.MemberExpression &&
69
+ node.property.type === AST_NODE_TYPES.Identifier &&
70
+ node.property.name === 'body',
68
71
  );
69
72
  // e.g. response.headers / response.header / response.get()
70
73
  results.headersReferences = responseReferences.filter(
71
- (node): node is MemberExpression =>
72
- node?.type === 'MemberExpression' &&
73
- node.property.type === 'Identifier' &&
74
+ (node): node is TSESTree.MemberExpression =>
75
+ node?.type === AST_NODE_TYPES.MemberExpression &&
76
+ node.property.type === AST_NODE_TYPES.Identifier &&
74
77
  (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
75
78
  );
76
79
  // e.g. response.status / response.statusCode
77
80
  results.statusReferences = responseReferences.filter(
78
- (node): node is MemberExpression =>
79
- node?.type === 'MemberExpression' &&
80
- node.property.type === 'Identifier' &&
81
+ (node): node is TSESTree.MemberExpression =>
82
+ node?.type === AST_NODE_TYPES.MemberExpression &&
83
+ node.property.type === AST_NODE_TYPES.Identifier &&
81
84
  (node.property.name === 'status' || node.property.name === 'statusCode'),
82
85
  );
83
86
  } else if (
84
87
  // body reference through destruction/renaming, e.g. "const { body } = ..."
85
- identifierParent.type === 'Property' &&
86
- identifierParent.key.type === 'Identifier' &&
88
+ identifierParent.type === AST_NODE_TYPES.Property &&
89
+ identifierParent.key.type === AST_NODE_TYPES.Identifier &&
87
90
  identifierParent.key.name === 'body'
88
91
  ) {
89
92
  results.destructuringBodyVariable = responseVariable;
90
93
  } else if (
91
94
  // header reference through destruction/renaming, e.g. "const { headers } = ..."
92
- identifierParent.type === 'Property' &&
93
- identifierParent.key.type === 'Identifier' &&
95
+ identifierParent.type === AST_NODE_TYPES.Property &&
96
+ identifierParent.key.type === AST_NODE_TYPES.Identifier &&
94
97
  identifierParent.key.name === 'headers'
95
98
  ) {
96
99
  results.destructuringHeadersVariable = responseVariable;
@@ -98,23 +101,27 @@ export function analyzeResponseReferences(
98
101
  .map((reference) => reference.identifier)
99
102
  .map(getParent)
100
103
  .filter(
101
- (parent): parent is MemberExpression =>
102
- parent?.type === 'MemberExpression' &&
103
- parent.property.type === 'Identifier' &&
104
+ (parent): parent is TSESTree.MemberExpression =>
105
+ parent?.type === AST_NODE_TYPES.MemberExpression &&
106
+ parent.property.type === AST_NODE_TYPES.Identifier &&
104
107
  parent.property.name !== 'get' &&
105
- getParent(parent)?.type !== 'CallExpression',
108
+ getParent(parent)?.type !== AST_NODE_TYPES.CallExpression,
106
109
  );
107
- } else if (identifierParent.type === 'Property') {
110
+ } else if (identifierParent.type === AST_NODE_TYPES.Property) {
108
111
  const parent = getParent(identifierParent);
109
- if (parent?.type === 'ObjectPattern') {
112
+ if (parent?.type === AST_NODE_TYPES.ObjectPattern) {
110
113
  // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..."
111
114
  const parent2 = getParent(parent);
112
- if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') {
115
+ if (
116
+ parent2?.type === AST_NODE_TYPES.Property &&
117
+ parent2.key.type === AST_NODE_TYPES.Identifier &&
118
+ parent2.key.name === 'body'
119
+ ) {
113
120
  results.destructuringBodyVariable = parent;
114
121
  }
115
122
  if (
116
- parent2?.type === 'Property' &&
117
- parent2.key.type === 'Identifier' &&
123
+ parent2?.type === AST_NODE_TYPES.Property &&
124
+ parent2.key.type === AST_NODE_TYPES.Identifier &&
118
125
  (parent2.key.name === 'header' || parent2.key.name === 'headers')
119
126
  ) {
120
127
  results.destructuringHeadersVariable = parent;