@checkdigit/eslint-plugin 6.6.0-PR.75-e80b → 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/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
- root: AwaitExpression | ReturnStatement;
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
- function getParent(node: Node): Node | undefined | null {
39
- return (node as unknown as NodeParentExtension).parent;
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 === 'AwaitExpression' || parent.type === 'ReturnStatement') {
48
- // no more assertions, return the await expression of the fixture call
49
- results.root = parent;
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,97 +79,37 @@ 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
- analyze(nextCall, results);
73
- }
74
- }
75
-
76
- function replaceEndpointUrlPrefixWithBasePath(url: string) {
77
- // eslint-disable-next-line no-template-curly-in-string
78
- return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
79
- }
80
-
81
- function isValidPropertyName(name: unknown) {
82
- return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
83
- }
84
-
85
- function appendAssertions(expects: Expression[][], sourceCode: SourceCode, variableName: string) {
86
- const assertions: string[] = [];
87
- for (const expectArguments of expects) {
88
- if (expectArguments.length === 1) {
89
- const [assertionArgument] = expectArguments;
90
- assert.ok(assertionArgument);
91
- if (
92
- (assertionArgument.type === 'MemberExpression' &&
93
- assertionArgument.object.type === 'Identifier' &&
94
- assertionArgument.object.name === 'StatusCodes') ||
95
- assertionArgument.type === 'Literal'
96
- ) {
97
- // status code assertion
98
- assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`);
99
- } else if (assertionArgument.type === 'ArrowFunctionExpression') {
100
- // callback assertion
101
- assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
102
- } else if (assertionArgument.type === 'Identifier') {
103
- // callback assertion
104
- assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
105
- } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
106
- // body deep equal assertion
107
- assertions.push(`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`);
108
- } else {
109
- throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
110
- }
111
- } else if (expectArguments.length === 2) {
112
- // header assertion
113
- const [headerName, headerValue] = expectArguments;
114
- assert.ok(headerName && headerValue);
115
- if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
116
- assertions.push(
117
- `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
118
- );
119
- } else {
120
- assertions.push(
121
- `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
122
- );
123
- }
124
- }
125
- }
126
- return assertions;
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;
82
+ analyzeFixtureCall(nextCall, results);
135
83
  }
136
- return getAncestor(parent, matchType, quitType);
137
84
  }
138
85
 
139
- function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) {
86
+ // analyze response related variables and their references0
87
+ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) {
140
88
  const results: {
141
- responseVariable?: Scope.Variable;
142
- responseBodyReferences: MemberExpression[];
143
- responseHeadersReferences: MemberExpression[];
144
- spreadResponseBodyVariable?: Scope.Variable;
145
- spreadResponseHeadersVariable?: Scope.Variable;
89
+ variable?: Scope.Variable;
90
+ bodyReferences: MemberExpression[];
91
+ headersReferences: MemberExpression[];
92
+ statusReferences: MemberExpression[];
93
+ spreadBodyVariable?: Scope.Variable;
94
+ spreadHeadersVariable?: Scope.Variable;
146
95
  } = {
147
- responseBodyReferences: [],
148
- responseHeadersReferences: [],
96
+ bodyReferences: [],
97
+ headersReferences: [],
98
+ statusReferences: [],
149
99
  };
150
100
 
151
- const variableDeclaration = getAncestor(fixtureCall, 'VariableDeclaration', 'FunctionDeclaration');
152
- if (variableDeclaration && variableDeclaration.type === 'VariableDeclaration') {
153
- const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration);
101
+ if (fixtureInformation.variableDeclaration) {
102
+ const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration);
154
103
  for (const responseVariable of responseVariables) {
155
104
  const identifier = responseVariable.identifiers[0];
156
105
  assert.ok(identifier);
157
106
  const identifierParent = getParent(identifier);
158
107
  assert.ok(identifierParent);
159
108
  if (identifierParent.type === 'VariableDeclarator') {
160
- // if (declarator.id.type === 'Identifier') {
161
- results.responseVariable = responseVariable;
162
- results.responseBodyReferences = responseVariable.references
109
+ // e.g. const response = ...
110
+ results.variable = responseVariable;
111
+ // e.g. response.body
112
+ results.bodyReferences = responseVariable.references
163
113
  .map((responseBodyReference) => getParent(responseBodyReference.identifier))
164
114
  .filter(
165
115
  (node): node is MemberExpression =>
@@ -169,7 +119,8 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope
169
119
  node.property.type === 'Identifier' &&
170
120
  node.property.name === 'body',
171
121
  );
172
- results.responseHeadersReferences = responseVariable.references
122
+ // e.g. response.headers / response.header / response.get()
123
+ results.headersReferences = responseVariable.references
173
124
  .map((responseHeadersReference) => getParent(responseHeadersReference.identifier))
174
125
  .filter(
175
126
  (node): node is MemberExpression =>
@@ -179,30 +130,102 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope
179
130
  node.property.type === 'Identifier' &&
180
131
  (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
181
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
+ );
182
144
  } else if (
145
+ // body reference through destruction/renaming, e.g. "const { body } = ..."
183
146
  identifierParent.type === 'Property' &&
184
147
  identifierParent.key.type === 'Identifier' &&
185
148
  identifierParent.key.name === 'body'
186
149
  ) {
187
- results.spreadResponseBodyVariable = responseVariable;
150
+ results.spreadBodyVariable = responseVariable;
188
151
  } else if (
152
+ // header reference through destruction/renaming, e.g. "const { headers } = ..."
189
153
  identifierParent.type === 'Property' &&
190
154
  identifierParent.key.type === 'Identifier' &&
191
155
  identifierParent.key.name === 'headers'
192
156
  ) {
193
- results.spreadResponseHeadersVariable = responseVariable;
157
+ results.spreadHeadersVariable = responseVariable;
158
+ } else {
159
+ throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
194
160
  }
195
161
  }
196
162
  }
197
163
  return results;
198
164
  }
199
165
 
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] : '';
166
+ // `/sample-service/v1/ping` -> `${BASE_PATH}/ping`
167
+ function replaceEndpointUrlPrefixWithBasePath(url: string) {
168
+ // eslint-disable-next-line no-template-curly-in-string
169
+ return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
170
+ }
171
+
172
+ function isValidPropertyName(name: unknown) {
173
+ return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
174
+ }
175
+
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 ?? []) {
185
+ if (expectArguments.length === 1) {
186
+ const [assertionArgument] = expectArguments;
187
+ assert.ok(assertionArgument);
188
+ if (
189
+ (assertionArgument.type === 'MemberExpression' &&
190
+ assertionArgument.object.type === 'Identifier' &&
191
+ assertionArgument.object.name === 'StatusCodes') ||
192
+ assertionArgument.type === 'Literal'
193
+ ) {
194
+ // status code assertion
195
+ statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`;
196
+ } else if (assertionArgument.type === 'ArrowFunctionExpression') {
197
+ // callback assertion
198
+ nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
199
+ } else if (assertionArgument.type === 'Identifier') {
200
+ // callback assertion
201
+ nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
202
+ } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
203
+ // body deep equal assertion
204
+ nonStatusAssertions.push(
205
+ `assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`,
206
+ );
207
+ } else {
208
+ throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
209
+ }
210
+ } else if (expectArguments.length === 2) {
211
+ // header assertion
212
+ const [headerName, headerValue] = expectArguments;
213
+ assert.ok(headerName && headerValue);
214
+ if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
215
+ nonStatusAssertions.push(
216
+ `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
217
+ );
218
+ } else {
219
+ nonStatusAssertions.push(
220
+ `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
221
+ );
222
+ }
223
+ }
224
+ }
225
+ return {
226
+ statusAssertion,
227
+ nonStatusAssertions,
228
+ };
206
229
  }
207
230
 
208
231
  const rule: Rule.RuleModule = {
@@ -223,136 +246,123 @@ const rule: Rule.RuleModule = {
223
246
  create(context) {
224
247
  const sourceCode = context.sourceCode;
225
248
  const scopeManager = sourceCode.scopeManager;
226
- let variableCounter = 0;
249
+ let responseVariableCounter = 0;
227
250
 
228
251
  return {
229
- 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (fixtureCall: Node) => {
252
+ 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
253
+ fixtureCall: CallExpression,
254
+ ) => {
230
255
  try {
231
256
  assert.ok(fixtureCall.type === 'CallExpression');
232
- const fixtureFunction = fixtureCall.callee; // node - fixture.api.get
257
+ const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
233
258
  assert.ok(fixtureFunction.type === 'MemberExpression');
234
- const methodNode = fixtureFunction.property; // get/put/etc.
235
- assert.ok(methodNode.type === 'Identifier');
236
259
  const indentation = getIndentation(fixtureCall, sourceCode);
237
260
 
238
- const [urlArgumentNode] = fixtureCall.arguments; // node - `/smartdata/v1/ping`
261
+ const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
239
262
  assert.ok(urlArgumentNode !== undefined);
240
263
 
241
264
  const fixtureCallInformation = {} as FixtureCallInformation;
242
- analyze(fixtureCall, fixtureCallInformation);
265
+ analyzeFixtureCall(fixtureCall, fixtureCallInformation);
243
266
 
244
267
  const {
245
- responseVariable,
246
- responseBodyReferences,
247
- responseHeadersReferences,
248
- spreadResponseBodyVariable,
249
- spreadResponseHeadersVariable,
250
- } = analyzeReferences(fixtureCallInformation.root, scopeManager);
251
- let variableNameToUse: string;
252
- let isResponseVariableDeclared = false;
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;
253
304
  if (responseVariable === undefined) {
254
- variableNameToUse = `response${variableCounter === 0 ? '' : variableCounter.toString()}`;
255
- variableCounter++;
305
+ responseVariableNameToUse = `response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`;
306
+ responseVariableCounter++;
256
307
  } else {
257
- isResponseVariableDeclared = true;
258
- variableNameToUse = responseVariable.name;
308
+ responseVariableNameToUse = responseVariable.name;
259
309
  }
260
310
 
261
- // convert fixture.api.get to fetch
262
- const fixtureApiCallText = sourceCode.getText(fixtureCall); // e.g. "fixture.api.get(`/smartdata/v1/ping`)""
263
- const fixtureMethodText = sourceCode.getText(fixtureFunction); // e.g. "fixture.api.get"
264
-
265
- const fetchStatementStart =
266
- fixtureCallInformation.root.type === 'ReturnStatement' && fixtureCallInformation.assertions === undefined
267
- ? 'return'
268
- : 'await';
269
- let replacedText = fixtureApiCallText.replace(fixtureMethodText, `${fetchStatementStart} fetch`);
270
-
271
- // convert `/smartdata/v1/ping` to `${BASE_PATH}/ping`
272
- const fixtureArgumentText = sourceCode.getText(urlArgumentNode); // text - e.g. `/smartdata/v1/ping`
273
- let fetchArgumentText = replaceEndpointUrlPrefixWithBasePath(fixtureArgumentText); // test - e.g. `${BASE_PATH}/ping`
274
-
275
- // add request argument if deeded
276
- if (
277
- methodNode.name !== 'get' ||
278
- fixtureCallInformation.requestBody !== undefined ||
279
- fixtureCallInformation.requestHeaders !== undefined
280
- ) {
281
- fetchArgumentText += [
282
- ', {',
283
- ` method: '${methodNode.name.toUpperCase()}',`,
284
- ...(fixtureCallInformation.requestBody
285
- ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
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 =
311
+ const needResponseVariableRedefine =
305
312
  spreadResponseBodyVariable !== undefined ||
306
- (responseVariable === undefined && fixtureCallInformation.assertions) !== undefined;
307
-
308
- if (needVariableRedefine) {
309
- replacedText = [
310
- replacedText,
311
- ...(spreadResponseBodyVariable
312
- ? [`const ${spreadResponseBodyVariable.name} = await ${variableNameToUse}.json()`]
313
- : []),
314
- ...(spreadResponseHeadersVariable
315
- ? [`const ${spreadResponseHeadersVariable.name} = ${variableNameToUse}.headers`]
316
- : []),
317
- ].join(`;\n${indentation}`);
318
- }
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
+ );
319
331
 
320
- if (fixtureCallInformation.assertions) {
321
- // add variable declaration if needed
322
- if (!isResponseVariableDeclared) {
323
- replacedText = `const ${variableNameToUse} = ${replacedText}`;
324
- }
325
- // externalize response assertions
326
- replacedText = [
327
- replacedText,
328
- ...appendAssertions(fixtureCallInformation.assertions, sourceCode, variableNameToUse),
329
- ].join(`;\n${indentation}`);
330
- }
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}`);
331
347
 
332
348
  context.report({
333
349
  node: fixtureCall,
334
350
  messageId: 'preferNativeFetch',
335
351
  *fix(fixer) {
336
- let replacementRootNode: AwaitExpression | ReturnStatement | Node = fixtureCallInformation.root;
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);
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
+ );
349
359
 
350
- // handle response body
360
+ // handle response body references
351
361
  for (const responseBodyReference of responseBodyReferences) {
352
- yield fixer.replaceText(responseBodyReference, `await ${variableNameToUse}.json()`);
362
+ yield fixer.replaceText(responseBodyReference, `await ${responseVariableNameToUse}.json()`);
353
363
  }
354
364
 
355
- // handle response headers
365
+ // handle response headers references
356
366
  for (const responseHeadersReference of responseHeadersReferences) {
357
367
  const parent = getParent(responseHeadersReference);
358
368
  assert.ok(parent);
@@ -367,34 +377,29 @@ const rule: Rule.RuleModule = {
367
377
  headerName = sourceCode.getText(headerNameNode);
368
378
  }
369
379
  assert.ok(headerName);
370
- yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`);
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
+ }
371
391
  }
372
392
 
373
393
  // handle direct return without await
374
394
  if (
375
- fixtureCallInformation.root.type === 'ReturnStatement' &&
395
+ fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
376
396
  fixtureCallInformation.assertions !== undefined
377
397
  ) {
378
398
  yield fixer.insertTextAfter(
379
- fixtureCallInformation.root,
380
- `;\n${indentation}return ${variableNameToUse};`,
399
+ fixtureCallInformation.rootNode,
400
+ `\n${indentation}return ${responseVariableNameToUse};`,
381
401
  );
382
402
  }
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
403
  },
399
404
  });
400
405
  } catch (error) {
@@ -411,4 +416,5 @@ const rule: Rule.RuleModule = {
411
416
  };
412
417
  },
413
418
  };
419
+
414
420
  export default rule;