@checkdigit/eslint-plugin 7.6.0-PR.75-1b09 → 7.6.0-PR.75-04b9
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-mjs/agent/fetch-then.mjs +30 -26
- package/dist-mjs/agent/fetch.mjs +7 -6
- package/dist-mjs/agent/no-fixture.mjs +44 -41
- package/dist-mjs/agent/no-supertest.mjs +44 -41
- package/dist-mjs/agent/response-reference.mjs +14 -14
- package/dist-types/agent/fetch-then.d.ts +2 -2
- package/dist-types/agent/fetch.d.ts +3 -3
- package/dist-types/agent/no-fixture.d.ts +2 -2
- package/dist-types/agent/no-supertest.d.ts +2 -2
- package/dist-types/agent/response-reference.d.ts +10 -10
- package/package.json +1 -1
- package/src/agent/fetch-then.ts +49 -47
- package/src/agent/fetch.ts +13 -13
- package/src/agent/no-fixture.ts +87 -84
- package/src/agent/no-supertest.ts +88 -85
- package/src/agent/response-reference.ts +48 -41
package/src/agent/fetch-then.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { strict as assert } from 'node:assert';
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
11
|
+
import { ScopeManager, Variable } from '@typescript-eslint/scope-manager';
|
|
12
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
|
13
|
+
import type { SourceCode } from '@typescript-eslint/utils/ts-eslint';
|
|
13
14
|
|
|
14
|
-
import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/tree';
|
|
15
|
+
import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree';
|
|
15
16
|
import getDocumentationUrl from '../get-documentation-url';
|
|
16
17
|
import { getIndentation } from '../library/format';
|
|
17
18
|
import { isValidPropertyName } from '../library/variable';
|
|
@@ -21,48 +22,48 @@ import { replaceEndpointUrlPrefixWithBasePath } from './url';
|
|
|
21
22
|
export const ruleId = 'fetch-then';
|
|
22
23
|
|
|
23
24
|
interface FixtureCallInformation {
|
|
24
|
-
fixtureNode:
|
|
25
|
-
requestBody?: Expression;
|
|
26
|
-
requestHeaders?: { name: Expression; value: Expression }[];
|
|
27
|
-
assertions?: Expression[][];
|
|
25
|
+
fixtureNode: TSESTree.CallExpression;
|
|
26
|
+
requestBody?: TSESTree.Expression;
|
|
27
|
+
requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[];
|
|
28
|
+
assertions?: TSESTree.Expression[][];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
31
|
-
function analyzeFixtureCall(call:
|
|
32
|
+
function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
|
|
32
33
|
const parent = getParent(call);
|
|
33
34
|
if (!parent) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
let nextCall;
|
|
38
|
-
if (parent.type !==
|
|
39
|
+
if (parent.type !== AST_NODE_TYPES.MemberExpression) {
|
|
39
40
|
results.fixtureNode = call;
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
if (parent.property.type ===
|
|
44
|
+
if (parent.property.type === AST_NODE_TYPES.Identifier) {
|
|
44
45
|
if (parent.property.name === 'expect') {
|
|
45
46
|
// supertest assertions
|
|
46
47
|
const assertionCall = getParent(parent);
|
|
47
|
-
assert.ok(assertionCall && assertionCall.type ===
|
|
48
|
-
results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
|
|
48
|
+
assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression);
|
|
49
|
+
results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]];
|
|
49
50
|
nextCall = assertionCall;
|
|
50
51
|
} else if (parent.property.name === 'send') {
|
|
51
52
|
// request body
|
|
52
53
|
const sendRequestBodyCall = getParent(parent);
|
|
53
|
-
assert.ok(sendRequestBodyCall && sendRequestBodyCall.type ===
|
|
54
|
-
results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
|
|
54
|
+
assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression);
|
|
55
|
+
results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression;
|
|
55
56
|
nextCall = sendRequestBodyCall;
|
|
56
57
|
} else if (parent.property.name === 'set') {
|
|
57
58
|
// request headers
|
|
58
59
|
const setRequestHeaderCall = getParent(parent);
|
|
59
|
-
assert.ok(setRequestHeaderCall && setRequestHeaderCall.type ===
|
|
60
|
-
const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
|
|
60
|
+
assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression);
|
|
61
|
+
const [name, value] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression];
|
|
61
62
|
results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }];
|
|
62
63
|
nextCall = setRequestHeaderCall;
|
|
63
64
|
}
|
|
64
65
|
} else {
|
|
65
|
-
throw new Error(`Unexpected
|
|
66
|
+
throw new Error(`Unexpected TSESTree.Expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
|
|
66
67
|
}
|
|
67
68
|
if (nextCall) {
|
|
68
69
|
analyzeFixtureCall(nextCall, results, sourceCode);
|
|
@@ -82,20 +83,20 @@ function createResponseAssertions(
|
|
|
82
83
|
const [assertionArgument] = expectArguments;
|
|
83
84
|
assert.ok(assertionArgument);
|
|
84
85
|
if (
|
|
85
|
-
(assertionArgument.type ===
|
|
86
|
-
assertionArgument.object.type ===
|
|
86
|
+
(assertionArgument.type === AST_NODE_TYPES.MemberExpression &&
|
|
87
|
+
assertionArgument.object.type === AST_NODE_TYPES.Identifier &&
|
|
87
88
|
assertionArgument.object.name === 'StatusCodes') ||
|
|
88
|
-
assertionArgument.type ===
|
|
89
|
+
assertionArgument.type === AST_NODE_TYPES.Literal ||
|
|
89
90
|
sourceCode.getText(assertionArgument).includes('StatusCodes.')
|
|
90
91
|
) {
|
|
91
92
|
// status code assertion
|
|
92
93
|
statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
|
|
93
|
-
} else if (assertionArgument.type ===
|
|
94
|
+
} else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
94
95
|
// callback assertion using arrow function
|
|
95
96
|
let functionBody = sourceCode.getText(assertionArgument.body);
|
|
96
97
|
|
|
97
98
|
const [originalResponseArgument] = assertionArgument.params;
|
|
98
|
-
assert.ok(originalResponseArgument?.type ===
|
|
99
|
+
assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier);
|
|
99
100
|
const originalResponseArgumentName = originalResponseArgument.name;
|
|
100
101
|
if (originalResponseArgumentName !== responseVariableName) {
|
|
101
102
|
functionBody = functionBody.replace(
|
|
@@ -104,12 +105,15 @@ function createResponseAssertions(
|
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
|
|
107
|
-
} else if (assertionArgument.type ===
|
|
108
|
+
} else if (assertionArgument.type === AST_NODE_TYPES.Identifier) {
|
|
108
109
|
// callback assertion using function reference
|
|
109
110
|
nonStatusAssertions.push(
|
|
110
111
|
`assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
|
|
111
112
|
);
|
|
112
|
-
} else if (
|
|
113
|
+
} else if (
|
|
114
|
+
assertionArgument.type === AST_NODE_TYPES.ObjectExpression ||
|
|
115
|
+
assertionArgument.type === AST_NODE_TYPES.CallExpression
|
|
116
|
+
) {
|
|
113
117
|
// body deep equal assertion
|
|
114
118
|
nonStatusAssertions.push(
|
|
115
119
|
`assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
|
|
@@ -122,7 +126,7 @@ function createResponseAssertions(
|
|
|
122
126
|
const [headerName, headerValue] = expectArguments;
|
|
123
127
|
assert.ok(headerName && headerValue);
|
|
124
128
|
const headersReference = `${responseVariableName}.headers`;
|
|
125
|
-
if (headerValue.type ===
|
|
129
|
+
if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) {
|
|
126
130
|
nonStatusAssertions.push(
|
|
127
131
|
`assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
|
|
128
132
|
);
|
|
@@ -139,16 +143,12 @@ function createResponseAssertions(
|
|
|
139
143
|
};
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
function getResponseHeadersAccesses(
|
|
143
|
-
|
|
144
|
-
scopeManager: Scope.ScopeManager,
|
|
145
|
-
sourceCode: SourceCode,
|
|
146
|
-
) {
|
|
147
|
-
const responseHeadersAccesses: MemberExpression[] = [];
|
|
146
|
+
function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) {
|
|
147
|
+
const responseHeadersAccesses: TSESTree.MemberExpression[] = [];
|
|
148
148
|
for (const responseVariable of responseVariables) {
|
|
149
149
|
for (const responseReference of responseVariable.references) {
|
|
150
150
|
const responseAccess = getParent(responseReference.identifier);
|
|
151
|
-
if (!responseAccess || responseAccess.type !==
|
|
151
|
+
if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) {
|
|
152
152
|
continue;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -158,8 +158,8 @@ function getResponseHeadersAccesses(
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
if (
|
|
161
|
-
responseAccessParent.type ===
|
|
162
|
-
responseAccessParent.arguments[0]?.type ===
|
|
161
|
+
responseAccessParent.type === AST_NODE_TYPES.CallExpression &&
|
|
162
|
+
responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression
|
|
163
163
|
) {
|
|
164
164
|
// map-like operation against responses, e.g. responses.map((response) => response.headers.etag)
|
|
165
165
|
responseHeadersAccesses.push(
|
|
@@ -174,8 +174,8 @@ function getResponseHeadersAccesses(
|
|
|
174
174
|
|
|
175
175
|
if (
|
|
176
176
|
responseAccess.computed &&
|
|
177
|
-
responseAccess.property.type ===
|
|
178
|
-
responseAccessParent.type ===
|
|
177
|
+
responseAccess.property.type === AST_NODE_TYPES.Literal &&
|
|
178
|
+
responseAccessParent.type === AST_NODE_TYPES.MemberExpression
|
|
179
179
|
) {
|
|
180
180
|
// header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc.
|
|
181
181
|
responseHeadersAccesses.push(responseAccessParent);
|
|
@@ -187,7 +187,9 @@ function getResponseHeadersAccesses(
|
|
|
187
187
|
return responseHeadersAccesses;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
const
|
|
190
|
+
const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
|
|
191
|
+
const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shouldUseHeaderGetter'> = createRule({
|
|
192
|
+
name: ruleId,
|
|
191
193
|
meta: {
|
|
192
194
|
type: 'suggestion',
|
|
193
195
|
docs: {
|
|
@@ -202,14 +204,15 @@ const rule: Rule.RuleModule = {
|
|
|
202
204
|
fixable: 'code',
|
|
203
205
|
schema: [],
|
|
204
206
|
},
|
|
205
|
-
|
|
207
|
+
defaultOptions: [],
|
|
206
208
|
create(context) {
|
|
207
209
|
const sourceCode = context.sourceCode;
|
|
208
210
|
const scopeManager = sourceCode.scopeManager;
|
|
211
|
+
assert.ok(scopeManager);
|
|
209
212
|
|
|
210
213
|
return {
|
|
211
214
|
'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (
|
|
212
|
-
fixtureCall: CallExpression,
|
|
215
|
+
fixtureCall: TSESTree.CallExpression,
|
|
213
216
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
214
217
|
) => {
|
|
215
218
|
try {
|
|
@@ -222,9 +225,8 @@ const rule: Rule.RuleModule = {
|
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
|
-
assert.ok(fixtureCall.type === 'CallExpression');
|
|
226
228
|
const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
|
|
227
|
-
assert.ok(fixtureFunction.type ===
|
|
229
|
+
assert.ok(fixtureFunction.type === AST_NODE_TYPES.MemberExpression);
|
|
228
230
|
const indentation = getIndentation(fixtureCall, sourceCode);
|
|
229
231
|
|
|
230
232
|
const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
|
|
@@ -239,7 +241,7 @@ const rule: Rule.RuleModule = {
|
|
|
239
241
|
|
|
240
242
|
// fetch request argument
|
|
241
243
|
const methodNode = fixtureFunction.property; // get/put/etc.
|
|
242
|
-
assert.ok(methodNode.type ===
|
|
244
|
+
assert.ok(methodNode.type === AST_NODE_TYPES.Identifier);
|
|
243
245
|
const fetchRequestArgumentLines = [
|
|
244
246
|
'{',
|
|
245
247
|
` method: '${methodNode.name.toUpperCase()}',`,
|
|
@@ -252,7 +254,7 @@ const rule: Rule.RuleModule = {
|
|
|
252
254
|
...fixtureCallInformation.requestHeaders.map(
|
|
253
255
|
({ name, value }) =>
|
|
254
256
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
|
|
255
|
-
` ${name.type ===
|
|
257
|
+
` ${name.type === AST_NODE_TYPES.Literal ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
|
|
256
258
|
),
|
|
257
259
|
` },`,
|
|
258
260
|
]
|
|
@@ -306,7 +308,7 @@ const rule: Rule.RuleModule = {
|
|
|
306
308
|
for (const responseHeadersAccess of responseHeadersAccesses) {
|
|
307
309
|
if (isInvalidResponseHeadersAccess(responseHeadersAccess)) {
|
|
308
310
|
const headerAccess = getParent(responseHeadersAccess);
|
|
309
|
-
if (headerAccess?.type ===
|
|
311
|
+
if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) {
|
|
310
312
|
const headerNameNode = headerAccess.property;
|
|
311
313
|
const headerName = headerAccess.computed
|
|
312
314
|
? sourceCode.getText(headerNameNode)
|
|
@@ -321,8 +323,8 @@ const rule: Rule.RuleModule = {
|
|
|
321
323
|
},
|
|
322
324
|
});
|
|
323
325
|
} else if (
|
|
324
|
-
headerAccess?.type ===
|
|
325
|
-
responseHeadersAccess.property.type ===
|
|
326
|
+
headerAccess?.type === AST_NODE_TYPES.CallExpression &&
|
|
327
|
+
responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier &&
|
|
326
328
|
responseHeadersAccess.property.name === 'get'
|
|
327
329
|
) {
|
|
328
330
|
const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`;
|
|
@@ -352,6 +354,6 @@ const rule: Rule.RuleModule = {
|
|
|
352
354
|
},
|
|
353
355
|
};
|
|
354
356
|
},
|
|
355
|
-
};
|
|
357
|
+
});
|
|
356
358
|
|
|
357
359
|
export default rule;
|
package/src/agent/fetch.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// agent/fetch.ts
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
|
|
4
4
|
|
|
5
|
-
import { getParent, isBlockStatement } from '../library/tree';
|
|
5
|
+
import { getParent, isBlockStatement } from '../library/ts-tree';
|
|
6
6
|
|
|
7
7
|
export function getResponseBodyRetrievalText(responseVariableName: string) {
|
|
8
8
|
return `await ${responseVariableName}.json()`;
|
|
@@ -12,29 +12,29 @@ export function getResponseHeadersRetrievalText(responseVariableName: string) {
|
|
|
12
12
|
return `${responseVariableName}.headers`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean {
|
|
15
|
+
export function isInvalidResponseHeadersAccess(responseHeadersAccess: TSESTree.Node): boolean {
|
|
16
16
|
const responseHeaderAccessParent = getParent(responseHeadersAccess);
|
|
17
|
-
if (responseHeaderAccessParent?.type ===
|
|
17
|
+
if (responseHeaderAccessParent?.type === AST_NODE_TYPES.VariableDeclarator) {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
if (
|
|
22
|
-
responseHeaderAccessParent?.type ===
|
|
23
|
-
responseHeaderAccessParent.callee.type ===
|
|
24
|
-
responseHeaderAccessParent.callee.property.type ===
|
|
22
|
+
responseHeaderAccessParent?.type === AST_NODE_TYPES.CallExpression &&
|
|
23
|
+
responseHeaderAccessParent.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
24
|
+
responseHeaderAccessParent.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
25
25
|
responseHeaderAccessParent.callee.property.name === 'get'
|
|
26
26
|
) {
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
return !(
|
|
31
|
-
responseHeaderAccessParent?.type ===
|
|
32
|
-
responseHeaderAccessParent.property.type ===
|
|
31
|
+
responseHeaderAccessParent?.type === AST_NODE_TYPES.MemberExpression &&
|
|
32
|
+
responseHeaderAccessParent.property.type === AST_NODE_TYPES.Identifier &&
|
|
33
33
|
responseHeaderAccessParent.property.name === 'get'
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function hasAssertions(fixtureCall: Node): boolean {
|
|
37
|
+
export function hasAssertions(fixtureCall: TSESTree.Node): boolean {
|
|
38
38
|
if (isBlockStatement(fixtureCall)) {
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
@@ -45,10 +45,10 @@ export function hasAssertions(fixtureCall: Node): boolean {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (
|
|
48
|
-
parent.type ===
|
|
49
|
-
parent.property.type ===
|
|
48
|
+
parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
49
|
+
parent.property.type === AST_NODE_TYPES.Identifier &&
|
|
50
50
|
parent.property.name === 'expect' &&
|
|
51
|
-
getParent(parent)?.type ===
|
|
51
|
+
getParent(parent)?.type === AST_NODE_TYPES.CallExpression
|
|
52
52
|
) {
|
|
53
53
|
return true;
|
|
54
54
|
}
|