@checkdigit/eslint-plugin 6.6.0-PR.75-e80b → 6.6.0-PR.75-b2d2
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-cjs/index.cjs +208 -160
- package/dist-cjs/metafile.json +40 -3
- package/dist-mjs/ast/format.mjs +13 -0
- package/dist-mjs/ast/tree.mjs +20 -0
- package/dist-mjs/no-fixture.mjs +172 -148
- package/dist-types/ast/format.d.ts +3 -0
- package/dist-types/ast/tree.d.ts +3 -0
- package/package.json +1 -1
- package/src/ast/format.ts +19 -0
- package/src/ast/tree.ts +31 -0
- package/src/no-fixture.ts +269 -225
package/src/no-fixture.ts
CHANGED
|
@@ -10,55 +10,66 @@
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
AwaitExpression,
|
|
13
|
+
CallExpression,
|
|
13
14
|
Expression,
|
|
14
15
|
MemberExpression,
|
|
15
16
|
Node,
|
|
16
17
|
ReturnStatement,
|
|
17
18
|
SimpleCallExpression,
|
|
19
|
+
VariableDeclaration,
|
|
18
20
|
} from 'estree';
|
|
19
21
|
import type { Rule, Scope, SourceCode } from 'eslint';
|
|
22
|
+
import { getAncestor, getParent } from './ast/tree';
|
|
20
23
|
import { strict as assert } from 'node:assert';
|
|
21
24
|
import getDocumentationUrl from './get-documentation-url';
|
|
25
|
+
import { getIndentation } from './ast/format';
|
|
22
26
|
|
|
23
27
|
export const ruleId = 'no-fixture';
|
|
24
28
|
|
|
25
|
-
type NodeParent = Node | undefined | null;
|
|
26
|
-
|
|
27
|
-
interface NodeParentExtension {
|
|
28
|
-
parent: NodeParent;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
29
|
interface FixtureCallInformation {
|
|
32
|
-
|
|
30
|
+
rootNode: AwaitExpression | ReturnStatement | VariableDeclaration;
|
|
31
|
+
fixtureNode: AwaitExpression | SimpleCallExpression;
|
|
32
|
+
variableDeclaration?: VariableDeclaration;
|
|
33
33
|
requestBody?: Expression;
|
|
34
34
|
requestHeaders?: { name: Expression; value: Expression }[];
|
|
35
35
|
assertions?: Expression[][];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function analyze(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
38
|
+
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
39
|
+
function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
43
40
|
const parent = getParent(call);
|
|
44
41
|
assert.ok(parent, 'parent should exist for fixture/supertest call node');
|
|
45
42
|
|
|
46
43
|
let nextCall;
|
|
47
|
-
if (parent.type === '
|
|
48
|
-
//
|
|
49
|
-
results.
|
|
44
|
+
if (parent.type === 'ReturnStatement') {
|
|
45
|
+
// direct return, no variable declaration / await
|
|
46
|
+
results.fixtureNode = call;
|
|
47
|
+
results.rootNode = parent;
|
|
48
|
+
} else if (parent.type === 'AwaitExpression') {
|
|
49
|
+
results.fixtureNode = call;
|
|
50
|
+
// [TODO:] should we consider variable declaration without await??
|
|
51
|
+
const variableDeclaration = getAncestor(parent, 'VariableDeclaration', 'FunctionDeclaration');
|
|
52
|
+
if (variableDeclaration?.type === 'VariableDeclaration') {
|
|
53
|
+
results.variableDeclaration = variableDeclaration;
|
|
54
|
+
results.rootNode = variableDeclaration;
|
|
55
|
+
} else {
|
|
56
|
+
results.rootNode = parent;
|
|
57
|
+
}
|
|
50
58
|
} else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') {
|
|
51
59
|
if (parent.property.name === 'expect') {
|
|
60
|
+
// supertest assertions
|
|
52
61
|
const assertionCall = getParent(parent);
|
|
53
62
|
assert.ok(assertionCall && assertionCall.type === 'CallExpression');
|
|
54
63
|
results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
|
|
55
64
|
nextCall = assertionCall;
|
|
56
65
|
} else if (parent.property.name === 'send') {
|
|
66
|
+
// request body
|
|
57
67
|
const sendRequestBodyCall = getParent(parent);
|
|
58
68
|
assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression');
|
|
59
69
|
results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
|
|
60
70
|
nextCall = sendRequestBodyCall;
|
|
61
71
|
} else if (parent.property.name === 'set') {
|
|
72
|
+
// request headers
|
|
62
73
|
const setRequestHeaderCall = getParent(parent);
|
|
63
74
|
assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression');
|
|
64
75
|
const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
|
|
@@ -69,10 +80,91 @@ function analyze(call: SimpleCallExpression, results: FixtureCallInformation) {
|
|
|
69
80
|
throw new Error(`Unexpected expression in fixture/supertest call ${String(parent)}`);
|
|
70
81
|
}
|
|
71
82
|
if (nextCall) {
|
|
72
|
-
|
|
83
|
+
analyzeFixtureCall(nextCall, results);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// analyze response related variables and their references0
|
|
88
|
+
function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) {
|
|
89
|
+
const results: {
|
|
90
|
+
variable?: Scope.Variable;
|
|
91
|
+
bodyReferences: MemberExpression[];
|
|
92
|
+
headersReferences: MemberExpression[];
|
|
93
|
+
statusReferences: MemberExpression[];
|
|
94
|
+
spreadBodyVariable?: Scope.Variable;
|
|
95
|
+
spreadHeadersVariable?: Scope.Variable;
|
|
96
|
+
} = {
|
|
97
|
+
bodyReferences: [],
|
|
98
|
+
headersReferences: [],
|
|
99
|
+
statusReferences: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (fixtureInformation.variableDeclaration) {
|
|
103
|
+
const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration);
|
|
104
|
+
for (const responseVariable of responseVariables) {
|
|
105
|
+
const identifier = responseVariable.identifiers[0];
|
|
106
|
+
assert.ok(identifier);
|
|
107
|
+
const identifierParent = getParent(identifier);
|
|
108
|
+
assert.ok(identifierParent);
|
|
109
|
+
if (identifierParent.type === 'VariableDeclarator') {
|
|
110
|
+
// e.g. const response = ...
|
|
111
|
+
results.variable = responseVariable;
|
|
112
|
+
// e.g. response.body
|
|
113
|
+
results.bodyReferences = responseVariable.references
|
|
114
|
+
.map((responseBodyReference) => getParent(responseBodyReference.identifier))
|
|
115
|
+
.filter(
|
|
116
|
+
(node): node is MemberExpression =>
|
|
117
|
+
node !== null &&
|
|
118
|
+
node !== undefined &&
|
|
119
|
+
node.type === 'MemberExpression' &&
|
|
120
|
+
node.property.type === 'Identifier' &&
|
|
121
|
+
node.property.name === 'body',
|
|
122
|
+
);
|
|
123
|
+
// e.g. response.headers / response.header / response.get()
|
|
124
|
+
results.headersReferences = responseVariable.references
|
|
125
|
+
.map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
|
|
126
|
+
.filter(
|
|
127
|
+
(node): node is MemberExpression =>
|
|
128
|
+
node !== null &&
|
|
129
|
+
node !== undefined &&
|
|
130
|
+
node.type === 'MemberExpression' &&
|
|
131
|
+
node.property.type === 'Identifier' &&
|
|
132
|
+
(node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
|
|
133
|
+
);
|
|
134
|
+
// e.g. response.status / response.statusCode
|
|
135
|
+
results.statusReferences = responseVariable.references
|
|
136
|
+
.map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
|
|
137
|
+
.filter(
|
|
138
|
+
(node): node is MemberExpression =>
|
|
139
|
+
node !== null &&
|
|
140
|
+
node !== undefined &&
|
|
141
|
+
node.type === 'MemberExpression' &&
|
|
142
|
+
node.property.type === 'Identifier' &&
|
|
143
|
+
(node.property.name === 'status' || node.property.name === 'statusCode'),
|
|
144
|
+
);
|
|
145
|
+
} else if (
|
|
146
|
+
// body reference through destruction/renaming, e.g. "const { body } = ..."
|
|
147
|
+
identifierParent.type === 'Property' &&
|
|
148
|
+
identifierParent.key.type === 'Identifier' &&
|
|
149
|
+
identifierParent.key.name === 'body'
|
|
150
|
+
) {
|
|
151
|
+
results.spreadBodyVariable = responseVariable;
|
|
152
|
+
} else if (
|
|
153
|
+
// header reference through destruction/renaming, e.g. "const { headers } = ..."
|
|
154
|
+
identifierParent.type === 'Property' &&
|
|
155
|
+
identifierParent.key.type === 'Identifier' &&
|
|
156
|
+
identifierParent.key.name === 'headers'
|
|
157
|
+
) {
|
|
158
|
+
results.spreadHeadersVariable = responseVariable;
|
|
159
|
+
} else {
|
|
160
|
+
throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
73
163
|
}
|
|
164
|
+
return results;
|
|
74
165
|
}
|
|
75
166
|
|
|
167
|
+
// `/sample-service/v1/ping` -> `${BASE_PATH}/ping`
|
|
76
168
|
function replaceEndpointUrlPrefixWithBasePath(url: string) {
|
|
77
169
|
// eslint-disable-next-line no-template-curly-in-string
|
|
78
170
|
return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
|
|
@@ -82,9 +174,15 @@ function isValidPropertyName(name: unknown) {
|
|
|
82
174
|
return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
|
|
83
175
|
}
|
|
84
176
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
177
|
+
function createResponseAssertions(
|
|
178
|
+
fixtureCallInformation: FixtureCallInformation,
|
|
179
|
+
sourceCode: SourceCode,
|
|
180
|
+
variableName: string,
|
|
181
|
+
) {
|
|
182
|
+
// [TODO:] make sure status assertion is ordered as the first
|
|
183
|
+
let statusAssertion: string | undefined;
|
|
184
|
+
const nonStatusAssertions: string[] = [];
|
|
185
|
+
for (const expectArguments of fixtureCallInformation.assertions ?? []) {
|
|
88
186
|
if (expectArguments.length === 1) {
|
|
89
187
|
const [assertionArgument] = expectArguments;
|
|
90
188
|
assert.ok(assertionArgument);
|
|
@@ -95,16 +193,18 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia
|
|
|
95
193
|
assertionArgument.type === 'Literal'
|
|
96
194
|
) {
|
|
97
195
|
// status code assertion
|
|
98
|
-
|
|
196
|
+
statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`;
|
|
99
197
|
} else if (assertionArgument.type === 'ArrowFunctionExpression') {
|
|
100
198
|
// callback assertion
|
|
101
|
-
|
|
199
|
+
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
|
|
102
200
|
} else if (assertionArgument.type === 'Identifier') {
|
|
103
201
|
// callback assertion
|
|
104
|
-
|
|
202
|
+
nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
|
|
105
203
|
} else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
|
|
106
204
|
// body deep equal assertion
|
|
107
|
-
|
|
205
|
+
nonStatusAssertions.push(
|
|
206
|
+
`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`,
|
|
207
|
+
);
|
|
108
208
|
} else {
|
|
109
209
|
throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
|
|
110
210
|
}
|
|
@@ -113,96 +213,59 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia
|
|
|
113
213
|
const [headerName, headerValue] = expectArguments;
|
|
114
214
|
assert.ok(headerName && headerValue);
|
|
115
215
|
if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
|
|
116
|
-
|
|
216
|
+
nonStatusAssertions.push(
|
|
117
217
|
`assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
|
|
118
218
|
);
|
|
119
219
|
} else {
|
|
120
|
-
|
|
220
|
+
nonStatusAssertions.push(
|
|
121
221
|
`assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
|
|
122
222
|
);
|
|
123
223
|
}
|
|
124
224
|
}
|
|
125
225
|
}
|
|
126
|
-
return
|
|
226
|
+
return {
|
|
227
|
+
statusAssertion,
|
|
228
|
+
nonStatusAssertions,
|
|
229
|
+
};
|
|
127
230
|
}
|
|
128
231
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
232
|
+
function getResponseVariableNameToUse(
|
|
233
|
+
scopeManager: Scope.ScopeManager,
|
|
234
|
+
fixtureCallInformation: FixtureCallInformation,
|
|
235
|
+
scopeVariablesMap: Map<Scope.Scope, string[]>,
|
|
236
|
+
) {
|
|
237
|
+
if (fixtureCallInformation.variableDeclaration) {
|
|
238
|
+
const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0];
|
|
239
|
+
// [TODO:] double check if it works for destruction/rename declaration
|
|
240
|
+
if (firstDeclaration && firstDeclaration.id.type === 'Identifier') {
|
|
241
|
+
return firstDeclaration.id.name;
|
|
242
|
+
}
|
|
135
243
|
}
|
|
136
|
-
return getAncestor(parent, matchType, quitType);
|
|
137
|
-
}
|
|
138
244
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
245
|
+
const closestFunctionExpression = getAncestor(fixtureCallInformation.rootNode, (node: Node) =>
|
|
246
|
+
['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type),
|
|
247
|
+
);
|
|
248
|
+
scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); /*?*/
|
|
249
|
+
assert.ok(closestFunctionExpression);
|
|
250
|
+
const scope = scopeManager.acquire(closestFunctionExpression);
|
|
251
|
+
assert.ok(scope !== null);
|
|
252
|
+
let scopeVariables = scopeVariablesMap.get(scope);
|
|
253
|
+
if (!scopeVariables) {
|
|
254
|
+
scopeVariables = [...scope.set.keys()];
|
|
255
|
+
scopeVariablesMap.set(scope, scopeVariables);
|
|
256
|
+
}
|
|
150
257
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
assert.ok(identifierParent);
|
|
159
|
-
if (identifierParent.type === 'VariableDeclarator') {
|
|
160
|
-
// if (declarator.id.type === 'Identifier') {
|
|
161
|
-
results.responseVariable = responseVariable;
|
|
162
|
-
results.responseBodyReferences = responseVariable.references
|
|
163
|
-
.map((responseBodyReference) => getParent(responseBodyReference.identifier))
|
|
164
|
-
.filter(
|
|
165
|
-
(node): node is MemberExpression =>
|
|
166
|
-
node !== null &&
|
|
167
|
-
node !== undefined &&
|
|
168
|
-
node.type === 'MemberExpression' &&
|
|
169
|
-
node.property.type === 'Identifier' &&
|
|
170
|
-
node.property.name === 'body',
|
|
171
|
-
);
|
|
172
|
-
results.responseHeadersReferences = responseVariable.references
|
|
173
|
-
.map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
|
|
174
|
-
.filter(
|
|
175
|
-
(node): node is MemberExpression =>
|
|
176
|
-
node !== null &&
|
|
177
|
-
node !== undefined &&
|
|
178
|
-
node.type === 'MemberExpression' &&
|
|
179
|
-
node.property.type === 'Identifier' &&
|
|
180
|
-
(node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
|
|
181
|
-
);
|
|
182
|
-
} else if (
|
|
183
|
-
identifierParent.type === 'Property' &&
|
|
184
|
-
identifierParent.key.type === 'Identifier' &&
|
|
185
|
-
identifierParent.key.name === 'body'
|
|
186
|
-
) {
|
|
187
|
-
results.spreadResponseBodyVariable = responseVariable;
|
|
188
|
-
} else if (
|
|
189
|
-
identifierParent.type === 'Property' &&
|
|
190
|
-
identifierParent.key.type === 'Identifier' &&
|
|
191
|
-
identifierParent.key.name === 'headers'
|
|
192
|
-
) {
|
|
193
|
-
results.spreadResponseHeadersVariable = responseVariable;
|
|
194
|
-
}
|
|
258
|
+
let responseVariableCounter = 0;
|
|
259
|
+
let responseVariableNameToUse;
|
|
260
|
+
while (responseVariableNameToUse === undefined) {
|
|
261
|
+
responseVariableCounter++;
|
|
262
|
+
responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`;
|
|
263
|
+
if (scopeVariables.includes(responseVariableNameToUse)) {
|
|
264
|
+
responseVariableNameToUse = undefined;
|
|
195
265
|
}
|
|
196
266
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
function getIndentation(node: Node, sourceCode: SourceCode) {
|
|
201
|
-
assert.ok(node.loc);
|
|
202
|
-
const line = sourceCode.lines[node.loc.start.line - 1];
|
|
203
|
-
assert.ok(line);
|
|
204
|
-
const indentMatch = line.match(/^\s*/u);
|
|
205
|
-
return indentMatch ? indentMatch[0] : '';
|
|
267
|
+
scopeVariables.push(responseVariableNameToUse);
|
|
268
|
+
return responseVariableNameToUse;
|
|
206
269
|
}
|
|
207
270
|
|
|
208
271
|
const rule: Rule.RuleModule = {
|
|
@@ -223,136 +286,121 @@ const rule: Rule.RuleModule = {
|
|
|
223
286
|
create(context) {
|
|
224
287
|
const sourceCode = context.sourceCode;
|
|
225
288
|
const scopeManager = sourceCode.scopeManager;
|
|
226
|
-
|
|
289
|
+
const scopeVariablesMap = new Map<Scope.Scope, string[]>();
|
|
227
290
|
|
|
228
291
|
return {
|
|
229
|
-
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
292
|
+
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
293
|
+
fixtureCall: CallExpression,
|
|
294
|
+
) => {
|
|
230
295
|
try {
|
|
231
296
|
assert.ok(fixtureCall.type === 'CallExpression');
|
|
232
|
-
const fixtureFunction = fixtureCall.callee; //
|
|
297
|
+
const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
|
|
233
298
|
assert.ok(fixtureFunction.type === 'MemberExpression');
|
|
234
|
-
const methodNode = fixtureFunction.property; // get/put/etc.
|
|
235
|
-
assert.ok(methodNode.type === 'Identifier');
|
|
236
299
|
const indentation = getIndentation(fixtureCall, sourceCode);
|
|
237
300
|
|
|
238
|
-
const [urlArgumentNode] = fixtureCall.arguments; //
|
|
301
|
+
const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
|
|
239
302
|
assert.ok(urlArgumentNode !== undefined);
|
|
240
303
|
|
|
241
304
|
const fixtureCallInformation = {} as FixtureCallInformation;
|
|
242
|
-
|
|
305
|
+
analyzeFixtureCall(fixtureCall, fixtureCallInformation);
|
|
243
306
|
|
|
244
307
|
const {
|
|
245
|
-
responseVariable,
|
|
246
|
-
responseBodyReferences,
|
|
247
|
-
responseHeadersReferences,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
fixtureCallInformation.
|
|
267
|
-
?
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
...(fixtureCallInformation.requestHeaders
|
|
288
|
-
? [
|
|
289
|
-
` headers: {`,
|
|
290
|
-
...fixtureCallInformation.requestHeaders.map(
|
|
291
|
-
({ name, value }) =>
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
|
|
293
|
-
` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
|
|
294
|
-
),
|
|
295
|
-
` },`,
|
|
296
|
-
]
|
|
297
|
-
: []),
|
|
298
|
-
'}',
|
|
299
|
-
].join(`\n${indentation}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
replacedText = replacedText.replace(fixtureArgumentText, fetchArgumentText);
|
|
303
|
-
|
|
304
|
-
const needVariableRedefine =
|
|
308
|
+
variable: responseVariable,
|
|
309
|
+
bodyReferences: responseBodyReferences,
|
|
310
|
+
headersReferences: responseHeadersReferences,
|
|
311
|
+
statusReferences: responseStatusReferences,
|
|
312
|
+
spreadBodyVariable: spreadResponseBodyVariable,
|
|
313
|
+
spreadHeadersVariable: spreadResponseHeadersVariable,
|
|
314
|
+
} = analyzeResponseReferences(fixtureCallInformation, scopeManager);
|
|
315
|
+
|
|
316
|
+
// convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
|
|
317
|
+
const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
|
|
318
|
+
const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
|
|
319
|
+
|
|
320
|
+
// fetch request argument
|
|
321
|
+
const methodNode = fixtureFunction.property; // get/put/etc.
|
|
322
|
+
assert.ok(methodNode.type === 'Identifier');
|
|
323
|
+
const fetchRequestArgumentLines = [
|
|
324
|
+
'{',
|
|
325
|
+
` method: '${methodNode.name.toUpperCase()}',`,
|
|
326
|
+
...(fixtureCallInformation.requestBody
|
|
327
|
+
? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
|
|
328
|
+
: []),
|
|
329
|
+
...(fixtureCallInformation.requestHeaders
|
|
330
|
+
? [
|
|
331
|
+
` headers: {`,
|
|
332
|
+
...fixtureCallInformation.requestHeaders.map(
|
|
333
|
+
({ name, value }) =>
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
|
|
335
|
+
` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
|
|
336
|
+
),
|
|
337
|
+
` },`,
|
|
338
|
+
]
|
|
339
|
+
: []),
|
|
340
|
+
'}',
|
|
341
|
+
].join(`\n${indentation}`);
|
|
342
|
+
|
|
343
|
+
const responseVariableNameToUse = getResponseVariableNameToUse(
|
|
344
|
+
scopeManager,
|
|
345
|
+
fixtureCallInformation,
|
|
346
|
+
scopeVariablesMap,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const needResponseVariableRedefine =
|
|
305
350
|
spreadResponseBodyVariable !== undefined ||
|
|
306
|
-
(responseVariable === undefined && fixtureCallInformation.assertions
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
351
|
+
(responseVariable === undefined && fixtureCallInformation.assertions !== undefined);
|
|
352
|
+
|
|
353
|
+
const responseBodyHeadersVariableRedefineLines = needResponseVariableRedefine
|
|
354
|
+
? [
|
|
355
|
+
...(spreadResponseBodyVariable
|
|
356
|
+
? [`const ${spreadResponseBodyVariable.name} = await ${responseVariableNameToUse}.json()`]
|
|
357
|
+
: []),
|
|
358
|
+
...(spreadResponseHeadersVariable
|
|
359
|
+
? [`const ${spreadResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`]
|
|
360
|
+
: []),
|
|
361
|
+
]
|
|
362
|
+
: [];
|
|
363
|
+
|
|
364
|
+
const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
|
|
365
|
+
fixtureCallInformation,
|
|
366
|
+
sourceCode,
|
|
367
|
+
responseVariableNameToUse,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// add variable declaration if needed
|
|
371
|
+
const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
|
|
372
|
+
const fetchStatementText = !needResponseVariableRedefine
|
|
373
|
+
? fetchCallText
|
|
374
|
+
: `const ${responseVariableNameToUse} = await ${fetchCallText}`;
|
|
375
|
+
|
|
376
|
+
const nodeToReplace = needResponseVariableRedefine
|
|
377
|
+
? fixtureCallInformation.rootNode
|
|
378
|
+
: fixtureCallInformation.fixtureNode;
|
|
379
|
+
const appendingAssignmentAndAssertionText = [
|
|
380
|
+
'',
|
|
381
|
+
...(statusAssertion !== undefined ? [statusAssertion] : []),
|
|
382
|
+
...responseBodyHeadersVariableRedefineLines,
|
|
383
|
+
...nonStatusAssertions,
|
|
384
|
+
].join(`;\n${indentation}`);
|
|
331
385
|
|
|
332
386
|
context.report({
|
|
333
387
|
node: fixtureCall,
|
|
334
388
|
messageId: 'preferNativeFetch',
|
|
335
389
|
*fix(fixer) {
|
|
336
|
-
|
|
337
|
-
if (spreadResponseBodyVariable) {
|
|
338
|
-
const identifier = spreadResponseBodyVariable.identifiers[0];
|
|
339
|
-
assert.ok(identifier);
|
|
340
|
-
const variableDeclaration = getAncestor(identifier, 'VariableDeclaration', 'FunctionDeclaration');
|
|
341
|
-
assert.ok(variableDeclaration);
|
|
342
|
-
replacementRootNode = variableDeclaration;
|
|
343
|
-
} else if (fixtureCallInformation.assertions !== undefined && responseVariable === undefined) {
|
|
344
|
-
replacementRootNode =
|
|
345
|
-
getAncestor(fixtureCallInformation.root, 'VariableDeclaration', 'FunctionDeclaration') ??
|
|
346
|
-
fixtureCallInformation.root;
|
|
347
|
-
}
|
|
348
|
-
yield fixer.replaceText(replacementRootNode, replacedText);
|
|
390
|
+
yield fixer.replaceText(nodeToReplace, fetchStatementText);
|
|
349
391
|
|
|
350
|
-
|
|
392
|
+
const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
|
|
393
|
+
yield fixer.insertTextAfter(
|
|
394
|
+
nodeToReplace,
|
|
395
|
+
needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// handle response body references
|
|
351
399
|
for (const responseBodyReference of responseBodyReferences) {
|
|
352
|
-
yield fixer.replaceText(responseBodyReference, `await ${
|
|
400
|
+
yield fixer.replaceText(responseBodyReference, `await ${responseVariableNameToUse}.json()`);
|
|
353
401
|
}
|
|
354
402
|
|
|
355
|
-
// handle response headers
|
|
403
|
+
// handle response headers references
|
|
356
404
|
for (const responseHeadersReference of responseHeadersReferences) {
|
|
357
405
|
const parent = getParent(responseHeadersReference);
|
|
358
406
|
assert.ok(parent);
|
|
@@ -367,34 +415,29 @@ const rule: Rule.RuleModule = {
|
|
|
367
415
|
headerName = sourceCode.getText(headerNameNode);
|
|
368
416
|
}
|
|
369
417
|
assert.ok(headerName);
|
|
370
|
-
yield fixer.replaceText(parent, `${
|
|
418
|
+
yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// convert response.statusCode to response.status
|
|
422
|
+
for (const responseStatusReference of responseStatusReferences) {
|
|
423
|
+
if (
|
|
424
|
+
responseStatusReference.property.type === 'Identifier' &&
|
|
425
|
+
responseStatusReference.property.name === 'statusCode'
|
|
426
|
+
) {
|
|
427
|
+
yield fixer.replaceText(responseStatusReference.property, `status`);
|
|
428
|
+
}
|
|
371
429
|
}
|
|
372
430
|
|
|
373
431
|
// handle direct return without await
|
|
374
432
|
if (
|
|
375
|
-
fixtureCallInformation.
|
|
433
|
+
fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
|
|
376
434
|
fixtureCallInformation.assertions !== undefined
|
|
377
435
|
) {
|
|
378
436
|
yield fixer.insertTextAfter(
|
|
379
|
-
fixtureCallInformation.
|
|
380
|
-
|
|
437
|
+
fixtureCallInformation.rootNode,
|
|
438
|
+
`\n${indentation}return ${responseVariableNameToUse};`,
|
|
381
439
|
);
|
|
382
440
|
}
|
|
383
|
-
|
|
384
|
-
// convert statusCode to status
|
|
385
|
-
function* statusCodeReplacer(reference: Scope.Reference) {
|
|
386
|
-
const parent = getParent(reference.identifier);
|
|
387
|
-
if (
|
|
388
|
-
parent?.type === 'MemberExpression' &&
|
|
389
|
-
parent.property.type === 'Identifier' &&
|
|
390
|
-
parent.property.name === 'statusCode'
|
|
391
|
-
) {
|
|
392
|
-
yield fixer.replaceText(parent.property, 'status');
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
for (const reference of responseVariable?.references ?? []) {
|
|
396
|
-
yield* statusCodeReplacer(reference);
|
|
397
|
-
}
|
|
398
441
|
},
|
|
399
442
|
});
|
|
400
443
|
} catch (error) {
|
|
@@ -411,4 +454,5 @@ const rule: Rule.RuleModule = {
|
|
|
411
454
|
};
|
|
412
455
|
},
|
|
413
456
|
};
|
|
457
|
+
|
|
414
458
|
export default rule;
|