@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.
@@ -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 { getParent } from '../ast/tree';
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
- disableLintComment,
212
- `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
213
- appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
214
- ` return ${responseVariableNameToUse};`,
215
- `})`,
216
- ].join(`\n${indentation}`);
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, MemberExpression, VariableDeclarator } from 'estree';
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 directHeaderReferences = responseHeadersReferences
46
- .map(getParent)
47
- .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression');
46
+ const directHeadersReferences = responseHeadersReferences.filter((headersReference) => {
47
+ const headersAccess = getParent(headersReference);
48
+ return headersAccess?.type !== 'VariableDeclarator';
49
+ });
48
50
 
49
- const indirectHeaderReferences = responseHeadersReferences
50
- .map((reference) => getParent(reference))
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 invalidHeaderReferences = [...directHeaderReferences, ...indirectHeaderReferences].filter(
67
- (reference) => !(reference.property.type === 'Identifier' && reference.property.name === 'get'),
64
+ const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].filter(
65
+ isInvalidResponseHeadersAccess,
68
66
  );
69
67
 
70
- invalidHeaderReferences.forEach((reference) => {
71
- const headerNameNode = reference.property;
72
- const headerName = reference.computed
73
- ? sourceCode.getText(headerNameNode)
74
- : `'${sourceCode.getText(headerNameNode)}'`;
75
- const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`;
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
- context.report({
78
- node: reference,
79
- messageId: 'shouldUseHeaderGetter',
80
- fix(fixer) {
81
- return fixer.replaceText(reference, replacementText);
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
  };
@@ -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 !== null &&
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 !== null &&
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 !== null &&
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
  );