@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/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
- root: AwaitExpression | ReturnStatement;
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
- function getParent(node: Node): Node | undefined | null {
39
- return (node as unknown as NodeParentExtension).parent;
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 === 'AwaitExpression' || parent.type === 'ReturnStatement') {
48
- // no more assertions, return the await expression of the fixture call
49
- results.root = parent;
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
- analyze(nextCall, results);
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 appendAssertions(expects: Expression[][], sourceCode: SourceCode, variableName: string) {
86
- const assertions: string[] = [];
87
- for (const expectArguments of expects) {
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
- assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`);
196
+ statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`;
99
197
  } else if (assertionArgument.type === 'ArrowFunctionExpression') {
100
198
  // callback assertion
101
- assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
199
+ nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`);
102
200
  } else if (assertionArgument.type === 'Identifier') {
103
201
  // callback assertion
104
- assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`);
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
- assertions.push(`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`);
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
- assertions.push(
216
+ nonStatusAssertions.push(
117
217
  `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
118
218
  );
119
219
  } else {
120
- assertions.push(
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 assertions;
226
+ return {
227
+ statusAssertion,
228
+ nonStatusAssertions,
229
+ };
127
230
  }
128
231
 
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;
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
- function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) {
140
- const results: {
141
- responseVariable?: Scope.Variable;
142
- responseBodyReferences: MemberExpression[];
143
- responseHeadersReferences: MemberExpression[];
144
- spreadResponseBodyVariable?: Scope.Variable;
145
- spreadResponseHeadersVariable?: Scope.Variable;
146
- } = {
147
- responseBodyReferences: [],
148
- responseHeadersReferences: [],
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
- const variableDeclaration = getAncestor(fixtureCall, 'VariableDeclaration', 'FunctionDeclaration');
152
- if (variableDeclaration && variableDeclaration.type === 'VariableDeclaration') {
153
- const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration);
154
- for (const responseVariable of responseVariables) {
155
- const identifier = responseVariable.identifiers[0];
156
- assert.ok(identifier);
157
- const identifierParent = getParent(identifier);
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
- return results;
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
- let variableCounter = 0;
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"]': (fixtureCall: Node) => {
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; // node - fixture.api.get
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; // node - `/smartdata/v1/ping`
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
- analyze(fixtureCall, fixtureCallInformation);
305
+ analyzeFixtureCall(fixtureCall, fixtureCallInformation);
243
306
 
244
307
  const {
245
- responseVariable,
246
- responseBodyReferences,
247
- responseHeadersReferences,
248
- spreadResponseBodyVariable,
249
- spreadResponseHeadersVariable,
250
- } = analyzeReferences(fixtureCallInformation.root, scopeManager);
251
- let variableNameToUse: string;
252
- let isResponseVariableDeclared = false;
253
- if (responseVariable === undefined) {
254
- variableNameToUse = `response${variableCounter === 0 ? '' : variableCounter.toString()}`;
255
- variableCounter++;
256
- } else {
257
- isResponseVariableDeclared = true;
258
- variableNameToUse = responseVariable.name;
259
- }
260
-
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 =
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) !== 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
- }
319
-
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
- }
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
- 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);
390
+ yield fixer.replaceText(nodeToReplace, fetchStatementText);
349
391
 
350
- // handle response body
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 ${variableNameToUse}.json()`);
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, `${variableNameToUse}.headers.get(${headerName})`);
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.root.type === 'ReturnStatement' &&
433
+ fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
376
434
  fixtureCallInformation.assertions !== undefined
377
435
  ) {
378
436
  yield fixer.insertTextAfter(
379
- fixtureCallInformation.root,
380
- `;\n${indentation}return ${variableNameToUse};`,
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;