@checkdigit/eslint-plugin 7.6.0-PR.75-4751 → 7.6.0-PR.75-a611

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,570 @@
1
+ // agent/no-expect-assertion.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
+ import ts from 'typescript';
15
+
16
+ import {
17
+ getEnclosingFunction,
18
+ getEnclosingScopeNode,
19
+ getEnclosingStatement,
20
+ getParent,
21
+ isUsedInArrayOrAsArgument,
22
+ } from '../library/ts-tree';
23
+ import getDocumentationUrl from '../get-documentation-url';
24
+ import { getIndentation } from '../library/format';
25
+ import { analyzeResponseReferences } from './response-reference';
26
+ import {
27
+ getResponseBodyRetrievalText,
28
+ getResponseHeadersRetrievalText,
29
+ getResponseStatusRetrievalText,
30
+ isFetchResponse,
31
+ } from './fetch';
32
+
33
+ export const ruleId = 'no-expect-assertion';
34
+
35
+ interface FixtureCallInformation {
36
+ rootNode:
37
+ | TSESTree.AwaitExpression
38
+ | TSESTree.ReturnStatement
39
+ | TSESTree.VariableDeclaration
40
+ | TSESTree.CallExpression
41
+ | TSESTree.ExpressionStatement;
42
+ fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression;
43
+ variableDeclaration?: TSESTree.VariableDeclaration;
44
+ variableAssignment?: TSESTree.ExpressionStatement;
45
+ assertions?: TSESTree.Expression[][];
46
+ inlineStatementNode?: TSESTree.Node;
47
+ inlineBodyReference?: TSESTree.MemberExpression;
48
+ inlineStatusReference?: TSESTree.MemberExpression;
49
+ inlineHeadersReference?: TSESTree.MemberExpression;
50
+ }
51
+
52
+ // recursively analyze the fixture/supertest call chain to collect information of request/response
53
+ // eslint-disable-next-line sonarjs/cognitive-complexity
54
+ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
55
+ const parent = getParent(call);
56
+ assert.ok(parent, 'parent should exist for fixture/supertest call node');
57
+
58
+ let nextCall;
59
+ if (parent.type === AST_NODE_TYPES.ReturnStatement) {
60
+ // direct return, no variable declaration or await
61
+ results.fixtureNode = call;
62
+ results.rootNode = parent;
63
+ } else if (
64
+ parent.type === AST_NODE_TYPES.ArrayExpression ||
65
+ parent.type === AST_NODE_TYPES.CallExpression ||
66
+ parent.type === AST_NODE_TYPES.ArrowFunctionExpression
67
+ ) {
68
+ // direct return, no variable declaration or await
69
+ results.fixtureNode = call;
70
+ results.rootNode = call;
71
+ } else if (parent.type === AST_NODE_TYPES.AwaitExpression) {
72
+ results.fixtureNode = call;
73
+ const enclosingStatement = getEnclosingStatement(parent);
74
+ assert.ok(enclosingStatement);
75
+ const awaitParent = getParent(parent);
76
+ if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) {
77
+ results.rootNode = parent;
78
+ results.inlineStatementNode = enclosingStatement;
79
+ if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') {
80
+ results.inlineBodyReference = awaitParent;
81
+ }
82
+ if (
83
+ awaitParent.property.type === AST_NODE_TYPES.Identifier &&
84
+ (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode')
85
+ ) {
86
+ results.inlineStatusReference = awaitParent;
87
+ }
88
+ if (
89
+ awaitParent.property.type === AST_NODE_TYPES.Identifier &&
90
+ (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers')
91
+ ) {
92
+ results.inlineHeadersReference = awaitParent;
93
+ }
94
+ } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) {
95
+ results.variableDeclaration = enclosingStatement;
96
+ results.rootNode = enclosingStatement;
97
+ } else if (
98
+ enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement &&
99
+ enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression
100
+ ) {
101
+ results.variableAssignment = enclosingStatement;
102
+ results.rootNode = enclosingStatement;
103
+ } else {
104
+ results.rootNode = parent;
105
+ }
106
+ } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) {
107
+ if (parent.property.name === 'expect') {
108
+ // supertest assertions
109
+ const assertionCall = getParent(parent);
110
+ assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression);
111
+ results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]];
112
+ nextCall = assertionCall;
113
+ }
114
+ } else {
115
+ throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
116
+ }
117
+ if (nextCall) {
118
+ analyzeFixtureCall(nextCall, results, sourceCode);
119
+ }
120
+ }
121
+
122
+ // eslint-disable-next-line sonarjs/cognitive-complexity
123
+ function createResponseAssertions(
124
+ fixtureCallInformation: FixtureCallInformation,
125
+ sourceCode: SourceCode,
126
+ responseVariableName: string,
127
+ destructuringResponseHeadersVariable: Variable | undefined,
128
+ ) {
129
+ let statusAssertion: string | undefined;
130
+ const nonStatusAssertions: string[] = [];
131
+ for (const expectArguments of fixtureCallInformation.assertions ?? []) {
132
+ if (expectArguments.length === 1) {
133
+ const [assertionArgument] = expectArguments;
134
+ assert.ok(assertionArgument);
135
+ if (
136
+ (assertionArgument.type === AST_NODE_TYPES.MemberExpression &&
137
+ assertionArgument.object.type === AST_NODE_TYPES.Identifier &&
138
+ assertionArgument.object.name === 'StatusCodes') ||
139
+ assertionArgument.type === AST_NODE_TYPES.Literal ||
140
+ sourceCode.getText(assertionArgument).includes('StatusCodes.')
141
+ ) {
142
+ // status code assertion
143
+ statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
144
+ } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) {
145
+ // callback assertion using arrow function
146
+ let functionBody = sourceCode.getText(assertionArgument.body);
147
+
148
+ const [originalResponseArgument] = assertionArgument.params;
149
+ assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier);
150
+ const originalResponseArgumentName = originalResponseArgument.name;
151
+ if (originalResponseArgumentName !== responseVariableName) {
152
+ functionBody = functionBody.replace(
153
+ new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
154
+ responseVariableName,
155
+ );
156
+ }
157
+ nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
158
+ } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) {
159
+ // callback assertion using function reference
160
+ nonStatusAssertions.push(
161
+ `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
162
+ );
163
+ } else if (
164
+ assertionArgument.type === AST_NODE_TYPES.ObjectExpression ||
165
+ assertionArgument.type === AST_NODE_TYPES.CallExpression
166
+ ) {
167
+ // body deep equal assertion
168
+ nonStatusAssertions.push(
169
+ `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
170
+ );
171
+ } else {
172
+ throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
173
+ }
174
+ } else if (expectArguments.length === 2) {
175
+ // header assertion
176
+ const [headerName, headerValue] = expectArguments;
177
+ assert.ok(headerName && headerValue);
178
+ const headersReference =
179
+ destructuringResponseHeadersVariable !== undefined
180
+ ? destructuringResponseHeadersVariable.name
181
+ : `${responseVariableName}.headers`;
182
+ if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) {
183
+ nonStatusAssertions.push(
184
+ `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
185
+ );
186
+ } else {
187
+ nonStatusAssertions.push(
188
+ `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
189
+ );
190
+ }
191
+ }
192
+ }
193
+ return {
194
+ statusAssertion,
195
+ nonStatusAssertions,
196
+ };
197
+ }
198
+
199
+ // eslint-disable-next-line sonarjs/cognitive-complexity
200
+ function getResponseVariableNameToUse(
201
+ fetchFunction: TSESTree.CallExpression,
202
+ fixtureCallInformation: FixtureCallInformation,
203
+ sourceCode: SourceCode,
204
+ scopeManager: ScopeManager,
205
+ scopeVariablesMap: Map<Scope, string[]>,
206
+ ) {
207
+ // use existing variable assignment if it's already defined
208
+ if (fixtureCallInformation.variableAssignment) {
209
+ assert.ok(
210
+ fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression &&
211
+ fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier,
212
+ );
213
+ return fixtureCallInformation.variableAssignment.expression.left.name;
214
+ }
215
+
216
+ // use existing variable declaration if it's already defined
217
+ if (fixtureCallInformation.variableDeclaration) {
218
+ const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0];
219
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
220
+ if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) {
221
+ return firstDeclaration.id.name;
222
+ }
223
+ }
224
+
225
+ // prepare scope variables for checking if the variable name is already used
226
+ const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode);
227
+ assert.ok(enclosingScopeNode);
228
+ const scope = scopeManager.acquire(enclosingScopeNode);
229
+ assert.ok(scope);
230
+ let scopeVariables = scopeVariablesMap.get(scope);
231
+ if (!scopeVariables) {
232
+ scopeVariables = [...scope.set.keys()];
233
+ scopeVariablesMap.set(scope, scopeVariables);
234
+ }
235
+
236
+ let responseVariableNameBase: string | undefined;
237
+ if (fetchFunction.callee.type === AST_NODE_TYPES.Identifier && fetchFunction.callee.name === 'fetch') {
238
+ const [urlArg, initArg] = fetchFunction.arguments;
239
+ if (urlArg?.type === AST_NODE_TYPES.Literal || urlArg?.type === AST_NODE_TYPES.TemplateLiteral) {
240
+ const urlValue = urlArg.type === AST_NODE_TYPES.Literal ? String(urlArg.value) : sourceCode.getText(urlArg);
241
+
242
+ const urlWithoutQuotes = urlValue.replace(/['"`]/gu, '');
243
+ const urlWithoutQuery = urlWithoutQuotes.includes('?')
244
+ ? urlWithoutQuotes.slice(0, urlWithoutQuotes.indexOf('?'))
245
+ : urlWithoutQuotes;
246
+ const parts = urlWithoutQuery.startsWith('${')
247
+ ? urlWithoutQuery.split('/').slice(1)
248
+ : // eslint-disable-next-line no-magic-numbers
249
+ urlWithoutQuery.split('/').slice(3);
250
+
251
+ let methodName;
252
+ if (initArg?.type === AST_NODE_TYPES.ObjectExpression) {
253
+ methodName = /method:\s*['"`](?<method>\w+)['"`]/u.exec(sourceCode.getText(initArg))?.groups?.['method'];
254
+ }
255
+ methodName ??= 'GET';
256
+ responseVariableNameBase = [...parts.filter((part) => part !== 'tenant'), methodName.toLowerCase()]
257
+ .map((part) => part.split(/[-]/u))
258
+ .flat()
259
+ .filter((part) => part.trim() !== '' && !/\$\{.*\}/u.test(part)) // remove path parameter placeholders
260
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
261
+ .join('');
262
+ responseVariableNameBase = `${responseVariableNameBase[0]?.toLowerCase() ?? ''}${responseVariableNameBase.slice(1)}`;
263
+ }
264
+ } else {
265
+ // this should be the case that a reference to utility function is used
266
+ const fullUtilityFunctionReference = sourceCode.getText(fetchFunction.callee);
267
+ responseVariableNameBase = fullUtilityFunctionReference.split('.').pop();
268
+ }
269
+ responseVariableNameBase =
270
+ responseVariableNameBase === undefined ? 'response' : `${responseVariableNameBase}Response`;
271
+
272
+ let responseVariableCounter = 0;
273
+ let responseVariableNameToUse = responseVariableNameBase;
274
+ while (scopeVariables.includes(responseVariableNameToUse)) {
275
+ responseVariableCounter++;
276
+ responseVariableNameToUse = `${responseVariableNameBase}${String(responseVariableCounter)}`;
277
+ }
278
+ scopeVariables.push(responseVariableNameToUse);
279
+ return responseVariableNameToUse;
280
+ }
281
+
282
+ function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean {
283
+ const parent = getParent(responseBodyReference);
284
+ return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier;
285
+ }
286
+
287
+ const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
288
+
289
+ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({
290
+ name: ruleId,
291
+ meta: {
292
+ type: 'suggestion',
293
+ docs: {
294
+ description: 'Transform supertest assersions to regular node assertions.',
295
+ url: getDocumentationUrl(ruleId),
296
+ },
297
+ messages: {
298
+ preferNativeFetch: 'Transform supertest assersions to regular node assertions.',
299
+ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
300
+ },
301
+ fixable: 'code',
302
+ schema: [],
303
+ },
304
+ defaultOptions: [],
305
+ // eslint-disable-next-line max-lines-per-function
306
+ create(context) {
307
+ const sourceCode = context.sourceCode;
308
+ const parserServices = ESLintUtils.getParserServices(context);
309
+ const typeChecker = parserServices.program.getTypeChecker();
310
+ const scopeManager = sourceCode.scopeManager;
311
+ assert.ok(scopeManager !== null);
312
+ const scopeVariablesMap = new Map<Scope, string[]>();
313
+
314
+ return {
315
+ // eslint-disable-next-line max-lines-per-function
316
+ 'CallExpression[callee.property.name="expect"]': (
317
+ expectCall: TSESTree.CallExpression,
318
+ // eslint-disable-next-line sonarjs/cognitive-complexity
319
+ ) => {
320
+ try {
321
+ if (
322
+ expectCall.callee.type !== AST_NODE_TYPES.MemberExpression ||
323
+ expectCall.callee.object.type !== AST_NODE_TYPES.CallExpression
324
+ ) {
325
+ return;
326
+ }
327
+
328
+ // Check if it's a Promise<Response> like object
329
+ const calleeObject = expectCall.callee.object;
330
+ const calleeObjectTsNode = parserServices.esTreeNodeToTSNodeMap.get(calleeObject);
331
+ const calleeObjectType = typeChecker.getTypeAtLocation(calleeObjectTsNode);
332
+ const calleeObjectTypeSymbol = calleeObjectType.getSymbol();
333
+ if (!calleeObjectTypeSymbol || calleeObjectTypeSymbol.name !== 'Promise') {
334
+ return;
335
+ }
336
+ const [calleeObjectPromiseType] = typeChecker.getTypeArguments(calleeObjectType as ts.TypeReference);
337
+ if (calleeObjectPromiseType === undefined || !isFetchResponse(calleeObjectPromiseType)) {
338
+ return;
339
+ }
340
+
341
+ const indentation = getIndentation(expectCall, sourceCode);
342
+
343
+ const fixtureCallInformation = {} as FixtureCallInformation;
344
+ const fetchFunction = expectCall.callee.object;
345
+ analyzeFixtureCall(fetchFunction, fixtureCallInformation, sourceCode);
346
+
347
+ const {
348
+ variable: responseVariable,
349
+ bodyReferences: responseBodyReferences,
350
+ // headersReferences: responseHeadersReferences,
351
+ statusReferences: responseStatusReferences,
352
+ destructuringBodyVariable: destructuringResponseBodyVariable,
353
+ destructuringHeadersVariable: destructuringResponseHeadersVariable,
354
+ destructuringStatusVariable: destructuringResponseStatusVariable,
355
+ } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager);
356
+
357
+ const shouldUsePromiseThen =
358
+ isUsedInArrayOrAsArgument(expectCall) || getEnclosingFunction(expectCall)?.async === false;
359
+ if (shouldUsePromiseThen) {
360
+ const responseVariableNameToUse = 'res';
361
+ const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
362
+ fixtureCallInformation,
363
+ sourceCode,
364
+ responseVariableNameToUse,
365
+ destructuringResponseHeadersVariable as Variable | undefined,
366
+ );
367
+ const fetchCallText = sourceCode.getText(fetchFunction);
368
+ const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method';
369
+ const appendingAssignmentAndAssertionText = [
370
+ ...(statusAssertion !== undefined ? [statusAssertion] : []),
371
+ ...nonStatusAssertions,
372
+ ].join(`;\n${indentation}`);
373
+ const replacementText = fixtureCallInformation.assertions
374
+ ? [
375
+ disableLintComment,
376
+ `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
377
+ appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
378
+ ` return ${responseVariableNameToUse};`,
379
+ `})`,
380
+ ].join(`\n${indentation}`)
381
+ : fetchCallText;
382
+ context.report({
383
+ node: fixtureCallInformation.rootNode,
384
+ messageId: 'preferNativeFetch',
385
+ fix(fixer) {
386
+ return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
387
+ },
388
+ });
389
+ } else {
390
+ const responseVariableNameToUse = getResponseVariableNameToUse(
391
+ fetchFunction,
392
+ fixtureCallInformation,
393
+ sourceCode,
394
+ scopeManager,
395
+ scopeVariablesMap,
396
+ );
397
+
398
+ const isResponseBodyVariableRedefinitionNeeded =
399
+ destructuringResponseBodyVariable !== undefined ||
400
+ fixtureCallInformation.inlineBodyReference !== undefined ||
401
+ (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
402
+ const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
403
+
404
+ const isResponseStatusVariableRedefinitionNeeded =
405
+ destructuringResponseStatusVariable !== undefined ||
406
+ fixtureCallInformation.inlineStatusReference !== undefined;
407
+ const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`;
408
+
409
+ const isResponseHeadersVariableRedefinitionNeeded =
410
+ (destructuringResponseHeadersVariable !== undefined &&
411
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
412
+ (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type ===
413
+ AST_NODE_TYPES.ObjectPattern) ||
414
+ fixtureCallInformation.inlineHeadersReference !== undefined;
415
+ const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`;
416
+
417
+ const isResponseVariableRedefinitionNeeded =
418
+ (fixtureCallInformation.variableAssignment === undefined &&
419
+ responseVariable === undefined &&
420
+ fixtureCallInformation.assertions !== undefined) ||
421
+ isResponseBodyVariableRedefinitionNeeded ||
422
+ isResponseStatusVariableRedefinitionNeeded ||
423
+ isResponseHeadersVariableRedefinitionNeeded;
424
+
425
+ const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
426
+ ? [
427
+ // eslint-disable-next-line no-nested-ternary
428
+ ...(destructuringResponseBodyVariable
429
+ ? [
430
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
431
+ `${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)}`,
432
+ ]
433
+ : isResponseBodyVariableRedefinitionNeeded
434
+ ? [
435
+ `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
436
+ ]
437
+ : []),
438
+ // eslint-disable-next-line no-nested-ternary
439
+ ...(destructuringResponseStatusVariable
440
+ ? [
441
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
442
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`,
443
+ ]
444
+ : isResponseStatusVariableRedefinitionNeeded
445
+ ? [
446
+ `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`,
447
+ ]
448
+ : []),
449
+ // eslint-disable-next-line no-nested-ternary
450
+ ...(destructuringResponseHeadersVariable
451
+ ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
452
+ (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type ===
453
+ AST_NODE_TYPES.ObjectPattern
454
+ ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => {
455
+ assert.ok(property.type === AST_NODE_TYPES.Property);
456
+ assert.ok(property.value.type === AST_NODE_TYPES.Identifier);
457
+ // eslint-disable-next-line sonarjs/no-nested-template-literals
458
+ 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)}'`})`;
459
+ })
460
+ : [
461
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
462
+ ]
463
+ : isResponseHeadersVariableRedefinitionNeeded
464
+ ? [
465
+ `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
466
+ ]
467
+ : []),
468
+ ]
469
+ : [];
470
+
471
+ const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
472
+ fixtureCallInformation,
473
+ sourceCode,
474
+ responseVariableNameToUse,
475
+ destructuringResponseHeadersVariable as Variable | undefined,
476
+ );
477
+
478
+ // add variable declaration if needed
479
+ const fetchCallText = sourceCode.getText(fetchFunction);
480
+ const fetchStatementText = !isResponseVariableRedefinitionNeeded
481
+ ? fetchCallText
482
+ : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`;
483
+
484
+ const nodeToReplace = isResponseVariableRedefinitionNeeded
485
+ ? fixtureCallInformation.rootNode
486
+ : fixtureCallInformation.fixtureNode;
487
+ const appendingAssignmentAndAssertionText = [
488
+ '',
489
+ ...(statusAssertion !== undefined ? [statusAssertion] : []),
490
+ ...responseBodyHeadersVariableRedefineLines,
491
+ ...nonStatusAssertions,
492
+ ].join(`;\n${indentation}`);
493
+
494
+ context.report({
495
+ node: expectCall,
496
+ messageId: 'preferNativeFetch',
497
+
498
+ *fix(fixer) {
499
+ if (fixtureCallInformation.inlineStatementNode) {
500
+ const preInlineDeclaration = [
501
+ fetchStatementText,
502
+ `${appendingAssignmentAndAssertionText};\n${indentation}`,
503
+ ].join(``);
504
+ yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration);
505
+ } else {
506
+ yield fixer.replaceText(nodeToReplace, fetchStatementText);
507
+
508
+ const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
509
+ yield fixer.insertTextAfter(
510
+ nodeToReplace,
511
+ needEndingSemiColon
512
+ ? `${appendingAssignmentAndAssertionText};`
513
+ : appendingAssignmentAndAssertionText,
514
+ );
515
+ }
516
+
517
+ // handle response body references
518
+ for (const responseBodyReference of responseBodyReferences) {
519
+ yield fixer.replaceText(
520
+ responseBodyReference,
521
+ isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference)
522
+ ? redefineResponseBodyVariableName
523
+ : getResponseBodyRetrievalText(responseVariableNameToUse),
524
+ );
525
+ }
526
+ if (fixtureCallInformation.inlineBodyReference) {
527
+ yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName);
528
+ }
529
+
530
+ // convert response.statusCode to response.status
531
+ for (const responseStatusReference of responseStatusReferences) {
532
+ if (
533
+ responseStatusReference.property.type === AST_NODE_TYPES.Identifier &&
534
+ responseStatusReference.property.name === 'statusCode'
535
+ ) {
536
+ yield fixer.replaceText(responseStatusReference.property, `status`);
537
+ }
538
+ }
539
+
540
+ // handle direct return statement without await, e.g. "return fixture.api.get(...);"
541
+ if (
542
+ fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement &&
543
+ fixtureCallInformation.assertions !== undefined
544
+ ) {
545
+ yield fixer.insertTextAfter(
546
+ fixtureCallInformation.rootNode,
547
+ `\n${indentation}return ${responseVariableNameToUse};`,
548
+ );
549
+ }
550
+ },
551
+ });
552
+ }
553
+ } catch (error) {
554
+ // eslint-disable-next-line no-console
555
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
556
+ context.report({
557
+ node: expectCall,
558
+ messageId: 'unknownError',
559
+ data: {
560
+ fileName: context.filename,
561
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
562
+ },
563
+ });
564
+ }
565
+ },
566
+ };
567
+ },
568
+ });
569
+
570
+ export default rule;