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