@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.
- package/dist-cjs/index.cjs +206 -161
- package/dist-cjs/metafile.json +89 -89
- package/dist-mjs/ast/tree.mjs +32 -6
- package/dist-mjs/fixture/{concurrent-promises.mjs → fetch-then.mjs} +18 -10
- package/dist-mjs/fixture/fetch.mjs +16 -2
- package/dist-mjs/fixture/no-fixture.mjs +15 -8
- package/dist-mjs/index.mjs +4 -4
- package/dist-types/ast/tree.d.ts +3 -0
- package/dist-types/fixture/{concurrent-promises.d.ts → fetch-then.d.ts} +1 -1
- package/dist-types/fixture/fetch.d.ts +1 -0
- package/dist-types/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/ast/tree.ts +43 -5
- package/src/fixture/{concurrent-promises.ts → fetch-then.ts} +136 -122
- package/src/fixture/fetch.ts +23 -1
- package/src/fixture/no-fixture.ts +22 -9
- package/src/index.ts +3 -3
|
@@ -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;
|
package/dist-types/index.d.ts
CHANGED
|
@@ -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
|
-
"
|
|
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/
|
|
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-
|
|
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/
|
|
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 = '
|
|
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
|
|
36
|
+
if (parent.type !== 'MemberExpression') {
|
|
37
37
|
results.fixtureNode = call;
|
|
38
|
-
|
|
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.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
219
|
+
if (!(isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
|
|
228
|
+
const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
|
|
229
|
+
assert.ok(urlArgumentNode !== undefined);
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
261
|
+
const responseVariableNameToUse = 'res';
|
|
262
|
+
const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
|
|
263
|
+
fixtureCallInformation,
|
|
264
|
+
sourceCode,
|
|
265
|
+
responseVariableNameToUse,
|
|
266
|
+
);
|
|
278
267
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
};
|
package/src/fixture/fetch.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
[
|
|
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/${
|
|
57
|
+
[`@checkdigit/${fetchThenRuleId}`]: 'error',
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
60
|
recommended: {
|