@checkdigit/eslint-plugin 6.6.0-PR.75-66d8 → 6.6.0-PR.75-0dbb

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.
@@ -1,4 +1,4 @@
1
1
  import { type Rule } from 'eslint';
2
- export declare const ruleId = "concurrent-promises";
2
+ export declare const ruleId = "fetch-then";
3
3
  declare const rule: Rule.RuleModule;
4
4
  export default rule;
@@ -1,3 +1,4 @@
1
1
  import type { Node } from 'estree';
2
2
  export declare function getResponseBodyRetrievalText(responseVariableName: string): string;
3
3
  export declare function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean;
4
+ export declare function hasAssertions(fixtureCall: Node): boolean;
@@ -13,7 +13,7 @@ declare const _default: {
13
13
  "no-promise-instance-method": import("eslint").Rule.RuleModule;
14
14
  "no-fixture": import("eslint").Rule.RuleModule;
15
15
  "fetch-header-getter": import("eslint").Rule.RuleModule;
16
- "concurrent-promises": import("eslint").Rule.RuleModule;
16
+ "fetch-then": import("eslint").Rule.RuleModule;
17
17
  };
18
18
  configs: {
19
19
  all: {
@@ -31,7 +31,7 @@ declare const _default: {
31
31
  "@checkdigit/no-promise-instance-method": string;
32
32
  "@checkdigit/no-fixture": string;
33
33
  "@checkdigit/fetch-header-getter": string;
34
- "@checkdigit/concurrent-promises": string;
34
+ "@checkdigit/fetch-then": string;
35
35
  };
36
36
  };
37
37
  recommended: {
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-66d8","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs","build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"6.0.0","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.16.1","@typescript-eslint/parser":"^7.16.1","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-0dbb","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs","build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"6.0.0","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.16.1","@typescript-eslint/parser":"^7.16.1","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
package/src/ast/tree.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * This code is licensed under the MIT license (see LICENSE.txt for details).
7
7
  */
8
8
 
9
- import type { Node } from 'estree';
9
+ import type { Expression, Node } from 'estree';
10
10
 
11
11
  type NodeParent = Node | undefined | null;
12
12
 
@@ -38,11 +38,12 @@ export function getAncestor(
38
38
  return getAncestor(parent, matcher, exitMatcher);
39
39
  }
40
40
 
41
+ export function isBlockStatement(node: Node) {
42
+ return node.type.endsWith('Statement') || node.type.endsWith('Declaration');
43
+ }
44
+
41
45
  export function getEnclosingStatement(node: Node) {
42
- return getAncestor(
43
- node,
44
- (parentNode) => parentNode.type.endsWith('Statement') || parentNode.type.endsWith('Declaration'),
45
- );
46
+ return getAncestor(node, isBlockStatement);
46
47
  }
47
48
 
48
49
  export function getEnclosingScopeNode(node: Node) {
@@ -50,3 +51,40 @@ export function getEnclosingScopeNode(node: Node) {
50
51
  ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type),
51
52
  );
52
53
  }
54
+
55
+ export function isUsedInArrayOrAsArgument(node: Node) {
56
+ if (isBlockStatement(node)) {
57
+ return false;
58
+ }
59
+
60
+ const parent = getParent(node);
61
+ if (!parent) {
62
+ return false;
63
+ }
64
+
65
+ if (
66
+ parent.type === 'ArrayExpression' ||
67
+ (parent.type === 'CallExpression' && parent.arguments.includes(node as Expression))
68
+ ) {
69
+ return true;
70
+ }
71
+
72
+ // recurse up the tree until hitting a block statement
73
+ return isUsedInArrayOrAsArgument(parent);
74
+ }
75
+
76
+ export function getEnclosingFunction(node: Node) {
77
+ if (
78
+ node.type === 'FunctionDeclaration' ||
79
+ node.type === 'FunctionExpression' ||
80
+ node.type === 'ArrowFunctionExpression'
81
+ ) {
82
+ return node;
83
+ }
84
+
85
+ const parent = getParent(node);
86
+ if (!parent) {
87
+ return;
88
+ }
89
+ return getEnclosingFunction(parent);
90
+ }
@@ -1,4 +1,4 @@
1
- // fixture/concurrent-promises.ts
1
+ // fixture/fetch-then.ts
2
2
 
3
3
  /*
4
4
  * Copyright (c) 2021-2024 Check Digit, LLC
@@ -8,15 +8,15 @@
8
8
 
9
9
  import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree';
10
10
  import { type Rule, type Scope, SourceCode } from 'eslint';
11
- import { getEnclosingStatement, getParent } from '../ast/tree';
11
+ import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../ast/tree';
12
+ import { hasAssertions, isInvalidResponseHeadersAccess } from './fetch';
12
13
  import { strict as assert } from 'node:assert';
13
14
  import getDocumentationUrl from '../get-documentation-url';
14
15
  import { getIndentation } from '../ast/format';
15
- import { isInvalidResponseHeadersAccess } from './fetch';
16
16
  import { isValidPropertyName } from './variable';
17
17
  import { replaceEndpointUrlPrefixWithBasePath } from './url';
18
18
 
19
- export const ruleId = 'concurrent-promises';
19
+ export const ruleId = 'fetch-then';
20
20
 
21
21
  interface FixtureCallInformation {
22
22
  fixtureNode: SimpleCallExpression;
@@ -33,9 +33,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
33
33
  }
34
34
 
35
35
  let nextCall;
36
- if (parent.type === 'ArrayExpression') {
36
+ if (parent.type !== 'MemberExpression') {
37
37
  results.fixtureNode = call;
38
- } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') {
38
+ return;
39
+ }
40
+
41
+ if (parent.property.type === 'Identifier') {
39
42
  if (parent.property.name === 'expect') {
40
43
  // supertest assertions
41
44
  const assertionCall = getParent(parent);
@@ -203,137 +206,148 @@ const rule: Rule.RuleModule = {
203
206
 
204
207
  return {
205
208
  // eslint-disable-next-line max-lines-per-function
206
- 'CallExpression[callee.object.name="Promise"] > ArrayExpression CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]':
207
- (fixtureCall: CallExpression) => {
208
- try {
209
- assert.ok(fixtureCall.type === 'CallExpression');
210
- const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
211
- assert.ok(fixtureFunction.type === 'MemberExpression');
212
- const indentation = getIndentation(fixtureCall, sourceCode);
209
+ 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
210
+ fixtureCall: CallExpression,
211
+ // eslint-disable-next-line sonarjs/cognitive-complexity
212
+ ) => {
213
+ try {
214
+ if (!hasAssertions(fixtureCall)) {
215
+ // skip if there are no assertions, let "no-fixture" rule to handle the conversion
216
+ return;
217
+ }
213
218
 
214
- const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
215
- assert.ok(urlArgumentNode !== undefined);
219
+ if (!(isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)) {
220
+ return;
221
+ }
216
222
 
217
- const fixtureCallInformation = {} as FixtureCallInformation;
218
- analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
223
+ assert.ok(fixtureCall.type === 'CallExpression');
224
+ const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
225
+ assert.ok(fixtureFunction.type === 'MemberExpression');
226
+ const indentation = getIndentation(fixtureCall, sourceCode);
219
227
 
220
- // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
221
- const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
222
- const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
228
+ const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
229
+ assert.ok(urlArgumentNode !== undefined);
223
230
 
224
- // fetch request argument
225
- const methodNode = fixtureFunction.property; // get/put/etc.
226
- assert.ok(methodNode.type === 'Identifier');
227
- const fetchRequestArgumentLines = [
228
- '{',
229
- ` method: '${methodNode.name.toUpperCase()}',`,
230
- ...(fixtureCallInformation.requestBody
231
- ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
232
- : []),
233
- ...(fixtureCallInformation.requestHeaders
234
- ? [
235
- ` headers: {`,
236
- ...fixtureCallInformation.requestHeaders.map(
237
- ({ name, value }) =>
238
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
239
- ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
240
- ),
241
- ` },`,
242
- ]
243
- : []),
244
- '}',
245
- ].join(`\n${indentation}`);
231
+ const fixtureCallInformation = {} as FixtureCallInformation;
232
+ analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
246
233
 
247
- const responseVariableNameToUse = 'res';
248
- const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
249
- fixtureCallInformation,
250
- sourceCode,
251
- responseVariableNameToUse,
252
- );
234
+ // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
235
+ const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
236
+ const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
253
237
 
254
- // add variable declaration if needed
255
- const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method';
256
- const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
257
- const appendingAssignmentAndAssertionText = [
258
- ...(statusAssertion !== undefined ? [statusAssertion] : []),
259
- ...nonStatusAssertions,
260
- ].join(`;\n${indentation}`);
261
- const replacementText = fixtureCallInformation.assertions
238
+ // fetch request argument
239
+ const methodNode = fixtureFunction.property; // get/put/etc.
240
+ assert.ok(methodNode.type === 'Identifier');
241
+ const fetchRequestArgumentLines = [
242
+ '{',
243
+ ` method: '${methodNode.name.toUpperCase()}',`,
244
+ ...(fixtureCallInformation.requestBody
245
+ ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
246
+ : []),
247
+ ...(fixtureCallInformation.requestHeaders
262
248
  ? [
263
- disableLintComment,
264
- `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
265
- appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
266
- ` return ${responseVariableNameToUse};`,
267
- `})`,
268
- ].join(`\n${indentation}`)
269
- : fetchCallText;
249
+ ` headers: {`,
250
+ ...fixtureCallInformation.requestHeaders.map(
251
+ ({ name, value }) =>
252
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
253
+ ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
254
+ ),
255
+ ` },`,
256
+ ]
257
+ : []),
258
+ '}',
259
+ ].join(`\n${indentation}`);
270
260
 
271
- context.report({
272
- node: fixtureCall,
273
- messageId: 'preferNativeFetch',
274
- fix(fixer) {
275
- return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
276
- },
277
- });
261
+ const responseVariableNameToUse = 'res';
262
+ const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
263
+ fixtureCallInformation,
264
+ sourceCode,
265
+ responseVariableNameToUse,
266
+ );
278
267
 
279
- const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode);
280
- if (!responsesVariable) {
281
- return;
282
- }
268
+ // add variable declaration if needed
269
+ const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method';
270
+ const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
271
+ const appendingAssignmentAndAssertionText = [
272
+ ...(statusAssertion !== undefined ? [statusAssertion] : []),
273
+ ...nonStatusAssertions,
274
+ ].join(`;\n${indentation}`);
275
+ const replacementText = fixtureCallInformation.assertions
276
+ ? [
277
+ disableLintComment,
278
+ `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
279
+ appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
280
+ ` return ${responseVariableNameToUse};`,
281
+ `})`,
282
+ ].join(`\n${indentation}`)
283
+ : fetchCallText;
283
284
 
284
- const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable);
285
- const responseHeadersAccesses = getResponseHeadersAccesses(
286
- responseVariableReferences,
287
- scopeManager,
288
- sourceCode,
289
- );
290
- for (const responseHeadersAccess of responseHeadersAccesses) {
291
- if (isInvalidResponseHeadersAccess(responseHeadersAccess)) {
292
- const headerAccess = getParent(responseHeadersAccess);
293
- if (headerAccess?.type === 'MemberExpression') {
294
- const headerNameNode = headerAccess.property;
295
- const headerName = headerAccess.computed
296
- ? sourceCode.getText(headerNameNode)
297
- : `'${sourceCode.getText(headerNameNode)}'`;
298
- const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`;
285
+ context.report({
286
+ node: fixtureCall,
287
+ messageId: 'preferNativeFetch',
288
+ fix(fixer) {
289
+ return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
290
+ },
291
+ });
299
292
 
300
- context.report({
301
- node: headerAccess,
302
- messageId: 'shouldUseHeaderGetter',
303
- fix(fixer) {
304
- return fixer.replaceText(headerAccess, headerAccessReplacementText);
305
- },
306
- });
307
- } else if (
308
- headerAccess?.type === 'CallExpression' &&
309
- responseHeadersAccess.property.type === 'Identifier' &&
310
- responseHeadersAccess.property.name === 'get'
311
- ) {
312
- const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`;
293
+ const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode);
294
+ if (!responsesVariable) {
295
+ return;
296
+ }
313
297
 
314
- context.report({
315
- node: headerAccess,
316
- messageId: 'shouldUseHeaderGetter',
317
- fix(fixer) {
318
- return fixer.replaceText(headerAccess, headerAccessReplacementText);
319
- },
320
- });
321
- }
298
+ const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable);
299
+ const responseHeadersAccesses = getResponseHeadersAccesses(
300
+ responseVariableReferences,
301
+ scopeManager,
302
+ sourceCode,
303
+ );
304
+ for (const responseHeadersAccess of responseHeadersAccesses) {
305
+ if (isInvalidResponseHeadersAccess(responseHeadersAccess)) {
306
+ const headerAccess = getParent(responseHeadersAccess);
307
+ if (headerAccess?.type === 'MemberExpression') {
308
+ const headerNameNode = headerAccess.property;
309
+ const headerName = headerAccess.computed
310
+ ? sourceCode.getText(headerNameNode)
311
+ : `'${sourceCode.getText(headerNameNode)}'`;
312
+ const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`;
313
+
314
+ context.report({
315
+ node: headerAccess,
316
+ messageId: 'shouldUseHeaderGetter',
317
+ fix(fixer) {
318
+ return fixer.replaceText(headerAccess, headerAccessReplacementText);
319
+ },
320
+ });
321
+ } else if (
322
+ headerAccess?.type === 'CallExpression' &&
323
+ responseHeadersAccess.property.type === 'Identifier' &&
324
+ responseHeadersAccess.property.name === 'get'
325
+ ) {
326
+ const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`;
327
+
328
+ context.report({
329
+ node: headerAccess,
330
+ messageId: 'shouldUseHeaderGetter',
331
+ fix(fixer) {
332
+ return fixer.replaceText(headerAccess, headerAccessReplacementText);
333
+ },
334
+ });
322
335
  }
323
336
  }
324
- } catch (error) {
325
- // eslint-disable-next-line no-console
326
- console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
327
- context.report({
328
- node: fixtureCall,
329
- messageId: 'unknownError',
330
- data: {
331
- fileName: context.filename,
332
- error: error instanceof Error ? error.toString() : JSON.stringify(error),
333
- },
334
- });
335
337
  }
336
- },
338
+ } catch (error) {
339
+ // eslint-disable-next-line no-console
340
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
341
+ context.report({
342
+ node: fixtureCall,
343
+ messageId: 'unknownError',
344
+ data: {
345
+ fileName: context.filename,
346
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
347
+ },
348
+ });
349
+ }
350
+ },
337
351
  };
338
352
  },
339
353
  };
@@ -1,7 +1,7 @@
1
1
  // fixture/fetch.ts
2
2
 
3
+ import { getParent, isBlockStatement } from '../ast/tree';
3
4
  import type { Node } from 'estree';
4
- import { getParent } from '../ast/tree';
5
5
 
6
6
  export function getResponseBodyRetrievalText(responseVariableName: string) {
7
7
  return `await ${responseVariableName}.json()`;
@@ -28,3 +28,25 @@ export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node) {
28
28
  responseHeaderAccessParent.property.name === 'get'
29
29
  );
30
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
+ }
@@ -17,19 +17,25 @@ import type {
17
17
  VariableDeclaration,
18
18
  } from 'estree';
19
19
  import { type Rule, type Scope, SourceCode } from 'eslint';
20
- import { getEnclosingScopeNode, getEnclosingStatement, getParent } from '../ast/tree';
20
+ import {
21
+ getEnclosingFunction,
22
+ getEnclosingScopeNode,
23
+ getEnclosingStatement,
24
+ getParent,
25
+ isUsedInArrayOrAsArgument,
26
+ } from '../ast/tree';
27
+ import { getResponseBodyRetrievalText, hasAssertions } from './fetch';
21
28
  import { analyzeResponseReferences } from './response-reference';
22
29
  import { strict as assert } from 'node:assert';
23
30
  import getDocumentationUrl from '../get-documentation-url';
24
31
  import { getIndentation } from '../ast/format';
25
- import { getResponseBodyRetrievalText } from './fetch';
26
32
  import { isValidPropertyName } from './variable';
27
33
  import { replaceEndpointUrlPrefixWithBasePath } from './url';
28
34
 
29
35
  export const ruleId = 'no-fixture';
30
36
 
31
37
  interface FixtureCallInformation {
32
- rootNode: AwaitExpression | ReturnStatement | VariableDeclaration;
38
+ rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression;
33
39
  fixtureNode: AwaitExpression | SimpleCallExpression;
34
40
  variableDeclaration?: VariableDeclaration;
35
41
  requestBody?: Expression;
@@ -37,10 +43,10 @@ interface FixtureCallInformation {
37
43
  assertions?: Expression[][];
38
44
  inlineStatementNode?: Node;
39
45
  inlineBodyReference?: MemberExpression;
40
- isConcurrent?: boolean;
41
46
  }
42
47
 
43
48
  // recursively analyze the fixture/supertest call chain to collect information of request/response
49
+ // eslint-disable-next-line sonarjs/cognitive-complexity
44
50
  function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
45
51
  const parent = getParent(call);
46
52
  assert.ok(parent, 'parent should exist for fixture/supertest call node');
@@ -50,8 +56,10 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
50
56
  // direct return, no variable declaration or await
51
57
  results.fixtureNode = call;
52
58
  results.rootNode = parent;
53
- } else if (parent.type === 'ArrayExpression') {
54
- results.isConcurrent = true;
59
+ } else if (parent.type === 'ArrayExpression' || parent.type === 'CallExpression') {
60
+ // direct return, no variable declaration or await
61
+ results.fixtureNode = call;
62
+ results.rootNode = call;
55
63
  } else if (parent.type === 'AwaitExpression') {
56
64
  results.fixtureNode = call;
57
65
  const enclosingStatement = getEnclosingStatement(parent);
@@ -238,6 +246,14 @@ const rule: Rule.RuleModule = {
238
246
  fixtureCall: CallExpression,
239
247
  ) => {
240
248
  try {
249
+ if (
250
+ hasAssertions(fixtureCall) &&
251
+ (isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)
252
+ ) {
253
+ // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here
254
+ return;
255
+ }
256
+
241
257
  assert.ok(fixtureCall.type === 'CallExpression');
242
258
  const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
243
259
  assert.ok(fixtureFunction.type === 'MemberExpression');
@@ -248,9 +264,6 @@ const rule: Rule.RuleModule = {
248
264
 
249
265
  const fixtureCallInformation = {} as FixtureCallInformation;
250
266
  analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
251
- if (fixtureCallInformation.isConcurrent === true) {
252
- return;
253
- }
254
267
 
255
268
  const {
256
269
  variable: responseVariable,
package/src/index.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * This code is licensed under the MIT license (see LICENSE.txt for details).
7
7
  */
8
8
 
9
- import concurrentPromises, { ruleId as concurrentPromisesRuleId } from './fixture/concurrent-promises';
10
9
  import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter';
10
+ import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then';
11
11
  import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify';
12
12
  import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture';
13
13
  import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method';
@@ -36,7 +36,7 @@ export default {
36
36
  [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod,
37
37
  [noFixtureRuleId]: noFixture,
38
38
  [fetchHeaderGetterRuleId]: fetchHeaderGetter,
39
- [concurrentPromisesRuleId]: concurrentPromises,
39
+ [fetchThenRuleId]: fetchThen,
40
40
  },
41
41
  configs: {
42
42
  all: {
@@ -54,7 +54,7 @@ export default {
54
54
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
55
55
  [`@checkdigit/${noFixtureRuleId}`]: 'error',
56
56
  [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error',
57
- [`@checkdigit/${concurrentPromisesRuleId}`]: 'error',
57
+ [`@checkdigit/${fetchThenRuleId}`]: 'error',
58
58
  },
59
59
  },
60
60
  recommended: {