@checkdigit/eslint-plugin 6.6.0-PR.75-9891 → 6.6.0-PR.75-66d8
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 +135 -45
- package/dist-cjs/metafile.json +40 -24
- package/dist-mjs/fixture/concurrent-promises.mjs +74 -4
- package/dist-mjs/fixture/fetch-header-getter.mjs +24 -17
- package/dist-mjs/fixture/fetch.mjs +14 -2
- package/dist-mjs/fixture/no-fixture.mjs +6 -1
- package/dist-mjs/fixture/response-reference.mjs +4 -4
- package/dist-types/fixture/fetch.d.ts +2 -0
- package/package.json +1 -1
- package/src/fixture/concurrent-promises.ts +109 -10
- package/src/fixture/fetch-header-getter.ts +27 -26
- package/src/fixture/fetch.ts +25 -0
- package/src/fixture/no-fixture.ts +6 -0
- package/src/fixture/response-reference.ts +3 -11
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* This code is licensed under the MIT license (see LICENSE.txt for details).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { CallExpression, Expression, SimpleCallExpression } from 'estree';
|
|
10
|
-
import { type Rule, SourceCode } from 'eslint';
|
|
9
|
+
import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree';
|
|
10
|
+
import { type Rule, type Scope, SourceCode } from 'eslint';
|
|
11
|
+
import { getEnclosingStatement, getParent } from '../ast/tree';
|
|
11
12
|
import { strict as assert } from 'node:assert';
|
|
12
13
|
import getDocumentationUrl from '../get-documentation-url';
|
|
13
14
|
import { getIndentation } from '../ast/format';
|
|
14
|
-
import {
|
|
15
|
+
import { isInvalidResponseHeadersAccess } from './fetch';
|
|
15
16
|
import { isValidPropertyName } from './variable';
|
|
16
17
|
import { replaceEndpointUrlPrefixWithBasePath } from './url';
|
|
17
18
|
|
|
@@ -131,6 +132,54 @@ function createResponseAssertions(
|
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
function getResponseHeadersAccesses(
|
|
136
|
+
responseVariables: Scope.Variable[],
|
|
137
|
+
scopeManager: Scope.ScopeManager,
|
|
138
|
+
sourceCode: SourceCode,
|
|
139
|
+
) {
|
|
140
|
+
const responseHeadersAccesses: MemberExpression[] = [];
|
|
141
|
+
for (const responseVariable of responseVariables) {
|
|
142
|
+
for (const responseReference of responseVariable.references) {
|
|
143
|
+
const responseAccess = getParent(responseReference.identifier);
|
|
144
|
+
if (!responseAccess || responseAccess.type !== 'MemberExpression') {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const responseAccessParent = getParent(responseAccess);
|
|
149
|
+
if (!responseAccessParent) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
responseAccessParent.type === 'CallExpression' &&
|
|
155
|
+
responseAccessParent.arguments[0]?.type === 'ArrowFunctionExpression'
|
|
156
|
+
) {
|
|
157
|
+
// map-like operation against responses, e.g. responses.map((response) => response.headers.etag)
|
|
158
|
+
responseHeadersAccesses.push(
|
|
159
|
+
...getResponseHeadersAccesses(
|
|
160
|
+
scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]),
|
|
161
|
+
scopeManager,
|
|
162
|
+
sourceCode,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
responseAccess.computed &&
|
|
170
|
+
responseAccess.property.type === 'Literal' &&
|
|
171
|
+
responseAccessParent.type === 'MemberExpression'
|
|
172
|
+
) {
|
|
173
|
+
// header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc.
|
|
174
|
+
responseHeadersAccesses.push(responseAccessParent);
|
|
175
|
+
} else {
|
|
176
|
+
responseHeadersAccesses.push(responseAccess);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return responseHeadersAccesses;
|
|
181
|
+
}
|
|
182
|
+
|
|
134
183
|
const rule: Rule.RuleModule = {
|
|
135
184
|
meta: {
|
|
136
185
|
type: 'suggestion',
|
|
@@ -140,6 +189,7 @@ const rule: Rule.RuleModule = {
|
|
|
140
189
|
},
|
|
141
190
|
messages: {
|
|
142
191
|
preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
|
|
192
|
+
shouldUseHeaderGetter: 'Getter should be used to access response headers.',
|
|
143
193
|
unknownError:
|
|
144
194
|
'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
|
|
145
195
|
},
|
|
@@ -149,6 +199,7 @@ const rule: Rule.RuleModule = {
|
|
|
149
199
|
// eslint-disable-next-line max-lines-per-function
|
|
150
200
|
create(context) {
|
|
151
201
|
const sourceCode = context.sourceCode;
|
|
202
|
+
const scopeManager = sourceCode.scopeManager;
|
|
152
203
|
|
|
153
204
|
return {
|
|
154
205
|
// eslint-disable-next-line max-lines-per-function
|
|
@@ -207,13 +258,15 @@ const rule: Rule.RuleModule = {
|
|
|
207
258
|
...(statusAssertion !== undefined ? [statusAssertion] : []),
|
|
208
259
|
...nonStatusAssertions,
|
|
209
260
|
].join(`;\n${indentation}`);
|
|
210
|
-
const replacementText =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
const replacementText = fixtureCallInformation.assertions
|
|
262
|
+
? [
|
|
263
|
+
disableLintComment,
|
|
264
|
+
`${fetchCallText}.then((${responseVariableNameToUse}) => {`,
|
|
265
|
+
appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
|
|
266
|
+
` return ${responseVariableNameToUse};`,
|
|
267
|
+
`})`,
|
|
268
|
+
].join(`\n${indentation}`)
|
|
269
|
+
: fetchCallText;
|
|
217
270
|
|
|
218
271
|
context.report({
|
|
219
272
|
node: fixtureCall,
|
|
@@ -222,6 +275,52 @@ const rule: Rule.RuleModule = {
|
|
|
222
275
|
return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
|
|
223
276
|
},
|
|
224
277
|
});
|
|
278
|
+
|
|
279
|
+
const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode);
|
|
280
|
+
if (!responsesVariable) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
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})`;
|
|
299
|
+
|
|
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])})`;
|
|
313
|
+
|
|
314
|
+
context.report({
|
|
315
|
+
node: headerAccess,
|
|
316
|
+
messageId: 'shouldUseHeaderGetter',
|
|
317
|
+
fix(fixer) {
|
|
318
|
+
return fixer.replaceText(headerAccess, headerAccessReplacementText);
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
225
324
|
} catch (error) {
|
|
226
325
|
// eslint-disable-next-line no-console
|
|
227
326
|
console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* This code is licensed under the MIT license (see LICENSE.txt for details).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { Identifier,
|
|
9
|
+
import type { Identifier, VariableDeclarator } from 'estree';
|
|
10
10
|
import type { Rule } from 'eslint';
|
|
11
11
|
import { analyzeResponseReferences } from './response-reference';
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
13
|
import getDocumentationUrl from '../get-documentation-url';
|
|
14
14
|
import { getParent } from '../ast/tree';
|
|
15
|
+
import { isInvalidResponseHeadersAccess } from './fetch';
|
|
15
16
|
|
|
16
17
|
export const ruleId = 'fetch-header-getter';
|
|
17
18
|
|
|
@@ -42,12 +43,13 @@ const rule: Rule.RuleModule = {
|
|
|
42
43
|
);
|
|
43
44
|
assert.ok(responseVariable);
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const directHeadersReferences = responseHeadersReferences.filter((headersReference) => {
|
|
47
|
+
const headersAccess = getParent(headersReference);
|
|
48
|
+
return headersAccess?.type !== 'VariableDeclarator';
|
|
49
|
+
});
|
|
48
50
|
|
|
49
|
-
const
|
|
50
|
-
.map(
|
|
51
|
+
const indirectHeadersReferences = responseHeadersReferences
|
|
52
|
+
.map(getParent)
|
|
51
53
|
.filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator')
|
|
52
54
|
.map((declarator) => (declarator.id as Identifier).name)
|
|
53
55
|
.map((redefinedHeadersVariableName) => {
|
|
@@ -55,32 +57,31 @@ const rule: Rule.RuleModule = {
|
|
|
55
57
|
const identifier = variable.identifiers[0];
|
|
56
58
|
return identifier?.type === 'Identifier' && identifier.name === redefinedHeadersVariableName;
|
|
57
59
|
});
|
|
58
|
-
return (
|
|
59
|
-
headersVariable?.references
|
|
60
|
-
.map((reference) => getParent(reference.identifier))
|
|
61
|
-
.filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression') ?? []
|
|
62
|
-
);
|
|
60
|
+
return headersVariable?.references.map((reference) => reference.identifier) ?? [];
|
|
63
61
|
})
|
|
64
62
|
.flat();
|
|
65
63
|
|
|
66
|
-
const
|
|
67
|
-
|
|
64
|
+
const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].filter(
|
|
65
|
+
isInvalidResponseHeadersAccess,
|
|
68
66
|
);
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
invalidHeadersReferences.forEach((headersReference) => {
|
|
69
|
+
const headerAccess = getParent(headersReference);
|
|
70
|
+
if (headerAccess?.type === 'MemberExpression') {
|
|
71
|
+
const headerNameNode = headerAccess.property;
|
|
72
|
+
const headerName = headerAccess.computed
|
|
73
|
+
? sourceCode.getText(headerNameNode)
|
|
74
|
+
: `'${sourceCode.getText(headerNameNode)}'`;
|
|
75
|
+
const replacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`;
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
context.report({
|
|
78
|
+
node: headerAccess,
|
|
79
|
+
messageId: 'shouldUseHeaderGetter',
|
|
80
|
+
fix(fixer) {
|
|
81
|
+
return fixer.replaceText(headerAccess, replacementText);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
84
85
|
});
|
|
85
86
|
},
|
|
86
87
|
};
|
package/src/fixture/fetch.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
// fixture/fetch.ts
|
|
2
2
|
|
|
3
|
+
import type { Node } from 'estree';
|
|
4
|
+
import { getParent } from '../ast/tree';
|
|
5
|
+
|
|
3
6
|
export function getResponseBodyRetrievalText(responseVariableName: string) {
|
|
4
7
|
return `await ${responseVariableName}.json()`;
|
|
5
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
|
+
}
|
|
@@ -37,6 +37,7 @@ interface FixtureCallInformation {
|
|
|
37
37
|
assertions?: Expression[][];
|
|
38
38
|
inlineStatementNode?: Node;
|
|
39
39
|
inlineBodyReference?: MemberExpression;
|
|
40
|
+
isConcurrent?: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// recursively analyze the fixture/supertest call chain to collect information of request/response
|
|
@@ -49,6 +50,8 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
|
|
|
49
50
|
// direct return, no variable declaration or await
|
|
50
51
|
results.fixtureNode = call;
|
|
51
52
|
results.rootNode = parent;
|
|
53
|
+
} else if (parent.type === 'ArrayExpression') {
|
|
54
|
+
results.isConcurrent = true;
|
|
52
55
|
} else if (parent.type === 'AwaitExpression') {
|
|
53
56
|
results.fixtureNode = call;
|
|
54
57
|
const enclosingStatement = getEnclosingStatement(parent);
|
|
@@ -245,6 +248,9 @@ const rule: Rule.RuleModule = {
|
|
|
245
248
|
|
|
246
249
|
const fixtureCallInformation = {} as FixtureCallInformation;
|
|
247
250
|
analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
|
|
251
|
+
if (fixtureCallInformation.isConcurrent === true) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
248
254
|
|
|
249
255
|
const {
|
|
250
256
|
variable: responseVariable,
|
|
@@ -52,27 +52,19 @@ export function analyzeResponseReferences(
|
|
|
52
52
|
// e.g. response.body
|
|
53
53
|
results.bodyReferences = responseReferences.filter(
|
|
54
54
|
(node): node is MemberExpression =>
|
|
55
|
-
node
|
|
56
|
-
node !== undefined &&
|
|
57
|
-
node.type === 'MemberExpression' &&
|
|
58
|
-
node.property.type === 'Identifier' &&
|
|
59
|
-
node.property.name === 'body',
|
|
55
|
+
node?.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'body',
|
|
60
56
|
);
|
|
61
57
|
// e.g. response.headers / response.header / response.get()
|
|
62
58
|
results.headersReferences = responseReferences.filter(
|
|
63
59
|
(node): node is MemberExpression =>
|
|
64
|
-
node
|
|
65
|
-
node !== undefined &&
|
|
66
|
-
node.type === 'MemberExpression' &&
|
|
60
|
+
node?.type === 'MemberExpression' &&
|
|
67
61
|
node.property.type === 'Identifier' &&
|
|
68
62
|
(node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
|
|
69
63
|
);
|
|
70
64
|
// e.g. response.status / response.statusCode
|
|
71
65
|
results.statusReferences = responseReferences.filter(
|
|
72
66
|
(node): node is MemberExpression =>
|
|
73
|
-
node
|
|
74
|
-
node !== undefined &&
|
|
75
|
-
node.type === 'MemberExpression' &&
|
|
67
|
+
node?.type === 'MemberExpression' &&
|
|
76
68
|
node.property.type === 'Identifier' &&
|
|
77
69
|
(node.property.name === 'status' || node.property.name === 'statusCode'),
|
|
78
70
|
);
|