@checkdigit/eslint-plugin 7.3.0-PR.75-f90a → 7.3.0-PR.75-4919

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/README.md CHANGED
@@ -15,7 +15,7 @@ Copyright (c) 2021-2024 [Check Digit, LLC](https://checkdigit.com)
15
15
  - `@checkdigit/no-test-import`
16
16
  - `@checkdigit/no-promise-instance-method`
17
17
  - `@checkdigit/invalid-json-stringify`
18
- - `@checkdigit/no-full-response`
18
+ - `@checkdigit/no-legacy-service-typing`
19
19
  - `@checkdigit/require-resolve-full-response`
20
20
  - `@checkdigit/require-type-out-of-type-only-imports`
21
21
 
@@ -1,6 +1,8 @@
1
1
  // src/agent/fetch-response-body-json.ts
2
+ import { strict as assert } from "node:assert";
2
3
  import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
3
4
  import getDocumentationUrl from "../get-documentation-url.mjs";
5
+ import { getAncestor } from "../library/ts-tree.mjs";
4
6
  var ruleId = "fetch-response-body-json";
5
7
  var createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
6
8
  var rule = createRule({
@@ -21,29 +23,47 @@ var rule = createRule({
21
23
  create(context) {
22
24
  const parserServices = ESLintUtils.getParserServices(context);
23
25
  const typeChecker = parserServices.program.getTypeChecker();
24
- const sourceCode = context.sourceCode;
26
+ const allChanges = /* @__PURE__ */ new Map();
25
27
  return {
26
- 'MemberExpression[property.name="body"]': (responseBody) => {
28
+ 'MemberExpression[property.name="body"]': (responseBodyNode) => {
27
29
  try {
28
- const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBody.object);
30
+ const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBodyNode.object);
29
31
  const responseType = typeChecker.getTypeAtLocation(responseNode);
30
32
  const shouldReplace = responseType.getProperties().some((symbol) => symbol.name === "body") && responseType.getProperties().some((symbol) => symbol.name === "json");
31
33
  if (shouldReplace) {
32
- const responseText = sourceCode.getText(responseBody.object);
33
- const needAwait = responseBody.parent.type !== AST_NODE_TYPES.ReturnStatement;
34
- const replacementText = needAwait ? `(await ${responseText}.json())` : `${responseText}.json()`;
35
- context.report({
36
- messageId: "replaceBodyWithJson",
37
- node: responseBody,
38
- fix(fixer) {
39
- return fixer.replaceText(responseBody, replacementText);
40
- }
41
- });
34
+ const enclosingFunction = getAncestor(
35
+ responseBodyNode,
36
+ (node) => node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.FunctionDeclaration
37
+ );
38
+ const enclosingStatement = getAncestor(
39
+ responseBodyNode,
40
+ (node) => (node.type === AST_NODE_TYPES.VariableDeclaration || node.type === AST_NODE_TYPES.ExpressionStatement || node.type === AST_NODE_TYPES.ReturnStatement) && node.parent.type === AST_NODE_TYPES.BlockStatement
41
+ );
42
+ const enclosingStatementIndex = enclosingFunction.body.body.indexOf(
43
+ enclosingStatement
44
+ );
45
+ const responseVariableName = responseBodyNode.object.name;
46
+ const isResponseBodyVariableDeclared = enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration && enclosingStatement.declarations.some((declaration) => declaration.init === responseBodyNode);
47
+ const responseBodyVariableName = isResponseBodyVariableDeclared ? enclosingStatement.declarations.find((declaration) => declaration.init === responseBodyNode)?.id : `${responseBodyNode.object.name}Body`;
48
+ const change = {
49
+ enclosingFunction,
50
+ enclosingStatement,
51
+ enclosingStatementIndex,
52
+ responseVariableName,
53
+ responseBodyNode,
54
+ responseBodyVariableName,
55
+ isResponseBodyVariableDeclared
56
+ };
57
+ const changesByFunction = allChanges.get(enclosingFunction) ?? /* @__PURE__ */ new Map();
58
+ const changesByResponse = changesByFunction.get(responseVariableName) ?? [];
59
+ changesByResponse.push(change);
60
+ changesByFunction.set(responseVariableName, changesByResponse);
61
+ allChanges.set(enclosingFunction, changesByFunction);
42
62
  }
43
63
  } catch (error) {
44
64
  console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
45
65
  context.report({
46
- node: responseBody,
66
+ node: responseBodyNode,
47
67
  messageId: "unknownError",
48
68
  data: {
49
69
  fileName: context.filename,
@@ -51,6 +71,57 @@ var rule = createRule({
51
71
  }
52
72
  });
53
73
  }
74
+ },
75
+ "Program:exit": () => {
76
+ if (allChanges.size === 0) {
77
+ return;
78
+ }
79
+ const fixes = [];
80
+ for (const changesByFunction of allChanges.values()) {
81
+ for (const changesByResponse of changesByFunction.values()) {
82
+ const orderedChanges = changesByResponse.sort(
83
+ (changeA, changeB) => changeA.enclosingStatementIndex - changeB.enclosingStatementIndex
84
+ );
85
+ const firstChange = orderedChanges[0];
86
+ assert(firstChange);
87
+ const {
88
+ responseBodyNode,
89
+ responseVariableName,
90
+ responseBodyVariableName,
91
+ isResponseBodyVariableDeclared,
92
+ enclosingStatement
93
+ } = firstChange;
94
+ let remainingChanges;
95
+ if (!isResponseBodyVariableDeclared) {
96
+ fixes.push({
97
+ node: enclosingStatement,
98
+ text: `const ${responseBodyVariableName} = await ${responseVariableName}.json();
99
+ `,
100
+ insert: true
101
+ });
102
+ remainingChanges = orderedChanges;
103
+ } else {
104
+ fixes.push({
105
+ node: responseBodyNode,
106
+ text: `await ${responseVariableName}.json()`,
107
+ insert: false
108
+ });
109
+ remainingChanges = orderedChanges.slice(1);
110
+ }
111
+ for (const change of remainingChanges) {
112
+ fixes.push({ node: change.responseBodyNode, text: responseBodyVariableName, insert: false });
113
+ }
114
+ }
115
+ }
116
+ for (const fix of fixes) {
117
+ context.report({
118
+ node: fix.node,
119
+ messageId: "replaceBodyWithJson",
120
+ fix(fixer) {
121
+ return fix.insert ? fixer.insertTextBefore(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text);
122
+ }
123
+ });
124
+ }
54
125
  }
55
126
  };
56
127
  }
@@ -60,4 +131,4 @@ export {
60
131
  fetch_response_body_json_default as default,
61
132
  ruleId
62
133
  };
63
- //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vc3JjL2FnZW50L2ZldGNoLXJlc3BvbnNlLWJvZHktanNvbi50cyJdLAogICJtYXBwaW5ncyI6ICI7QUFRQSxTQUFTLGdCQUFnQixtQkFBNkI7QUFFdEQsT0FBTyx5QkFBeUI7QUFFekIsSUFBTSxTQUFTO0FBRXRCLElBQU0sYUFBYSxZQUFZLFlBQVksQ0FBQyxTQUFTLG9CQUFvQixJQUFJLENBQUM7QUFFOUUsSUFBTSxPQUF1RSxXQUFXO0FBQUEsRUFDdEYsTUFBTTtBQUFBLEVBQ04sTUFBTTtBQUFBLElBQ0osTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLE1BQ0osYUFBYTtBQUFBLElBQ2Y7QUFBQSxJQUNBLFVBQVU7QUFBQSxNQUNSLHFCQUFxQjtBQUFBLE1BQ3JCLGNBQWM7QUFBQSxJQUNoQjtBQUFBLElBQ0EsU0FBUztBQUFBLElBQ1QsUUFBUSxDQUFDO0FBQUEsRUFDWDtBQUFBLEVBQ0EsZ0JBQWdCLENBQUM7QUFBQSxFQUNqQixPQUFPLFNBQVM7QUFDZCxVQUFNLGlCQUFpQixZQUFZLGtCQUFrQixPQUFPO0FBQzVELFVBQU0sY0FBYyxlQUFlLFFBQVEsZUFBZTtBQUMxRCxVQUFNLGFBQWEsUUFBUTtBQUUzQixXQUFPO0FBQUEsTUFDTCwwQ0FBMEMsQ0FBQyxpQkFBNEM7QUFDckYsWUFBSTtBQUNGLGdCQUFNLGVBQWUsZUFBZSxzQkFBc0IsSUFBSSxhQUFhLE1BQU07QUFDakYsZ0JBQU0sZUFBZSxZQUFZLGtCQUFrQixZQUFZO0FBRS9ELGdCQUFNLGdCQUNKLGFBQWEsY0FBYyxFQUFFLEtBQUssQ0FBQyxXQUFXLE9BQU8sU0FBUyxNQUFNLEtBQ3BFLGFBQWEsY0FBYyxFQUFFLEtBQUssQ0FBQyxXQUFXLE9BQU8sU0FBUyxNQUFNO0FBRXRFLGNBQUksZUFBZTtBQUNqQixrQkFBTSxlQUFlLFdBQVcsUUFBUSxhQUFhLE1BQU07QUFDM0Qsa0JBQU0sWUFBWSxhQUFhLE9BQU8sU0FBUyxlQUFlO0FBQzlELGtCQUFNLGtCQUFrQixZQUFZLFVBQVUsWUFBWSxhQUFhLEdBQUcsWUFBWTtBQUV0RixvQkFBUSxPQUFPO0FBQUEsY0FDYixXQUFXO0FBQUEsY0FDWCxNQUFNO0FBQUEsY0FDTixJQUFJLE9BQU87QUFDVCx1QkFBTyxNQUFNLFlBQVksY0FBYyxlQUFlO0FBQUEsY0FDeEQ7QUFBQSxZQUNGLENBQUM7QUFBQSxVQUNIO0FBQUEsUUFDRixTQUFTLE9BQU87QUFFZCxrQkFBUSxNQUFNLG1CQUFtQixNQUFNLG1CQUFtQixRQUFRLFFBQVEsTUFBTSxLQUFLO0FBQ3JGLGtCQUFRLE9BQU87QUFBQSxZQUNiLE1BQU07QUFBQSxZQUNOLFdBQVc7QUFBQSxZQUNYLE1BQU07QUFBQSxjQUNKLFVBQVUsUUFBUTtBQUFBLGNBQ2xCLE9BQU8saUJBQWlCLFFBQVEsTUFBTSxTQUFTLElBQUksS0FBSyxVQUFVLEtBQUs7QUFBQSxZQUN6RTtBQUFBLFVBQ0YsQ0FBQztBQUFBLFFBQ0g7QUFBQSxNQUNGO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFDRixDQUFDO0FBRUQsSUFBTyxtQ0FBUTsiLAogICJuYW1lcyI6IFtdCn0K
134
+ //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vc3JjL2FnZW50L2ZldGNoLXJlc3BvbnNlLWJvZHktanNvbi50cyJdLAogICJtYXBwaW5ncyI6ICI7QUFRQSxTQUFTLFVBQVUsY0FBYztBQUVqQyxTQUFTLGdCQUFnQixtQkFBNkI7QUFFdEQsT0FBTyx5QkFBeUI7QUFDaEMsU0FBUyxtQkFBbUI7QUFFckIsSUFBTSxTQUFTO0FBRXRCLElBQU0sYUFBYSxZQUFZLFlBQVksQ0FBQyxTQUFTLG9CQUFvQixJQUFJLENBQUM7QUFhOUUsSUFBTSxPQUF1RSxXQUFXO0FBQUEsRUFDdEYsTUFBTTtBQUFBLEVBQ04sTUFBTTtBQUFBLElBQ0osTUFBTTtBQUFBLElBQ04sTUFBTTtBQUFBLE1BQ0osYUFBYTtBQUFBLElBQ2Y7QUFBQSxJQUNBLFVBQVU7QUFBQSxNQUNSLHFCQUFxQjtBQUFBLE1BQ3JCLGNBQWM7QUFBQSxJQUNoQjtBQUFBLElBQ0EsU0FBUztBQUFBLElBQ1QsUUFBUSxDQUFDO0FBQUEsRUFDWDtBQUFBLEVBQ0EsZ0JBQWdCLENBQUM7QUFBQSxFQUNqQixPQUFPLFNBQVM7QUFDZCxVQUFNLGlCQUFpQixZQUFZLGtCQUFrQixPQUFPO0FBQzVELFVBQU0sY0FBYyxlQUFlLFFBQVEsZUFBZTtBQUMxRCxVQUFNLGFBQWEsb0JBQUksSUFBMEM7QUFFakUsV0FBTztBQUFBLE1BQ0wsMENBQTBDLENBQUMscUJBQWdEO0FBQ3pGLFlBQUk7QUFDRixnQkFBTSxlQUFlLGVBQWUsc0JBQXNCLElBQUksaUJBQWlCLE1BQU07QUFDckYsZ0JBQU0sZUFBZSxZQUFZLGtCQUFrQixZQUFZO0FBRS9ELGdCQUFNLGdCQUNKLGFBQWEsY0FBYyxFQUFFLEtBQUssQ0FBQyxXQUFXLE9BQU8sU0FBUyxNQUFNLEtBQ3BFLGFBQWEsY0FBYyxFQUFFLEtBQUssQ0FBQyxXQUFXLE9BQU8sU0FBUyxNQUFNO0FBRXRFLGNBQUksZUFBZTtBQUNqQixrQkFBTSxvQkFBb0I7QUFBQSxjQUN4QjtBQUFBLGNBQ0EsQ0FBQyxT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
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"7.3.0-PR.75-f90a","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-4919","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"}}
@@ -6,14 +6,28 @@
6
6
  * This code is licensed under the MIT license (see LICENSE.txt for details).
7
7
  */
8
8
 
9
+ import { strict as assert } from 'node:assert';
10
+
9
11
  import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils';
10
12
 
11
13
  import getDocumentationUrl from '../get-documentation-url';
14
+ import { getAncestor } from '../library/ts-tree';
12
15
 
13
16
  export const ruleId = 'fetch-response-body-json';
14
17
 
15
18
  const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name));
16
19
 
20
+ interface Change {
21
+ enclosingFunction: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration;
22
+ enclosingStatement: TSESTree.VariableDeclaration | TSESTree.ExpressionStatement | TSESTree.ReturnStatement;
23
+ enclosingStatementIndex: number;
24
+ responseBodyNode: TSESTree.MemberExpression;
25
+ responseVariableName: string;
26
+ responseBodyVariableName: string;
27
+ isResponseBodyVariableDeclared: boolean;
28
+ // replacementText: string;
29
+ }
30
+
17
31
  const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = createRule({
18
32
  name: ruleId,
19
33
  meta: {
@@ -32,12 +46,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
32
46
  create(context) {
33
47
  const parserServices = ESLintUtils.getParserServices(context);
34
48
  const typeChecker = parserServices.program.getTypeChecker();
35
- const sourceCode = context.sourceCode;
49
+ const allChanges = new Map<TSESTree.Node, Map<string, Change[]>>();
36
50
 
37
51
  return {
38
- 'MemberExpression[property.name="body"]': (responseBody: TSESTree.MemberExpression) => {
52
+ 'MemberExpression[property.name="body"]': (responseBodyNode: TSESTree.MemberExpression) => {
39
53
  try {
40
- const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBody.object);
54
+ const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBodyNode.object);
41
55
  const responseType = typeChecker.getTypeAtLocation(responseNode);
42
56
 
43
57
  const shouldReplace =
@@ -45,23 +59,54 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
45
59
  responseType.getProperties().some((symbol) => symbol.name === 'json');
46
60
 
47
61
  if (shouldReplace) {
48
- const responseText = sourceCode.getText(responseBody.object);
49
- const needAwait = responseBody.parent.type !== AST_NODE_TYPES.ReturnStatement;
50
- const replacementText = needAwait ? `(await ${responseText}.json())` : `${responseText}.json()`;
51
-
52
- context.report({
53
- messageId: 'replaceBodyWithJson',
54
- node: responseBody,
55
- fix(fixer) {
56
- return fixer.replaceText(responseBody, replacementText);
57
- },
58
- });
62
+ const enclosingFunction = getAncestor(
63
+ responseBodyNode,
64
+ (node: TSESTree.Node) =>
65
+ node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
66
+ node.type === AST_NODE_TYPES.FunctionExpression ||
67
+ node.type === AST_NODE_TYPES.FunctionDeclaration,
68
+ ) as TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration;
69
+ const enclosingStatement = getAncestor(
70
+ responseBodyNode,
71
+ (node: TSESTree.Node) =>
72
+ (node.type === AST_NODE_TYPES.VariableDeclaration ||
73
+ node.type === AST_NODE_TYPES.ExpressionStatement ||
74
+ node.type === AST_NODE_TYPES.ReturnStatement) &&
75
+ node.parent.type === AST_NODE_TYPES.BlockStatement,
76
+ ) as TSESTree.VariableDeclaration | TSESTree.ExpressionStatement | TSESTree.ReturnStatement;
77
+ const enclosingStatementIndex = (enclosingFunction.body as TSESTree.BlockStatement).body.indexOf(
78
+ enclosingStatement,
79
+ );
80
+ const responseVariableName = (responseBodyNode.object as TSESTree.Identifier).name;
81
+ const isResponseBodyVariableDeclared =
82
+ enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration &&
83
+ enclosingStatement.declarations.some((declaration) => declaration.init === responseBodyNode);
84
+ 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`;
88
+
89
+ const change: Change = {
90
+ enclosingFunction,
91
+ enclosingStatement,
92
+ enclosingStatementIndex,
93
+ responseVariableName,
94
+ responseBodyNode,
95
+ responseBodyVariableName,
96
+ isResponseBodyVariableDeclared,
97
+ };
98
+
99
+ const changesByFunction = allChanges.get(enclosingFunction) ?? new Map<string, Change[]>();
100
+ const changesByResponse = changesByFunction.get(responseVariableName) ?? [];
101
+ changesByResponse.push(change);
102
+ changesByFunction.set(responseVariableName, changesByResponse);
103
+ allChanges.set(enclosingFunction, changesByFunction);
59
104
  }
60
105
  } catch (error) {
61
106
  // eslint-disable-next-line no-console
62
107
  console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
63
108
  context.report({
64
- node: responseBody,
109
+ node: responseBodyNode,
65
110
  messageId: 'unknownError',
66
111
  data: {
67
112
  fileName: context.filename,
@@ -70,6 +115,62 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre
70
115
  });
71
116
  }
72
117
  },
118
+
119
+ 'Program:exit': () => {
120
+ if (allChanges.size === 0) {
121
+ return;
122
+ }
123
+
124
+ const fixes: { node: TSESTree.Node; text: string; insert: boolean }[] = [];
125
+ for (const changesByFunction of allChanges.values()) {
126
+ for (const changesByResponse of changesByFunction.values()) {
127
+ const orderedChanges = changesByResponse.sort(
128
+ (changeA, changeB) => changeA.enclosingStatementIndex - changeB.enclosingStatementIndex,
129
+ );
130
+ const firstChange = orderedChanges[0];
131
+ assert(firstChange);
132
+
133
+ const {
134
+ responseBodyNode,
135
+ responseVariableName,
136
+ responseBodyVariableName,
137
+ isResponseBodyVariableDeclared,
138
+ enclosingStatement,
139
+ } = firstChange;
140
+
141
+ let remainingChanges;
142
+ if (!isResponseBodyVariableDeclared) {
143
+ fixes.push({
144
+ node: enclosingStatement,
145
+ text: `const ${responseBodyVariableName} = await ${responseVariableName}.json();\n`,
146
+ insert: true,
147
+ });
148
+ remainingChanges = orderedChanges;
149
+ } else {
150
+ fixes.push({
151
+ node: responseBodyNode,
152
+ text: `await ${responseVariableName}.json()`,
153
+ insert: false,
154
+ });
155
+ remainingChanges = orderedChanges.slice(1);
156
+ }
157
+
158
+ for (const change of remainingChanges) {
159
+ fixes.push({ node: change.responseBodyNode, text: responseBodyVariableName, insert: false });
160
+ }
161
+ }
162
+ }
163
+
164
+ for (const fix of fixes) {
165
+ context.report({
166
+ node: fix.node,
167
+ messageId: 'replaceBodyWithJson',
168
+ fix(fixer) {
169
+ return fix.insert ? fixer.insertTextBefore(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text);
170
+ },
171
+ });
172
+ }
173
+ },
73
174
  };
74
175
  },
75
176
  });
@@ -1,4 +1,4 @@
1
- // no-full-response.ts
1
+ // no-legacy-service-typing.ts
2
2
 
3
3
  /*
4
4
  * Copyright (c) 2021-2024 Check Digit, LLC