@checkdigit/eslint-plugin 6.6.0-PR.75-f33d → 6.6.0-PR.75-9891

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.
@@ -0,0 +1,242 @@
1
+ // fixture/concurrent-promises.ts
2
+
3
+ /*
4
+ * Copyright (c) 2021-2024 Check Digit, LLC
5
+ *
6
+ * This code is licensed under the MIT license (see LICENSE.txt for details).
7
+ */
8
+
9
+ import type { CallExpression, Expression, SimpleCallExpression } from 'estree';
10
+ import { type Rule, SourceCode } from 'eslint';
11
+ import { strict as assert } from 'node:assert';
12
+ import getDocumentationUrl from '../get-documentation-url';
13
+ import { getIndentation } from '../ast/format';
14
+ import { getParent } from '../ast/tree';
15
+ import { isValidPropertyName } from './variable';
16
+ import { replaceEndpointUrlPrefixWithBasePath } from './url';
17
+
18
+ export const ruleId = 'concurrent-promises';
19
+
20
+ interface FixtureCallInformation {
21
+ fixtureNode: SimpleCallExpression;
22
+ requestBody?: Expression;
23
+ requestHeaders?: { name: Expression; value: Expression }[];
24
+ assertions?: Expression[][];
25
+ }
26
+
27
+ // recursively analyze the fixture/supertest call chain to collect information of request/response
28
+ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) {
29
+ const parent = getParent(call);
30
+ if (!parent) {
31
+ return;
32
+ }
33
+
34
+ let nextCall;
35
+ if (parent.type === 'ArrayExpression') {
36
+ results.fixtureNode = call;
37
+ } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') {
38
+ if (parent.property.name === 'expect') {
39
+ // supertest assertions
40
+ const assertionCall = getParent(parent);
41
+ assert.ok(assertionCall && assertionCall.type === 'CallExpression');
42
+ results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]];
43
+ nextCall = assertionCall;
44
+ } else if (parent.property.name === 'send') {
45
+ // request body
46
+ const sendRequestBodyCall = getParent(parent);
47
+ assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression');
48
+ results.requestBody = sendRequestBodyCall.arguments[0] as Expression;
49
+ nextCall = sendRequestBodyCall;
50
+ } else if (parent.property.name === 'set') {
51
+ // request headers
52
+ const setRequestHeaderCall = getParent(parent);
53
+ assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression');
54
+ const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression];
55
+ results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }];
56
+ nextCall = setRequestHeaderCall;
57
+ }
58
+ } else {
59
+ throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
60
+ }
61
+ if (nextCall) {
62
+ analyzeFixtureCall(nextCall, results, sourceCode);
63
+ }
64
+ }
65
+
66
+ // eslint-disable-next-line sonarjs/cognitive-complexity
67
+ function createResponseAssertions(
68
+ fixtureCallInformation: FixtureCallInformation,
69
+ sourceCode: SourceCode,
70
+ responseVariableName: string,
71
+ ) {
72
+ let statusAssertion: string | undefined;
73
+ const nonStatusAssertions: string[] = [];
74
+ for (const expectArguments of fixtureCallInformation.assertions ?? []) {
75
+ if (expectArguments.length === 1) {
76
+ const [assertionArgument] = expectArguments;
77
+ assert.ok(assertionArgument);
78
+ if (
79
+ (assertionArgument.type === 'MemberExpression' &&
80
+ assertionArgument.object.type === 'Identifier' &&
81
+ assertionArgument.object.name === 'StatusCodes') ||
82
+ assertionArgument.type === 'Literal' ||
83
+ sourceCode.getText(assertionArgument).includes('StatusCodes.')
84
+ ) {
85
+ // status code assertion
86
+ statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`;
87
+ } else if (assertionArgument.type === 'ArrowFunctionExpression') {
88
+ // callback assertion using arrow function
89
+ let functionBody = sourceCode.getText(assertionArgument.body);
90
+
91
+ const [originalResponseArgument] = assertionArgument.params;
92
+ assert.ok(originalResponseArgument?.type === 'Identifier');
93
+ const originalResponseArgumentName = originalResponseArgument.name;
94
+ if (originalResponseArgumentName !== responseVariableName) {
95
+ functionBody = functionBody.replace(
96
+ new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'),
97
+ responseVariableName,
98
+ );
99
+ }
100
+ nonStatusAssertions.push(`assert.ok(${functionBody})`);
101
+ } else if (assertionArgument.type === 'Identifier') {
102
+ // callback assertion using function reference
103
+ nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
104
+ } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
105
+ // body deep equal assertion
106
+ nonStatusAssertions.push(
107
+ `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`,
108
+ );
109
+ } else {
110
+ throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`);
111
+ }
112
+ } else if (expectArguments.length === 2) {
113
+ // header assertion
114
+ const [headerName, headerValue] = expectArguments;
115
+ assert.ok(headerName && headerValue);
116
+ const headersReference = `${responseVariableName}.headers`;
117
+ if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) {
118
+ nonStatusAssertions.push(
119
+ `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`,
120
+ );
121
+ } else {
122
+ nonStatusAssertions.push(
123
+ `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`,
124
+ );
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ statusAssertion,
130
+ nonStatusAssertions,
131
+ };
132
+ }
133
+
134
+ const rule: Rule.RuleModule = {
135
+ meta: {
136
+ type: 'suggestion',
137
+ docs: {
138
+ description: 'Prefer native fetch API over customized fixture API.',
139
+ url: getDocumentationUrl(ruleId),
140
+ },
141
+ messages: {
142
+ preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
143
+ unknownError:
144
+ 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
145
+ },
146
+ fixable: 'code',
147
+ schema: [],
148
+ },
149
+ // eslint-disable-next-line max-lines-per-function
150
+ create(context) {
151
+ const sourceCode = context.sourceCode;
152
+
153
+ return {
154
+ // eslint-disable-next-line max-lines-per-function
155
+ 'CallExpression[callee.object.name="Promise"] > ArrayExpression CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]':
156
+ (fixtureCall: CallExpression) => {
157
+ try {
158
+ assert.ok(fixtureCall.type === 'CallExpression');
159
+ const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get
160
+ assert.ok(fixtureFunction.type === 'MemberExpression');
161
+ const indentation = getIndentation(fixtureCall, sourceCode);
162
+
163
+ const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping`
164
+ assert.ok(urlArgumentNode !== undefined);
165
+
166
+ const fixtureCallInformation = {} as FixtureCallInformation;
167
+ analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode);
168
+
169
+ // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
170
+ const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
171
+ const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText);
172
+
173
+ // fetch request argument
174
+ const methodNode = fixtureFunction.property; // get/put/etc.
175
+ assert.ok(methodNode.type === 'Identifier');
176
+ const fetchRequestArgumentLines = [
177
+ '{',
178
+ ` method: '${methodNode.name.toUpperCase()}',`,
179
+ ...(fixtureCallInformation.requestBody
180
+ ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`]
181
+ : []),
182
+ ...(fixtureCallInformation.requestHeaders
183
+ ? [
184
+ ` headers: {`,
185
+ ...fixtureCallInformation.requestHeaders.map(
186
+ ({ name, value }) =>
187
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals
188
+ ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`,
189
+ ),
190
+ ` },`,
191
+ ]
192
+ : []),
193
+ '}',
194
+ ].join(`\n${indentation}`);
195
+
196
+ const responseVariableNameToUse = 'res';
197
+ const { statusAssertion, nonStatusAssertions } = createResponseAssertions(
198
+ fixtureCallInformation,
199
+ sourceCode,
200
+ responseVariableNameToUse,
201
+ );
202
+
203
+ // add variable declaration if needed
204
+ const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method';
205
+ const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
206
+ const appendingAssignmentAndAssertionText = [
207
+ ...(statusAssertion !== undefined ? [statusAssertion] : []),
208
+ ...nonStatusAssertions,
209
+ ].join(`;\n${indentation}`);
210
+ const replacementText = [
211
+ disableLintComment,
212
+ `${fetchCallText}.then((${responseVariableNameToUse}) => {`,
213
+ appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`,
214
+ ` return ${responseVariableNameToUse};`,
215
+ `})`,
216
+ ].join(`\n${indentation}`);
217
+
218
+ context.report({
219
+ node: fixtureCall,
220
+ messageId: 'preferNativeFetch',
221
+ fix(fixer) {
222
+ return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText);
223
+ },
224
+ });
225
+ } catch (error) {
226
+ // eslint-disable-next-line no-console
227
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
228
+ context.report({
229
+ node: fixtureCall,
230
+ messageId: 'unknownError',
231
+ data: {
232
+ fileName: context.filename,
233
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
234
+ },
235
+ });
236
+ }
237
+ },
238
+ };
239
+ },
240
+ };
241
+
242
+ export default rule;
@@ -0,0 +1,90 @@
1
+ // fixture/no-fixture.ts
2
+
3
+ /*
4
+ * Copyright (c) 2021-2024 Check Digit, LLC
5
+ *
6
+ * This code is licensed under the MIT license (see LICENSE.txt for details).
7
+ */
8
+
9
+ import type { Identifier, MemberExpression, VariableDeclarator } from 'estree';
10
+ import type { Rule } from 'eslint';
11
+ import { analyzeResponseReferences } from './response-reference';
12
+ import { strict as assert } from 'node:assert';
13
+ import getDocumentationUrl from '../get-documentation-url';
14
+ import { getParent } from '../ast/tree';
15
+
16
+ export const ruleId = 'fetch-header-getter';
17
+
18
+ const rule: Rule.RuleModule = {
19
+ meta: {
20
+ type: 'problem',
21
+ docs: {
22
+ description: 'Make sure getter is used to access response headers.',
23
+ url: getDocumentationUrl(ruleId),
24
+ },
25
+ messages: {
26
+ shouldUseHeaderGetter: 'Getter should be used to access response headers.',
27
+ },
28
+ fixable: 'code',
29
+ schema: [],
30
+ },
31
+ create(context) {
32
+ const sourceCode = context.sourceCode;
33
+ const scopeManager = sourceCode.scopeManager;
34
+
35
+ return {
36
+ 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => {
37
+ const variableDeclaration = getParent(fetchCall);
38
+ assert.ok(variableDeclaration?.type === 'VariableDeclaration');
39
+ const { variable: responseVariable, headersReferences: responseHeadersReferences } = analyzeResponseReferences(
40
+ variableDeclaration,
41
+ scopeManager,
42
+ );
43
+ assert.ok(responseVariable);
44
+
45
+ const directHeaderReferences = responseHeadersReferences
46
+ .map(getParent)
47
+ .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression');
48
+
49
+ const indirectHeaderReferences = responseHeadersReferences
50
+ .map((reference) => getParent(reference))
51
+ .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator')
52
+ .map((declarator) => (declarator.id as Identifier).name)
53
+ .map((redefinedHeadersVariableName) => {
54
+ const headersVariable = responseVariable.scope.variables.find((variable) => {
55
+ const identifier = variable.identifiers[0];
56
+ return identifier?.type === 'Identifier' && identifier.name === redefinedHeadersVariableName;
57
+ });
58
+ return (
59
+ headersVariable?.references
60
+ .map((reference) => getParent(reference.identifier))
61
+ .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression') ?? []
62
+ );
63
+ })
64
+ .flat();
65
+
66
+ const invalidHeaderReferences = [...directHeaderReferences, ...indirectHeaderReferences].filter(
67
+ (reference) => !(reference.property.type === 'Identifier' && reference.property.name === 'get'),
68
+ );
69
+
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})`;
76
+
77
+ context.report({
78
+ node: reference,
79
+ messageId: 'shouldUseHeaderGetter',
80
+ fix(fixer) {
81
+ return fixer.replaceText(reference, replacementText);
82
+ },
83
+ });
84
+ });
85
+ },
86
+ };
87
+ },
88
+ };
89
+
90
+ export default rule;
@@ -0,0 +1,5 @@
1
+ // fixture/fetch.ts
2
+
3
+ export function getResponseBodyRetrievalText(responseVariableName: string) {
4
+ return `await ${responseVariableName}.json()`;
5
+ }
@@ -1,4 +1,4 @@
1
- // no-fixture.ts
1
+ // fixture/no-fixture.ts
2
2
 
3
3
  /*
4
4
  * Copyright (c) 2021-2024 Check Digit, LLC
@@ -17,10 +17,14 @@ 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 { getEnclosingScopeNode, getEnclosingStatement, getParent } from '../ast/tree';
21
+ import { analyzeResponseReferences } from './response-reference';
21
22
  import { strict as assert } from 'node:assert';
22
- import getDocumentationUrl from './get-documentation-url';
23
- import { getIndentation } from './ast/format';
23
+ import getDocumentationUrl from '../get-documentation-url';
24
+ import { getIndentation } from '../ast/format';
25
+ import { getResponseBodyRetrievalText } from './fetch';
26
+ import { isValidPropertyName } from './variable';
27
+ import { replaceEndpointUrlPrefixWithBasePath } from './url';
24
28
 
25
29
  export const ruleId = 'no-fixture';
26
30
 
@@ -42,7 +46,7 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
42
46
 
43
47
  let nextCall;
44
48
  if (parent.type === 'ReturnStatement') {
45
- // direct return, no variable declaration / await
49
+ // direct return, no variable declaration or await
46
50
  results.fixtureNode = call;
47
51
  results.rootNode = parent;
48
52
  } else if (parent.type === 'AwaitExpression') {
@@ -84,113 +88,13 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
84
88
  nextCall = setRequestHeaderCall;
85
89
  }
86
90
  } else {
87
- throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}`);
91
+ throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`);
88
92
  }
89
93
  if (nextCall) {
90
94
  analyzeFixtureCall(nextCall, results, sourceCode);
91
95
  }
92
96
  }
93
97
 
94
- // analyze response related variables and their references0
95
- function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) {
96
- const results: {
97
- variable?: Scope.Variable;
98
- bodyReferences: MemberExpression[];
99
- headersReferences: MemberExpression[];
100
- statusReferences: MemberExpression[];
101
- destructuringBodyVariable?: Scope.Variable;
102
- destructuringHeadersVariable?: Scope.Variable;
103
- destructuringHeadersReferences?: MemberExpression[] | undefined;
104
- } = {
105
- bodyReferences: [],
106
- headersReferences: [],
107
- statusReferences: [],
108
- };
109
-
110
- if (fixtureInformation.variableDeclaration) {
111
- const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration);
112
- for (const responseVariable of responseVariables) {
113
- const scope = responseVariable.scope;
114
- const identifier = responseVariable.identifiers[0];
115
- assert.ok(identifier);
116
- const identifierParent = getParent(identifier);
117
- assert.ok(identifierParent);
118
- if (identifierParent.type === 'VariableDeclarator') {
119
- // e.g. const response = ...
120
- results.variable = responseVariable;
121
- const responseReferences = responseVariable.references.map((responseReference) =>
122
- getParent(responseReference.identifier),
123
- );
124
- // e.g. response.body
125
- results.bodyReferences = responseReferences.filter(
126
- (node): node is MemberExpression =>
127
- node !== null &&
128
- node !== undefined &&
129
- node.type === 'MemberExpression' &&
130
- node.property.type === 'Identifier' &&
131
- node.property.name === 'body',
132
- );
133
- // e.g. response.headers / response.header / response.get()
134
- results.headersReferences = responseReferences.filter(
135
- (node): node is MemberExpression =>
136
- node !== null &&
137
- node !== undefined &&
138
- node.type === 'MemberExpression' &&
139
- node.property.type === 'Identifier' &&
140
- (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
141
- );
142
- // e.g. response.status / response.statusCode
143
- results.statusReferences = responseReferences.filter(
144
- (node): node is MemberExpression =>
145
- node !== null &&
146
- node !== undefined &&
147
- node.type === 'MemberExpression' &&
148
- node.property.type === 'Identifier' &&
149
- (node.property.name === 'status' || node.property.name === 'statusCode'),
150
- );
151
- } else if (
152
- // body reference through destruction/renaming, e.g. "const { body } = ..."
153
- identifierParent.type === 'Property' &&
154
- identifierParent.key.type === 'Identifier' &&
155
- identifierParent.key.name === 'body'
156
- ) {
157
- results.destructuringBodyVariable = responseVariable;
158
- } else if (
159
- // header reference through destruction/renaming, e.g. "const { headers } = ..."
160
- identifierParent.type === 'Property' &&
161
- identifierParent.key.type === 'Identifier' &&
162
- identifierParent.key.name === 'headers'
163
- ) {
164
- results.destructuringHeadersVariable = responseVariable;
165
- results.destructuringHeadersReferences = scope.set
166
- .get(responseVariable.name)
167
- ?.references.map((reference) => reference.identifier)
168
- .map(getParent)
169
- .filter(
170
- (parent): parent is MemberExpression =>
171
- parent?.type === 'MemberExpression' &&
172
- parent.property.type === 'Identifier' &&
173
- parent.property.name !== 'get' &&
174
- getParent(parent)?.type !== 'CallExpression',
175
- );
176
- } else {
177
- throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
178
- }
179
- }
180
- }
181
- return results;
182
- }
183
-
184
- // `/sample-service/v1/ping` -> `${BASE_PATH}/ping`
185
- function replaceEndpointUrlPrefixWithBasePath(url: string) {
186
- // eslint-disable-next-line no-template-curly-in-string
187
- return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
188
- }
189
-
190
- function isValidPropertyName(name: unknown) {
191
- return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
192
- }
193
-
194
98
  // eslint-disable-next-line sonarjs/cognitive-complexity
195
99
  function createResponseAssertions(
196
100
  fixtureCallInformation: FixtureCallInformation,
@@ -304,10 +208,6 @@ function isResponseBodyRedefinition(responseBodyReference: MemberExpression): bo
304
208
  return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier';
305
209
  }
306
210
 
307
- function getResponseBodyRetrievalText(responseVariableName: string) {
308
- return `await ${responseVariableName}.json()`;
309
- }
310
-
311
211
  const rule: Rule.RuleModule = {
312
212
  meta: {
313
213
  type: 'suggestion',
@@ -353,8 +253,7 @@ const rule: Rule.RuleModule = {
353
253
  statusReferences: responseStatusReferences,
354
254
  destructuringBodyVariable: destructuringResponseBodyVariable,
355
255
  destructuringHeadersVariable: destructuringResponseHeadersVariable,
356
- // destructuringHeadersReferences: destructuringResponseHeadersReferences,
357
- } = analyzeResponseReferences(fixtureCallInformation, scopeManager);
256
+ } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager);
358
257
 
359
258
  // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping`
360
259
  const originalUrlArgumentText = sourceCode.getText(urlArgumentNode);
@@ -491,19 +390,6 @@ const rule: Rule.RuleModule = {
491
390
  assert.ok(headerName !== undefined);
492
391
  yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`);
493
392
  }
494
- // if (destructuringResponseHeadersVariable !== undefined) {
495
- // for (const destructuringResponseHeadersReference of destructuringResponseHeadersReferences ?? []) {
496
- // const headerNameNode = destructuringResponseHeadersReference.property;
497
- // const headerName = destructuringResponseHeadersReference.computed
498
- // ? sourceCode.getText(headerNameNode)
499
- // : `'${sourceCode.getText(headerNameNode)}'`;
500
-
501
- // yield fixer.replaceText(
502
- // destructuringResponseHeadersReference,
503
- // `${destructuringResponseHeadersVariable.name}.get(${headerName})`,
504
- // );
505
- // }
506
- // }
507
393
 
508
394
  // convert response.statusCode to response.status
509
395
  for (const responseStatusReference of responseStatusReferences) {
@@ -0,0 +1,108 @@
1
+ // fixture/response-reference.ts
2
+
3
+ /*
4
+ * Copyright (c) 2021-2024 Check Digit, LLC
5
+ *
6
+ * This code is licensed under the MIT license (see LICENSE.txt for details).
7
+ */
8
+
9
+ import type { MemberExpression, VariableDeclaration } from 'estree';
10
+ import { type Scope } from 'eslint';
11
+ import { strict as assert } from 'node:assert';
12
+ import { getParent } from '../ast/tree';
13
+
14
+ /**
15
+ * analyze response related variables and their references
16
+ * the implementation is for fixture API, but it can be used for fetch API as well since the tree structure is similar
17
+ * @param variableDeclaration - variable declaration node
18
+ */
19
+ export function analyzeResponseReferences(
20
+ variableDeclaration: VariableDeclaration | undefined,
21
+ scopeManager: Scope.ScopeManager,
22
+ ) {
23
+ const results: {
24
+ variable?: Scope.Variable;
25
+ bodyReferences: MemberExpression[];
26
+ headersReferences: MemberExpression[];
27
+ statusReferences: MemberExpression[];
28
+ destructuringBodyVariable?: Scope.Variable;
29
+ destructuringHeadersVariable?: Scope.Variable;
30
+ destructuringHeadersReferences?: MemberExpression[] | undefined;
31
+ } = {
32
+ bodyReferences: [],
33
+ headersReferences: [],
34
+ statusReferences: [],
35
+ };
36
+ if (!variableDeclaration) {
37
+ return results;
38
+ }
39
+
40
+ const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration);
41
+ for (const responseVariable of responseVariables) {
42
+ const identifier = responseVariable.identifiers[0];
43
+ assert.ok(identifier);
44
+ const identifierParent = getParent(identifier);
45
+ assert.ok(identifierParent);
46
+ if (identifierParent.type === 'VariableDeclarator') {
47
+ // e.g. const response = ...
48
+ results.variable = responseVariable;
49
+ const responseReferences = responseVariable.references.map((responseReference) =>
50
+ getParent(responseReference.identifier),
51
+ );
52
+ // e.g. response.body
53
+ results.bodyReferences = responseReferences.filter(
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',
60
+ );
61
+ // e.g. response.headers / response.header / response.get()
62
+ results.headersReferences = responseReferences.filter(
63
+ (node): node is MemberExpression =>
64
+ node !== null &&
65
+ node !== undefined &&
66
+ node.type === 'MemberExpression' &&
67
+ node.property.type === 'Identifier' &&
68
+ (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'),
69
+ );
70
+ // e.g. response.status / response.statusCode
71
+ results.statusReferences = responseReferences.filter(
72
+ (node): node is MemberExpression =>
73
+ node !== null &&
74
+ node !== undefined &&
75
+ node.type === 'MemberExpression' &&
76
+ node.property.type === 'Identifier' &&
77
+ (node.property.name === 'status' || node.property.name === 'statusCode'),
78
+ );
79
+ } else if (
80
+ // body reference through destruction/renaming, e.g. "const { body } = ..."
81
+ identifierParent.type === 'Property' &&
82
+ identifierParent.key.type === 'Identifier' &&
83
+ identifierParent.key.name === 'body'
84
+ ) {
85
+ results.destructuringBodyVariable = responseVariable;
86
+ } else if (
87
+ // header reference through destruction/renaming, e.g. "const { headers } = ..."
88
+ identifierParent.type === 'Property' &&
89
+ identifierParent.key.type === 'Identifier' &&
90
+ identifierParent.key.name === 'headers'
91
+ ) {
92
+ results.destructuringHeadersVariable = responseVariable;
93
+ results.destructuringHeadersReferences = responseVariable.references
94
+ .map((reference) => reference.identifier)
95
+ .map(getParent)
96
+ .filter(
97
+ (parent): parent is MemberExpression =>
98
+ parent?.type === 'MemberExpression' &&
99
+ parent.property.type === 'Identifier' &&
100
+ parent.property.name !== 'get' &&
101
+ getParent(parent)?.type !== 'CallExpression',
102
+ );
103
+ } else {
104
+ throw new Error(`Unknown response variable reference: ${responseVariable.name}`);
105
+ }
106
+ }
107
+ return results;
108
+ }
@@ -0,0 +1,6 @@
1
+ // fixture/url.ts
2
+
3
+ export function replaceEndpointUrlPrefixWithBasePath(url: string) {
4
+ // eslint-disable-next-line no-template-curly-in-string
5
+ return url.replace(/`\/\w+(?<parts>-\w+)*\/v\d+\//u, '`${BASE_PATH}/');
6
+ }
@@ -0,0 +1,5 @@
1
+ // fixture/variable.ts
2
+
3
+ export function isValidPropertyName(name: unknown) {
4
+ return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name);
5
+ }
package/src/index.ts CHANGED
@@ -6,9 +6,10 @@
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
+ import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter';
9
11
  import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify';
10
- import noFixture, { ruleId as noFixtureRuleId } from './no-fixture';
11
- import noFixtureHeaders, { ruleId as noFixtureHeadersRuleId } from './no-fixture-headers';
12
+ import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture';
12
13
  import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method';
13
14
  import filePathComment from './file-path-comment';
14
15
  import noCardNumbers from './no-card-numbers';
@@ -34,7 +35,8 @@ export default {
34
35
  [invalidJsonStringifyRuleId]: invalidJsonStringify,
35
36
  [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod,
36
37
  [noFixtureRuleId]: noFixture,
37
- [noFixtureHeadersRuleId]: noFixtureHeaders,
38
+ [fetchHeaderGetterRuleId]: fetchHeaderGetter,
39
+ [concurrentPromisesRuleId]: concurrentPromises,
38
40
  },
39
41
  configs: {
40
42
  all: {
@@ -51,7 +53,8 @@ export default {
51
53
  [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error',
52
54
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
53
55
  [`@checkdigit/${noFixtureRuleId}`]: 'error',
54
- [`@checkdigit/${noFixtureHeadersRuleId}`]: 'error',
56
+ [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error',
57
+ [`@checkdigit/${concurrentPromisesRuleId}`]: 'error',
55
58
  },
56
59
  },
57
60
  recommended: {