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