@checkdigit/eslint-plugin 7.3.0-PR.75-d748 → 7.3.0-PR.93-561f

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.
Files changed (65) hide show
  1. package/dist-mjs/index.mjs +3 -122
  2. package/dist-mjs/require-resolve-full-response.mjs +1 -1
  3. package/package.json +1 -1
  4. package/src/index.ts +0 -119
  5. package/src/require-resolve-full-response.ts +1 -3
  6. package/dist-mjs/agent/add-assert-import.mjs +0 -58
  7. package/dist-mjs/agent/add-base-path-const.mjs +0 -65
  8. package/dist-mjs/agent/add-base-path-import.mjs +0 -60
  9. package/dist-mjs/agent/add-url-domain.mjs +0 -61
  10. package/dist-mjs/agent/agent-test-wiring.mjs +0 -196
  11. package/dist-mjs/agent/fetch-response-body-json.mjs +0 -146
  12. package/dist-mjs/agent/fetch-response-header-getter.mjs +0 -117
  13. package/dist-mjs/agent/fetch-then.mjs +0 -267
  14. package/dist-mjs/agent/fetch.mjs +0 -34
  15. package/dist-mjs/agent/file.mjs +0 -43
  16. package/dist-mjs/agent/fix-function-call-arguments.mjs +0 -153
  17. package/dist-mjs/agent/no-fixture.mjs +0 -336
  18. package/dist-mjs/agent/no-mapped-response.mjs +0 -75
  19. package/dist-mjs/agent/no-service-wrapper.mjs +0 -185
  20. package/dist-mjs/agent/no-status-code.mjs +0 -59
  21. package/dist-mjs/agent/no-unused-function-argument.mjs +0 -79
  22. package/dist-mjs/agent/no-unused-imports.mjs +0 -81
  23. package/dist-mjs/agent/no-unused-service-variable.mjs +0 -74
  24. package/dist-mjs/agent/response-reference.mjs +0 -67
  25. package/dist-mjs/agent/url.mjs +0 -32
  26. package/dist-types/agent/add-assert-import.d.ts +0 -4
  27. package/dist-types/agent/add-base-path-const.d.ts +0 -4
  28. package/dist-types/agent/add-base-path-import.d.ts +0 -4
  29. package/dist-types/agent/add-url-domain.d.ts +0 -4
  30. package/dist-types/agent/agent-test-wiring.d.ts +0 -4
  31. package/dist-types/agent/fetch-response-body-json.d.ts +0 -4
  32. package/dist-types/agent/fetch-response-header-getter.d.ts +0 -4
  33. package/dist-types/agent/fetch-then.d.ts +0 -4
  34. package/dist-types/agent/fetch.d.ts +0 -4
  35. package/dist-types/agent/file.d.ts +0 -7
  36. package/dist-types/agent/fix-function-call-arguments.d.ts +0 -9
  37. package/dist-types/agent/no-fixture.d.ts +0 -4
  38. package/dist-types/agent/no-mapped-response.d.ts +0 -4
  39. package/dist-types/agent/no-service-wrapper.d.ts +0 -4
  40. package/dist-types/agent/no-status-code.d.ts +0 -4
  41. package/dist-types/agent/no-unused-function-argument.d.ts +0 -4
  42. package/dist-types/agent/no-unused-imports.d.ts +0 -4
  43. package/dist-types/agent/no-unused-service-variable.d.ts +0 -4
  44. package/dist-types/agent/response-reference.d.ts +0 -16
  45. package/dist-types/agent/url.d.ts +0 -4
  46. package/src/agent/add-assert-import.ts +0 -74
  47. package/src/agent/add-base-path-const.ts +0 -81
  48. package/src/agent/add-base-path-import.ts +0 -69
  49. package/src/agent/add-url-domain.ts +0 -76
  50. package/src/agent/agent-test-wiring.ts +0 -246
  51. package/src/agent/fetch-response-body-json.ts +0 -197
  52. package/src/agent/fetch-response-header-getter.ts +0 -148
  53. package/src/agent/fetch-then.ts +0 -355
  54. package/src/agent/fetch.ts +0 -53
  55. package/src/agent/file.ts +0 -42
  56. package/src/agent/fix-function-call-arguments.ts +0 -200
  57. package/src/agent/no-fixture.ts +0 -480
  58. package/src/agent/no-mapped-response.ts +0 -84
  59. package/src/agent/no-service-wrapper.ts +0 -241
  60. package/src/agent/no-status-code.ts +0 -72
  61. package/src/agent/no-unused-function-argument.ts +0 -98
  62. package/src/agent/no-unused-imports.ts +0 -103
  63. package/src/agent/no-unused-service-variable.ts +0 -93
  64. package/src/agent/response-reference.ts +0 -122
  65. package/src/agent/url.ts +0 -32
@@ -1,200 +0,0 @@
1
- // agent/fix-function-call-arguments.ts
2
-
3
- /*
4
- * Copyright (c) 2021-2024 Check Digit, LLC
5
- *
6
- * This code is licensed under the MIT license (see LICENSE.txt for details).
7
- */
8
-
9
- import { strict as assert } from 'node:assert';
10
- import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
11
- import debug from 'debug';
12
- import getDocumentationUrl from '../get-documentation-url';
13
-
14
- export const ruleId = 'fix-function-call-arguments';
15
-
16
- export interface FixFunctionCallArgumentsRuleOptions {
17
- typesToCheck: string[];
18
- }
19
- const DEFAULT_OPTIONS = {
20
- typesToCheck: [
21
- 'Configuration<ResolvedServices>',
22
- 'Fixture<ResolvedServices>',
23
- 'InboundContext',
24
- '{ get: () => string; }',
25
- 'Api',
26
- ],
27
- };
28
-
29
- const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
30
- const log = debug('eslint-plugin:fix-function-call-arguments');
31
-
32
- const rule: ESLintUtils.RuleModule<
33
- 'removeIncompatibleFunctionArguments' | 'unknownError',
34
- [FixFunctionCallArgumentsRuleOptions]
35
- > = createRule({
36
- name: ruleId,
37
- meta: {
38
- type: 'suggestion',
39
- docs: {
40
- description: 'Remove incompatible function arguments.',
41
- },
42
- messages: {
43
- removeIncompatibleFunctionArguments: 'Removing incompatible function arguments.',
44
- unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
45
- },
46
- fixable: 'code',
47
- schema: [
48
- {
49
- type: 'object',
50
- properties: {
51
- typesToCheck: {
52
- description: 'Text representation of the types of which the function call parameters will be examine',
53
- type: 'array',
54
- items: {
55
- type: 'string',
56
- },
57
- },
58
- },
59
- additionalProperties: false,
60
- },
61
- ],
62
- },
63
- defaultOptions: [DEFAULT_OPTIONS],
64
- create(context) {
65
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
66
- const { typesToCheck } = context.options[0] ?? DEFAULT_OPTIONS;
67
- const parserServices = ESLintUtils.getParserServices(context);
68
- const typeChecker = parserServices.program.getTypeChecker();
69
- const sourceCode = context.sourceCode;
70
-
71
- return {
72
- CallExpression(callExpression) {
73
- // ignore calls like `foo.bar()` which are likely to be 3rd party module calls
74
- // we only focus on calls against local functions or functions imported from the same module
75
- // if (callExpression.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
76
- // return;
77
- // }
78
-
79
- log('===== file name:', context.filename);
80
- log('callExpression:', sourceCode.getText(callExpression));
81
-
82
- try {
83
- const actualParameters = callExpression.arguments;
84
- if (
85
- !actualParameters.some((actualParameter) => {
86
- const actualType = typeChecker.getTypeAtLocation(
87
- parserServices.esTreeNodeToTSNodeMap.get(actualParameter),
88
- );
89
- const actualTypeString = typeChecker.typeToString(actualType);
90
- return typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType');
91
- })
92
- ) {
93
- return;
94
- }
95
-
96
- const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee);
97
- const calleeType = typeChecker.getTypeAtLocation(calleeTsNode);
98
-
99
- const signatures = calleeType.getCallSignatures();
100
- if (signatures.length > 1) {
101
- // ignore complex signatures with overloads
102
- return;
103
- }
104
-
105
- const signature = signatures[0];
106
- assert(signature);
107
- // if (
108
- // signature === undefined ||
109
- // (signature.typeParameters !== undefined && signature.typeParameters.length > 0)
110
- // ) {
111
- // // ignore complex signatures with type parameters
112
- // return;
113
- // }
114
-
115
- log('signature:', signature.getDeclaration().getText());
116
- const expectedParameters = signature.getParameters();
117
- log(
118
- 'expected parameters:',
119
- expectedParameters.map((expectedParameter) =>
120
- typeChecker.typeToString(typeChecker.getTypeOfSymbol(expectedParameter)),
121
- ),
122
- );
123
- const expectedParametersCount = expectedParameters.length;
124
- const actualParametersCount = actualParameters.length;
125
- if (actualParametersCount === 0) {
126
- return;
127
- }
128
-
129
- const parametersToKeep: TSESTree.CallExpressionArgument[] = [];
130
- let expectedParameterIndex = 0;
131
- for (const [actualParameterIndex, actualParameter] of actualParameters.entries()) {
132
- if (expectedParameterIndex >= expectedParametersCount) {
133
- break;
134
- }
135
-
136
- const expectedParameter = expectedParameters[expectedParameterIndex];
137
- assert.ok(expectedParameter, 'Expected parameter not found.');
138
-
139
- const expectedType = typeChecker.getTypeOfSymbol(expectedParameter);
140
- const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(actualParameter));
141
- const actualTypeString = typeChecker.typeToString(actualType);
142
- log(
143
- 'expected type: #',
144
- expectedParameterIndex,
145
- expectedParameter.escapedName,
146
- typeChecker.typeToString(expectedType),
147
- );
148
- log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString);
149
-
150
- if (
151
- (typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType')) &&
152
- !typeChecker.isTypeAssignableTo(actualType, expectedType)
153
- ) {
154
- log('removing un-matched parameter', sourceCode.getText(actualParameter));
155
- continue;
156
- }
157
- parametersToKeep.push(actualParameter);
158
- expectedParameterIndex++;
159
- }
160
-
161
- if (parametersToKeep.length === actualParametersCount) {
162
- return;
163
- }
164
-
165
- const firstParameter = actualParameters[0];
166
- const lastParameter = actualParameters.at(-1);
167
- assert.ok(firstParameter !== undefined && lastParameter !== undefined);
168
- const tokenAfterParameters = sourceCode.getTokenAfter(lastParameter);
169
-
170
- context.report({
171
- node: callExpression,
172
- messageId: 'removeIncompatibleFunctionArguments',
173
- fix(fixer) {
174
- return fixer.replaceTextRange(
175
- [
176
- firstParameter.range[0],
177
- tokenAfterParameters?.value === ',' ? tokenAfterParameters.range[1] : lastParameter.range[1],
178
- ],
179
- parametersToKeep.map((arg) => sourceCode.getText(arg)).join(', '),
180
- );
181
- },
182
- });
183
- } catch (error) {
184
- // eslint-disable-next-line no-console
185
- console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
186
- context.report({
187
- node: callExpression,
188
- messageId: 'unknownError',
189
- data: {
190
- fileName: context.filename,
191
- error: error instanceof Error ? error.toString() : JSON.stringify(error),
192
- },
193
- });
194
- }
195
- },
196
- };
197
- },
198
- });
199
-
200
- export default rule;
@@ -1,480 +0,0 @@
1
- // agent/no-fixture.ts
2
-
3
- /*
4
- * Copyright (c) 2021-2024 Check Digit, LLC
5
- *
6
- * This code is licensed under the MIT license (see LICENSE.txt for details).
7
- */
8
-
9
- import { strict as assert } from 'node:assert';
10
-
11
- import type {
12
- AwaitExpression,
13
- CallExpression,
14
- Expression,
15
- ExpressionStatement,
16
- MemberExpression,
17
- Node,
18
- ObjectPattern,
19
- ReturnStatement,
20
- SimpleCallExpression,
21
- VariableDeclaration,
22
- } from 'estree';
23
- import { type Rule, type Scope, SourceCode } from 'eslint';
24
-
25
- import {
26
- getEnclosingFunction,
27
- getEnclosingScopeNode,
28
- getEnclosingStatement,
29
- getParent,
30
- isUsedInArrayOrAsArgument,
31
- } from '../library/tree';
32
- import getDocumentationUrl from '../get-documentation-url';
33
- import { getIndentation } from '../library/format';
34
- import { isValidPropertyName } from '../library/variable';
35
- import { analyzeResponseReferences } from './response-reference';
36
- import { getResponseBodyRetrievalText, hasAssertions } from './fetch';
37
- import { replaceEndpointUrlPrefixWithBasePath } from './url';
38
-
39
- export const ruleId = 'no-fixture';
40
-
41
- interface FixtureCallInformation {
42
- rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression | ExpressionStatement;
43
- fixtureNode: AwaitExpression | SimpleCallExpression;
44
- variableDeclaration?: VariableDeclaration;
45
- variableAssignment?: ExpressionStatement;
46
- requestBody?: Expression;
47
- requestHeaders?: { name: Expression; value: Expression }[];
48
- assertions?: Expression[][];
49
- inlineStatementNode?: Node;
50
- inlineBodyReference?: MemberExpression;
51
- }
52
-
53
- // recursively analyze the fixture/supertest call chain to collect information of request/response
54
- // eslint-disable-next-line sonarjs/cognitive-complexity
55
- function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
56
- const parent = getParent(call);
57
- assert.ok(parent, 'parent should exist for fixture/supertest call node');
58
-
59
- let nextCall;
60
- if (parent.type === 'ReturnStatement') {
61
- // direct return, no variable declaration or await
62
- results.fixtureNode = call;
63
- results.rootNode = parent;
64
- } else if (
65
- parent.type === 'ArrayExpression' ||
66
- parent.type === 'CallExpression' ||
67
- parent.type === 'ArrowFunctionExpression'
68
- ) {
69
- // direct return, no variable declaration or await
70
- results.fixtureNode = call;
71
- results.rootNode = call;
72
- } else if (parent.type === 'AwaitExpression') {
73
- results.fixtureNode = call;
74
- const enclosingStatement = getEnclosingStatement(parent);
75
- assert.ok(enclosingStatement);
76
- const awaitParent = getParent(parent);
77
- if (awaitParent?.type === 'MemberExpression') {
78
- results.rootNode = parent;
79
- results.inlineStatementNode = enclosingStatement;
80
- if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') {
81
- results.inlineBodyReference = awaitParent;
82
- }
83
- } else if (enclosingStatement.type === 'VariableDeclaration') {
84
- results.variableDeclaration = enclosingStatement;
85
- results.rootNode = enclosingStatement;
86
- } else if (
87
- enclosingStatement.type === 'ExpressionStatement' &&
88
- enclosingStatement.expression.type === 'AssignmentExpression'
89
- ) {
90
- results.variableAssignment = enclosingStatement;
91
- results.rootNode = enclosingStatement;
92
- } else {
93
- results.rootNode = parent;
94
- }
95
- } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') {
96
- if (parent.property.name === 'expect') {
97
- // supertest assertions
98
- const assertionCall = getParent(parent);
99
- assert.ok(assertionCall && assertionCall.type === 'CallExpression');
100
- results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
101
- nextCall = assertionCall;
102
- } else if (parent.property.name === 'send') {
103
- // request body
104
- const sendRequestBodyCall = getParent(parent);
105
- assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression');
106
- results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
107
- nextCall = sendRequestBodyCall;
108
- } else if (parent.property.name === 'set') {
109
- // request headers
110
- const setRequestHeaderCall = getParent(parent);
111
- assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression');
112
- const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
113
- results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }];
114
- nextCall = setRequestHeaderCall;
115
- }
116
- } else {
117
- throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
118
- }
119
- if (nextCall) {
120
- analyzeFixtureCall(nextCall, results, sourceCode);
121
- }
122
- }
123
-
124
- // eslint-disable-next-line sonarjs/cognitive-complexity
125
- function createResponseAssertions(
126
- fixtureCallInformation: FixtureCallInformation,
127
- sourceCode: SourceCode,
128
- responseVariableName: string,
129
- destructuringResponseHeadersVariable: Scope.Variable | undefined,
130
- ) {
131
- let statusAssertion: string | undefined;
132
- const nonStatusAssertions: string[] = [];
133
- for (const expectArguments of fixtureCallInformation.assertions ?? []) {
134
- if (expectArguments.length === 1) {
135
- const [assertionArgument] = expectArguments;
136
- assert.ok(assertionArgument);
137
- if (
138
- (assertionArgument.type === 'MemberExpression' &&
139
- assertionArgument.object.type === 'Identifier' &&
140
- assertionArgument.object.name === 'StatusCodes') ||
141
- assertionArgument.type === 'Literal' ||
142
- sourceCode.getText(assertionArgument).includes('StatusCodes.')
143
- ) {
144
- // status code assertion
145
- statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
146
- } else if (assertionArgument.type === 'ArrowFunctionExpression') {
147
- // callback assertion using arrow function
148
- let functionBody = sourceCode.getText(assertionArgument.body);
149
-
150
- const [originalResponseArgument] = assertionArgument.params;
151
- assert.ok(originalResponseArgument?.type === 'Identifier');
152
- const originalResponseArgumentName = originalResponseArgument.name;
153
- if (originalResponseArgumentName !== responseVariableName) {
154
- functionBody = functionBody.replace(
155
- new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
156
- responseVariableName,
157
- );
158
- }
159
- nonStatusAssertions.push(`assert.ok(${functionBody})`);
160
- } else if (assertionArgument.type === 'Identifier') {
161
- // callback assertion using function reference
162
- nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
163
- } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
164
- // body deep equal assertion
165
- nonStatusAssertions.push(
166
- `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
167
- );
168
- } else {
169
- throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
170
- }
171
- } else if (expectArguments.length === 2) {
172
- // header assertion
173
- const [headerName, headerValue] = expectArguments;
174
- assert.ok(headerName && headerValue);
175
- const headersReference =
176
- destructuringResponseHeadersVariable !== undefined
177
- ? destructuringResponseHeadersVariable.name
178
- : `${responseVariableName}.headers`;
179
- if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
180
- nonStatusAssertions.push(
181
- `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
182
- );
183
- } else {
184
- nonStatusAssertions.push(
185
- `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
186
- );
187
- }
188
- }
189
- }
190
- return {
191
- statusAssertion,
192
- nonStatusAssertions,
193
- };
194
- }
195
-
196
- function getResponseVariableNameToUse(
197
- scopeManager: Scope.ScopeManager,
198
- fixtureCallInformation: FixtureCallInformation,
199
- scopeVariablesMap: Map<Scope.Scope, string[]>,
200
- ) {
201
- if (fixtureCallInformation.variableAssignment) {
202
- assert.ok(
203
- fixtureCallInformation.variableAssignment.expression.type === 'AssignmentExpression' &&
204
- fixtureCallInformation.variableAssignment.expression.left.type === 'Identifier',
205
- );
206
- return fixtureCallInformation.variableAssignment.expression.left.name;
207
- }
208
-
209
- if (fixtureCallInformation.variableDeclaration) {
210
- const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0];
211
- if (firstDeclaration && firstDeclaration.id.type === 'Identifier') {
212
- return firstDeclaration.id.name;
213
- }
214
- }
215
-
216
- const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode);
217
- scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode);
218
- assert.ok(enclosingScopeNode);
219
- const scope = scopeManager.acquire(enclosingScopeNode);
220
- assert.ok(scope !== null);
221
- let scopeVariables = scopeVariablesMap.get(scope);
222
- if (!scopeVariables) {
223
- scopeVariables = [...scope.set.keys()];
224
- scopeVariablesMap.set(scope, scopeVariables);
225
- }
226
-
227
- let responseVariableCounter = 0;
228
- let responseVariableNameToUse;
229
- while (responseVariableNameToUse === undefined) {
230
- responseVariableCounter++;
231
- responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`;
232
- if (scopeVariables.includes(responseVariableNameToUse)) {
233
- responseVariableNameToUse = undefined;
234
- }
235
- }
236
- scopeVariables.push(responseVariableNameToUse);
237
- return responseVariableNameToUse;
238
- }
239
-
240
- function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean {
241
- const parent = getParent(responseBodyReference);
242
- return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier';
243
- }
244
-
245
- const rule: Rule.RuleModule = {
246
- meta: {
247
- type: 'suggestion',
248
- docs: {
249
- description: 'Prefer native fetch API over customized fixture API.',
250
- url: getDocumentationUrl(ruleId),
251
- },
252
- messages: {
253
- preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
254
- unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
255
- },
256
- fixable: 'code',
257
- schema: [],
258
- },
259
- // eslint-disable-next-line max-lines-per-function
260
- create(context) {
261
- const sourceCode = context.sourceCode;
262
- const scopeManager = sourceCode.scopeManager;
263
- const scopeVariablesMap = new Map<Scope.Scope, string[]>();
264
-
265
- return {
266
- // eslint-disable-next-line max-lines-per-function
267
- 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
268
- fixtureCall: CallExpression,
269
- // eslint-disable-next-line sonarjs/cognitive-complexity
270
- ) => {
271
- try {
272
- if (
273
- hasAssertions(fixtureCall) &&
274
- (isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)
275
- ) {
276
- // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here
277
- return;
278
- }
279
-
280
- assert.ok(fixtureCall.type === 'CallExpression');
281
- const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
282
- assert.ok(fixtureFunction.type === 'MemberExpression');
283
- const indentation = getIndentation(fixtureCall, sourceCode);
284
-
285
- const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
286
- assert.ok(urlArgumentNode !== undefined);
287
-
288
- const fixtureCallInformation = {} as FixtureCallInformation;
289
- analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
290
-
291
- const {
292
- variable: responseVariable,
293
- bodyReferences: responseBodyReferences,
294
- headersReferences: responseHeadersReferences,
295
- statusReferences: responseStatusReferences,
296
- destructuringBodyVariable: destructuringResponseBodyVariable,
297
- destructuringHeadersVariable: destructuringResponseHeadersVariable,
298
- } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager);
299
-
300
- // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
301
- const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
302
- const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
303
-
304
- // fetch request argument
305
- const methodNode = fixtureFunction.property; // get/put/etc.
306
- assert.ok(methodNode.type === 'Identifier');
307
- const methodName = methodNode.name.toUpperCase();
308
-
309
- const fetchRequestArgumentLines = [
310
- '{',
311
- ` method: '${methodName === 'DEL' ? 'DELETE' : methodName}',`,
312
- ...(fixtureCallInformation.requestBody
313
- ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
314
- : []),
315
- ...(fixtureCallInformation.requestHeaders
316
- ? [
317
- ` headers: {`,
318
- ...fixtureCallInformation.requestHeaders.map(
319
- ({ name, value }) =>
320
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
321
- ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
322
- ),
323
- ` },`,
324
- ]
325
- : []),
326
- '}',
327
- ].join(`\n${indentation}`);
328
-
329
- const responseVariableNameToUse = getResponseVariableNameToUse(
330
- scopeManager,
331
- fixtureCallInformation,
332
- scopeVariablesMap,
333
- );
334
-
335
- const isResponseBodyVariableRedefinitionNeeded =
336
- destructuringResponseBodyVariable !== undefined ||
337
- fixtureCallInformation.inlineBodyReference !== undefined ||
338
- (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
339
- const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
340
-
341
- const isResponseVariableRedefinitionNeeded =
342
- (fixtureCallInformation.variableAssignment === undefined &&
343
- responseVariable === undefined &&
344
- fixtureCallInformation.assertions !== undefined) ||
345
- isResponseBodyVariableRedefinitionNeeded;
346
-
347
- const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
348
- ? [
349
- // eslint-disable-next-line no-nested-ternary
350
- ...(destructuringResponseBodyVariable
351
- ? [
352
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
353
- `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
354
- ]
355
- : isResponseBodyVariableRedefinitionNeeded
356
- ? [
357
- `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
358
- ]
359
- : []),
360
- ...(destructuringResponseHeadersVariable
361
- ? [`const ${destructuringResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`]
362
- : []),
363
- ]
364
- : [];
365
-
366
- const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
367
- fixtureCallInformation,
368
- sourceCode,
369
- responseVariableNameToUse,
370
- destructuringResponseHeadersVariable,
371
- );
372
-
373
- // add variable declaration if needed
374
- const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
375
- const fetchStatementText = !isResponseVariableRedefinitionNeeded
376
- ? fetchCallText
377
- : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`;
378
-
379
- const nodeToReplace = isResponseVariableRedefinitionNeeded
380
- ? fixtureCallInformation.rootNode
381
- : fixtureCallInformation.fixtureNode;
382
- const appendingAssignmentAndAssertionText = [
383
- '',
384
- ...(statusAssertion !== undefined ? [statusAssertion] : []),
385
- ...responseBodyHeadersVariableRedefineLines,
386
- ...nonStatusAssertions,
387
- ].join(`;\n${indentation}`);
388
-
389
- context.report({
390
- node: fixtureCall,
391
- messageId: 'preferNativeFetch',
392
-
393
- *fix(fixer) {
394
- if (fixtureCallInformation.inlineStatementNode) {
395
- const preInlineDeclaration = [
396
- fetchStatementText,
397
- `${appendingAssignmentAndAssertionText};\n${indentation}`,
398
- ].join(``);
399
- yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration);
400
- } else {
401
- yield fixer.replaceText(nodeToReplace, fetchStatementText);
402
-
403
- const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';');
404
- yield fixer.insertTextAfter(
405
- nodeToReplace,
406
- needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText,
407
- );
408
- }
409
-
410
- // handle response body references
411
- for (const responseBodyReference of responseBodyReferences) {
412
- yield fixer.replaceText(
413
- responseBodyReference,
414
- isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference)
415
- ? redefineResponseBodyVariableName
416
- : getResponseBodyRetrievalText(responseVariableNameToUse),
417
- );
418
- }
419
- if (fixtureCallInformation.inlineBodyReference) {
420
- yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName);
421
- }
422
-
423
- // handle response headers references
424
- for (const responseHeadersReference of responseHeadersReferences) {
425
- const parent = getParent(responseHeadersReference);
426
- assert.ok(parent);
427
- let headerName;
428
- if (parent.type === 'MemberExpression') {
429
- const headerNameNode = parent.property;
430
- headerName = parent.computed
431
- ? sourceCode.getText(headerNameNode)
432
- : `'${sourceCode.getText(headerNameNode)}'`;
433
- } else if (parent.type === 'CallExpression') {
434
- const headerNameNode = parent.arguments[0];
435
- headerName = sourceCode.getText(headerNameNode);
436
- }
437
- assert.ok(headerName !== undefined);
438
- yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`);
439
- }
440
-
441
- // convert response.statusCode to response.status
442
- for (const responseStatusReference of responseStatusReferences) {
443
- if (
444
- responseStatusReference.property.type === 'Identifier' &&
445
- responseStatusReference.property.name === 'statusCode'
446
- ) {
447
- yield fixer.replaceText(responseStatusReference.property, `status`);
448
- }
449
- }
450
-
451
- // handle direct return statement without await, e.g. "return fixture.api.get(...);"
452
- if (
453
- fixtureCallInformation.rootNode.type === 'ReturnStatement' &&
454
- fixtureCallInformation.assertions !== undefined
455
- ) {
456
- yield fixer.insertTextAfter(
457
- fixtureCallInformation.rootNode,
458
- `\n${indentation}return ${responseVariableNameToUse};`,
459
- );
460
- }
461
- },
462
- });
463
- } catch (error) {
464
- // eslint-disable-next-line no-console
465
- console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
466
- context.report({
467
- node: fixtureCall,
468
- messageId: 'unknownError',
469
- data: {
470
- fileName: context.filename,
471
- error: error instanceof Error ? error.toString() : JSON.stringify(error),
472
- },
473
- });
474
- }
475
- },
476
- };
477
- },
478
- };
479
-
480
- export default rule;