@checkdigit/eslint-plugin 7.3.0-PR.75-1f63 → 7.3.0-PR.75-743b

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.
@@ -1,4 +1,4 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
2
  export declare const ruleId = "fetch-response-body-json";
3
- declare const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'>;
3
+ declare const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'refactorNeeded'>;
4
4
  export default rule;
@@ -1,4 +1,5 @@
1
1
  import type { Node } from 'estree';
2
2
  export declare function getResponseBodyRetrievalText(responseVariableName: string): string;
3
+ export declare function getResponseHeadersRetrievalText(responseVariableName: string): string;
3
4
  export declare function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean;
4
5
  export declare function hasAssertions(fixtureCall: Node): boolean;
@@ -11,6 +11,6 @@ export declare function analyzeResponseReferences(variableDeclaration: VariableD
11
11
  headersReferences: MemberExpression[];
12
12
  statusReferences: MemberExpression[];
13
13
  destructuringBodyVariable?: Scope.Variable | ObjectPattern;
14
- destructuringHeadersVariable?: Scope.Variable;
14
+ destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
15
15
  destructuringHeadersReferences?: MemberExpression[] | undefined;
16
16
  };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"7.3.0-PR.75-1f63","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","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"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 .","lint:fix":"eslint --max-warnings 0 --fix .","prepare":"","prepublishOnly":"npm run build:dist-types && 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"},"dependencies":{"@typescript-eslint/type-utils":"8.13.0","@typescript-eslint/utils":"8.13.0","debug":"^4.3.7","ts-api-utils":"^1.4.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.1","@checkdigit/typescript-config":"^8.0.0","@eslint/js":"^9.14.0","@types/debug":"^4.1.12","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@typescript-eslint/parser":"8.13.0","@typescript-eslint/rule-tester":"8.13.0","eslint":"^9.14.0","eslint-config-prettier":"^9.1.0","eslint-import-resolver-typescript":"^3.6.3","eslint-plugin-eslint-plugin":"^6.3.1","eslint-plugin-import":"^2.31.0","eslint-plugin-no-only-tests":"^3.3.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"1.0.4","http-status-codes":"^2.3.0","rimraf":"^6.0.1","typescript-eslint":"8.13.0"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=20.17"}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"7.3.0-PR.75-743b","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","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"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 .","lint:fix":"eslint --max-warnings 0 --fix .","prepare":"","prepublishOnly":"npm run build:dist-types && 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"},"dependencies":{"@typescript-eslint/type-utils":"8.14.0","@typescript-eslint/utils":"8.14.0","debug":"^4.3.7","ts-api-utils":"^1.4.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.1","@checkdigit/typescript-config":"^8.0.0","@eslint/js":"^9.15.0","@types/debug":"^4.1.12","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@typescript-eslint/parser":"^8.14.0","@typescript-eslint/rule-tester":"^8.14.0","eslint":"^9.15.0","eslint-config-prettier":"^9.1.0","eslint-import-resolver-typescript":"^3.6.3","eslint-plugin-eslint-plugin":"^6.3.2","eslint-plugin-import":"^2.31.0","eslint-plugin-no-only-tests":"^3.3.0","eslint-plugin-no-secrets":"^1.1.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"1.0.4","http-status-codes":"^2.3.0","rimraf":"^6.0.1","typescript-eslint":"^8.14.0"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=20.17"}}
@@ -41,12 +41,14 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
41
41
  schema: [],
42
42
  },
43
43
  defaultOptions: [],
44
+ // eslint-disable-next-line max-lines-per-function
44
45
  create(context) {
45
46
  log('Processing file:', context.filename);
46
47
  const sourceCode = context.sourceCode;
47
48
  const importDeclarations = new Map<string, TSESTree.ImportDeclaration>();
48
49
  let isFixtureUsed = false;
49
50
  let beforeAll: TSESTree.CallExpression | undefined;
51
+ let beforeEach: TSESTree.CallExpression | undefined;
50
52
  let afterAll: TSESTree.CallExpression | undefined;
51
53
 
52
54
  return {
@@ -68,11 +70,16 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
68
70
  'CallExpression[callee.name="beforeAll"]': (callExpression: TSESTree.CallExpression) => {
69
71
  beforeAll = callExpression;
70
72
  },
73
+ 'CallExpression[callee.name="beforeEach"]': (callExpression: TSESTree.CallExpression) => {
74
+ beforeEach = callExpression;
75
+ },
71
76
  'CallExpression[callee.name="afterAll"]': (callExpression: TSESTree.CallExpression) => {
72
77
  afterAll = callExpression;
73
78
  },
79
+ // eslint-disable-next-line sonarjs/cognitive-complexity
74
80
  'Program:exit'(program) {
75
- if (!isFixtureUsed || beforeAll === undefined) {
81
+ if (!isFixtureUsed || (beforeAll === undefined && beforeEach === undefined)) {
82
+ // only update test wiring if fixture is used
76
83
  return;
77
84
  }
78
85
 
@@ -81,7 +88,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
81
88
  let agentImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
82
89
  let fixturePluginImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
83
90
  let agentDeclarationFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
84
- let beforeAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
91
+ let beforeAllOrEachFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
85
92
  let afterAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined;
86
93
 
87
94
  const lastImportDeclaration = [...importDeclarations.values()].at(-1);
@@ -89,18 +96,21 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
89
96
 
90
97
  // make sure that afterAll is imported from jest
91
98
  const jestImportDeclaration = importDeclarations.get('@jest/globals');
92
- if (
93
- jestImportDeclaration &&
94
- !jestImportDeclaration.specifiers.some(
95
- (specifier) =>
96
- specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
97
- specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier &&
98
- specifier.imported.name === 'afterAll',
99
- )
100
- ) {
99
+ assert.ok(jestImportDeclaration);
100
+ const importsToAdd = ['afterAll', 'beforeAll'].filter(
101
+ (jestHook) =>
102
+ !jestImportDeclaration.specifiers.some(
103
+ (specifier) =>
104
+ specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
105
+ specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier &&
106
+ specifier.imported.name === jestHook,
107
+ ),
108
+ );
109
+ if (importsToAdd.length > 0) {
101
110
  const firstImportSpecifier = jestImportDeclaration.specifiers[0];
102
111
  assert.ok(firstImportSpecifier);
103
- jestImportFixer = (fixer: RuleFixer) => fixer.insertTextBefore(firstImportSpecifier, 'afterAll, ');
112
+ jestImportFixer = (fixer: RuleFixer) =>
113
+ fixer.insertTextBefore(firstImportSpecifier, `${importsToAdd.join(', ')}, `);
104
114
  }
105
115
 
106
116
  // make sure that agent is imported
@@ -124,45 +134,60 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
124
134
  fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '${fixturePluginImportPath}';`);
125
135
  }
126
136
 
127
- // inject agent declaration and initialization to `beforeAll` block
128
- const beforeAllArgument = beforeAll.arguments[0];
129
- assert.ok(beforeAllArgument !== undefined);
130
- if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) {
131
- if (
132
- beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression &&
133
- beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement
134
- ) {
135
- const fixtureResetStatement = beforeAllArgument.body.body.find(
136
- (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED,
137
+ // inject agent declaration and initialization
138
+ if (beforeAll === undefined) {
139
+ // create `beforeAll` block if it doesn't exist
140
+ beforeAllOrEachFixer = (fixer: RuleFixer) =>
141
+ fixer.insertTextBefore(
142
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143
+ beforeEach!,
144
+ [
145
+ STATEMENT_AGENT_DECLARATION,
146
+ `beforeAll(async () => {`,
147
+ [STATEMENT_AGENT_CREATION, STATEMENT_AGENT_REGISTER, STATEMENT_AGENT_ENABLE].join('\n'),
148
+ `});\n`,
149
+ ].join('\n'),
137
150
  );
138
- assert.ok(fixtureResetStatement !== undefined);
139
- beforeAllFixer = (fixer: RuleFixer) =>
140
- fixer.replaceText(
141
- fixtureResetStatement,
142
- [
143
- STATEMENT_AGENT_CREATION,
144
- STATEMENT_AGENT_REGISTER,
145
- STATEMENT_AGENT_ENABLE,
146
- STATEMENT_FIXTURE_RESET_AWAITED,
147
- ].join('\n'),
148
- );
149
- } else {
150
- beforeAllFixer = (fixer: RuleFixer) =>
151
- fixer.replaceText(
152
- beforeAllArgument,
153
- [
154
- `async () => {`,
155
- STATEMENT_AGENT_CREATION,
156
- STATEMENT_AGENT_REGISTER,
157
- STATEMENT_AGENT_ENABLE,
158
- STATEMENT_FIXTURE_RESET_AWAITED,
159
- `}`,
160
- ].join('\n'),
151
+ } else {
152
+ const beforeAllArgument = beforeAll.arguments[0];
153
+ assert.ok(beforeAllArgument !== undefined);
154
+ if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) {
155
+ if (
156
+ beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression &&
157
+ beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement
158
+ ) {
159
+ const fixtureResetStatement = beforeAllArgument.body.body.find(
160
+ (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED,
161
161
  );
162
+ assert.ok(fixtureResetStatement !== undefined);
163
+ beforeAllOrEachFixer = (fixer: RuleFixer) =>
164
+ fixer.replaceText(
165
+ fixtureResetStatement,
166
+ [
167
+ STATEMENT_AGENT_CREATION,
168
+ STATEMENT_AGENT_REGISTER,
169
+ STATEMENT_AGENT_ENABLE,
170
+ STATEMENT_FIXTURE_RESET_AWAITED,
171
+ ].join('\n'),
172
+ );
173
+ } else {
174
+ beforeAllOrEachFixer = (fixer: RuleFixer) =>
175
+ fixer.replaceText(
176
+ beforeAllArgument,
177
+ [
178
+ `async () => {`,
179
+ STATEMENT_AGENT_CREATION,
180
+ STATEMENT_AGENT_REGISTER,
181
+ STATEMENT_AGENT_ENABLE,
182
+ STATEMENT_FIXTURE_RESET_AWAITED,
183
+ `}`,
184
+ ].join('\n'),
185
+ );
186
+ }
187
+ agentDeclarationFixer = (fixer: RuleFixer) =>
188
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
189
+ fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`);
162
190
  }
163
- agentDeclarationFixer = (fixer: RuleFixer) =>
164
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
165
- fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`);
166
191
  }
167
192
 
168
193
  // inject agent disposal to `afterAll` block
@@ -182,7 +207,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
182
207
  afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastStatement, STATEMENT_AGENT_DISPOSE);
183
208
  }
184
209
  } else {
185
- const nextToken = sourceCode.getTokenAfter(beforeAll);
210
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
211
+ const nextToken = sourceCode.getTokenAfter(beforeAll ?? beforeEach!);
186
212
  afterAllFixer = (fixer: RuleFixer) =>
187
213
  fixer.insertTextAfter(
188
214
  nextToken !== null && nextToken.type === AST_TOKEN_TYPES.Punctuator
@@ -198,12 +224,13 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
198
224
  agentImportFixer !== undefined ||
199
225
  fixturePluginImportFixer !== undefined ||
200
226
  agentDeclarationFixer !== undefined ||
201
- beforeAllFixer !== undefined ||
227
+ beforeAllOrEachFixer !== undefined ||
202
228
  afterAllFixer !== undefined
203
229
  ) {
204
230
  context.report({
205
231
  messageId: 'updateTestWiring',
206
- node: beforeAll,
232
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
233
+ node: beforeAll ?? beforeEach!,
207
234
  *fix(fixer) {
208
235
  if (jestImportFixer !== undefined) {
209
236
  yield jestImportFixer(fixer);
@@ -217,8 +244,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create
217
244
  if (agentDeclarationFixer !== undefined) {
218
245
  yield agentDeclarationFixer(fixer);
219
246
  }
220
- if (beforeAllFixer !== undefined) {
221
- yield beforeAllFixer(fixer);
247
+ if (beforeAllOrEachFixer !== undefined) {
248
+ yield beforeAllOrEachFixer(fixer);
222
249
  }
223
250
  if (afterAllFixer !== undefined) {
224
251
  yield afterAllFixer(fixer);
@@ -28,7 +28,7 @@ interface Change {
28
28
  // replacementText: string;
29
29
  }
30
30
 
31
- const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = createRule({
31
+ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'refactorNeeded'> = createRule({
32
32
  name: ruleId,
33
33
  meta: {
34
34
  type: 'suggestion',
@@ -36,6 +36,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
36
36
  description: 'Replace "response.body" with "await response.json()".',
37
37
  },
38
38
  messages: {
39
+ refactorNeeded:
40
+ 'Please extract the fetch call and check its reponse status code before accessing its response body.',
39
41
  replaceBodyWithJson: 'Replace "response.body" with "await response.json()".',
40
42
  unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.',
41
43
  },
@@ -59,6 +61,14 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
59
61
  responseType.getProperties().some((symbol) => symbol.name === 'json');
60
62
 
61
63
  if (shouldReplace) {
64
+ if (responseBodyNode.object.type !== AST_NODE_TYPES.Identifier) {
65
+ context.report({
66
+ node: responseBodyNode,
67
+ messageId: 'refactorNeeded',
68
+ });
69
+ return;
70
+ }
71
+
62
72
  const enclosingFunction = getAncestor(
63
73
  responseBodyNode,
64
74
  (node: TSESTree.Node) =>
@@ -77,14 +87,23 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
77
87
  const enclosingStatementIndex = (enclosingFunction.body as TSESTree.BlockStatement).body.indexOf(
78
88
  enclosingStatement,
79
89
  );
80
- const responseVariableName = (responseBodyNode.object as TSESTree.Identifier).name;
90
+ const responseVariableName = responseBodyNode.object.name;
81
91
  const isResponseBodyVariableDeclared =
82
92
  enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration &&
83
- enclosingStatement.declarations.some((declaration) => declaration.init === responseBodyNode);
93
+ enclosingStatement.declarations.some(
94
+ (declaration) =>
95
+ declaration.init === responseBodyNode ||
96
+ (declaration.init?.type === AST_NODE_TYPES.TSAsExpression &&
97
+ declaration.init.expression === responseBodyNode),
98
+ );
84
99
  const responseBodyVariableName = isResponseBodyVariableDeclared
85
- ? (enclosingStatement.declarations.find((declaration) => declaration.init === responseBodyNode)
86
- ?.id as unknown as string)
87
- : `${(responseBodyNode.object as TSESTree.Identifier).name}Body`;
100
+ ? (enclosingStatement.declarations.find(
101
+ (declaration) =>
102
+ declaration.init === responseBodyNode ||
103
+ (declaration.init?.type === AST_NODE_TYPES.TSAsExpression &&
104
+ declaration.init.expression === responseBodyNode),
105
+ )?.id as unknown as string)
106
+ : `${responseBodyNode.object.name}Body`;
88
107
 
89
108
  const change: Change = {
90
109
  enclosingFunction,
@@ -121,7 +140,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
121
140
  return;
122
141
  }
123
142
 
124
- const fixes: { node: TSESTree.Node; text: string; insert: boolean }[] = [];
143
+ const fixes: { node: TSESTree.Node | TSESTree.Token; text: string; insert: boolean }[] = [];
125
144
  for (const changesByFunction of allChanges.values()) {
126
145
  for (const changesByResponse of changesByFunction.values()) {
127
146
  const orderedChanges = changesByResponse.sort(
@@ -141,8 +160,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
141
160
  let remainingChanges;
142
161
  if (!isResponseBodyVariableDeclared) {
143
162
  fixes.push({
144
- node: enclosingStatement,
145
- text: `const ${responseBodyVariableName} = await ${responseVariableName}.json();\n`,
163
+ node: context.sourceCode.getTokenBefore(enclosingStatement) as TSESTree.Token,
164
+ text: `\nconst ${responseBodyVariableName} = await ${responseVariableName}.json();`,
146
165
  insert: true,
147
166
  });
148
167
  remainingChanges = orderedChanges;
@@ -161,12 +180,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
161
180
  }
162
181
  }
163
182
 
164
- for (const fix of fixes) {
183
+ for (const fix of fixes.reverse()) {
165
184
  context.report({
166
185
  node: fix.node,
167
186
  messageId: 'replaceBodyWithJson',
168
187
  fix(fixer) {
169
- return fix.insert ? fixer.insertTextBefore(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text);
188
+ return fix.insert ? fixer.insertTextAfter(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text);
170
189
  },
171
190
  });
172
191
  }
@@ -103,10 +103,12 @@ function createResponseAssertions(
103
103
  responseVariableName,
104
104
  );
105
105
  }
106
- nonStatusAssertions.push(`assert.ok(${functionBody})`);
106
+ nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
107
107
  } else if (assertionArgument.type === 'Identifier') {
108
108
  // callback assertion using function reference
109
- nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
109
+ nonStatusAssertions.push(
110
+ `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
111
+ );
110
112
  } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
111
113
  // body deep equal assertion
112
114
  nonStatusAssertions.push(
@@ -8,6 +8,10 @@ export function getResponseBodyRetrievalText(responseVariableName: string) {
8
8
  return `await ${responseVariableName}.json()`;
9
9
  }
10
10
 
11
+ export function getResponseHeadersRetrievalText(responseVariableName: string) {
12
+ return `${responseVariableName}.headers`;
13
+ }
14
+
11
15
  export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean {
12
16
  const responseHeaderAccessParent = getParent(responseHeadersAccess);
13
17
  if (responseHeaderAccessParent?.type === 'VariableDeclarator') {
@@ -22,6 +22,7 @@ const DEFAULT_OPTIONS = {
22
22
  'Fixture<ResolvedServices>',
23
23
  'InboundContext',
24
24
  '{ get: () => string; }',
25
+ 'Api',
25
26
  ],
26
27
  };
27
28
 
@@ -77,7 +78,21 @@ const rule: ESLintUtils.RuleModule<
77
78
 
78
79
  log('===== file name:', context.filename);
79
80
  log('callExpression:', sourceCode.getText(callExpression));
81
+
80
82
  try {
83
+ const actualParameters = callExpression.arguments;
84
+ if (
85
+ !actualParameters.some((actualParameter) => {
86
+ const actualType = typeChecker.getTypeAtLocation(
87
+ parserServices.esTreeNodeToTSNodeMap.get(actualParameter),
88
+ );
89
+ const actualTypeString = typeChecker.typeToString(actualType);
90
+ return typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType');
91
+ })
92
+ ) {
93
+ return;
94
+ }
95
+
81
96
  const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee);
82
97
  const calleeType = typeChecker.getTypeAtLocation(calleeTsNode);
83
98
 
@@ -88,13 +103,14 @@ const rule: ESLintUtils.RuleModule<
88
103
  }
89
104
 
90
105
  const signature = signatures[0];
91
- if (
92
- signature === undefined ||
93
- (signature.typeParameters !== undefined && signature.typeParameters.length > 0)
94
- ) {
95
- // ignore complex signatures with type parameters
96
- return;
97
- }
106
+ assert(signature);
107
+ // if (
108
+ // signature === undefined ||
109
+ // (signature.typeParameters !== undefined && signature.typeParameters.length > 0)
110
+ // ) {
111
+ // // ignore complex signatures with type parameters
112
+ // return;
113
+ // }
98
114
 
99
115
  log('signature:', signature.getDeclaration().getText());
100
116
  const expectedParameters = signature.getParameters();
@@ -105,7 +121,6 @@ const rule: ESLintUtils.RuleModule<
105
121
  ),
106
122
  );
107
123
  const expectedParametersCount = expectedParameters.length;
108
- const actualParameters = callExpression.arguments;
109
124
  const actualParametersCount = actualParameters.length;
110
125
  if (actualParametersCount === 0) {
111
126
  return;
@@ -132,18 +147,15 @@ const rule: ESLintUtils.RuleModule<
132
147
  );
133
148
  log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString);
134
149
 
135
- if (!typesToCheck.includes(actualTypeString) && !actualTypeString.endsWith('RequestType')) {
136
- // skip the parameter type checking if it's not in the candidate types
137
- parametersToKeep.push(actualParameter);
138
- expectedParameterIndex++;
139
- log('skipped');
140
- } else if (typeChecker.isTypeAssignableTo(actualType, expectedType)) {
141
- parametersToKeep.push(actualParameter);
142
- expectedParameterIndex++;
143
- log('matched');
144
- } else {
145
- log('not matched');
150
+ if (
151
+ (typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType')) &&
152
+ !typeChecker.isTypeAssignableTo(actualType, expectedType)
153
+ ) {
154
+ log('removing un-matched parameter', sourceCode.getText(actualParameter));
155
+ continue;
146
156
  }
157
+ parametersToKeep.push(actualParameter);
158
+ expectedParameterIndex++;
147
159
  }
148
160
 
149
161
  if (parametersToKeep.length === actualParametersCount) {
@@ -33,7 +33,7 @@ import getDocumentationUrl from '../get-documentation-url';
33
33
  import { getIndentation } from '../library/format';
34
34
  import { isValidPropertyName } from '../library/variable';
35
35
  import { analyzeResponseReferences } from './response-reference';
36
- import { getResponseBodyRetrievalText, hasAssertions } from './fetch';
36
+ import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, hasAssertions } from './fetch';
37
37
  import { replaceEndpointUrlPrefixWithBasePath } from './url';
38
38
 
39
39
  export const ruleId = 'no-fixture';
@@ -48,6 +48,7 @@ interface FixtureCallInformation {
48
48
  assertions?: Expression[][];
49
49
  inlineStatementNode?: Node;
50
50
  inlineBodyReference?: MemberExpression;
51
+ inlineHeadersReference?: MemberExpression;
51
52
  }
52
53
 
53
54
  // recursively analyze the fixture/supertest call chain to collect information of request/response
@@ -80,6 +81,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo
80
81
  if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') {
81
82
  results.inlineBodyReference = awaitParent;
82
83
  }
84
+ if (
85
+ awaitParent.property.type === 'Identifier' &&
86
+ (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers')
87
+ ) {
88
+ results.inlineHeadersReference = awaitParent;
89
+ }
83
90
  } else if (enclosingStatement.type === 'VariableDeclaration') {
84
91
  results.variableDeclaration = enclosingStatement;
85
92
  results.rootNode = enclosingStatement;
@@ -156,10 +163,12 @@ function createResponseAssertions(
156
163
  responseVariableName,
157
164
  );
158
165
  }
159
- nonStatusAssertions.push(`assert.ok(${functionBody})`);
166
+ nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`);
160
167
  } else if (assertionArgument.type === 'Identifier') {
161
168
  // callback assertion using function reference
162
- nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`);
169
+ nonStatusAssertions.push(
170
+ `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`,
171
+ );
163
172
  } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') {
164
173
  // body deep equal assertion
165
174
  nonStatusAssertions.push(
@@ -338,11 +347,19 @@ const rule: Rule.RuleModule = {
338
347
  (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition));
339
348
  const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`;
340
349
 
350
+ const isResponseHeadersVariableRedefinitionNeeded =
351
+ (destructuringResponseHeadersVariable !== undefined &&
352
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
353
+ (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') ||
354
+ fixtureCallInformation.inlineHeadersReference !== undefined;
355
+ const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`;
356
+
341
357
  const isResponseVariableRedefinitionNeeded =
342
358
  (fixtureCallInformation.variableAssignment === undefined &&
343
359
  responseVariable === undefined &&
344
360
  fixtureCallInformation.assertions !== undefined) ||
345
- isResponseBodyVariableRedefinitionNeeded;
361
+ isResponseBodyVariableRedefinitionNeeded ||
362
+ isResponseHeadersVariableRedefinitionNeeded;
346
363
 
347
364
  const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded
348
365
  ? [
@@ -350,16 +367,24 @@ const rule: Rule.RuleModule = {
350
367
  ...(destructuringResponseBodyVariable
351
368
  ? [
352
369
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
353
- `const ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
370
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
354
371
  ]
355
372
  : isResponseBodyVariableRedefinitionNeeded
356
373
  ? [
357
374
  `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`,
358
375
  ]
359
376
  : []),
377
+ // eslint-disable-next-line no-nested-ternary
360
378
  ...(destructuringResponseHeadersVariable
361
- ? [`const ${destructuringResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`]
362
- : []),
379
+ ? [
380
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
381
+ `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseHeadersVariable as ObjectPattern) : (destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
382
+ ]
383
+ : isResponseHeadersVariableRedefinitionNeeded
384
+ ? [
385
+ `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`,
386
+ ]
387
+ : []),
363
388
  ]
364
389
  : [];
365
390
 
@@ -367,14 +392,14 @@ const rule: Rule.RuleModule = {
367
392
  fixtureCallInformation,
368
393
  sourceCode,
369
394
  responseVariableNameToUse,
370
- destructuringResponseHeadersVariable,
395
+ destructuringResponseHeadersVariable as Scope.Variable | undefined,
371
396
  );
372
397
 
373
398
  // add variable declaration if needed
374
399
  const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`;
375
400
  const fetchStatementText = !isResponseVariableRedefinitionNeeded
376
401
  ? fetchCallText
377
- : `const ${responseVariableNameToUse} = await ${fetchCallText}`;
402
+ : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`;
378
403
 
379
404
  const nodeToReplace = isResponseVariableRedefinitionNeeded
380
405
  ? fixtureCallInformation.rootNode
@@ -29,7 +29,7 @@ export function analyzeResponseReferences(
29
29
  headersReferences: MemberExpression[];
30
30
  statusReferences: MemberExpression[];
31
31
  destructuringBodyVariable?: Scope.Variable | ObjectPattern;
32
- destructuringHeadersVariable?: Scope.Variable;
32
+ destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
33
33
  destructuringHeadersReferences?: MemberExpression[] | undefined;
34
34
  } {
35
35
  const results: {
@@ -38,7 +38,7 @@ export function analyzeResponseReferences(
38
38
  headersReferences: MemberExpression[];
39
39
  statusReferences: MemberExpression[];
40
40
  destructuringBodyVariable?: Scope.Variable | ObjectPattern;
41
- destructuringHeadersVariable?: Scope.Variable;
41
+ destructuringHeadersVariable?: Scope.Variable | ObjectPattern;
42
42
  destructuringHeadersReferences?: MemberExpression[] | undefined;
43
43
  } = {
44
44
  bodyReferences: [],
@@ -105,13 +105,20 @@ export function analyzeResponseReferences(
105
105
  getParent(parent)?.type !== 'CallExpression',
106
106
  );
107
107
  } else if (identifierParent.type === 'Property') {
108
- // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..."
109
108
  const parent = getParent(identifierParent);
110
109
  if (parent?.type === 'ObjectPattern') {
110
+ // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..."
111
111
  const parent2 = getParent(parent);
112
112
  if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') {
113
113
  results.destructuringBodyVariable = parent;
114
114
  }
115
+ if (
116
+ parent2?.type === 'Property' &&
117
+ parent2.key.type === 'Identifier' &&
118
+ (parent2.key.name === 'header' || parent2.key.name === 'headers')
119
+ ) {
120
+ results.destructuringHeadersVariable = parent;
121
+ }
115
122
  }
116
123
  } else {
117
124
  log('+++++++ can not handle identifierParent', identifierParent);
@@ -141,7 +141,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu
141
141
 
142
142
  const enclosingScopeNode = getEnclosingScopeNode(serviceCall);
143
143
  assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined');
144
- const scope = scopeManager?.acquire(enclosingScopeNode); /*?*/
144
+ const scope = scopeManager?.acquire(enclosingScopeNode);
145
145
  assert.ok(scope, 'scope is undefined');
146
146
  const urlArgument = serviceCall.arguments[0];
147
147
  if (!isUrlArgumentValid(urlArgument, scope)) {