@checkdigit/eslint-plugin 7.6.0-PR.75-7ee9 → 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.
- package/dist-mjs/agent/fetch-response-body-json.mjs +1 -1
- package/dist-mjs/agent/fetch-then.mjs +4 -72
- package/dist-mjs/agent/fetch.mjs +2 -2
- package/dist-mjs/agent/no-expect-assertion.mjs +394 -0
- package/dist-mjs/agent/no-fixture.mjs +34 -280
- package/dist-mjs/agent/response-reference.mjs +3 -6
- package/dist-mjs/index.mjs +15 -17
- package/dist-types/agent/fetch-then.d.ts +1 -1
- package/dist-types/agent/{no-supertest.d.ts → no-expect-assertion.d.ts} +1 -1
- package/dist-types/agent/response-reference.d.ts +0 -1
- package/package.json +1 -1
- package/src/agent/fetch-response-body-json.ts +1 -1
- package/src/agent/fetch-then.ts +87 -88
- package/src/agent/fetch.ts +4 -2
- package/src/agent/no-expect-assertion.ts +570 -0
- package/src/agent/no-fixture.ts +46 -424
- package/src/agent/response-reference.ts +11 -11
- package/src/index.ts +16 -16
- package/dist-mjs/agent/no-supertest.mjs +0 -346
- package/src/agent/no-supertest.ts +0 -517
|
@@ -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;
|