@checkdigit/eslint-plugin 6.6.0-PR.75-0fc6 → 6.6.0-PR.77-885a

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 (64) hide show
  1. package/dist-cjs/index.cjs +779 -2464
  2. package/dist-cjs/metafile.json +203 -795
  3. package/dist-mjs/index.mjs +3 -77
  4. package/dist-mjs/require-resolve-full-response.mjs +5 -2
  5. package/dist-types/index.d.ts +1 -51
  6. package/dist-types/require-resolve-full-response.d.ts +3 -1
  7. package/package.json +1 -1
  8. package/src/index.ts +0 -74
  9. package/src/library/format.ts +1 -1
  10. package/src/library/tree.ts +1 -1
  11. package/src/library/ts-tree.ts +1 -1
  12. package/src/library/variable.ts +1 -1
  13. package/src/require-resolve-full-response.ts +2 -1
  14. package/dist-mjs/agent/add-url-domain.mjs +0 -61
  15. package/dist-mjs/agent/fetch-response-body-json.mjs +0 -63
  16. package/dist-mjs/agent/fetch-response-header-getter.mjs +0 -117
  17. package/dist-mjs/agent/fetch-then.mjs +0 -269
  18. package/dist-mjs/agent/fetch.mjs +0 -34
  19. package/dist-mjs/agent/no-fixture.mjs +0 -328
  20. package/dist-mjs/agent/no-full-response.mjs +0 -67
  21. package/dist-mjs/agent/no-mapped-response.mjs +0 -75
  22. package/dist-mjs/agent/no-service-wrapper.mjs +0 -184
  23. package/dist-mjs/agent/no-status-code.mjs +0 -59
  24. package/dist-mjs/agent/no-unused-function-argument.mjs +0 -79
  25. package/dist-mjs/agent/no-unused-service-variable.mjs +0 -75
  26. package/dist-mjs/agent/response-reference.mjs +0 -56
  27. package/dist-mjs/agent/url.mjs +0 -26
  28. package/dist-mjs/no-duplicated-imports.mjs +0 -87
  29. package/dist-mjs/require-fixed-services-import.mjs +0 -46
  30. package/dist-mjs/require-type-out-of-type-only-imports.mjs +0 -48
  31. package/dist-types/agent/add-url-domain.d.ts +0 -4
  32. package/dist-types/agent/fetch-response-body-json.d.ts +0 -4
  33. package/dist-types/agent/fetch-response-header-getter.d.ts +0 -4
  34. package/dist-types/agent/fetch-then.d.ts +0 -4
  35. package/dist-types/agent/fetch.d.ts +0 -4
  36. package/dist-types/agent/no-fixture.d.ts +0 -4
  37. package/dist-types/agent/no-full-response.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-service-variable.d.ts +0 -4
  43. package/dist-types/agent/response-reference.d.ts +0 -16
  44. package/dist-types/agent/url.d.ts +0 -5
  45. package/dist-types/no-duplicated-imports.d.ts +0 -4
  46. package/dist-types/require-fixed-services-import.d.ts +0 -4
  47. package/dist-types/require-type-out-of-type-only-imports.d.ts +0 -4
  48. package/src/agent/add-url-domain.ts +0 -75
  49. package/src/agent/fetch-response-body-json.ts +0 -76
  50. package/src/agent/fetch-response-header-getter.ts +0 -148
  51. package/src/agent/fetch-then.ts +0 -354
  52. package/src/agent/fetch.ts +0 -52
  53. package/src/agent/no-fixture.ts +0 -453
  54. package/src/agent/no-full-response.ts +0 -75
  55. package/src/agent/no-mapped-response.ts +0 -84
  56. package/src/agent/no-service-wrapper.ts +0 -238
  57. package/src/agent/no-status-code.ts +0 -71
  58. package/src/agent/no-unused-function-argument.ts +0 -96
  59. package/src/agent/no-unused-service-variable.ts +0 -92
  60. package/src/agent/response-reference.ts +0 -100
  61. package/src/agent/url.ts +0 -23
  62. package/src/no-duplicated-imports.ts +0 -116
  63. package/src/require-fixed-services-import.ts +0 -52
  64. package/src/require-type-out-of-type-only-imports.ts +0 -63
@@ -1,354 +0,0 @@
1
- // agent/fetch-then.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 type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree';
10
- import { type Rule, type Scope, SourceCode } from 'eslint';
11
- import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/tree';
12
- import { hasAssertions, isInvalidResponseHeadersAccess } from './fetch';
13
- import { strict as assert } from 'node:assert';
14
- import getDocumentationUrl from '../get-documentation-url';
15
- import { getIndentation } from '../library/format';
16
- import { isValidPropertyName } from '../library/variable';
17
- import { replaceEndpointUrlPrefixWithBasePath } from './url';
18
-
19
- export const ruleId = 'fetch-then';
20
-
21
- interface FixtureCallInformation {
22
- fixtureNode: SimpleCallExpression;
23
- requestBody?: Expression;
24
- requestHeaders?: { name: Expression; value: Expression }[];
25
- assertions?: Expression[][];
26
- }
27
-
28
- // recursively analyze the fixture/supertest call chain to collect information of request/response
29
- function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
30
- const parent = getParent(call);
31
- if (!parent) {
32
- return;
33
- }
34
-
35
- let nextCall;
36
- if (parent.type !== 'MemberExpression') {
37
- results.fixtureNode = call;
38
- return;
39
- }
40
-
41
- if (parent.property.type === 'Identifier') {
42
- if (parent.property.name === 'expect') {
43
- // supertest assertions
44
- const assertionCall = getParent(parent);
45
- assert.ok(assertionCall && assertionCall.type === 'CallExpression');
46
- results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
47
- nextCall = assertionCall;
48
- } else if (parent.property.name === 'send') {
49
- // request body
50
- const sendRequestBodyCall = getParent(parent);
51
- assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression');
52
- results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
53
- nextCall = sendRequestBodyCall;
54
- } else if (parent.property.name === 'set') {
55
- // request headers
56
- const setRequestHeaderCall = getParent(parent);
57
- assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression');
58
- const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
59
- results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }];
60
- nextCall = setRequestHeaderCall;
61
- }
62
- } else {
63
- throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
64
- }
65
- if (nextCall) {
66
- analyzeFixtureCall(nextCall, results, sourceCode);
67
- }
68
- }
69
-
70
- // eslint-disable-next-line sonarjs/cognitive-complexity
71
- function createResponseAssertions(
72
- fixtureCallInformation: FixtureCallInformation,
73
- sourceCode: SourceCode,
74
- responseVariableName: string,
75
- ) {
76
- let statusAssertion: string | undefined;
77
- const nonStatusAssertions: string[] = [];
78
- for (const expectArguments of fixtureCallInformation.assertions ?? []) {
79
- if (expectArguments.length === 1) {
80
- const [assertionArgument] = expectArguments;
81
- assert.ok(assertionArgument);
82
- if (
83
- (assertionArgument.type === 'MemberExpression' &&
84
- assertionArgument.object.type === 'Identifier' &&
85
- assertionArgument.object.name === 'StatusCodes') ||
86
- assertionArgument.type === 'Literal' ||
87
- sourceCode.getText(assertionArgument).includes('StatusCodes.')
88
- ) {
89
- // status code assertion
90
- statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
91
- } else if (assertionArgument.type === 'ArrowFunctionExpression') {
92
- // callback assertion using arrow function
93
- let functionBody = sourceCode.getText(assertionArgument.body);
94
-
95
- const [originalResponseArgument] = assertionArgument.params;
96
- assert.ok(originalResponseArgument?.type === 'Identifier');
97
- const originalResponseArgumentName = originalResponseArgument.name;
98
- if (originalResponseArgumentName !== responseVariableName) {
99
- functionBody = functionBody.replace(
100
- new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
101
- responseVariableName,
102
- );
103
- }
104
- nonStatusAssertions.push(`assert.ok(${functionBody})`);
105
- } else if (assertionArgument.type === 'Identifier') {
106
- // callback assertion using function reference
107
- nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
108
- } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
109
- // body deep equal assertion
110
- nonStatusAssertions.push(
111
- `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
112
- );
113
- } else {
114
- throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
115
- }
116
- } else if (expectArguments.length === 2) {
117
- // header assertion
118
- const [headerName, headerValue] = expectArguments;
119
- assert.ok(headerName && headerValue);
120
- const headersReference = `${responseVariableName}.headers`;
121
- if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
122
- nonStatusAssertions.push(
123
- `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
124
- );
125
- } else {
126
- nonStatusAssertions.push(
127
- `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
128
- );
129
- }
130
- }
131
- }
132
- return {
133
- statusAssertion,
134
- nonStatusAssertions,
135
- };
136
- }
137
-
138
- function getResponseHeadersAccesses(
139
- responseVariables: Scope.Variable[],
140
- scopeManager: Scope.ScopeManager,
141
- sourceCode: SourceCode,
142
- ) {
143
- const responseHeadersAccesses: MemberExpression[] = [];
144
- for (const responseVariable of responseVariables) {
145
- for (const responseReference of responseVariable.references) {
146
- const responseAccess = getParent(responseReference.identifier);
147
- if (!responseAccess || responseAccess.type !== 'MemberExpression') {
148
- continue;
149
- }
150
-
151
- const responseAccessParent = getParent(responseAccess);
152
- if (!responseAccessParent) {
153
- continue;
154
- }
155
-
156
- if (
157
- responseAccessParent.type === 'CallExpression' &&
158
- responseAccessParent.arguments[0]?.type === 'ArrowFunctionExpression'
159
- ) {
160
- // map-like operation against responses, e.g. responses.map((response) => response.headers.etag)
161
- responseHeadersAccesses.push(
162
- ...getResponseHeadersAccesses(
163
- scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]),
164
- scopeManager,
165
- sourceCode,
166
- ),
167
- );
168
- continue;
169
- }
170
-
171
- if (
172
- responseAccess.computed &&
173
- responseAccess.property.type === 'Literal' &&
174
- responseAccessParent.type === 'MemberExpression'
175
- ) {
176
- // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc.
177
- responseHeadersAccesses.push(responseAccessParent);
178
- } else {
179
- responseHeadersAccesses.push(responseAccess);
180
- }
181
- }
182
- }
183
- return responseHeadersAccesses;
184
- }
185
-
186
- const rule: Rule.RuleModule = {
187
- meta: {
188
- type: 'suggestion',
189
- docs: {
190
- description: 'Prefer native fetch API over customized fixture API.',
191
- url: getDocumentationUrl(ruleId),
192
- },
193
- messages: {
194
- preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
195
- shouldUseHeaderGetter: 'Getter should be used to access response headers.',
196
- unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
197
- },
198
- fixable: 'code',
199
- schema: [],
200
- },
201
- // eslint-disable-next-line max-lines-per-function
202
- create(context) {
203
- const sourceCode = context.sourceCode;
204
- const scopeManager = sourceCode.scopeManager;
205
-
206
- return {
207
- // eslint-disable-next-line max-lines-per-function
208
- 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
209
- fixtureCall: CallExpression,
210
- // eslint-disable-next-line sonarjs/cognitive-complexity
211
- ) => {
212
- try {
213
- if (!hasAssertions(fixtureCall)) {
214
- // skip if there are no assertions, let "no-fixture" rule to handle the conversion
215
- return;
216
- }
217
-
218
- if (!(isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)) {
219
- return;
220
- }
221
-
222
- assert.ok(fixtureCall.type === 'CallExpression');
223
- const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
224
- assert.ok(fixtureFunction.type === 'MemberExpression');
225
- const indentation = getIndentation(fixtureCall, sourceCode);
226
-
227
- const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
228
- assert.ok(urlArgumentNode !== undefined);
229
-
230
- const fixtureCallInformation = {} as FixtureCallInformation;
231
- analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
232
-
233
- // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
234
- const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
235
- const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
236
-
237
- // fetch request argument
238
- const methodNode = fixtureFunction.property; // get/put/etc.
239
- assert.ok(methodNode.type === 'Identifier');
240
- const fetchRequestArgumentLines = [
241
- '{',
242
- ` method: '${methodNode.name.toUpperCase()}',`,
243
- ...(fixtureCallInformation.requestBody
244
- ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
245
- : []),
246
- ...(fixtureCallInformation.requestHeaders
247
- ? [
248
- ` headers: {`,
249
- ...fixtureCallInformation.requestHeaders.map(
250
- ({ name, value }) =>
251
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
252
- ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
253
- ),
254
- ` },`,
255
- ]
256
- : []),
257
- '}',
258
- ].join(`\n${indentation}`);
259
-
260
- const responseVariableNameToUse = 'res';
261
- const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
262
- fixtureCallInformation,
263
- sourceCode,
264
- responseVariableNameToUse,
265
- );
266
-
267
- // add variable declaration if needed
268
- const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method';
269
- const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
270
- const appendingAssignmentAndAssertionText = [
271
- ...(statusAssertion !== undefined ? [statusAssertion] : []),
272
- ...nonStatusAssertions,
273
- ].join(`;\n${indentation}`);
274
- const replacementText = fixtureCallInformation.assertions
275
- ? [
276
- disableLintComment,
277
- `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
278
- appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
279
- ` return ${responseVariableNameToUse};`,
280
- `})`,
281
- ].join(`\n${indentation}`)
282
- : fetchCallText;
283
-
284
- context.report({
285
- node: fixtureCall,
286
- messageId: 'preferNativeFetch',
287
- fix(fixer) {
288
- return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
289
- },
290
- });
291
-
292
- const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode);
293
- if (!responsesVariable) {
294
- return;
295
- }
296
-
297
- const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable);
298
- const responseHeadersAccesses = getResponseHeadersAccesses(
299
- responseVariableReferences,
300
- scopeManager,
301
- sourceCode,
302
- );
303
- for (const responseHeadersAccess of responseHeadersAccesses) {
304
- if (isInvalidResponseHeadersAccess(responseHeadersAccess)) {
305
- const headerAccess = getParent(responseHeadersAccess);
306
- if (headerAccess?.type === 'MemberExpression') {
307
- const headerNameNode = headerAccess.property;
308
- const headerName = headerAccess.computed
309
- ? sourceCode.getText(headerNameNode)
310
- : `'${sourceCode.getText(headerNameNode)}'`;
311
- const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`;
312
-
313
- context.report({
314
- node: headerAccess,
315
- messageId: 'shouldUseHeaderGetter',
316
- fix(fixer) {
317
- return fixer.replaceText(headerAccess, headerAccessReplacementText);
318
- },
319
- });
320
- } else if (
321
- headerAccess?.type === 'CallExpression' &&
322
- responseHeadersAccess.property.type === 'Identifier' &&
323
- responseHeadersAccess.property.name === 'get'
324
- ) {
325
- const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`;
326
-
327
- context.report({
328
- node: headerAccess,
329
- messageId: 'shouldUseHeaderGetter',
330
- fix(fixer) {
331
- return fixer.replaceText(headerAccess, headerAccessReplacementText);
332
- },
333
- });
334
- }
335
- }
336
- }
337
- } catch (error) {
338
- // eslint-disable-next-line no-console
339
- console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
340
- context.report({
341
- node: fixtureCall,
342
- messageId: 'unknownError',
343
- data: {
344
- fileName: context.filename,
345
- error: error instanceof Error ? error.toString() : JSON.stringify(error),
346
- },
347
- });
348
- }
349
- },
350
- };
351
- },
352
- };
353
-
354
- export default rule;
@@ -1,52 +0,0 @@
1
- // agent/fetch.ts
2
-
3
- import { getParent, isBlockStatement } from '../library/tree';
4
- import type { Node } from 'estree';
5
-
6
- export function getResponseBodyRetrievalText(responseVariableName: string) {
7
- return `await ${responseVariableName}.json()`;
8
- }
9
-
10
- export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node) {
11
- const responseHeaderAccessParent = getParent(responseHeadersAccess);
12
- if (responseHeaderAccessParent?.type === 'VariableDeclarator') {
13
- return false;
14
- }
15
-
16
- if (
17
- responseHeaderAccessParent?.type === 'CallExpression' &&
18
- responseHeaderAccessParent.callee.type === 'MemberExpression' &&
19
- responseHeaderAccessParent.callee.property.type === 'Identifier' &&
20
- responseHeaderAccessParent.callee.property.name === 'get'
21
- ) {
22
- return true;
23
- }
24
-
25
- return !(
26
- responseHeaderAccessParent?.type === 'MemberExpression' &&
27
- responseHeaderAccessParent.property.type === 'Identifier' &&
28
- responseHeaderAccessParent.property.name === 'get'
29
- );
30
- }
31
-
32
- export function hasAssertions(fixtureCall: Node) {
33
- if (isBlockStatement(fixtureCall)) {
34
- return false;
35
- }
36
-
37
- const parent = getParent(fixtureCall);
38
- if (!parent) {
39
- return false;
40
- }
41
-
42
- if (
43
- parent.type === 'MemberExpression' &&
44
- parent.property.type === 'Identifier' &&
45
- parent.property.name === 'expect' &&
46
- getParent(parent)?.type === 'CallExpression'
47
- ) {
48
- return true;
49
- }
50
-
51
- return hasAssertions(parent);
52
- }