@checkdigit/eslint-plugin 7.2.0 → 7.3.0-PR.75-aa6d

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