From 6d2357b0b01d82f0b3993e601d6ab71850b624e2 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 18 Jul 2024 23:33:43 -0400 Subject: [PATCH 001/115] auto-fix to replace fixture usage with native fetch api --- docs/rules/no-fixture.md | 25 ++ package-lock.json | 578 +++++++++++++++------------------------ package.json | 8 +- src/index.ts | 4 + src/no-fixture.spec.ts | 114 ++++++++ src/no-fixture.ts | 260 ++++++++++++++++++ 6 files changed, 622 insertions(+), 367 deletions(-) create mode 100644 docs/rules/no-fixture.md create mode 100644 src/no-fixture.spec.ts create mode 100644 src/no-fixture.ts diff --git a/docs/rules/no-fixture.md b/docs/rules/no-fixture.md new file mode 100644 index 0000000..2e4d3eb --- /dev/null +++ b/docs/rules/no-fixture.md @@ -0,0 +1,25 @@ +# To ease the transition from fixture to native fetch API, this rule convert relevant code automatically. + +## Before + +```js +it('returns current server time', async () => { + const response = await fixture.api.get(`/smartdata/v1/ping`).expect(StatusCodes.OK); + const body = response.body; + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); +}); +``` + +## After + +```js +it('returns current server time', async () => { + // assume the existence of - const BASE_PATH = 'https://smartdata.checkdigit/smartdata/v1'; + const response = await fetch(`${BASE_PATH}/ping`); + assert.equal(response.status, StatusCodes.OK); + const body = await response.json(); + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); +}); +``` diff --git a/package-lock.json b/package-lock.json index 1e533f7..3c93a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.5.0", + "version": "6.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "6.5.0", + "version": "6.6.0", "license": "MIT", "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", "eslint-plugin-import": "^2.29.1", @@ -24,7 +24,7 @@ "eslint-plugin-sonarjs": "0.24.0" }, "engines": { - "node": ">=20.13" + "node": ">=20.14" }, "peerDependencies": { "eslint": ">=8 <9" @@ -61,9 +61,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", "dev": true, "license": "MIT", "peer": true, @@ -72,23 +72,23 @@ } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -115,14 +115,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.24.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -132,16 +132,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -219,9 +219,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", "dev": true, "license": "MIT", "peer": true, @@ -240,9 +240,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "license": "MIT", "peer": true, @@ -280,9 +280,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "license": "MIT", "peer": true, @@ -302,9 +302,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "license": "MIT", "peer": true, @@ -313,15 +313,15 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -430,9 +430,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", "dev": true, "license": "MIT", "peer": true, @@ -665,21 +665,21 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", + "@babel/generator": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", "@babel/helper-hoist-variables": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -699,14 +699,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -1192,54 +1192,6 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -1266,30 +1218,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2018,9 +1946,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dev": true, "license": "MIT", "peer": true, @@ -2056,17 +1984,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2090,16 +2018,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", + "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4" }, "engines": { @@ -2119,14 +2047,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2137,14 +2065,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2165,9 +2093,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", "dev": true, "license": "MIT", "engines": { @@ -2179,14 +2107,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2207,17 +2135,43 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2231,13 +2185,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.16.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2323,20 +2277,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2543,14 +2483,14 @@ } }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "peer": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/babel-jest": { @@ -2690,13 +2630,13 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -2713,9 +2653,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -2734,10 +2674,10 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -2821,9 +2761,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001640", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", - "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "version": "1.0.30001642", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", + "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", "dev": true, "funding": [ { @@ -3256,9 +3196,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.820", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.820.tgz", - "integrity": "sha512-kK/4O/YunacfboFEk/BDf7VO1HoPmDudLTJAU9NmXIOSjsV7qVIX3OrI4REZo0VmdqhcpUcncQc6N8Q3aEXlHg==", + "version": "1.4.829", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz", + "integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==", "dev": true, "license": "ISC", "peer": true @@ -3687,17 +3627,6 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -3721,19 +3650,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3789,30 +3705,6 @@ "eslint": ">=5.16.0" } }, - "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-node/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-node/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3891,31 +3783,31 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "license": "MIT", "peer": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": "*" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { + "node_modules/eslint/node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", @@ -3933,6 +3825,35 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4409,46 +4330,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -4781,9 +4662,9 @@ } }, "node_modules/is-core-module": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "dev": true, "license": "MIT", "dependencies": { @@ -6035,19 +5916,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -6081,9 +5958,9 @@ "peer": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz", + "integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", "dev": true, "license": "MIT", "peer": true @@ -6943,9 +6820,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -7420,32 +7297,6 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7612,9 +7463,10 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, "license": "(MIT OR CC0-1.0)", "peer": true, "engines": { diff --git a/package.json b/package.json index 9c7b8bf..14ce3b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.5.0", + "version": "6.6.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", @@ -66,8 +66,8 @@ "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", "eslint-plugin-import": "^2.29.1", @@ -80,6 +80,6 @@ "eslint": ">=8 <9" }, "engines": { - "node": ">=20.13" + "node": ">=20.14" } } diff --git a/src/index.ts b/src/index.ts index dc4e178..2def8ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ */ import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; +import noFixture, { ruleId as noFixtureRuleId } from './no-fixture'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; @@ -31,6 +32,7 @@ export default { 'object-literal-response': objectLiteralResponse, [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, + [noFixtureRuleId]: noFixture, }, configs: { all: { @@ -46,6 +48,7 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'error', }, }, recommended: { @@ -61,6 +64,7 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'error', }, }, }, diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts new file mode 100644 index 0000000..8325329 --- /dev/null +++ b/src/no-fixture.spec.ts @@ -0,0 +1,114 @@ +// no-fixture.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-fixture'; +import createTester from './tester.test'; +import { describe } from '@jest/globals'; + +describe(ruleId, () => { + createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + // no assertion + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/smartdata/v1/ping\`); + }); + `, + output: ` + it('GET /ping', async () => { + await fetch(\`\${BASE_PATH}/ping\`); + }); + `, + errors: 1, + }, + { + // assertion with variable declaration + code: ` + it('GET /ping', async () => { + const pingResponse = await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + const body = pingResponse.body; + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }); + `, + output: ` + it('GET /ping', async () => { + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(pingResponse.status, StatusCodes.OK); + const body = await pingResponse.json(); + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }); + `, + errors: 1, + }, + { + // assertion without variable declaration + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + }); + `, + errors: 1, + }, + { + // put with request body + code: ` + it('PUT /card', async () => { + await fixture.api.put(\`/vault/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); + }); + `, + output: ` + it('PUT /card', async () => { + const response = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + method: 'PUT', + body: JSON.stringify(cardCreationData), + }); + assert.equal(response.status, StatusCodes.BAD_REQUEST); + }); + `, + errors: 1, + }, + { + // put with request header + code: ` + it('PUT /card/:cardId/block', async () => { + const noFraudResponse = await fixture.api + .put(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .set(IF_MATCH_HEADER, originalCard.version) + .set('abc', originalCard.name) + .set('x-y-z', '123') + .expect(StatusCodes.NO_CONTENT); + }); + `, + output: ` + it('PUT /card/:cardId/block', async () => { + const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + method: 'PUT', + headers: { + [IF_MATCH_HEADER]: originalCard.version, + abc: originalCard.name, + 'x-y-z': '123', + }, + }); + assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); + }); + `, + errors: 1, + }, + ], + }); +}); diff --git a/src/no-fixture.ts b/src/no-fixture.ts new file mode 100644 index 0000000..763b9ad --- /dev/null +++ b/src/no-fixture.ts @@ -0,0 +1,260 @@ +// no-fixture.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +/* eslint-disable no-console */ + +import type { AwaitExpression, Expression, Node, SimpleCallExpression } from 'estree'; +import type { Rule, Scope, SourceCode } from 'eslint'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'no-fixture'; + +type NodeParent = Node | undefined | null; + +interface NodeParentExtension { + parent: NodeParent; +} + +interface FixtureCallInformation { + root: AwaitExpression; + requestBody?: Expression; + requestHeaders?: { name: Expression; value: Expression }[]; + assertions?: Expression[][]; +} + +function getParent(node: Node): Node | undefined | null { + return (node as unknown as NodeParentExtension).parent; +} + +function analyze(call: SimpleCallExpression, results: FixtureCallInformation) { + const parent = getParent(call); + assert.ok(parent, 'parent should exist for fixture/supertest call node'); + + let nextCall; + if (parent.type === 'AwaitExpression') { + // no more assertions, return the await expression of the fixture call + results.root = parent; + } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + if (parent.property.name === 'expect') { + const assertionCall = getParent(parent); + assert.ok(assertionCall && assertionCall.type === 'CallExpression'); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + nextCall = assertionCall; + } else if (parent.property.name === 'send') { + const sendRequestBodyCall = getParent(parent); + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); + results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + nextCall = sendRequestBodyCall; + } else if (parent.property.name === 'set') { + const setRequestHeaderCall = getParent(parent); + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); + const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression]; + results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }]; + nextCall = setRequestHeaderCall; + } + } else { + throw new Error(`Unexpected expression in fixture/supertest call ${String(parent)}`); + } + if (nextCall) { + analyze(nextCall, results); + } +} + +function replaceEndpointUrlPrefixWithBasePath(url: string) { + // eslint-disable-next-line no-template-curly-in-string + return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); +} + +function isValidPropertyName(name: unknown) { + return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); +} + +function appendAssertions(expects: Expression[][], sourceCode: SourceCode, variableName: string) { + const assertions: string[] = []; + for (const expectArguments of expects) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + // status + if ( + assertionArgument.type === 'MemberExpression' && + assertionArgument.object.type === 'Identifier' && + assertionArgument.object.name === 'StatusCodes' + ) { + assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`); + } + } + } + return assertions; +} + +function getAncestor(node: Node, matchType: string, quitType: string) { + const parent = getParent(node); + if (!parent || parent.type === quitType) { + return undefined; + } else if (parent.type === matchType) { + return parent; + } + return getAncestor(parent, matchType, quitType); +} + +function getResponseBodyReferences(fixtureCallAwait: AwaitExpression, scopeManager: Scope.ScopeManager) { + const results: { responseVariableName?: string; responseBodyReferences: Node[] } = { + responseBodyReferences: [], + }; + + const variableDeclaration = getAncestor(fixtureCallAwait, 'VariableDeclaration', 'FunctionDeclaration'); + if (variableDeclaration && variableDeclaration.type === 'VariableDeclaration') { + const [responseVariable] = scopeManager.getDeclaredVariables(variableDeclaration); + assert.ok(responseVariable); + + results.responseVariableName = responseVariable.name; + results.responseBodyReferences = responseVariable.references + .map((responseBodyReference) => getParent(responseBodyReference.identifier)) + .filter( + (responseBodyNode): responseBodyNode is Node => + responseBodyNode !== null && + responseBodyNode !== undefined && + responseBodyNode.type === 'MemberExpression' && + responseBodyNode.property.type === 'Identifier' && + responseBodyNode.property.name === 'body', + ); + } + return results; +} + +function getIndentation(node: Node, sourceCode: SourceCode) { + assert.ok(node.loc); + const line = sourceCode.lines[node.loc.start.line - 1]; + assert.ok(line); + const indentMatch = line.match(/^\s*/u); + return indentMatch ? indentMatch[0] : ''; +} + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch API over customized fixture API.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + unknownError: + 'Unknown error occurred: {{ error }}. Please manually convert the fixture API call to fetch API call.', + }, + fixable: 'code', + schema: [], + }, + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + let variableCounter = 0; + + return { + 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (fixtureCall: Node) => { + try { + assert.ok(fixtureCall.type === 'CallExpression'); + const fixtureFunction = fixtureCall.callee; // node - fixture.api.get + assert.ok(fixtureFunction.type === 'MemberExpression'); + const methodNode = fixtureFunction.property; // get/put/etc. + assert.ok(methodNode.type === 'Identifier'); + const indentation = getIndentation(fixtureCall, sourceCode); + + const [urlArgumentNode] = fixtureCall.arguments; // node - `/smartdata/v1/ping` + assert.ok(urlArgumentNode !== undefined); + + const fixtureCallInformation = {} as FixtureCallInformation; + analyze(fixtureCall, fixtureCallInformation); + + const { responseVariableName, responseBodyReferences } = getResponseBodyReferences( + fixtureCallInformation.root, + scopeManager, + ); + let variableNameToUse: string; + let isResponseVariableDeclared = false; + if (responseVariableName === undefined) { + variableNameToUse = `response${variableCounter === 0 ? '' : variableCounter.toString()}`; + variableCounter++; + } else { + isResponseVariableDeclared = true; + variableNameToUse = responseVariableName; + } + + // convert fixture.api.get to fetch + const fixtureApiCallText = sourceCode.getText(fixtureCall); // e.g. "fixture.api.get(`/smartdata/v1/ping`)"" + const fixtureMethodText = sourceCode.getText(fixtureFunction); // e.g. "fixture.api.get" + let replacedText = fixtureApiCallText.replace(fixtureMethodText, 'await fetch'); + + // convert `/smartdata/v1/ping` to `${BASE_PATH}/ping` + const fixtureArgumentText = sourceCode.getText(urlArgumentNode); // text - e.g. `/smartdata/v1/ping` + let fetchArgumentText = replaceEndpointUrlPrefixWithBasePath(fixtureArgumentText); // test - e.g. `${BASE_PATH}/ping` + + // add request argument if deeded + if (fixtureCallInformation.requestBody !== undefined || fixtureCallInformation.requestHeaders !== undefined) { + fetchArgumentText += [ + ', {', + ` method: '${methodNode.name.toUpperCase()}',`, + ...(fixtureCallInformation.requestBody + ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] + : []), + ...(fixtureCallInformation.requestHeaders + ? [ + ` headers: {`, + ...fixtureCallInformation.requestHeaders.map( + ({ name, value }) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals + ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ), + ` },`, + ] + : []), + '}', + ].join(`\n${indentation}`); + } + + replacedText = replacedText.replace(fixtureArgumentText, fetchArgumentText); + + if (fixtureCallInformation.assertions) { + // add variable declaration if needed + if (!isResponseVariableDeclared) { + replacedText = `const ${variableNameToUse} = ${replacedText}`; + } + // externalize response assertions + replacedText = [ + replacedText, + ...appendAssertions(fixtureCallInformation.assertions, sourceCode, variableNameToUse), + ].join(`;\n${indentation}`); + } + + context.report({ + node: fixtureCall, + messageId: 'preferNativeFetch', + *fix(fixer) { + yield fixer.replaceText(fixtureCallInformation.root, replacedText); + + for (const responseBodyReference of responseBodyReferences) { + yield fixer.replaceText(responseBodyReference, `await ${variableNameToUse}.json()`); + } + }, + }); + } catch (error) { + context.report({ + node: fixtureCall, + messageId: 'unknownError', + data: { + error: String(error), + }, + }); + } + }, + }; + }, +}; +export default rule; From 3ca9645ad8e63ae2dd20c40039f9bdf19e032bee Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 18 Jul 2024 23:35:02 -0400 Subject: [PATCH 002/115] bump release version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c93a5a..9a78574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.6.0", + "version": "6.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "6.6.0", + "version": "6.7.0", "license": "MIT", "devDependencies": { "@checkdigit/jest-config": "^6.0.2", diff --git a/package.json b/package.json index 14ce3b2..a469250 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.6.0", + "version": "6.7.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", From 17f3a154600c3f76ef1ad13eb85f9c171d09eedf Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 11:25:27 -0400 Subject: [PATCH 003/115] handle request method in case of no body/headers --- src/no-fixture.spec.ts | 27 +++++++++++++++++++++++---- src/no-fixture.ts | 6 +++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 8325329..bd8753c 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -65,7 +65,7 @@ describe(ruleId, () => { errors: 1, }, { - // put with request body + // PUT with request body code: ` it('PUT /card', async () => { await fixture.api.put(\`/vault/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); @@ -83,11 +83,11 @@ describe(ruleId, () => { errors: 1, }, { - // put with request header + // PUT with request header code: ` it('PUT /card/:cardId/block', async () => { const noFraudResponse = await fixture.api - .put(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .post(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .set(IF_MATCH_HEADER, originalCard.version) .set('abc', originalCard.name) .set('x-y-z', '123') @@ -97,7 +97,7 @@ describe(ruleId, () => { output: ` it('PUT /card/:cardId/block', async () => { const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { - method: 'PUT', + method: 'POST', headers: { [IF_MATCH_HEADER]: originalCard.version, abc: originalCard.name, @@ -109,6 +109,25 @@ describe(ruleId, () => { `, errors: 1, }, + { + // POST without request header/body + code: ` + it('PUT /card/:cardId/block', async () => { + await fixture.api + .post(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .expect(StatusCodes.NO_CONTENT); + }); + `, + output: ` + it('PUT /card/:cardId/block', async () => { + const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + method: 'POST', + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 763b9ad..01aff85 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -197,7 +197,11 @@ const rule: Rule.RuleModule = { let fetchArgumentText = replaceEndpointUrlPrefixWithBasePath(fixtureArgumentText); // test - e.g. `${BASE_PATH}/ping` // add request argument if deeded - if (fixtureCallInformation.requestBody !== undefined || fixtureCallInformation.requestHeaders !== undefined) { + if ( + methodNode.name !== 'get' || + fixtureCallInformation.requestBody !== undefined || + fixtureCallInformation.requestHeaders !== undefined + ) { fetchArgumentText += [ ', {', ` method: '${methodNode.name.toUpperCase()}',`, From f95934f8d254db4c1646e4fd30dc5c5051992dc9 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 15:51:59 -0400 Subject: [PATCH 004/115] convert response headers access --- src/no-fixture.spec.ts | 27 ++++++++++++++++++++++++ src/no-fixture.ts | 47 ++++++++++++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index bd8753c..b67e092 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -128,6 +128,33 @@ describe(ruleId, () => { `, errors: 1, }, + { + // headers references should use getter + code: ` + it('GET /ping', async () => { + const response = await fixture.api.get(\`/vault/v2/ping\`).expect(StatusCodes.OK); + assert.ok(response.headers.etag); + assert.ok(response.headers[ETAG]); + assert.ok(response.headers['content-type']); + assert.ok(response.header.etag); + assert.ok(response.header[ETAG]); + assert.ok(response.header['content-type']); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + assert.ok(response.headers.get('etag')); + assert.ok(response.headers.get(ETAG)); + assert.ok(response.headers.get('content-type')); + assert.ok(response.headers.get('etag')); + assert.ok(response.headers.get(ETAG)); + assert.ok(response.headers.get('content-type')); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 01aff85..3ef4d2e 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -8,7 +8,7 @@ /* eslint-disable no-console */ -import type { AwaitExpression, Expression, Node, SimpleCallExpression } from 'estree'; +import type { AwaitExpression, Expression, MemberExpression, Node, SimpleCallExpression } from 'estree'; import type { Rule, Scope, SourceCode } from 'eslint'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; @@ -104,9 +104,14 @@ function getAncestor(node: Node, matchType: string, quitType: string) { return getAncestor(parent, matchType, quitType); } -function getResponseBodyReferences(fixtureCallAwait: AwaitExpression, scopeManager: Scope.ScopeManager) { - const results: { responseVariableName?: string; responseBodyReferences: Node[] } = { +function analyzeReferences(fixtureCallAwait: AwaitExpression, scopeManager: Scope.ScopeManager) { + const results: { + responseVariableName?: string; + responseBodyReferences: MemberExpression[]; + responseHeadersReferences: MemberExpression[]; + } = { responseBodyReferences: [], + responseHeadersReferences: [], }; const variableDeclaration = getAncestor(fixtureCallAwait, 'VariableDeclaration', 'FunctionDeclaration'); @@ -117,13 +122,23 @@ function getResponseBodyReferences(fixtureCallAwait: AwaitExpression, scopeManag results.responseVariableName = responseVariable.name; results.responseBodyReferences = responseVariable.references .map((responseBodyReference) => getParent(responseBodyReference.identifier)) - .filter( - (responseBodyNode): responseBodyNode is Node => - responseBodyNode !== null && - responseBodyNode !== undefined && - responseBodyNode.type === 'MemberExpression' && - responseBodyNode.property.type === 'Identifier' && - responseBodyNode.property.name === 'body', + .filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + node.property.name === 'body', + ); + results.responseHeadersReferences = responseVariable.references + .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) + .filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'header' || node.property.name === 'headers'), ); } return results; @@ -173,7 +188,7 @@ const rule: Rule.RuleModule = { const fixtureCallInformation = {} as FixtureCallInformation; analyze(fixtureCall, fixtureCallInformation); - const { responseVariableName, responseBodyReferences } = getResponseBodyReferences( + const { responseVariableName, responseBodyReferences, responseHeadersReferences } = analyzeReferences( fixtureCallInformation.root, scopeManager, ); @@ -246,6 +261,16 @@ const rule: Rule.RuleModule = { for (const responseBodyReference of responseBodyReferences) { yield fixer.replaceText(responseBodyReference, `await ${variableNameToUse}.json()`); } + for (const responseHeadersReference of responseHeadersReferences) { + const parent = getParent(responseHeadersReference); + assert.ok(parent?.type === 'MemberExpression'); + const headerNameNode = parent.property; + const headerName = + // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions + parent.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; + assert.ok(headerName); + yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`); + } }, }); } catch (error) { From 55799e57e8e69975cbf6a14ec29d7501e20bf5b3 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 20:33:11 -0400 Subject: [PATCH 005/115] handle supertest header assertion --- src/no-fixture.spec.ts | 23 +++++++++++++++++++++++ src/no-fixture.ts | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index b67e092..1837b4a 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -155,6 +155,29 @@ describe(ruleId, () => { `, errors: 1, }, + { + // response headers assertion should be externalized with new variable declared if necessary + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/vault/v2/ping\`) + .expect(StatusCodes.OK) + .expect('etag', '123') + .expect('content-type', 'application/json') + .expect(ETAG, correctVersion); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get('etag'), '123'); + assert.equal(response.headers.get('content-type'), 'application/json'); + assert.equal(response.headers.get(ETAG), correctVersion); + }); + `, + errors: 1, + only: true, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 3ef4d2e..c8b8b44 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -89,6 +89,13 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia ) { assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`); } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + assertions.push( + `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); } } return assertions; From 46100483fd3aefa4427a35fdfcb73983fa6d0fff Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 20:46:16 -0400 Subject: [PATCH 006/115] handle header regex assertion --- src/no-fixture.spec.ts | 4 +++- src/no-fixture.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 1837b4a..cdd1378 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -163,7 +163,8 @@ describe(ruleId, () => { .expect(StatusCodes.OK) .expect('etag', '123') .expect('content-type', 'application/json') - .expect(ETAG, correctVersion); + .expect(ETAG, correctVersion) + .expect(ETAG, /1.*/u); }); `, output: ` @@ -173,6 +174,7 @@ describe(ruleId, () => { assert.equal(response.headers.get('etag'), '123'); assert.equal(response.headers.get('content-type'), 'application/json'); assert.equal(response.headers.get(ETAG), correctVersion); + assert.ok(response.headers.get(ETAG).match(/1.*/u)); }); `, errors: 1, diff --git a/src/no-fixture.ts b/src/no-fixture.ts index c8b8b44..0c7e167 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -93,9 +93,15 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia // header assertion const [headerName, headerValue] = expectArguments; assert.ok(headerName && headerValue); - assertions.push( - `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, - ); + if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + assertions.push( + `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + assertions.push( + `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } } } return assertions; From bad7b8d6e46db010c7e07601c621a2bb0fb07ad9 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 21:01:11 -0400 Subject: [PATCH 007/115] handle supertest callback --- src/no-fixture.spec.ts | 19 ++++++++++++++++++- src/no-fixture.ts | 10 +++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index cdd1378..89cb551 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -178,7 +178,24 @@ describe(ruleId, () => { }); `, errors: 1, - only: true, + }, + { + // response callback assertion + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/vault/v2/ping\`) + .expect(validate) + .expect((response)=>console.log(response)); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.ok(validate(response)); + assert.ok((response)=>console.log(response)); + }); + `, + errors: 1, }, ], }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 0c7e167..0da3c11 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -81,13 +81,21 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia if (expectArguments.length === 1) { const [assertionArgument] = expectArguments; assert.ok(assertionArgument); - // status if ( assertionArgument.type === 'MemberExpression' && assertionArgument.object.type === 'Identifier' && assertionArgument.object.name === 'StatusCodes' ) { + // status code assertion assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`); + } else if (assertionArgument.type === 'ArrowFunctionExpression') { + // callback assertion + assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`); + } else if (assertionArgument.type === 'Identifier') { + // callback assertion + assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); + } else { + throw new Error(`Unexpected assertion argument: ${sourceCode.getText(assertionArgument)}`); } } else if (expectArguments.length === 2) { // header assertion From c8062c3bc704af94e6a9d854f6432afc2d1dac18 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 21:16:32 -0400 Subject: [PATCH 008/115] handle response body deep equality assertion --- src/no-fixture.spec.ts | 15 +++++++++++++++ src/no-fixture.ts | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 89cb551..0e406f8 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -179,6 +179,21 @@ describe(ruleId, () => { `, errors: 1, }, + { + // response body assertion + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/vault/v2/ping\`).expect({message:'pong'}); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.deepEqual(response.body, {message:'pong'}); + }); + `, + errors: 1, + }, { // response callback assertion code: ` diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 0da3c11..381f8ae 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -94,6 +94,9 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia } else if (assertionArgument.type === 'Identifier') { // callback assertion assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); + } else if (assertionArgument.type === 'ObjectExpression') { + // body deep equal assertion + assertions.push(`assert.deepEqual(${variableName}.body, ${sourceCode.getText(assertionArgument)})`); } else { throw new Error(`Unexpected assertion argument: ${sourceCode.getText(assertionArgument)}`); } From 4afc2c775aa638154471265502a65d68d539fd4f Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 19 Jul 2024 21:39:00 -0400 Subject: [PATCH 009/115] multiple fixture calls in the same test --- package-lock.json | 4 ++-- package.json | 2 +- src/no-fixture.spec.ts | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a78574..3c93a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.7.0", + "version": "6.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "6.7.0", + "version": "6.6.0", "license": "MIT", "devDependencies": { "@checkdigit/jest-config": "^6.0.2", diff --git a/package.json b/package.json index a469250..14ce3b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.7.0", + "version": "6.6.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 0e406f8..4e25244 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -15,15 +15,19 @@ describe(ruleId, () => { valid: [], invalid: [ { - // no assertion + // response callback assertion code: ` it('GET /ping', async () => { - await fixture.api.get(\`/smartdata/v1/ping\`); + await fixture.api.get(\`/vault/v2/ping\`) + .expect(validate) + .expect((response)=>console.log(response)); }); `, output: ` it('GET /ping', async () => { - await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.ok(validate(response)); + assert.ok((response)=>console.log(response)); }); `, errors: 1, @@ -212,6 +216,31 @@ describe(ruleId, () => { `, errors: 1, }, + { + // multiple fixture calls in the same test + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + const pingResponse = await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/smartdata/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); + await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(pingResponse.status, StatusCodes.OK); + const response1 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`); + assert.equal(response1.status, StatusCodes.OK); + assert.deepEqual(response1.body, {message:'pong'}); + const response2 = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response2.status, StatusCodes.OK); + }); + `, + errors: 4, + }, ], }); }); From 9da24028deac5069818606ca9fa72a4acf9a9cad Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sat, 20 Jul 2024 17:55:14 -0400 Subject: [PATCH 010/115] handle directly returned fixture call --- src/no-fixture.spec.ts | 52 ++++++++++++++++++++++++++++++++++++++++++ src/no-fixture.ts | 31 +++++++++++++++++++++---- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 4e25244..c7d9994 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -241,6 +241,58 @@ describe(ruleId, () => { `, errors: 4, }, + { + // directly return (no await) fixture call + code: ` + it('GET /ping', async () => { + return fixture.api.get(\`/smartdata/v1/ping\`); + }); + `, + output: ` + it('GET /ping', async () => { + return fetch(\`\${BASE_PATH}/ping\`) + }); + `, + errors: 1, + }, + { + // directly return (no await) fixture call with assertion + code: ` + it('GET /ping', async () => { + return fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + return response; + }); + `, + errors: 1, + }, + { + // directly return (no await) fixture call with body/headers + code: ` + it('PUT /card', async () => { + return fixture.api.put(\`/vault/v2/card/\${uuid()}\`) + .set(IF_MATCH_HEADER, originalCard.version) + .send({}); + }); + `, + output: ` + it('PUT /card', async () => { + return fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + method: 'PUT', + body: JSON.stringify({}), + headers: { + [IF_MATCH_HEADER]: originalCard.version, + }, + }) + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 381f8ae..91d3a50 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -8,7 +8,14 @@ /* eslint-disable no-console */ -import type { AwaitExpression, Expression, MemberExpression, Node, SimpleCallExpression } from 'estree'; +import type { + AwaitExpression, + Expression, + MemberExpression, + Node, + ReturnStatement, + SimpleCallExpression, +} from 'estree'; import type { Rule, Scope, SourceCode } from 'eslint'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; @@ -22,7 +29,7 @@ interface NodeParentExtension { } interface FixtureCallInformation { - root: AwaitExpression; + root: AwaitExpression | ReturnStatement; requestBody?: Expression; requestHeaders?: { name: Expression; value: Expression }[]; assertions?: Expression[][]; @@ -37,7 +44,7 @@ function analyze(call: SimpleCallExpression, results: FixtureCallInformation) { assert.ok(parent, 'parent should exist for fixture/supertest call node'); let nextCall; - if (parent.type === 'AwaitExpression') { + if (parent.type === 'AwaitExpression' || parent.type === 'ReturnStatement') { // no more assertions, return the await expression of the fixture call results.root = parent; } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { @@ -128,7 +135,7 @@ function getAncestor(node: Node, matchType: string, quitType: string) { return getAncestor(parent, matchType, quitType); } -function analyzeReferences(fixtureCallAwait: AwaitExpression, scopeManager: Scope.ScopeManager) { +function analyzeReferences(fixtureCallAwait: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) { const results: { responseVariableName?: string; responseBodyReferences: MemberExpression[]; @@ -229,7 +236,12 @@ const rule: Rule.RuleModule = { // convert fixture.api.get to fetch const fixtureApiCallText = sourceCode.getText(fixtureCall); // e.g. "fixture.api.get(`/smartdata/v1/ping`)"" const fixtureMethodText = sourceCode.getText(fixtureFunction); // e.g. "fixture.api.get" - let replacedText = fixtureApiCallText.replace(fixtureMethodText, 'await fetch'); + + const fetchStatementStart = + fixtureCallInformation.root.type === 'ReturnStatement' && fixtureCallInformation.assertions === undefined + ? 'return' + : 'await'; + let replacedText = fixtureApiCallText.replace(fixtureMethodText, `${fetchStatementStart} fetch`); // convert `/smartdata/v1/ping` to `${BASE_PATH}/ping` const fixtureArgumentText = sourceCode.getText(urlArgumentNode); // text - e.g. `/smartdata/v1/ping` @@ -295,6 +307,15 @@ const rule: Rule.RuleModule = { assert.ok(headerName); yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`); } + if ( + fixtureCallInformation.root.type === 'ReturnStatement' && + fixtureCallInformation.assertions !== undefined + ) { + yield fixer.insertTextAfter( + fixtureCallInformation.root, + `;\n${indentation}return ${variableNameToUse};`, + ); + } }, }); } catch (error) { From 8195d05ad9ba507b265b886018e543023445390c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 22 Jul 2024 12:07:42 -0400 Subject: [PATCH 011/115] replace statusCode with status --- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 2 +- src/no-fixture.spec.ts | 22 ++++++++++++++++++++++ src/no-fixture.ts | 30 +++++++++++++++++++++++++----- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c93a5a..ed80f6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", + "@prettier/sync": "0.5.2", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", @@ -1793,6 +1794,22 @@ "prettier": "^3.0.0" } }, + "node_modules/@prettier/sync": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.5.2.tgz", + "integrity": "sha512-Yb569su456XNx5BsH/Vyem7xD6g/y9iLmLUzRKM1a/dhU/D7HqqvkAG72znulXlMXztbV0iiu9O5AL8K98TzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "make-synchronized": "^0.2.8" + }, + "funding": { + "url": "https://github.com/prettier/prettier-synchronized?sponsor=1" + }, + "peerDependencies": { + "prettier": "*" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -5853,6 +5870,16 @@ "license": "ISC", "peer": true }, + "node_modules/make-synchronized": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/make-synchronized/-/make-synchronized-0.2.9.tgz", + "integrity": "sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/fisker/make-synchronized?sponsor=1" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", diff --git a/package.json b/package.json index 14ce3b2..b99cb3e 100644 --- a/package.json +++ b/package.json @@ -82,4 +82,4 @@ "engines": { "node": ">=20.14" } -} +} \ No newline at end of file diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index c7d9994..c7b8794 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -293,6 +293,28 @@ describe(ruleId, () => { `, errors: 1, }, + { + // replace statusCode with status + code: ` + it('PUT /card', async () => { + const response = await fixture.api.get(\`/vault/v2/ping\`); + assert.equal(response.statusCode, StatusCodes.OK); + console.log('status:', response.statusCode); + const response2 = await fixture.api.get(\`/vault/v2/ping\`); + assert.equal(response2.status, StatusCodes.OK); + }); + `, + output: ` + it('PUT /card', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); + console.log('status:', response.status); + const response2 = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response2.status, StatusCodes.OK); + }); + `, + errors: 2, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 91d3a50..b47ae56 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -137,7 +137,7 @@ function getAncestor(node: Node, matchType: string, quitType: string) { function analyzeReferences(fixtureCallAwait: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) { const results: { - responseVariableName?: string; + responseVariable?: Scope.Variable; responseBodyReferences: MemberExpression[]; responseHeadersReferences: MemberExpression[]; } = { @@ -150,7 +150,7 @@ function analyzeReferences(fixtureCallAwait: AwaitExpression | ReturnStatement, const [responseVariable] = scopeManager.getDeclaredVariables(variableDeclaration); assert.ok(responseVariable); - results.responseVariableName = responseVariable.name; + results.responseVariable = responseVariable; results.responseBodyReferences = responseVariable.references .map((responseBodyReference) => getParent(responseBodyReference.identifier)) .filter( @@ -219,18 +219,18 @@ const rule: Rule.RuleModule = { const fixtureCallInformation = {} as FixtureCallInformation; analyze(fixtureCall, fixtureCallInformation); - const { responseVariableName, responseBodyReferences, responseHeadersReferences } = analyzeReferences( + const { responseVariable, responseBodyReferences, responseHeadersReferences } = analyzeReferences( fixtureCallInformation.root, scopeManager, ); let variableNameToUse: string; let isResponseVariableDeclared = false; - if (responseVariableName === undefined) { + if (responseVariable === undefined) { variableNameToUse = `response${variableCounter === 0 ? '' : variableCounter.toString()}`; variableCounter++; } else { isResponseVariableDeclared = true; - variableNameToUse = responseVariableName; + variableNameToUse = responseVariable.name; } // convert fixture.api.get to fetch @@ -294,9 +294,12 @@ const rule: Rule.RuleModule = { *fix(fixer) { yield fixer.replaceText(fixtureCallInformation.root, replacedText); + // handle response body for (const responseBodyReference of responseBodyReferences) { yield fixer.replaceText(responseBodyReference, `await ${variableNameToUse}.json()`); } + + // handle response headers for (const responseHeadersReference of responseHeadersReferences) { const parent = getParent(responseHeadersReference); assert.ok(parent?.type === 'MemberExpression'); @@ -307,6 +310,8 @@ const rule: Rule.RuleModule = { assert.ok(headerName); yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`); } + + // handle direct return without await if ( fixtureCallInformation.root.type === 'ReturnStatement' && fixtureCallInformation.assertions !== undefined @@ -316,6 +321,21 @@ const rule: Rule.RuleModule = { `;\n${indentation}return ${variableNameToUse};`, ); } + + // convert statusCode to status + function* statusCodeReplacer(reference: Scope.Reference) { + const parent = getParent(reference.identifier); + if ( + parent?.type === 'MemberExpression' && + parent.property.type === 'Identifier' && + parent.property.name === 'statusCode' + ) { + yield fixer.replaceText(parent.property, 'status'); + } + } + for (const reference of responseVariable?.references ?? []) { + yield* statusCodeReplacer(reference); + } }, }); } catch (error) { From 5ac48bf86e7c2d4f85008c8172be134d3ce614dc Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 22 Jul 2024 12:09:06 -0400 Subject: [PATCH 012/115] prettier --- package-lock.json | 27 --------------------------- package.json | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed80f6d..3c93a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", - "@prettier/sync": "0.5.2", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", @@ -1794,22 +1793,6 @@ "prettier": "^3.0.0" } }, - "node_modules/@prettier/sync": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@prettier/sync/-/sync-0.5.2.tgz", - "integrity": "sha512-Yb569su456XNx5BsH/Vyem7xD6g/y9iLmLUzRKM1a/dhU/D7HqqvkAG72znulXlMXztbV0iiu9O5AL8K98TzZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "make-synchronized": "^0.2.8" - }, - "funding": { - "url": "https://github.com/prettier/prettier-synchronized?sponsor=1" - }, - "peerDependencies": { - "prettier": "*" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -5870,16 +5853,6 @@ "license": "ISC", "peer": true }, - "node_modules/make-synchronized": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/make-synchronized/-/make-synchronized-0.2.9.tgz", - "integrity": "sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/fisker/make-synchronized?sponsor=1" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", diff --git a/package.json b/package.json index b99cb3e..14ce3b2 100644 --- a/package.json +++ b/package.json @@ -82,4 +82,4 @@ "engines": { "node": ">=20.14" } -} \ No newline at end of file +} From b585412cef5f0b76985997fbd7825006ade1541c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 22 Jul 2024 12:45:46 -0400 Subject: [PATCH 013/115] replace header access through response.get() with response.headers.get() --- src/no-fixture.spec.ts | 22 ++++++++++++++++++++-- src/no-fixture.ts | 19 +++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index c7b8794..f81cec0 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -296,7 +296,7 @@ describe(ruleId, () => { { // replace statusCode with status code: ` - it('PUT /card', async () => { + it('GET /ping', async () => { const response = await fixture.api.get(\`/vault/v2/ping\`); assert.equal(response.statusCode, StatusCodes.OK); console.log('status:', response.statusCode); @@ -305,7 +305,7 @@ describe(ruleId, () => { }); `, output: ` - it('PUT /card', async () => { + it('GET /ping', async () => { const response = await fetch(\`\${BASE_PATH}/ping\`); assert.equal(response.status, StatusCodes.OK); console.log('status:', response.status); @@ -315,6 +315,24 @@ describe(ruleId, () => { `, errors: 2, }, + { + // replace header access through response.get() with response.headers.get() + code: ` + it('PUT /ping', async () => { + const response = await fixture.api.get(\`/vault/v2/ping\`); + assert.equal(response.get(ETAG), correctVersion); + assert.equal(response.get('etag'), correctVersion); + }); + `, + output: ` + it('PUT /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.headers.get(ETAG), correctVersion); + assert.equal(response.headers.get('etag'), correctVersion); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index b47ae56..50dc4d5 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -169,7 +169,7 @@ function analyzeReferences(fixtureCallAwait: AwaitExpression | ReturnStatement, node !== undefined && node.type === 'MemberExpression' && node.property.type === 'Identifier' && - (node.property.name === 'header' || node.property.name === 'headers'), + (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), ); } return results; @@ -302,11 +302,17 @@ const rule: Rule.RuleModule = { // handle response headers for (const responseHeadersReference of responseHeadersReferences) { const parent = getParent(responseHeadersReference); - assert.ok(parent?.type === 'MemberExpression'); - const headerNameNode = parent.property; - const headerName = - // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions - parent.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; + assert.ok(parent); + let headerName; + if (parent.type === 'MemberExpression') { + const headerNameNode = parent.property; + headerName = + // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions + parent.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; + } else if (parent.type === 'CallExpression') { + const headerNameNode = parent.arguments[0]; + headerName = sourceCode.getText(headerNameNode); + } assert.ok(headerName); yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`); } @@ -339,6 +345,7 @@ const rule: Rule.RuleModule = { }, }); } catch (error) { + console.log(error); context.report({ node: fixtureCall, messageId: 'unknownError', From e4340bbac3ba0bdb76cfbf5b32fe67045352b585 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 22 Jul 2024 13:22:58 -0400 Subject: [PATCH 014/115] support expect(200) --- src/no-fixture.spec.ts | 19 +++++++++++++++++-- src/no-fixture.ts | 7 ++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index f81cec0..c79363f 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -318,14 +318,14 @@ describe(ruleId, () => { { // replace header access through response.get() with response.headers.get() code: ` - it('PUT /ping', async () => { + it('GET /ping', async () => { const response = await fixture.api.get(\`/vault/v2/ping\`); assert.equal(response.get(ETAG), correctVersion); assert.equal(response.get('etag'), correctVersion); }); `, output: ` - it('PUT /ping', async () => { + it('GET /ping', async () => { const response = await fetch(\`\${BASE_PATH}/ping\`); assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); @@ -333,6 +333,21 @@ describe(ruleId, () => { `, errors: 1, }, + { + // work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/vault/v2/ping\`).expect(200); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, 200); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 50dc4d5..3a22744 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -89,9 +89,10 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia const [assertionArgument] = expectArguments; assert.ok(assertionArgument); if ( - assertionArgument.type === 'MemberExpression' && - assertionArgument.object.type === 'Identifier' && - assertionArgument.object.name === 'StatusCodes' + (assertionArgument.type === 'MemberExpression' && + assertionArgument.object.type === 'Identifier' && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === 'Literal' ) { // status code assertion assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`); From 4113178ca52b3dbce0f53b1d39838c0eae0ebd52 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 22 Jul 2024 13:56:25 -0400 Subject: [PATCH 015/115] treat function call result as response body assertion similar to object literial --- src/no-fixture.spec.ts | 22 ++++++++++++++++++++-- src/no-fixture.ts | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index c79363f..0a01b4e 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -193,7 +193,7 @@ describe(ruleId, () => { output: ` it('GET /ping', async () => { const response = await fetch(\`\${BASE_PATH}/ping\`); - assert.deepEqual(response.body, {message:'pong'}); + assert.deepEqual(await response.json(), {message:'pong'}); }); `, errors: 1, @@ -234,7 +234,7 @@ describe(ruleId, () => { assert.equal(pingResponse.status, StatusCodes.OK); const response1 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`); assert.equal(response1.status, StatusCodes.OK); - assert.deepEqual(response1.body, {message:'pong'}); + assert.deepEqual(await response1.json(), {message:'pong'}); const response2 = await fetch(\`\${BASE_PATH}/ping\`); assert.equal(response2.status, StatusCodes.OK); }); @@ -348,6 +348,24 @@ describe(ruleId, () => { `, errors: 1, }, + { + // assert response body against function call's return value ".expect(validateBody(response))" + code: ` + it('GET /ping', async () => { + const createdOn = Date.now().toUTCString(); + await fixture.api.get(\`/vault/v2/ping\`).expect(200).expect(validateBody(createdOn)); + }); + `, + output: ` + it('GET /ping', async () => { + const createdOn = Date.now().toUTCString(); + const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), validateBody(createdOn)); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 3a22744..357609a 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -102,11 +102,11 @@ function appendAssertions(expects: Expression[][], sourceCode: SourceCode, varia } else if (assertionArgument.type === 'Identifier') { // callback assertion assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); - } else if (assertionArgument.type === 'ObjectExpression') { + } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { // body deep equal assertion - assertions.push(`assert.deepEqual(${variableName}.body, ${sourceCode.getText(assertionArgument)})`); + assertions.push(`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`); } else { - throw new Error(`Unexpected assertion argument: ${sourceCode.getText(assertionArgument)}`); + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); } } else if (expectArguments.length === 2) { // header assertion From 4787804cf961ac8c7cf0ad62217c16e3965a11b6 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 23 Jul 2024 01:20:27 -0400 Subject: [PATCH 016/115] handle body/headers defined as spreading style variables --- src/no-fixture.spec.ts | 78 ++++++++++++++++++++++++++- src/no-fixture.ts | 116 +++++++++++++++++++++++++++++------------ 2 files changed, 161 insertions(+), 33 deletions(-) diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 0a01b4e..52f9283 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -319,7 +319,7 @@ describe(ruleId, () => { // replace header access through response.get() with response.headers.get() code: ` it('GET /ping', async () => { - const response = await fixture.api.get(\`/vault/v2/ping\`); + const response = await fixture.api.get(\`/vault/v2/ping\`).expect(StatusCodes.OK); assert.equal(response.get(ETAG), correctVersion); assert.equal(response.get('etag'), correctVersion); }); @@ -327,6 +327,7 @@ describe(ruleId, () => { output: ` it('GET /ping', async () => { const response = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(response.status, StatusCodes.OK); assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); }); @@ -366,6 +367,81 @@ describe(ruleId, () => { `, errors: 1, }, + { + // handle spreading variable declaration for body + code: ` + it('returns current server time', async () => { + const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }); + `, + output: ` + it('returns current server time', async () => { + const response = await fetch(\`$\{BASE_PATH}/ping\`); + const responseBody = await response.json(); + assert.equal(response.status, StatusCodes.OK) + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }); + `, + errors: 1, + }, + { + // handle spreading variable declaration for headers when body is presented as well + code: ` + it('returns current server time', async () => { + const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + assert(body); + assert.ok(headers2.get(ETAG)); + }); + `, + output: ` + it('returns current server time', async () => { + const response = await fetch(\`$\{BASE_PATH}/ping\`); + const body = await response.json(); + const headers2 = response.headers; + assert.equal(response.status, StatusCodes.OK) + assert(body); + assert.ok(headers2.get(ETAG)); + }); + `, + errors: 1, + }, + { + // handle spreading variable declaration for headers without body presented but with assertions used + code: ` + it('returns current server time', async () => { + const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + assert.ok(headers.get(ETAG)); + }); + `, + output: ` + it('returns current server time', async () => { + const response = await fetch(\`$\{BASE_PATH}/ping\`); + const headers = response.headers; + assert.equal(response.status, StatusCodes.OK) + assert.ok(headers.get(ETAG)); + }); + `, + errors: 1, + }, + { + // handle spreading variable declaration for headers without body/assertion presented doesn't need to change + code: ` + it('returns current server time', async () => { + const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); + assert.ok(headers.get(ETAG)); + }); + `, + output: ` + it('returns current server time', async () => { + const { headers } = await fetch(\`$\{BASE_PATH}/ping\`); + assert.ok(headers.get(ETAG)); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 357609a..8fea00c 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -136,42 +136,63 @@ function getAncestor(node: Node, matchType: string, quitType: string) { return getAncestor(parent, matchType, quitType); } -function analyzeReferences(fixtureCallAwait: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) { +function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) { const results: { responseVariable?: Scope.Variable; responseBodyReferences: MemberExpression[]; responseHeadersReferences: MemberExpression[]; + spreadResponseBodyVariable?: Scope.Variable; + spreadResponseHeadersVariable?: Scope.Variable; } = { responseBodyReferences: [], responseHeadersReferences: [], }; - const variableDeclaration = getAncestor(fixtureCallAwait, 'VariableDeclaration', 'FunctionDeclaration'); + const variableDeclaration = getAncestor(fixtureCall, 'VariableDeclaration', 'FunctionDeclaration'); if (variableDeclaration && variableDeclaration.type === 'VariableDeclaration') { - const [responseVariable] = scopeManager.getDeclaredVariables(variableDeclaration); - assert.ok(responseVariable); - - results.responseVariable = responseVariable; - results.responseBodyReferences = responseVariable.references - .map((responseBodyReference) => getParent(responseBodyReference.identifier)) - .filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - node.property.name === 'body', - ); - results.responseHeadersReferences = responseVariable.references - .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) - .filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), - ); + const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration); + for (const responseVariable of responseVariables) { + const identifier = responseVariable.identifiers[0]; + assert.ok(identifier); + const identifierParent = getParent(identifier); + assert.ok(identifierParent); + if (identifierParent.type === 'VariableDeclarator') { + // if (declarator.id.type === 'Identifier') { + results.responseVariable = responseVariable; + results.responseBodyReferences = responseVariable.references + .map((responseBodyReference) => getParent(responseBodyReference.identifier)) + .filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + node.property.name === 'body', + ); + results.responseHeadersReferences = responseVariable.references + .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) + .filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), + ); + } else if ( + identifierParent.type === 'Property' && + identifierParent.key.type === 'Identifier' && + identifierParent.key.name === 'body' + ) { + results.spreadResponseBodyVariable = responseVariable; + } else if ( + identifierParent.type === 'Property' && + identifierParent.key.type === 'Identifier' && + identifierParent.key.name === 'headers' + ) { + results.spreadResponseHeadersVariable = responseVariable; + } + } } return results; } @@ -220,10 +241,13 @@ const rule: Rule.RuleModule = { const fixtureCallInformation = {} as FixtureCallInformation; analyze(fixtureCall, fixtureCallInformation); - const { responseVariable, responseBodyReferences, responseHeadersReferences } = analyzeReferences( - fixtureCallInformation.root, - scopeManager, - ); + const { + responseVariable, + responseBodyReferences, + responseHeadersReferences, + spreadResponseBodyVariable, + spreadResponseHeadersVariable, + } = analyzeReferences(fixtureCallInformation.root, scopeManager); let variableNameToUse: string; let isResponseVariableDeclared = false; if (responseVariable === undefined) { @@ -277,6 +301,22 @@ const rule: Rule.RuleModule = { replacedText = replacedText.replace(fixtureArgumentText, fetchArgumentText); + const needVariableRedefine = + spreadResponseBodyVariable !== undefined || + (responseVariable === undefined && fixtureCallInformation.assertions) !== undefined; + + if (needVariableRedefine) { + replacedText = [ + replacedText, + ...(spreadResponseBodyVariable + ? [`const ${spreadResponseBodyVariable.name} = await ${variableNameToUse}.json()`] + : []), + ...(spreadResponseHeadersVariable + ? [`const ${spreadResponseHeadersVariable.name} = ${variableNameToUse}.headers`] + : []), + ].join(`;\n${indentation}`); + } + if (fixtureCallInformation.assertions) { // add variable declaration if needed if (!isResponseVariableDeclared) { @@ -293,7 +333,19 @@ const rule: Rule.RuleModule = { node: fixtureCall, messageId: 'preferNativeFetch', *fix(fixer) { - yield fixer.replaceText(fixtureCallInformation.root, replacedText); + let replacementRootNode: AwaitExpression | ReturnStatement | Node = fixtureCallInformation.root; + if (spreadResponseBodyVariable) { + const identifier = spreadResponseBodyVariable.identifiers[0]; + assert.ok(identifier); + const variableDeclaration = getAncestor(identifier, 'VariableDeclaration', 'FunctionDeclaration'); + assert.ok(variableDeclaration); + replacementRootNode = variableDeclaration; + } else if (fixtureCallInformation.assertions !== undefined && responseVariable === undefined) { + replacementRootNode = + getAncestor(fixtureCallInformation.root, 'VariableDeclaration', 'FunctionDeclaration') ?? + fixtureCallInformation.root; + } + yield fixer.replaceText(replacementRootNode, replacedText); // handle response body for (const responseBodyReference of responseBodyReferences) { @@ -346,7 +398,7 @@ const rule: Rule.RuleModule = { }, }); } catch (error) { - console.log(error); + console.error(`Failed to apply ${ruleId} rule. Error:`, error); context.report({ node: fixtureCall, messageId: 'unknownError', From 8928ca689e09fa8c0464f41c58d1206a338a46d9 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 23 Jul 2024 20:32:02 -0400 Subject: [PATCH 017/115] make status assertion before other assertions, refactoring --- docs/rules/no-fixture.md | 4 +- src/ast/format.ts | 19 ++ src/ast/tree.ts | 29 +++ src/no-fixture.spec.ts | 208 ++++++++++-------- src/no-fixture.ts | 456 ++++++++++++++++++++------------------- 5 files changed, 397 insertions(+), 319 deletions(-) create mode 100644 src/ast/format.ts create mode 100644 src/ast/tree.ts diff --git a/docs/rules/no-fixture.md b/docs/rules/no-fixture.md index 2e4d3eb..ac9fead 100644 --- a/docs/rules/no-fixture.md +++ b/docs/rules/no-fixture.md @@ -4,7 +4,7 @@ ```js it('returns current server time', async () => { - const response = await fixture.api.get(`/smartdata/v1/ping`).expect(StatusCodes.OK); + const response = await fixture.api.get(`/sample-service/v1/ping`).expect(StatusCodes.OK); const body = response.body; const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); @@ -15,7 +15,7 @@ it('returns current server time', async () => { ```js it('returns current server time', async () => { - // assume the existence of - const BASE_PATH = 'https://smartdata.checkdigit/smartdata/v1'; + // assume the existence of - const BASE_PATH = 'https://sample-service.checkdigit/sample-service/v1'; const response = await fetch(`${BASE_PATH}/ping`); assert.equal(response.status, StatusCodes.OK); const body = await response.json(); diff --git a/src/ast/format.ts b/src/ast/format.ts new file mode 100644 index 0000000..6b06a54 --- /dev/null +++ b/src/ast/format.ts @@ -0,0 +1,19 @@ +// format.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { Node } from 'estree'; +import type { SourceCode } from 'eslint'; +import { strict as assert } from 'node:assert'; + +export function getIndentation(node: Node, sourceCode: SourceCode) { + assert.ok(node.loc); + const line = sourceCode.lines[node.loc.start.line - 1]; + assert.ok(line); + const indentMatch = line.match(/^\s*/u); + return indentMatch ? indentMatch[0] : ''; +} diff --git a/src/ast/tree.ts b/src/ast/tree.ts new file mode 100644 index 0000000..b1152e6 --- /dev/null +++ b/src/ast/tree.ts @@ -0,0 +1,29 @@ +// tree.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { Node } from 'estree'; + +type NodeParent = Node | undefined | null; + +interface NodeParentExtension { + parent: NodeParent; +} + +export function getParent(node: Node): Node | undefined | null { + return (node as unknown as NodeParentExtension).parent; +} + +export function getAncestor(node: Node, typeToMatch: string, typeToExit: string) { + const parent = getParent(node); + if (!parent || parent.type === typeToExit) { + return undefined; + } else if (parent.type === typeToMatch) { + return parent; + } + return getAncestor(parent, typeToMatch, typeToExit); +} diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 52f9283..f23fd4f 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -1,4 +1,4 @@ -// no-fixture.ts +// no-fixture.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -15,28 +15,10 @@ describe(ruleId, () => { valid: [], invalid: [ { - // response callback assertion + name: 'assertion with variable declaration', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/vault/v2/ping\`) - .expect(validate) - .expect((response)=>console.log(response)); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); - assert.ok(validate(response)); - assert.ok((response)=>console.log(response)); - }); - `, - errors: 1, - }, - { - // assertion with variable declaration - code: ` - it('GET /ping', async () => { - const pingResponse = await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); const body = pingResponse.body; const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); @@ -44,7 +26,9 @@ describe(ruleId, () => { `, output: ` it('GET /ping', async () => { - const pingResponse = await fetch(\`\${BASE_PATH}/ping\`); + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(pingResponse.status, StatusCodes.OK); const body = await pingResponse.json(); const timeDifference = Date.now() - new Date(body.serverTime).getTime(); @@ -54,25 +38,27 @@ describe(ruleId, () => { errors: 1, }, { - // assertion without variable declaration + name: 'assertion without variable declaration', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); }); `, errors: 1, }, { - // PUT with request body + name: 'PUT with request body', code: ` it('PUT /card', async () => { - await fixture.api.put(\`/vault/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); + await fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); }); `, output: ` @@ -87,11 +73,11 @@ describe(ruleId, () => { errors: 1, }, { - // PUT with request header + name: 'PUT with request header', code: ` it('PUT /card/:cardId/block', async () => { const noFraudResponse = await fixture.api - .post(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .set(IF_MATCH_HEADER, originalCard.version) .set('abc', originalCard.name) .set('x-y-z', '123') @@ -114,11 +100,11 @@ describe(ruleId, () => { errors: 1, }, { - // POST without request header/body + name: 'POST without request header/body', code: ` it('PUT /card/:cardId/block', async () => { await fixture.api - .post(\`/vault/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .expect(StatusCodes.NO_CONTENT); }); `, @@ -133,10 +119,10 @@ describe(ruleId, () => { errors: 1, }, { - // headers references should use getter + name: 'headers references should use getter', code: ` it('GET /ping', async () => { - const response = await fixture.api.get(\`/vault/v2/ping\`).expect(StatusCodes.OK); + const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); assert.ok(response.headers.etag); assert.ok(response.headers[ETAG]); assert.ok(response.headers['content-type']); @@ -147,7 +133,9 @@ describe(ruleId, () => { `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); assert.ok(response.headers.get('etag')); assert.ok(response.headers.get(ETAG)); @@ -160,10 +148,10 @@ describe(ruleId, () => { errors: 1, }, { - // response headers assertion should be externalized with new variable declared if necessary + name: 'response headers assertion should be externalized with new variable declared if necessary', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/vault/v2/ping\`) + await fixture.api.get(\`/sample-service/v2/ping\`) .expect(StatusCodes.OK) .expect('etag', '123') .expect('content-type', 'application/json') @@ -173,7 +161,9 @@ describe(ruleId, () => { `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); assert.equal(response.headers.get('etag'), '123'); assert.equal(response.headers.get('content-type'), 'application/json'); @@ -184,87 +174,103 @@ describe(ruleId, () => { errors: 1, }, { - // response body assertion + name: 'response body assertion', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/vault/v2/ping\`).expect({message:'pong'}); + await fixture.api.get(\`/sample-service/v2/ping\`).expect({message:'pong'}); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.deepEqual(await response.json(), {message:'pong'}); }); `, errors: 1, }, { - // response callback assertion + name: 'response callback assertion', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/vault/v2/ping\`) - .expect(validate) - .expect((response)=>console.log(response)); + await fixture.api.get(\`/sample-service/v2/ping\`) + .expect(validate) + .expect((response)=>console.log(response)); }); - `, + `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.ok(validate(response)); assert.ok((response)=>console.log(response)); }); - `, + `, errors: 1, }, { - // multiple fixture calls in the same test + name: 'multiple fixture calls in the same test', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); - const pingResponse = await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); - await fixture.api.get(\`/smartdata/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); - await fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); }); - `, + `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); - const pingResponse = await fetch(\`\${BASE_PATH}/ping\`); + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(pingResponse.status, StatusCodes.OK); - const response1 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`); + const response1 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { + method: 'GET', + }); assert.equal(response1.status, StatusCodes.OK); assert.deepEqual(await response1.json(), {message:'pong'}); - const response2 = await fetch(\`\${BASE_PATH}/ping\`); + const response2 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response2.status, StatusCodes.OK); }); - `, + `, errors: 4, }, { - // directly return (no await) fixture call + name: 'directly return (no await) fixture call', code: ` it('GET /ping', async () => { - return fixture.api.get(\`/smartdata/v1/ping\`); + return fixture.api.get(\`/sample-service/v1/ping\`); }); `, output: ` it('GET /ping', async () => { - return fetch(\`\${BASE_PATH}/ping\`) + return fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); }); `, errors: 1, }, { - // directly return (no await) fixture call with assertion + name: 'directly return (no await) fixture call with assertion', code: ` it('GET /ping', async () => { - return fixture.api.get(\`/smartdata/v1/ping\`).expect(StatusCodes.OK); + return fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); return response; }); @@ -272,10 +278,10 @@ describe(ruleId, () => { errors: 1, }, { - // directly return (no await) fixture call with body/headers + name: 'directly return (no await) fixture call with body/headers', code: ` it('PUT /card', async () => { - return fixture.api.put(\`/vault/v2/card/\${uuid()}\`) + return fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`) .set(IF_MATCH_HEADER, originalCard.version) .send({}); }); @@ -288,45 +294,51 @@ describe(ruleId, () => { headers: { [IF_MATCH_HEADER]: originalCard.version, }, - }) + }); }); `, errors: 1, }, { - // replace statusCode with status + name: 'replace statusCode with status', code: ` it('GET /ping', async () => { - const response = await fixture.api.get(\`/vault/v2/ping\`); + const response = await fixture.api.get(\`/sample-service/v2/ping\`); assert.equal(response.statusCode, StatusCodes.OK); console.log('status:', response.statusCode); - const response2 = await fixture.api.get(\`/vault/v2/ping\`); + const response2 = await fixture.api.get(\`/sample-service/v2/ping\`); assert.equal(response2.status, StatusCodes.OK); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); console.log('status:', response.status); - const response2 = await fetch(\`\${BASE_PATH}/ping\`); + const response2 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response2.status, StatusCodes.OK); }); `, errors: 2, }, { - // replace header access through response.get() with response.headers.get() + name: 'replace header access through response.get() with response.headers.get()', code: ` it('GET /ping', async () => { - const response = await fixture.api.get(\`/vault/v2/ping\`).expect(StatusCodes.OK); + const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); assert.equal(response.get(ETAG), correctVersion); assert.equal(response.get('etag'), correctVersion); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, StatusCodes.OK); assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); @@ -335,32 +347,36 @@ describe(ruleId, () => { errors: 1, }, { - // work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well + name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', code: ` it('GET /ping', async () => { - await fixture.api.get(\`/vault/v2/ping\`).expect(200); + await fixture.api.get(\`/sample-service/v2/ping\`).expect(200); }); `, output: ` it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, 200); }); `, errors: 1, }, { - // assert response body against function call's return value ".expect(validateBody(response))" + name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', code: ` it('GET /ping', async () => { const createdOn = Date.now().toUTCString(); - await fixture.api.get(\`/vault/v2/ping\`).expect(200).expect(validateBody(createdOn)); + await fixture.api.get(\`/sample-service/v2/ping\`).expect(200).expect(validateBody(createdOn)); }); `, output: ` it('GET /ping', async () => { const createdOn = Date.now().toUTCString(); - const response = await fetch(\`\${BASE_PATH}/ping\`); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); assert.equal(response.status, 200); assert.deepEqual(await response.json(), validateBody(createdOn)); }); @@ -368,7 +384,7 @@ describe(ruleId, () => { errors: 1, }, { - // handle spreading variable declaration for body + name: 'handle spreading variable declaration for body', code: ` it('returns current server time', async () => { const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); @@ -378,9 +394,11 @@ describe(ruleId, () => { `, output: ` it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`); + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); const responseBody = await response.json(); - assert.equal(response.status, StatusCodes.OK) const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); }); @@ -388,7 +406,7 @@ describe(ruleId, () => { errors: 1, }, { - // handle spreading variable declaration for headers when body is presented as well + name: 'handle spreading variable declaration for headers when body is presented as well', code: ` it('returns current server time', async () => { const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); @@ -398,10 +416,12 @@ describe(ruleId, () => { `, output: ` it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`); + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); const body = await response.json(); const headers2 = response.headers; - assert.equal(response.status, StatusCodes.OK) assert(body); assert.ok(headers2.get(ETAG)); }); @@ -409,7 +429,7 @@ describe(ruleId, () => { errors: 1, }, { - // handle spreading variable declaration for headers without body presented but with assertions used + name: 'handle spreading variable declaration for headers without body presented but with assertions used', code: ` it('returns current server time', async () => { const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); @@ -418,16 +438,18 @@ describe(ruleId, () => { `, output: ` it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`); + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); const headers = response.headers; - assert.equal(response.status, StatusCodes.OK) assert.ok(headers.get(ETAG)); }); `, errors: 1, }, { - // handle spreading variable declaration for headers without body/assertion presented doesn't need to change + name: 'handle spreading variable declaration for headers without body/assertion presented does not need to change', code: ` it('returns current server time', async () => { const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); @@ -436,7 +458,9 @@ describe(ruleId, () => { `, output: ` it('returns current server time', async () => { - const { headers } = await fetch(\`$\{BASE_PATH}/ping\`); + const { headers } = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); assert.ok(headers.get(ETAG)); }); `, diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 8fea00c..ed71735 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -10,55 +10,65 @@ import type { AwaitExpression, + CallExpression, Expression, MemberExpression, - Node, ReturnStatement, SimpleCallExpression, + VariableDeclaration, } from 'estree'; import type { Rule, Scope, SourceCode } from 'eslint'; +import { getAncestor, getParent } from './ast/tree'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; +import { getIndentation } from './ast/format'; export const ruleId = 'no-fixture'; -type NodeParent = Node | undefined | null; - -interface NodeParentExtension { - parent: NodeParent; -} - interface FixtureCallInformation { - root: AwaitExpression | ReturnStatement; + rootNode: AwaitExpression | ReturnStatement | VariableDeclaration; + fixtureNode: AwaitExpression | SimpleCallExpression; + variableDeclaration?: VariableDeclaration; requestBody?: Expression; requestHeaders?: { name: Expression; value: Expression }[]; assertions?: Expression[][]; } -function getParent(node: Node): Node | undefined | null { - return (node as unknown as NodeParentExtension).parent; -} - -function analyze(call: SimpleCallExpression, results: FixtureCallInformation) { +// recursively analyze the fixture/supertest call chain to collect information of request/response +function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation) { const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); let nextCall; - if (parent.type === 'AwaitExpression' || parent.type === 'ReturnStatement') { - // no more assertions, return the await expression of the fixture call - results.root = parent; + if (parent.type === 'ReturnStatement') { + // direct return, no variable declaration / await + results.fixtureNode = call; + results.rootNode = parent; + } else if (parent.type === 'AwaitExpression') { + results.fixtureNode = call; + // [TODO:] should we consider variable declaration without await?? + const variableDeclaration = getAncestor(parent, 'VariableDeclaration', 'FunctionDeclaration'); + if (variableDeclaration?.type === 'VariableDeclaration') { + results.variableDeclaration = variableDeclaration; + results.rootNode = variableDeclaration; + } else { + results.rootNode = parent; + } } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { if (parent.property.name === 'expect') { + // supertest assertions const assertionCall = getParent(parent); assert.ok(assertionCall && assertionCall.type === 'CallExpression'); results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; nextCall = assertionCall; } else if (parent.property.name === 'send') { + // request body const sendRequestBodyCall = getParent(parent); assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); results.requestBody = sendRequestBodyCall.arguments[0] as Expression; nextCall = sendRequestBodyCall; } else if (parent.property.name === 'set') { + // request headers const setRequestHeaderCall = getParent(parent); assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression]; @@ -69,97 +79,37 @@ function analyze(call: SimpleCallExpression, results: FixtureCallInformation) { throw new Error(`Unexpected expression in fixture/supertest call ${String(parent)}`); } if (nextCall) { - analyze(nextCall, results); - } -} - -function replaceEndpointUrlPrefixWithBasePath(url: string) { - // eslint-disable-next-line no-template-curly-in-string - return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); -} - -function isValidPropertyName(name: unknown) { - return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); -} - -function appendAssertions(expects: Expression[][], sourceCode: SourceCode, variableName: string) { - const assertions: string[] = []; - for (const expectArguments of expects) { - if (expectArguments.length === 1) { - const [assertionArgument] = expectArguments; - assert.ok(assertionArgument); - if ( - (assertionArgument.type === 'MemberExpression' && - assertionArgument.object.type === 'Identifier' && - assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === 'Literal' - ) { - // status code assertion - assertions.push(`assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`); - } else if (assertionArgument.type === 'ArrowFunctionExpression') { - // callback assertion - assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`); - } else if (assertionArgument.type === 'Identifier') { - // callback assertion - assertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); - } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { - // body deep equal assertion - assertions.push(`assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`); - } else { - throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); - } - } else if (expectArguments.length === 2) { - // header assertion - const [headerName, headerValue] = expectArguments; - assert.ok(headerName && headerValue); - if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { - assertions.push( - `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, - ); - } else { - assertions.push( - `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, - ); - } - } - } - return assertions; -} - -function getAncestor(node: Node, matchType: string, quitType: string) { - const parent = getParent(node); - if (!parent || parent.type === quitType) { - return undefined; - } else if (parent.type === matchType) { - return parent; + analyzeFixtureCall(nextCall, results); } - return getAncestor(parent, matchType, quitType); } -function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scopeManager: Scope.ScopeManager) { +// analyze response related variables and their references0 +function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) { const results: { - responseVariable?: Scope.Variable; - responseBodyReferences: MemberExpression[]; - responseHeadersReferences: MemberExpression[]; - spreadResponseBodyVariable?: Scope.Variable; - spreadResponseHeadersVariable?: Scope.Variable; + variable?: Scope.Variable; + bodyReferences: MemberExpression[]; + headersReferences: MemberExpression[]; + statusReferences: MemberExpression[]; + spreadBodyVariable?: Scope.Variable; + spreadHeadersVariable?: Scope.Variable; } = { - responseBodyReferences: [], - responseHeadersReferences: [], + bodyReferences: [], + headersReferences: [], + statusReferences: [], }; - const variableDeclaration = getAncestor(fixtureCall, 'VariableDeclaration', 'FunctionDeclaration'); - if (variableDeclaration && variableDeclaration.type === 'VariableDeclaration') { - const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration); + if (fixtureInformation.variableDeclaration) { + const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration); for (const responseVariable of responseVariables) { const identifier = responseVariable.identifiers[0]; assert.ok(identifier); const identifierParent = getParent(identifier); assert.ok(identifierParent); if (identifierParent.type === 'VariableDeclarator') { - // if (declarator.id.type === 'Identifier') { - results.responseVariable = responseVariable; - results.responseBodyReferences = responseVariable.references + // e.g. const response = ... + results.variable = responseVariable; + // e.g. response.body + results.bodyReferences = responseVariable.references .map((responseBodyReference) => getParent(responseBodyReference.identifier)) .filter( (node): node is MemberExpression => @@ -169,7 +119,8 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope node.property.type === 'Identifier' && node.property.name === 'body', ); - results.responseHeadersReferences = responseVariable.references + // e.g. response.headers / response.header / response.get() + results.headersReferences = responseVariable.references .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) .filter( (node): node is MemberExpression => @@ -179,30 +130,102 @@ function analyzeReferences(fixtureCall: AwaitExpression | ReturnStatement, scope node.property.type === 'Identifier' && (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), ); + // e.g. response.status / response.statusCode + results.statusReferences = responseVariable.references + .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) + .filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'status' || node.property.name === 'statusCode'), + ); } else if ( + // body reference through destruction/renaming, e.g. "const { body } = ..." identifierParent.type === 'Property' && identifierParent.key.type === 'Identifier' && identifierParent.key.name === 'body' ) { - results.spreadResponseBodyVariable = responseVariable; + results.spreadBodyVariable = responseVariable; } else if ( + // header reference through destruction/renaming, e.g. "const { headers } = ..." identifierParent.type === 'Property' && identifierParent.key.type === 'Identifier' && identifierParent.key.name === 'headers' ) { - results.spreadResponseHeadersVariable = responseVariable; + results.spreadHeadersVariable = responseVariable; + } else { + throw new Error(`Unknown response variable reference: ${responseVariable.name}`); } } } return results; } -function getIndentation(node: Node, sourceCode: SourceCode) { - assert.ok(node.loc); - const line = sourceCode.lines[node.loc.start.line - 1]; - assert.ok(line); - const indentMatch = line.match(/^\s*/u); - return indentMatch ? indentMatch[0] : ''; +// `/sample-service/v1/ping` -> `${BASE_PATH}/ping` +function replaceEndpointUrlPrefixWithBasePath(url: string) { + // eslint-disable-next-line no-template-curly-in-string + return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); +} + +function isValidPropertyName(name: unknown) { + return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); +} + +function createResponseAssertions( + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + variableName: string, +) { + // [TODO:] make sure status assertion is ordered as the first + let statusAssertion: string | undefined; + const nonStatusAssertions: string[] = []; + for (const expectArguments of fixtureCallInformation.assertions ?? []) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + if ( + (assertionArgument.type === 'MemberExpression' && + assertionArgument.object.type === 'Identifier' && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === 'Literal' + ) { + // status code assertion + statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`; + } else if (assertionArgument.type === 'ArrowFunctionExpression') { + // callback assertion + nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`); + } else if (assertionArgument.type === 'Identifier') { + // callback assertion + nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); + } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + // body deep equal assertion + nonStatusAssertions.push( + `assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`, + ); + } else { + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); + } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + nonStatusAssertions.push( + `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + nonStatusAssertions.push( + `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } + } + } + return { + statusAssertion, + nonStatusAssertions, + }; } const rule: Rule.RuleModule = { @@ -223,136 +246,123 @@ const rule: Rule.RuleModule = { create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - let variableCounter = 0; + let responseVariableCounter = 0; return { - 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': (fixtureCall: Node) => { + 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( + fixtureCall: CallExpression, + ) => { try { assert.ok(fixtureCall.type === 'CallExpression'); - const fixtureFunction = fixtureCall.callee; // node - fixture.api.get + const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get assert.ok(fixtureFunction.type === 'MemberExpression'); - const methodNode = fixtureFunction.property; // get/put/etc. - assert.ok(methodNode.type === 'Identifier'); const indentation = getIndentation(fixtureCall, sourceCode); - const [urlArgumentNode] = fixtureCall.arguments; // node - `/smartdata/v1/ping` + const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` assert.ok(urlArgumentNode !== undefined); const fixtureCallInformation = {} as FixtureCallInformation; - analyze(fixtureCall, fixtureCallInformation); + analyzeFixtureCall(fixtureCall, fixtureCallInformation); const { - responseVariable, - responseBodyReferences, - responseHeadersReferences, - spreadResponseBodyVariable, - spreadResponseHeadersVariable, - } = analyzeReferences(fixtureCallInformation.root, scopeManager); - let variableNameToUse: string; - let isResponseVariableDeclared = false; + variable: responseVariable, + bodyReferences: responseBodyReferences, + headersReferences: responseHeadersReferences, + statusReferences: responseStatusReferences, + spreadBodyVariable: spreadResponseBodyVariable, + spreadHeadersVariable: spreadResponseHeadersVariable, + } = analyzeResponseReferences(fixtureCallInformation, scopeManager); + + // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` + const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); + const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); + + // fetch request argument + const methodNode = fixtureFunction.property; // get/put/etc. + assert.ok(methodNode.type === 'Identifier'); + const fetchRequestArgumentLines = [ + '{', + ` method: '${methodNode.name.toUpperCase()}',`, + ...(fixtureCallInformation.requestBody + ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] + : []), + ...(fixtureCallInformation.requestHeaders + ? [ + ` headers: {`, + ...fixtureCallInformation.requestHeaders.map( + ({ name, value }) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals + ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ), + ` },`, + ] + : []), + '}', + ].join(`\n${indentation}`); + + let responseVariableNameToUse: string; if (responseVariable === undefined) { - variableNameToUse = `response${variableCounter === 0 ? '' : variableCounter.toString()}`; - variableCounter++; + responseVariableNameToUse = `response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; + responseVariableCounter++; } else { - isResponseVariableDeclared = true; - variableNameToUse = responseVariable.name; + responseVariableNameToUse = responseVariable.name; } - // convert fixture.api.get to fetch - const fixtureApiCallText = sourceCode.getText(fixtureCall); // e.g. "fixture.api.get(`/smartdata/v1/ping`)"" - const fixtureMethodText = sourceCode.getText(fixtureFunction); // e.g. "fixture.api.get" - - const fetchStatementStart = - fixtureCallInformation.root.type === 'ReturnStatement' && fixtureCallInformation.assertions === undefined - ? 'return' - : 'await'; - let replacedText = fixtureApiCallText.replace(fixtureMethodText, `${fetchStatementStart} fetch`); - - // convert `/smartdata/v1/ping` to `${BASE_PATH}/ping` - const fixtureArgumentText = sourceCode.getText(urlArgumentNode); // text - e.g. `/smartdata/v1/ping` - let fetchArgumentText = replaceEndpointUrlPrefixWithBasePath(fixtureArgumentText); // test - e.g. `${BASE_PATH}/ping` - - // add request argument if deeded - if ( - methodNode.name !== 'get' || - fixtureCallInformation.requestBody !== undefined || - fixtureCallInformation.requestHeaders !== undefined - ) { - fetchArgumentText += [ - ', {', - ` method: '${methodNode.name.toUpperCase()}',`, - ...(fixtureCallInformation.requestBody - ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] - : []), - ...(fixtureCallInformation.requestHeaders - ? [ - ` headers: {`, - ...fixtureCallInformation.requestHeaders.map( - ({ name, value }) => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals - ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, - ), - ` },`, - ] - : []), - '}', - ].join(`\n${indentation}`); - } - - replacedText = replacedText.replace(fixtureArgumentText, fetchArgumentText); - - const needVariableRedefine = + const needResponseVariableRedefine = spreadResponseBodyVariable !== undefined || - (responseVariable === undefined && fixtureCallInformation.assertions) !== undefined; - - if (needVariableRedefine) { - replacedText = [ - replacedText, - ...(spreadResponseBodyVariable - ? [`const ${spreadResponseBodyVariable.name} = await ${variableNameToUse}.json()`] - : []), - ...(spreadResponseHeadersVariable - ? [`const ${spreadResponseHeadersVariable.name} = ${variableNameToUse}.headers`] - : []), - ].join(`;\n${indentation}`); - } + (responseVariable === undefined && fixtureCallInformation.assertions !== undefined); + + const responseBodyHeadersVariableRedefineLines = needResponseVariableRedefine + ? [ + ...(spreadResponseBodyVariable + ? [`const ${spreadResponseBodyVariable.name} = await ${responseVariableNameToUse}.json()`] + : []), + ...(spreadResponseHeadersVariable + ? [`const ${spreadResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`] + : []), + ] + : []; + + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + ); - if (fixtureCallInformation.assertions) { - // add variable declaration if needed - if (!isResponseVariableDeclared) { - replacedText = `const ${variableNameToUse} = ${replacedText}`; - } - // externalize response assertions - replacedText = [ - replacedText, - ...appendAssertions(fixtureCallInformation.assertions, sourceCode, variableNameToUse), - ].join(`;\n${indentation}`); - } + // add variable declaration if needed + const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; + const fetchStatementText = !needResponseVariableRedefine + ? fetchCallText + : `const ${responseVariableNameToUse} = await ${fetchCallText}`; + + const nodeToReplace = needResponseVariableRedefine + ? fixtureCallInformation.rootNode + : fixtureCallInformation.fixtureNode; + const appendingAssignmentAndAssertionText = [ + '', + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...responseBodyHeadersVariableRedefineLines, + ...nonStatusAssertions, + ].join(`;\n${indentation}`); context.report({ node: fixtureCall, messageId: 'preferNativeFetch', *fix(fixer) { - let replacementRootNode: AwaitExpression | ReturnStatement | Node = fixtureCallInformation.root; - if (spreadResponseBodyVariable) { - const identifier = spreadResponseBodyVariable.identifiers[0]; - assert.ok(identifier); - const variableDeclaration = getAncestor(identifier, 'VariableDeclaration', 'FunctionDeclaration'); - assert.ok(variableDeclaration); - replacementRootNode = variableDeclaration; - } else if (fixtureCallInformation.assertions !== undefined && responseVariable === undefined) { - replacementRootNode = - getAncestor(fixtureCallInformation.root, 'VariableDeclaration', 'FunctionDeclaration') ?? - fixtureCallInformation.root; - } - yield fixer.replaceText(replacementRootNode, replacedText); + yield fixer.replaceText(nodeToReplace, fetchStatementText); + + const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); + yield fixer.insertTextAfter( + nodeToReplace, + needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, + ); - // handle response body + // handle response body references for (const responseBodyReference of responseBodyReferences) { - yield fixer.replaceText(responseBodyReference, `await ${variableNameToUse}.json()`); + yield fixer.replaceText(responseBodyReference, `await ${responseVariableNameToUse}.json()`); } - // handle response headers + // handle response headers references for (const responseHeadersReference of responseHeadersReferences) { const parent = getParent(responseHeadersReference); assert.ok(parent); @@ -367,34 +377,29 @@ const rule: Rule.RuleModule = { headerName = sourceCode.getText(headerNameNode); } assert.ok(headerName); - yield fixer.replaceText(parent, `${variableNameToUse}.headers.get(${headerName})`); + yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); + } + + // convert response.statusCode to response.status + for (const responseStatusReference of responseStatusReferences) { + if ( + responseStatusReference.property.type === 'Identifier' && + responseStatusReference.property.name === 'statusCode' + ) { + yield fixer.replaceText(responseStatusReference.property, `status`); + } } // handle direct return without await if ( - fixtureCallInformation.root.type === 'ReturnStatement' && + fixtureCallInformation.rootNode.type === 'ReturnStatement' && fixtureCallInformation.assertions !== undefined ) { yield fixer.insertTextAfter( - fixtureCallInformation.root, - `;\n${indentation}return ${variableNameToUse};`, + fixtureCallInformation.rootNode, + `\n${indentation}return ${responseVariableNameToUse};`, ); } - - // convert statusCode to status - function* statusCodeReplacer(reference: Scope.Reference) { - const parent = getParent(reference.identifier); - if ( - parent?.type === 'MemberExpression' && - parent.property.type === 'Identifier' && - parent.property.name === 'statusCode' - ) { - yield fixer.replaceText(parent.property, 'status'); - } - } - for (const reference of responseVariable?.references ?? []) { - yield* statusCodeReplacer(reference); - } }, }); } catch (error) { @@ -411,4 +416,5 @@ const rule: Rule.RuleModule = { }; }, }; + export default rule; From 1a58717d67be60098678992b4210c6bced34be18 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 23 Jul 2024 22:27:40 -0400 Subject: [PATCH 018/115] manager response variable naming conflict better --- src/ast/tree.ts | 10 +++--- src/no-fixture.spec.ts | 72 +++++++++++++++++++++++++++++++++++++++--- src/no-fixture.ts | 54 ++++++++++++++++++++++++++----- 3 files changed, 119 insertions(+), 17 deletions(-) diff --git a/src/ast/tree.ts b/src/ast/tree.ts index b1152e6..28fb484 100644 --- a/src/ast/tree.ts +++ b/src/ast/tree.ts @@ -18,12 +18,14 @@ export function getParent(node: Node): Node | undefined | null { return (node as unknown as NodeParentExtension).parent; } -export function getAncestor(node: Node, typeToMatch: string, typeToExit: string) { +export function getAncestor(node: Node, matcher: string | ((testNode: Node) => boolean), typeToExit?: string) { const parent = getParent(node); - if (!parent || parent.type === typeToExit) { + if (!parent || (typeToExit !== undefined && parent.type === typeToExit)) { return undefined; - } else if (parent.type === typeToMatch) { + } else if (typeof matcher === 'string' && parent.type === matcher) { + return parent; + } else if (typeof matcher === 'function' && matcher(parent)) { return parent; } - return getAncestor(parent, typeToMatch, typeToExit); + return getAncestor(parent, matcher, typeToExit); } diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index f23fd4f..4291bdc 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -230,15 +230,15 @@ describe(ruleId, () => { method: 'GET', }); assert.equal(pingResponse.status, StatusCodes.OK); - const response1 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { + const response2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { method: 'GET', }); - assert.equal(response1.status, StatusCodes.OK); - assert.deepEqual(await response1.json(), {message:'pong'}); - const response2 = await fetch(\`\${BASE_PATH}/ping\`, { + assert.equal(response2.status, StatusCodes.OK); + assert.deepEqual(await response2.json(), {message:'pong'}); + const response3 = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response2.status, StatusCodes.OK); + assert.equal(response3.status, StatusCodes.OK); }); `, errors: 4, @@ -466,6 +466,68 @@ describe(ruleId, () => { `, errors: 1, }, + { + name: 'avoid response variable name conflict with existing variables in the same scope', + code: ` + it('returns current server time', async () => { + const response = 'foo'; + const response1 = 'bar'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('returns current server time', async () => { + const response = 'foo'; + const response1 = 'bar'; + const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + const response3 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response3.status, StatusCodes.OK); + }); + `, + errors: 2, + }, + { + name: 'response variable names in different scope do not conflict with each other', + code: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + }); + `, + errors: 2, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index ed71735..046300d 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -13,6 +13,7 @@ import type { CallExpression, Expression, MemberExpression, + Node, ReturnStatement, SimpleCallExpression, VariableDeclaration, @@ -228,6 +229,45 @@ function createResponseAssertions( }; } +function getResponseVariableNameToUse( + scopeManager: Scope.ScopeManager, + fixtureCallInformation: FixtureCallInformation, + scopeVariablesMap: Map, +) { + if (fixtureCallInformation.variableDeclaration) { + const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; + // [TODO:] double check if it works for destruction/rename declaration + if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { + return firstDeclaration.id.name; + } + } + + const closestFunctionExpression = getAncestor(fixtureCallInformation.rootNode, (node: Node) => + ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type), + ); + scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); /*?*/ + assert.ok(closestFunctionExpression); + const scope = scopeManager.acquire(closestFunctionExpression); + assert.ok(scope !== null); + let scopeVariables = scopeVariablesMap.get(scope); + if (!scopeVariables) { + scopeVariables = [...scope.set.keys()]; + scopeVariablesMap.set(scope, scopeVariables); + } + + let responseVariableCounter = 0; + let responseVariableNameToUse; + while (responseVariableNameToUse === undefined) { + responseVariableCounter++; + responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`; + if (scopeVariables.includes(responseVariableNameToUse)) { + responseVariableNameToUse = undefined; + } + } + scopeVariables.push(responseVariableNameToUse); + return responseVariableNameToUse; +} + const rule: Rule.RuleModule = { meta: { type: 'suggestion', @@ -246,7 +286,7 @@ const rule: Rule.RuleModule = { create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - let responseVariableCounter = 0; + const scopeVariablesMap = new Map(); return { 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( @@ -300,13 +340,11 @@ const rule: Rule.RuleModule = { '}', ].join(`\n${indentation}`); - let responseVariableNameToUse: string; - if (responseVariable === undefined) { - responseVariableNameToUse = `response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; - responseVariableCounter++; - } else { - responseVariableNameToUse = responseVariable.name; - } + const responseVariableNameToUse = getResponseVariableNameToUse( + scopeManager, + fixtureCallInformation, + scopeVariablesMap, + ); const needResponseVariableRedefine = spreadResponseBodyVariable !== undefined || From 6a801a1ee91a3d5a9e774a3f49b699639f7e30eb Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 26 Jul 2024 12:30:12 -0400 Subject: [PATCH 019/115] response body reference handling, inline response body access, response callback assertion, error message improvement, etc. --- src/ast/tree.ts | 27 ++++++- src/no-fixture.spec.ts | 87 ++++++++++++++++++++- src/no-fixture.ts | 166 +++++++++++++++++++++++++++-------------- 3 files changed, 222 insertions(+), 58 deletions(-) diff --git a/src/ast/tree.ts b/src/ast/tree.ts index 28fb484..a1a4393 100644 --- a/src/ast/tree.ts +++ b/src/ast/tree.ts @@ -18,14 +18,35 @@ export function getParent(node: Node): Node | undefined | null { return (node as unknown as NodeParentExtension).parent; } -export function getAncestor(node: Node, matcher: string | ((testNode: Node) => boolean), typeToExit?: string) { +export function getAncestor( + node: Node, + matcher: string | ((testNode: Node) => boolean), + exitMatcher?: string | ((testNode: Node) => boolean), +): Node | undefined { const parent = getParent(node); - if (!parent || (typeToExit !== undefined && parent.type === typeToExit)) { + if (!parent) { return undefined; } else if (typeof matcher === 'string' && parent.type === matcher) { return parent; } else if (typeof matcher === 'function' && matcher(parent)) { return parent; + } else if (typeof exitMatcher === 'string' && parent.type === exitMatcher) { + return undefined; + } else if (typeof exitMatcher === 'function' && exitMatcher(parent)) { + return undefined; } - return getAncestor(parent, matcher, typeToExit); + return getAncestor(parent, matcher, exitMatcher); +} + +export function getEnclosingStatement(node: Node) { + return getAncestor( + node, + (parentNode) => parentNode.type.endsWith('Statement') || parentNode.type.endsWith('Declaration'), + ); +} + +export function getEnclosingScopeNode(node: Node) { + return getAncestor(node, (parentNode) => + ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression'].includes(parentNode.type), + ); } diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index 4291bdc..bdd6683 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -54,6 +54,23 @@ describe(ruleId, () => { `, errors: 1, }, + { + name: 'assertion without variable declaration', + code: ` + it('GET /ping', async () => { + await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); + }); + `, + output: ` + it('GET /ping', async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); + }); + `, + errors: 1, + }, { name: 'PUT with request body', code: ` @@ -205,7 +222,7 @@ describe(ruleId, () => { method: 'GET', }); assert.ok(validate(response)); - assert.ok((response)=>console.log(response)); + assert.ok(console.log(response)); }); `, errors: 1, @@ -528,6 +545,74 @@ describe(ruleId, () => { `, errors: 2, }, + { + name: 'inline access to response body should be extracted to a variable', + code: ` + export async function validatePin( + fixture, + ) { + const paymentSecurityServicePublicKey = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; + } + `, + output: ` + export async function validatePin( + fixture, + ) { + const response = await fetch(\`\${BASE_PATH}/public-key\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const responseBody = await response.json(); + const paymentSecurityServicePublicKey = responseBody.publicKey; + } + `, + errors: 1, + }, + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: ` + it.each(temporalHeaders)('imports key using a $createdOnHeaderName header', async ({ createdOnHeaderName: _ }) => { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + await fixture.api + .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) + .set(CREATED_ON_HEADER, createdOn) + .send({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }) + .expect(StatusCodes.NO_CONTENT) + .expect(ETAG_HEADER, '1') + .expect((res) => verifyTemporalHeaders(res, createdOn)); + }); + `, + output: ` + it.each(temporalHeaders)('imports key using a $createdOnHeaderName header', async ({ createdOnHeaderName: _ }) => { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.ok(verifyTemporalHeaders(response, createdOn)); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 046300d..121228d 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -6,8 +6,6 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -/* eslint-disable no-console */ - import type { AwaitExpression, CallExpression, @@ -19,7 +17,7 @@ import type { VariableDeclaration, } from 'estree'; import type { Rule, Scope, SourceCode } from 'eslint'; -import { getAncestor, getParent } from './ast/tree'; +import { getEnclosingScopeNode, getEnclosingStatement, getParent } from './ast/tree'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; import { getIndentation } from './ast/format'; @@ -33,6 +31,8 @@ interface FixtureCallInformation { requestBody?: Expression; requestHeaders?: { name: Expression; value: Expression }[]; assertions?: Expression[][]; + inlineStatementNode?: Node; + inlineBodyReference?: MemberExpression; } // recursively analyze the fixture/supertest call chain to collect information of request/response @@ -47,11 +47,18 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo results.rootNode = parent; } else if (parent.type === 'AwaitExpression') { results.fixtureNode = call; - // [TODO:] should we consider variable declaration without await?? - const variableDeclaration = getAncestor(parent, 'VariableDeclaration', 'FunctionDeclaration'); - if (variableDeclaration?.type === 'VariableDeclaration') { - results.variableDeclaration = variableDeclaration; - results.rootNode = variableDeclaration; + const enclosingStatement = getEnclosingStatement(parent); + assert.ok(enclosingStatement); + const awaitParent = getParent(parent); + if (awaitParent?.type === 'MemberExpression') { + results.rootNode = parent; + results.inlineStatementNode = enclosingStatement; + if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') { + results.inlineBodyReference = awaitParent; + } + } else if (enclosingStatement.type === 'VariableDeclaration') { + results.variableDeclaration = enclosingStatement; + results.rootNode = enclosingStatement; } else { results.rootNode = parent; } @@ -91,8 +98,8 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s bodyReferences: MemberExpression[]; headersReferences: MemberExpression[]; statusReferences: MemberExpression[]; - spreadBodyVariable?: Scope.Variable; - spreadHeadersVariable?: Scope.Variable; + destructuringBodyVariable?: Scope.Variable; + destructuringHeadersVariable?: Scope.Variable; } = { bodyReferences: [], headersReferences: [], @@ -148,14 +155,14 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s identifierParent.key.type === 'Identifier' && identifierParent.key.name === 'body' ) { - results.spreadBodyVariable = responseVariable; + results.destructuringBodyVariable = responseVariable; } else if ( // header reference through destruction/renaming, e.g. "const { headers } = ..." identifierParent.type === 'Property' && identifierParent.key.type === 'Identifier' && identifierParent.key.name === 'headers' ) { - results.spreadHeadersVariable = responseVariable; + results.destructuringHeadersVariable = responseVariable; } else { throw new Error(`Unknown response variable reference: ${responseVariable.name}`); } @@ -174,12 +181,12 @@ function isValidPropertyName(name: unknown) { return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); } +// eslint-disable-next-line sonarjs/cognitive-complexity function createResponseAssertions( fixtureCallInformation: FixtureCallInformation, sourceCode: SourceCode, - variableName: string, + responseVariableName: string, ) { - // [TODO:] make sure status assertion is ordered as the first let statusAssertion: string | undefined; const nonStatusAssertions: string[] = []; for (const expectArguments of fixtureCallInformation.assertions ?? []) { @@ -190,20 +197,32 @@ function createResponseAssertions( (assertionArgument.type === 'MemberExpression' && assertionArgument.object.type === 'Identifier' && assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === 'Literal' + assertionArgument.type === 'Literal' || + sourceCode.getText(assertionArgument).includes('StatusCodes.') ) { // status code assertion - statusAssertion = `assert.equal(${variableName}.status, ${sourceCode.getText(assertionArgument)})`; + statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; } else if (assertionArgument.type === 'ArrowFunctionExpression') { - // callback assertion - nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)})`); + // callback assertion using arrow function + let functionBody = sourceCode.getText(assertionArgument.body); + + const [originalResponseArgument] = assertionArgument.params; + assert.ok(originalResponseArgument?.type === 'Identifier'); + const originalResponseArgumentName = originalResponseArgument.name; + if (originalResponseArgumentName !== responseVariableName) { + functionBody = functionBody.replace( + new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), + responseVariableName, + ); + } + nonStatusAssertions.push(`assert.ok(${functionBody})`); } else if (assertionArgument.type === 'Identifier') { - // callback assertion - nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${variableName}))`); + // callback assertion using function reference + nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`); } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { // body deep equal assertion nonStatusAssertions.push( - `assert.deepEqual(await ${variableName}.json(), ${sourceCode.getText(assertionArgument)})`, + `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, ); } else { throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); @@ -214,11 +233,11 @@ function createResponseAssertions( assert.ok(headerName && headerValue); if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { nonStatusAssertions.push( - `assert.ok(${variableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + `assert.ok(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, ); } else { nonStatusAssertions.push( - `assert.equal(${variableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + `assert.equal(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, ); } } @@ -242,12 +261,10 @@ function getResponseVariableNameToUse( } } - const closestFunctionExpression = getAncestor(fixtureCallInformation.rootNode, (node: Node) => - ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type), - ); - scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); /*?*/ - assert.ok(closestFunctionExpression); - const scope = scopeManager.acquire(closestFunctionExpression); + const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode); + scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); + assert.ok(enclosingScopeNode); + const scope = scopeManager.acquire(enclosingScopeNode); assert.ok(scope !== null); let scopeVariables = scopeVariablesMap.get(scope); if (!scopeVariables) { @@ -268,6 +285,15 @@ function getResponseVariableNameToUse( return responseVariableNameToUse; } +function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean { + const parent = getParent(responseBodyReference); + return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier'; +} + +function getResponseBodyRetrievalText(responseVariableName: string) { + return `await ${responseVariableName}.json()`; +} + const rule: Rule.RuleModule = { meta: { type: 'suggestion', @@ -278,11 +304,12 @@ const rule: Rule.RuleModule = { messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', unknownError: - 'Unknown error occurred: {{ error }}. Please manually convert the fixture API call to fetch API call.', + 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', }, fixable: 'code', schema: [], }, + // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; @@ -309,8 +336,8 @@ const rule: Rule.RuleModule = { bodyReferences: responseBodyReferences, headersReferences: responseHeadersReferences, statusReferences: responseStatusReferences, - spreadBodyVariable: spreadResponseBodyVariable, - spreadHeadersVariable: spreadResponseHeadersVariable, + destructuringBodyVariable: destructuringResponseBodyVariable, + destructuringHeadersVariable: destructuringResponseHeadersVariable, } = analyzeResponseReferences(fixtureCallInformation, scopeManager); // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` @@ -346,17 +373,30 @@ const rule: Rule.RuleModule = { scopeVariablesMap, ); - const needResponseVariableRedefine = - spreadResponseBodyVariable !== undefined || - (responseVariable === undefined && fixtureCallInformation.assertions !== undefined); + const isResponseBodyVariableRedefinitionNeeded = + destructuringResponseBodyVariable !== undefined || + fixtureCallInformation.inlineBodyReference !== undefined || + (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); + const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; - const responseBodyHeadersVariableRedefineLines = needResponseVariableRedefine + const isResponseVariableRedefinitionNeeded = + (responseVariable === undefined && fixtureCallInformation.assertions !== undefined) || + isResponseBodyVariableRedefinitionNeeded; + + const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded ? [ - ...(spreadResponseBodyVariable - ? [`const ${spreadResponseBodyVariable.name} = await ${responseVariableNameToUse}.json()`] - : []), - ...(spreadResponseHeadersVariable - ? [`const ${spreadResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`] + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseBodyVariable + ? [ + `const ${destructuringResponseBodyVariable.name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseBodyVariableRedefinitionNeeded + ? [ + `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : []), + ...(destructuringResponseHeadersVariable + ? [`const ${destructuringResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`] : []), ] : []; @@ -369,11 +409,11 @@ const rule: Rule.RuleModule = { // add variable declaration if needed const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; - const fetchStatementText = !needResponseVariableRedefine + const fetchStatementText = !isResponseVariableRedefinitionNeeded ? fetchCallText : `const ${responseVariableNameToUse} = await ${fetchCallText}`; - const nodeToReplace = needResponseVariableRedefine + const nodeToReplace = isResponseVariableRedefinitionNeeded ? fixtureCallInformation.rootNode : fixtureCallInformation.fixtureNode; const appendingAssignmentAndAssertionText = [ @@ -387,17 +427,33 @@ const rule: Rule.RuleModule = { node: fixtureCall, messageId: 'preferNativeFetch', *fix(fixer) { - yield fixer.replaceText(nodeToReplace, fetchStatementText); - - const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); - yield fixer.insertTextAfter( - nodeToReplace, - needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, - ); + if (fixtureCallInformation.inlineStatementNode) { + const preInlineDeclaration = [ + fetchStatementText, + `${appendingAssignmentAndAssertionText};\n${indentation}`, + ].join(``); + yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration); + } else { + yield fixer.replaceText(nodeToReplace, fetchStatementText); + + const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); + yield fixer.insertTextAfter( + nodeToReplace, + needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, + ); + } // handle response body references for (const responseBodyReference of responseBodyReferences) { - yield fixer.replaceText(responseBodyReference, `await ${responseVariableNameToUse}.json()`); + yield fixer.replaceText( + responseBodyReference, + isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference) + ? redefineResponseBodyVariableName + : getResponseBodyRetrievalText(responseVariableNameToUse), + ); + } + if (fixtureCallInformation.inlineBodyReference) { + yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); } // handle response headers references @@ -428,7 +484,7 @@ const rule: Rule.RuleModule = { } } - // handle direct return without await + // handle direct return statement without await, e.g. "return fixture.api.get(...);" if ( fixtureCallInformation.rootNode.type === 'ReturnStatement' && fixtureCallInformation.assertions !== undefined @@ -441,12 +497,14 @@ const rule: Rule.RuleModule = { }, }); } catch (error) { - console.error(`Failed to apply ${ruleId} rule. Error:`, error); + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); context.report({ node: fixtureCall, messageId: 'unknownError', data: { - error: String(error), + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), }, }); } From caea03a78bac5e55df113d55b99449ca05d63521 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 26 Jul 2024 22:44:13 -0400 Subject: [PATCH 020/115] experiment separated no-fixture-headers rule --- src/index.ts | 4 +- src/no-fixture-headers.spec.ts | 122 +++++++++++++++++++++++++++++ src/no-fixture-headers.ts | 138 +++++++++++++++++++++++++++++++++ src/no-fixture.spec.ts | 87 +++++++++++++++++++++ src/no-fixture.ts | 107 ++++++++++++++++--------- 5 files changed, 419 insertions(+), 39 deletions(-) create mode 100644 src/no-fixture-headers.spec.ts create mode 100644 src/no-fixture-headers.ts diff --git a/src/index.ts b/src/index.ts index 2def8ba..e97fe7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './no-fixture'; +import noFixtureHeaders, { ruleId as noFixtureHeadersRuleId } from './no-fixture-headers'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; @@ -33,6 +34,7 @@ export default { [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, + [noFixtureHeadersRuleId]: noFixtureHeaders, }, configs: { all: { @@ -49,6 +51,7 @@ export default { [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${noFixtureHeadersRuleId}`]: 'error', }, }, recommended: { @@ -64,7 +67,6 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', }, }, }, diff --git a/src/no-fixture-headers.spec.ts b/src/no-fixture-headers.spec.ts new file mode 100644 index 0000000..64ed166 --- /dev/null +++ b/src/no-fixture-headers.spec.ts @@ -0,0 +1,122 @@ +// no-fixture.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-fixture-headers'; +import createTester from './tester.test'; +import { describe } from '@jest/globals'; + +describe(ruleId, () => { + createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: ` + it.each(temporalHeaders)( + 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', + async () => { + const zoneKeyId = uuid(); + await importTestMultipartZoneKey(fixture, zoneKeyId); + const createdOn = new Date().toISOString(); + + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + const headers1 = response.headers; + assert.equal(headers1.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers[ETAG_HEADER], '1'); + assert.equal(response.headers.etag, '1'); + assert.ok(verifyTemporalHeaders(response)); + + const updatedOn = new Date().toISOString(); + const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '339923C206BA8B19EEBF995DEE6619F7', + checkValue: '1ADEE9', + }), + headers: { + [IF_MATCH_HEADER]: headers1[ETAG_HEADER], + [CREATED_ON_HEADER]: updatedOn, + }, + }); + assert.equal(response2.status, StatusCodes.NO_CONTENT); + const headers2 = response2.headers; + assert.equal(headers2.get(ETAG_HEADER), '2'); + assert.ok(verifyTemporalHeaders(response2)); + + // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); + // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); + }, + ); + `, + output: ` + it.each(temporalHeaders)( + 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', + async () => { + const zoneKeyId = uuid(); + await importTestMultipartZoneKey(fixture, zoneKeyId); + const createdOn = new Date().toISOString(); + + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + const headers1 = response.headers; + assert.equal(headers1.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get('etag'), '1'); + assert.ok(verifyTemporalHeaders(response)); + + const updatedOn = new Date().toISOString(); + const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '339923C206BA8B19EEBF995DEE6619F7', + checkValue: '1ADEE9', + }), + headers: { + [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), + [CREATED_ON_HEADER]: updatedOn, + }, + }); + assert.equal(response2.status, StatusCodes.NO_CONTENT); + const headers2 = response2.headers; + assert.equal(headers2.get(ETAG_HEADER), '2'); + assert.ok(verifyTemporalHeaders(response2)); + + // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); + // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); + }, + ); + `, + errors: 2, + only: true, + }, + ], + }); +}); diff --git a/src/no-fixture-headers.ts b/src/no-fixture-headers.ts new file mode 100644 index 0000000..8c0678a --- /dev/null +++ b/src/no-fixture-headers.ts @@ -0,0 +1,138 @@ +// no-fixture.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { + Identifier, + MemberExpression, + VariableDeclarator, +} from 'estree'; +import { getEnclosingScopeNode, getParent } from './ast/tree'; +import { type Rule } from 'eslint'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'no-fixture-headers'; + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch API over customized fixture API.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + unknownError: + 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', + }, + fixable: 'code', + schema: [], + }, + // eslint-disable-next-line max-lines-per-function + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + + return { + // eslint-disable-next-line max-lines-per-function + 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => { + try { + const enclosingScopeNode = getEnclosingScopeNode(fetchCall); + assert.ok(fetchCall.id.type === 'Identifier'); + const fetchVariableName = fetchCall.id.name; /*?*/ + assert.ok(enclosingScopeNode !== undefined, 'enclosing scope node should exist'); + const scope = scopeManager.acquire(enclosingScopeNode); + const responseVariable = scope?.variables.find((variable) => { + const identifier = variable.identifiers[0]; + return identifier?.type === 'Identifier' && identifier.name === fetchVariableName; + }); + if (responseVariable === undefined) { + return; + } + + const headersReferences = responseVariable.references + .map((reference) => getParent(reference.identifier)) + .filter( + (parent): parent is MemberExpression => + parent?.type === 'MemberExpression' && + parent.property.type === 'Identifier' && + parent.property.name === 'headers', + ); + const directHeadersReferences = headersReferences + .map(getParent) + .filter( + (parent): parent is MemberExpression => + parent?.type === 'MemberExpression' && + !(parent.property.type === 'Identifier' && parent.property.name === 'get'), + ); + directHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/ + + const reDeclaredHeadersVariableNames = headersReferences + .map((reference) => getParent(reference)) + .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator') + .map((declarator) => (declarator.id as Identifier).name); + + const indirectHeadersReferences = reDeclaredHeadersVariableNames + .map((variableName) => { + const headersVariable = scope?.variables.find((variable) => { + const identifier = variable.identifiers[0]; + return identifier?.type === 'Identifier' && identifier.name === variableName; + }); + return ( + headersVariable?.references + .map((reference) => getParent(reference.identifier)) + .filter( + (parent): parent is MemberExpression => + parent?.type === 'MemberExpression' && + !(parent.property.type === 'Identifier' && parent.property.name === 'get'), + ) ?? [] + ); + }) + .flat(); + indirectHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/ + + const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].map< + [MemberExpression, string] + >((reference) => { + sourceCode.getText(reference); /*?*/ + const headerNameNode = reference.property; /*?*/ + const headerName = + // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions + reference.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; /*?*/ + const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`; + return [reference, replacementText]; + }); + + context.report({ + node: fetchCall, + messageId: 'preferNativeFetch', + *fix(fixer) { + // handle response headers references + for (const [node, replacementText] of invalidHeadersReferences) { + yield fixer.replaceText(node, replacementText); + } + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: fetchCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts index bdd6683..c419320 100644 --- a/src/no-fixture.spec.ts +++ b/src/no-fixture.spec.ts @@ -613,6 +613,93 @@ describe(ruleId, () => { `, errors: 1, }, + // { + // name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + // code: ` + // it.each(temporalHeaders)( + // 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', + // async () => { + // const zoneKeyId = uuid(); + // await importTestMultipartZoneKey(fixture, zoneKeyId); + // const createdOn = new Date().toISOString(); + + // const keyId = uuid(); + // const { headers: headers1 } = await fixture.api + // .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) + // .set(CREATED_ON_HEADER, createdOn) + // .send({ + // key: '71CA52F757D7C0B45A16C6C04EAFD704', + // checkValue: '4F35C4', + // }) + // .expect(StatusCodes.NO_CONTENT) + // .expect(ETAG_HEADER, '1') + // .expect(verifyTemporalHeaders); + + // const updatedOn = new Date().toISOString(); + // const { headers: headers2 } = await fixture.api + // .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) + // .set(IF_MATCH_HEADER, headers1[ETAG_HEADER]) + // .set(CREATED_ON_HEADER, updatedOn) + // .send({ + // key: '339923C206BA8B19EEBF995DEE6619F7', + // checkValue: '1ADEE9', + // }) + // .expect(StatusCodes.NO_CONTENT) + // .expect(ETAG_HEADER, '2') + // .expect(verifyTemporalHeaders); + + // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); + // }, + // ); + // `, + // output: ` + // it.each(temporalHeaders)( + // 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', + // async () => { + // const zoneKeyId = uuid(); + // await importTestMultipartZoneKey(fixture, zoneKeyId); + // const createdOn = new Date().toISOString(); + + // const keyId = uuid(); + // const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + // method: 'PUT', + // body: JSON.stringify({ + // key: '71CA52F757D7C0B45A16C6C04EAFD704', + // checkValue: '4F35C4', + // }), + // headers: { + // [CREATED_ON_HEADER]: createdOn, + // }, + // }); + // assert.equal(response.status, StatusCodes.NO_CONTENT); + // const headers1 = response.headers; + // assert.equal(headers1.get(ETAG_HEADER), '1'); + // assert.ok(verifyTemporalHeaders(response)); + + // const updatedOn = new Date().toISOString(); + // const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + // method: 'PUT', + // body: JSON.stringify({ + // key: '339923C206BA8B19EEBF995DEE6619F7', + // checkValue: '1ADEE9', + // }), + // headers: { + // [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), + // [CREATED_ON_HEADER]: updatedOn, + // }, + // }); + // assert.equal(response2.status, StatusCodes.NO_CONTENT); + // const headers2 = response2.headers; + // assert.equal(headers2.get(ETAG_HEADER), '2'); + // assert.ok(verifyTemporalHeaders(response2)); + + // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); + // }, + // ); + // `, + // errors: 2, + // only: true, + // }, ], }); }); diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 121228d..8dbbdc0 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -16,7 +16,7 @@ import type { SimpleCallExpression, VariableDeclaration, } from 'estree'; -import type { Rule, Scope, SourceCode } from 'eslint'; +import { type Rule, type Scope, SourceCode } from 'eslint'; import { getEnclosingScopeNode, getEnclosingStatement, getParent } from './ast/tree'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; @@ -36,7 +36,7 @@ interface FixtureCallInformation { } // recursively analyze the fixture/supertest call chain to collect information of request/response -function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation) { +function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); @@ -84,10 +84,10 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo nextCall = setRequestHeaderCall; } } else { - throw new Error(`Unexpected expression in fixture/supertest call ${String(parent)}`); + throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}`); } if (nextCall) { - analyzeFixtureCall(nextCall, results); + analyzeFixtureCall(nextCall, results, sourceCode); } } @@ -100,6 +100,7 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s statusReferences: MemberExpression[]; destructuringBodyVariable?: Scope.Variable; destructuringHeadersVariable?: Scope.Variable; + destructuringHeadersReferences?: MemberExpression[] | undefined; } = { bodyReferences: [], headersReferences: [], @@ -109,6 +110,7 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s if (fixtureInformation.variableDeclaration) { const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration); for (const responseVariable of responseVariables) { + const scope = responseVariable.scope; const identifier = responseVariable.identifiers[0]; assert.ok(identifier); const identifierParent = getParent(identifier); @@ -116,39 +118,36 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s if (identifierParent.type === 'VariableDeclarator') { // e.g. const response = ... results.variable = responseVariable; + const responseReferences = responseVariable.references.map((responseReference) => + getParent(responseReference.identifier), + ); // e.g. response.body - results.bodyReferences = responseVariable.references - .map((responseBodyReference) => getParent(responseBodyReference.identifier)) - .filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - node.property.name === 'body', - ); + results.bodyReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + node.property.name === 'body', + ); // e.g. response.headers / response.header / response.get() - results.headersReferences = responseVariable.references - .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) - .filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), - ); + results.headersReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), + ); // e.g. response.status / response.statusCode - results.statusReferences = responseVariable.references - .map((responseHeadersReference) => getParent(responseHeadersReference.identifier)) - .filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - (node.property.name === 'status' || node.property.name === 'statusCode'), - ); + results.statusReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'status' || node.property.name === 'statusCode'), + ); } else if ( // body reference through destruction/renaming, e.g. "const { body } = ..." identifierParent.type === 'Property' && @@ -163,6 +162,17 @@ function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, s identifierParent.key.name === 'headers' ) { results.destructuringHeadersVariable = responseVariable; + results.destructuringHeadersReferences = scope.set + .get(responseVariable.name) + ?.references.map((reference) => reference.identifier) + .map(getParent) + .filter( + (parent): parent is MemberExpression => + parent?.type === 'MemberExpression' && + parent.property.type === 'Identifier' && + parent.property.name !== 'get' && + getParent(parent)?.type !== 'CallExpression', + ); } else { throw new Error(`Unknown response variable reference: ${responseVariable.name}`); } @@ -186,6 +196,7 @@ function createResponseAssertions( fixtureCallInformation: FixtureCallInformation, sourceCode: SourceCode, responseVariableName: string, + destructuringResponseHeadersVariable: Scope.Variable | undefined, ) { let statusAssertion: string | undefined; const nonStatusAssertions: string[] = []; @@ -231,13 +242,17 @@ function createResponseAssertions( // header assertion const [headerName, headerValue] = expectArguments; assert.ok(headerName && headerValue); + const headersReference = + destructuringResponseHeadersVariable !== undefined + ? destructuringResponseHeadersVariable.name + : `${responseVariableName}.headers`; if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { nonStatusAssertions.push( - `assert.ok(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, ); } else { nonStatusAssertions.push( - `assert.equal(${responseVariableName}.headers.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, ); } } @@ -255,7 +270,6 @@ function getResponseVariableNameToUse( ) { if (fixtureCallInformation.variableDeclaration) { const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; - // [TODO:] double check if it works for destruction/rename declaration if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { return firstDeclaration.id.name; } @@ -316,6 +330,7 @@ const rule: Rule.RuleModule = { const scopeVariablesMap = new Map(); return { + // eslint-disable-next-line max-lines-per-function 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( fixtureCall: CallExpression, ) => { @@ -329,7 +344,7 @@ const rule: Rule.RuleModule = { assert.ok(urlArgumentNode !== undefined); const fixtureCallInformation = {} as FixtureCallInformation; - analyzeFixtureCall(fixtureCall, fixtureCallInformation); + analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); const { variable: responseVariable, @@ -338,6 +353,7 @@ const rule: Rule.RuleModule = { statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, destructuringHeadersVariable: destructuringResponseHeadersVariable, + // destructuringHeadersReferences: destructuringResponseHeadersReferences, } = analyzeResponseReferences(fixtureCallInformation, scopeManager); // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` @@ -405,6 +421,7 @@ const rule: Rule.RuleModule = { fixtureCallInformation, sourceCode, responseVariableNameToUse, + destructuringResponseHeadersVariable, ); // add variable declaration if needed @@ -426,6 +443,7 @@ const rule: Rule.RuleModule = { context.report({ node: fixtureCall, messageId: 'preferNativeFetch', + // eslint-disable-next-line sonarjs/cognitive-complexity *fix(fixer) { if (fixtureCallInformation.inlineStatementNode) { const preInlineDeclaration = [ @@ -473,6 +491,19 @@ const rule: Rule.RuleModule = { assert.ok(headerName); yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); } + // if (destructuringResponseHeadersVariable !== undefined) { + // for (const destructuringResponseHeadersReference of destructuringResponseHeadersReferences ?? []) { + // const headerNameNode = destructuringResponseHeadersReference.property; + // const headerName = destructuringResponseHeadersReference.computed + // ? sourceCode.getText(headerNameNode) + // : `'${sourceCode.getText(headerNameNode)}'`; + + // yield fixer.replaceText( + // destructuringResponseHeadersReference, + // `${destructuringResponseHeadersVariable.name}.get(${headerName})`, + // ); + // } + // } // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { From 18208b7158e4d06f367142ba47ece87be5d6e0ce Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 26 Jul 2024 23:05:31 -0400 Subject: [PATCH 021/115] make espree external to pass the cjs esbuild --- package.json | 2 +- src/no-fixture-headers.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 14ce3b2..0ac62f9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", + "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", "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", diff --git a/src/no-fixture-headers.ts b/src/no-fixture-headers.ts index 8c0678a..31442f9 100644 --- a/src/no-fixture-headers.ts +++ b/src/no-fixture-headers.ts @@ -6,11 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import type { - Identifier, - MemberExpression, - VariableDeclarator, -} from 'estree'; +import type { Identifier, MemberExpression, VariableDeclarator } from 'estree'; import { getEnclosingScopeNode, getParent } from './ast/tree'; import { type Rule } from 'eslint'; import { strict as assert } from 'node:assert'; From 2a05895b45d8509a716f702751f84becc91945ab Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 26 Jul 2024 23:59:00 -0400 Subject: [PATCH 022/115] use typescript-config beta --- package-lock.json | 869 +++++++++++++++++++++++++++------------------- package.json | 4 +- src/ast/format.ts | 2 +- src/no-fixture.ts | 2 +- 4 files changed, 510 insertions(+), 367 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c93a5a..5dc7634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", - "@checkdigit/typescript-config": "6.0.0", + "@checkdigit/typescript-config": "7.1.3-PR.64-9fc8", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", @@ -61,9 +61,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", - "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.0.tgz", + "integrity": "sha512-P4fwKI2mjEb3ZU5cnMJzvRsRKGBUcs8jvxIoRmr6ufAY9Xk2Bz7JubRTTivkw55c7WQJfTECeqYVa+HZ0FzREg==", "dev": true, "license": "MIT", "peer": true, @@ -115,14 +115,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", - "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/types": "^7.24.9", + "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -160,49 +160,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", @@ -219,18 +176,17 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", - "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.0.tgz", + "integrity": "sha512-bIkOa2ZJYn7FHnepzr5iX9Kmz8FjIz4UKzJ9zhX3dnYuVW0xul9RuR3skBfoLu+FPTQw90EHW9rJsSZhyLQ3fQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -265,20 +221,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", @@ -313,15 +255,15 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", - "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.8" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -430,9 +372,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", + "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", "dev": true, "license": "MIT", "peer": true, @@ -649,37 +591,34 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", - "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.0.tgz", + "integrity": "sha512-ubALThHQy4GCf6mbb+5ZRNmLLCI7bJ3f8Q6LHBSRlSKSWj5a7dSUzJBLv3VuIhFrFPgjF4IzPF567YG/HSCdZA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -699,9 +638,9 @@ } }, "node_modules/@babel/types": { - "version": "7.24.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", - "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.0.tgz", + "integrity": "sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==", "dev": true, "license": "MIT", "peer": true, @@ -755,27 +694,45 @@ } }, "node_modules/@checkdigit/typescript-config": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-6.0.0.tgz", - "integrity": "sha512-J/+6vv9y2lHdPscJl0kQrq8bDG5k8MBgfgxJEl2tEwyTdUYyOjqkgm+QiGinVfx35NRt/N1cfkZFnO3aLnudUQ==", + "version": "7.1.3-PR.64-9fc8", + "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-7.1.3-PR.64-9fc8.tgz", + "integrity": "sha512-wIJZYjdKYCQ/bU9zRoRUFRLusIcqS4fL4GsraFGjTm5dqmNBpFDRO9PNBBgPc0PfrO05fuUWk+Es7Io5TXn90Q==", "dev": true, "license": "MIT", "bin": { "builder": "bin/builder.mjs" }, "engines": { - "node": ">=20.9" + "node": ">=20.14" }, "peerDependencies": { - "@types/node": ">=20.9", - "esbuild": "0.19.8", - "typescript": ">=5.3.2 <5.4.0" + "@types/node": ">=20.14", + "esbuild": "0.23.0", + "typescript": ">=5.5.3 <5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", "cpu": [ "arm" ], @@ -787,13 +744,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", "cpu": [ "arm64" ], @@ -805,13 +762,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", "cpu": [ "x64" ], @@ -823,13 +780,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "cpu": [ "arm64" ], @@ -841,13 +798,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", "cpu": [ "x64" ], @@ -859,13 +816,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", "cpu": [ "arm64" ], @@ -877,13 +834,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", "cpu": [ "x64" ], @@ -895,13 +852,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", "cpu": [ "arm" ], @@ -913,13 +870,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", "cpu": [ "arm64" ], @@ -931,13 +888,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", "cpu": [ "ia32" ], @@ -949,13 +906,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", "cpu": [ "loong64" ], @@ -967,13 +924,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", "cpu": [ "mips64el" ], @@ -985,13 +942,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", "cpu": [ "ppc64" ], @@ -1003,13 +960,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", "cpu": [ "riscv64" ], @@ -1021,13 +978,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", "cpu": [ "s390x" ], @@ -1039,13 +996,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", "cpu": [ "x64" ], @@ -1057,13 +1014,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", "cpu": [ "x64" ], @@ -1075,13 +1032,31 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", "cpu": [ "x64" ], @@ -1093,13 +1068,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", "cpu": [ "x64" ], @@ -1111,13 +1086,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", "cpu": [ "arm64" ], @@ -1129,13 +1104,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", "cpu": [ "ia32" ], @@ -1147,13 +1122,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "cpu": [ "x64" ], @@ -1165,7 +1140,7 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1192,6 +1167,72 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -1218,6 +1259,30 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1873,9 +1938,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "8.56.11", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", + "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1946,9 +2011,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.14.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", - "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", "dev": true, "license": "MIT", "peer": true, @@ -1984,17 +2049,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", - "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2018,16 +2083,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", - "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "engines": { @@ -2047,14 +2112,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", - "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2065,14 +2130,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", - "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2093,9 +2158,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", - "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true, "license": "MIT", "engines": { @@ -2107,14 +2172,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", - "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2135,43 +2200,17 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", - "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2185,13 +2224,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", - "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2277,6 +2316,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2630,13 +2683,13 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -2761,9 +2814,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", "dev": true, "funding": [ { @@ -3196,9 +3249,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.829", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz", - "integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", "dev": true, "license": "ISC", "peer": true @@ -3377,9 +3430,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3388,31 +3441,33 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" } }, "node_modules/escalade": { @@ -3627,6 +3682,17 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -3650,6 +3716,19 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3705,6 +3784,30 @@ "eslint": ">=5.16.0" } }, + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-node/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3783,28 +3886,15 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", "peer": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/espree": { @@ -3825,33 +3915,17 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "license": "MIT", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "peer": true, "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/esprima": { @@ -4330,6 +4404,46 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -4529,9 +4643,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "peer": true, @@ -5916,15 +6030,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -5958,9 +6076,9 @@ "peer": true }, "node_modules/node-releases": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz", - "integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true, "license": "MIT", "peer": true @@ -7297,6 +7415,32 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7463,10 +7607,9 @@ } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", "peer": true, "engines": { @@ -7554,9 +7697,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "peer": true, diff --git a/package.json b/package.json index 0ac62f9..0dabcbb 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", + "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", "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", @@ -64,7 +64,7 @@ "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", - "@checkdigit/typescript-config": "6.0.0", + "@checkdigit/typescript-config": "7.1.3-PR.64-9fc8", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/src/ast/format.ts b/src/ast/format.ts index 6b06a54..16bfaff 100644 --- a/src/ast/format.ts +++ b/src/ast/format.ts @@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert'; export function getIndentation(node: Node, sourceCode: SourceCode) { assert.ok(node.loc); const line = sourceCode.lines[node.loc.start.line - 1]; - assert.ok(line); + assert.ok(line !== undefined); const indentMatch = line.match(/^\s*/u); return indentMatch ? indentMatch[0] : ''; } diff --git a/src/no-fixture.ts b/src/no-fixture.ts index 8dbbdc0..2d83078 100644 --- a/src/no-fixture.ts +++ b/src/no-fixture.ts @@ -488,7 +488,7 @@ const rule: Rule.RuleModule = { const headerNameNode = parent.arguments[0]; headerName = sourceCode.getText(headerNameNode); } - assert.ok(headerName); + assert.ok(headerName !== undefined); yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); } // if (destructuringResponseHeadersVariable !== undefined) { From 6ea226f7dad6351c7614dfc5f48490bae570c30c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sat, 27 Jul 2024 00:03:39 -0400 Subject: [PATCH 023/115] don't load --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0dabcbb..5775614 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", + "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs", "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", From fc96291ddb597e30f3bd8aa4ef0943daff23dbfe Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sat, 27 Jul 2024 00:46:22 -0400 Subject: [PATCH 024/115] revert back --- package-lock.json | 334 +++++++++++++++++++--------------------------- package.json | 4 +- 2 files changed, 141 insertions(+), 197 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5dc7634..493488b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", - "@checkdigit/typescript-config": "7.1.3-PR.64-9fc8", + "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", @@ -694,45 +694,27 @@ } }, "node_modules/@checkdigit/typescript-config": { - "version": "7.1.3-PR.64-9fc8", - "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-7.1.3-PR.64-9fc8.tgz", - "integrity": "sha512-wIJZYjdKYCQ/bU9zRoRUFRLusIcqS4fL4GsraFGjTm5dqmNBpFDRO9PNBBgPc0PfrO05fuUWk+Es7Io5TXn90Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-6.0.0.tgz", + "integrity": "sha512-J/+6vv9y2lHdPscJl0kQrq8bDG5k8MBgfgxJEl2tEwyTdUYyOjqkgm+QiGinVfx35NRt/N1cfkZFnO3aLnudUQ==", "dev": true, "license": "MIT", "bin": { "builder": "bin/builder.mjs" }, "engines": { - "node": ">=20.14" + "node": ">=20.9" }, "peerDependencies": { - "@types/node": ">=20.14", - "esbuild": "0.23.0", - "typescript": ">=5.5.3 <5.6.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" + "@types/node": ">=20.9", + "esbuild": "0.19.8", + "typescript": ">=5.3.2 <5.4.0" } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", + "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", "cpu": [ "arm" ], @@ -744,13 +726,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", + "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", "cpu": [ "arm64" ], @@ -762,13 +744,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", + "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", "cpu": [ "x64" ], @@ -780,13 +762,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", + "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", "cpu": [ "arm64" ], @@ -798,13 +780,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", + "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", "cpu": [ "x64" ], @@ -816,13 +798,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", + "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", "cpu": [ "arm64" ], @@ -834,13 +816,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", + "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", "cpu": [ "x64" ], @@ -852,13 +834,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", + "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", "cpu": [ "arm" ], @@ -870,13 +852,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", + "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", "cpu": [ "arm64" ], @@ -888,13 +870,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", + "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", "cpu": [ "ia32" ], @@ -906,13 +888,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", + "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", "cpu": [ "loong64" ], @@ -924,13 +906,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", + "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", "cpu": [ "mips64el" ], @@ -942,13 +924,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", + "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", "cpu": [ "ppc64" ], @@ -960,13 +942,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", + "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", "cpu": [ "riscv64" ], @@ -978,13 +960,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", + "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", "cpu": [ "s390x" ], @@ -996,13 +978,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", + "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", "cpu": [ "x64" ], @@ -1014,13 +996,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", + "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", "cpu": [ "x64" ], @@ -1032,31 +1014,13 @@ ], "peer": true, "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", + "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", "cpu": [ "x64" ], @@ -1068,13 +1032,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", + "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", "cpu": [ "x64" ], @@ -1086,13 +1050,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", + "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", "cpu": [ "arm64" ], @@ -1104,13 +1068,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", + "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", "cpu": [ "ia32" ], @@ -1122,13 +1086,13 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", + "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", "cpu": [ "x64" ], @@ -1140,7 +1104,7 @@ ], "peer": true, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1202,24 +1166,6 @@ "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3430,9 +3376,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", + "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3441,33 +3387,31 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/android-arm": "0.19.8", + "@esbuild/android-arm64": "0.19.8", + "@esbuild/android-x64": "0.19.8", + "@esbuild/darwin-arm64": "0.19.8", + "@esbuild/darwin-x64": "0.19.8", + "@esbuild/freebsd-arm64": "0.19.8", + "@esbuild/freebsd-x64": "0.19.8", + "@esbuild/linux-arm": "0.19.8", + "@esbuild/linux-arm64": "0.19.8", + "@esbuild/linux-ia32": "0.19.8", + "@esbuild/linux-loong64": "0.19.8", + "@esbuild/linux-mips64el": "0.19.8", + "@esbuild/linux-ppc64": "0.19.8", + "@esbuild/linux-riscv64": "0.19.8", + "@esbuild/linux-s390x": "0.19.8", + "@esbuild/linux-x64": "0.19.8", + "@esbuild/netbsd-x64": "0.19.8", + "@esbuild/openbsd-x64": "0.19.8", + "@esbuild/sunos-x64": "0.19.8", + "@esbuild/win32-arm64": "0.19.8", + "@esbuild/win32-ia32": "0.19.8", + "@esbuild/win32-x64": "0.19.8" } }, "node_modules/escalade": { @@ -3897,7 +3841,20 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/espree": { + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", @@ -3915,19 +3872,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -7697,9 +7641,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "license": "Apache-2.0", "peer": true, diff --git a/package.json b/package.json index 5775614..7054c21 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs", + "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs", "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", @@ -64,7 +64,7 @@ "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", - "@checkdigit/typescript-config": "7.1.3-PR.64-9fc8", + "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", From e5bf77c7d1f20b2d77483d71192822d682ff4353 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sun, 28 Jul 2024 01:41:08 -0400 Subject: [PATCH 025/115] refactoring --- src/ast/tree.ts | 2 +- src/fixture/fetch-header-getter.spec.ts | 95 ++++ src/fixture/fetch-header-getter.ts | 90 +++ src/fixture/no-fixture.spec.ts | 501 +++++++++++++++++ src/{ => fixture}/no-fixture.ts | 117 +--- src/fixture/response-reference.ts | 108 ++++ src/index.ts | 8 +- src/no-fixture-headers.spec.ts | 122 ---- src/no-fixture-headers.ts | 134 ----- src/no-fixture.spec.ts | 705 ------------------------ 10 files changed, 806 insertions(+), 1076 deletions(-) create mode 100644 src/fixture/fetch-header-getter.spec.ts create mode 100644 src/fixture/fetch-header-getter.ts create mode 100644 src/fixture/no-fixture.spec.ts rename src/{ => fixture}/no-fixture.ts (79%) create mode 100644 src/fixture/response-reference.ts delete mode 100644 src/no-fixture-headers.spec.ts delete mode 100644 src/no-fixture-headers.ts delete mode 100644 src/no-fixture.spec.ts diff --git a/src/ast/tree.ts b/src/ast/tree.ts index a1a4393..519506e 100644 --- a/src/ast/tree.ts +++ b/src/ast/tree.ts @@ -47,6 +47,6 @@ export function getEnclosingStatement(node: Node) { export function getEnclosingScopeNode(node: Node) { return getAncestor(node, (parentNode) => - ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression'].includes(parentNode.type), + ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type), ); } diff --git a/src/fixture/fetch-header-getter.spec.ts b/src/fixture/fetch-header-getter.spec.ts new file mode 100644 index 0000000..6f0f500 --- /dev/null +++ b/src/fixture/fetch-header-getter.spec.ts @@ -0,0 +1,95 @@ +// no-fixture.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './fetch-header-getter'; +import createTester from '../tester.test'; +import { describe } from '@jest/globals'; + +describe(ruleId, () => { + createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'replace the access of headers property using getter instead of direct access', + code: `async() => { + const createdOn = new Date().toISOString(); + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify({ + checkValue: 'foo', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + const headers1 = response.headers; + assert.equal(headers1.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers[ETAG_HEADER], '1'); + assert.equal(response.headers.etag, '1'); + assert.ok(verifyTemporalHeaders(response)); + + const updatedOn = new Date().toISOString(); + const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify({ + checkValue: 'bar', + }), + headers: { + [IF_MATCH_HEADER]: headers1[ETAG_HEADER], + [CREATED_ON_HEADER]: updatedOn, + }, + }); + assert.equal(response2.status, StatusCodes.NO_CONTENT); + const headers2 = response2.headers; + assert.equal(headers2[ETAG_HEADER], '2'); + assert.ok(verifyTemporalHeaders(response2)); + }`, + output: `async() => { + const createdOn = new Date().toISOString(); + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify({ + checkValue: 'foo', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + const headers1 = response.headers; + assert.equal(headers1.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.equal(response.headers.get('etag'), '1'); + assert.ok(verifyTemporalHeaders(response)); + + const updatedOn = new Date().toISOString(); + const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify({ + checkValue: 'bar', + }), + headers: { + [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), + [CREATED_ON_HEADER]: updatedOn, + }, + }); + assert.equal(response2.status, StatusCodes.NO_CONTENT); + const headers2 = response2.headers; + assert.equal(headers2.get(ETAG_HEADER), '2'); + assert.ok(verifyTemporalHeaders(response2)); + }`, + errors: 4, + }, + ], + }); +}); diff --git a/src/fixture/fetch-header-getter.ts b/src/fixture/fetch-header-getter.ts new file mode 100644 index 0000000..c797238 --- /dev/null +++ b/src/fixture/fetch-header-getter.ts @@ -0,0 +1,90 @@ +// no-fixture.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { Identifier, MemberExpression, VariableDeclarator } from 'estree'; +import type { Rule } from 'eslint'; +import { analyzeResponseReferences } from './response-reference'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; +import { getParent } from '../ast/tree'; + +export const ruleId = 'fetch-header-getter'; + +const rule: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Make sure getter is used to access response headers.', + url: getDocumentationUrl(ruleId), + }, + messages: { + shouldUseHeaderGetter: 'Getter should be used to access response headers.', + }, + fixable: 'code', + schema: [], + }, + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + + return { + 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => { + const variableDeclaration = getParent(fetchCall); + assert.ok(variableDeclaration?.type === 'VariableDeclaration'); + const { variable: responseVariable, headersReferences: responseHeadersReferences } = analyzeResponseReferences( + variableDeclaration, + scopeManager, + ); + assert.ok(responseVariable); + + const directHeaderReferences = responseHeadersReferences + .map(getParent) + .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression'); + + const indirectHeaderReferences = responseHeadersReferences + .map((reference) => getParent(reference)) + .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator') + .map((declarator) => (declarator.id as Identifier).name) + .map((redefinedHeadersVariableName) => { + const headersVariable = responseVariable.scope.variables.find((variable) => { + const identifier = variable.identifiers[0]; + return identifier?.type === 'Identifier' && identifier.name === redefinedHeadersVariableName; + }); + return ( + headersVariable?.references + .map((reference) => getParent(reference.identifier)) + .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression') ?? [] + ); + }) + .flat(); + + const invalidHeaderReferences = [...directHeaderReferences, ...indirectHeaderReferences].filter( + (reference) => !(reference.property.type === 'Identifier' && reference.property.name === 'get'), + ); + + invalidHeaderReferences.forEach((reference) => { + const headerNameNode = reference.property; + const headerName = reference.computed + ? sourceCode.getText(headerNameNode) + : `'${sourceCode.getText(headerNameNode)}'`; + const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`; + + context.report({ + node: reference, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(reference, replacementText); + }, + }); + }); + }, + }; + }, +}; + +export default rule; diff --git a/src/fixture/no-fixture.spec.ts b/src/fixture/no-fixture.spec.ts new file mode 100644 index 0000000..ce6aefa --- /dev/null +++ b/src/fixture/no-fixture.spec.ts @@ -0,0 +1,501 @@ +// no-fixture.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-fixture'; +import createTester from '../tester.test'; +import { describe } from '@jest/globals'; + +describe(ruleId, () => { + createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'assertion with variable declaration', + code: ` + const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + const body = pingResponse.body; + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + `, + output: ` + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(pingResponse.status, StatusCodes.OK); + const body = await pingResponse.json(); + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + `, + errors: 1, + }, + { + name: 'assertion without variable declaration', + code: ` + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + errors: 1, + }, + { + name: 'assertion without variable declaration', + code: ` + await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); + `, + errors: 1, + }, + { + name: 'PUT with request body', + code: ` + await fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + method: 'PUT', + body: JSON.stringify(cardCreationData), + }); + assert.equal(response.status, StatusCodes.BAD_REQUEST); + `, + errors: 1, + }, + { + name: 'PUT with request header', + code: ` + const noFraudResponse = await fixture.api + .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .set(IF_MATCH_HEADER, originalCard.version) + .set('abc', originalCard.name) + .set('x-y-z', '123') + .expect(StatusCodes.NO_CONTENT); + `, + output: ` + const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + method: 'POST', + headers: { + [IF_MATCH_HEADER]: originalCard.version, + abc: originalCard.name, + 'x-y-z': '123', + }, + }); + assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); + `, + errors: 1, + }, + { + name: 'POST without request header/body', + code: ` + await fixture.api + .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .expect(StatusCodes.NO_CONTENT); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + method: 'POST', + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + `, + errors: 1, + }, + { + name: 'response headers assertion should be externalized with new variable declared if necessary', + code: ` + await fixture.api.get(\`/sample-service/v2/ping\`) + .expect(StatusCodes.OK) + .expect('etag', '123') + .expect('content-type', 'application/json') + .expect(ETAG, correctVersion) + .expect(ETAG, /1.*/u); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get('etag'), '123'); + assert.equal(response.headers.get('content-type'), 'application/json'); + assert.equal(response.headers.get(ETAG), correctVersion); + assert.ok(response.headers.get(ETAG).match(/1.*/u)); + `, + errors: 1, + }, + { + name: 'response body assertion', + code: ` + await fixture.api.get(\`/sample-service/v2/ping\`).expect({message:'pong'}); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.deepEqual(await response.json(), {message:'pong'}); + `, + errors: 1, + }, + { + name: 'response callback assertion', + code: ` + await fixture.api.get(\`/sample-service/v2/ping\`) + .expect(validate) + .expect((response)=>console.log(response)); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.ok(validate(response)); + assert.ok(console.log(response)); + `, + errors: 1, + }, + { + name: 'multiple fixture calls in the same test', + code: ` + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(pingResponse.status, StatusCodes.OK); + const response2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + assert.deepEqual(await response2.json(), {message:'pong'}); + const response3 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response3.status, StatusCodes.OK); + `, + errors: 4, + }, + { + name: 'directly return (no await) fixture call', + code: `() => { + return fixture.api.get(\`/sample-service/v1/ping\`); + }`, + output: `() => { + return fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + }`, + errors: 1, + }, + { + name: 'directly return (no await) fixture call with assertion', + code: `async () => { + return fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + }`, + output: `async () => { + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + return response; + }`, + errors: 1, + }, + { + name: 'directly return (no await) fixture call with body/headers', + code: `() => { + return fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`) + .set(IF_MATCH_HEADER, originalCard.version) + .send({}); + }`, + output: `() => { + return fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + method: 'PUT', + body: JSON.stringify({}), + headers: { + [IF_MATCH_HEADER]: originalCard.version, + }, + }); + }`, + errors: 1, + }, + { + name: 'replace statusCode with status', + code: ` + const response = await fixture.api.get(\`/sample-service/v2/ping\`); + assert.equal(response.statusCode, StatusCodes.OK); + console.log('status:', response.statusCode); + const response2 = await fixture.api.get(\`/sample-service/v2/ping\`); + assert.equal(response2.status, StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + console.log('status:', response.status); + const response2 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + `, + errors: 2, + }, + { + name: 'replace header access through response.get() with response.headers.get()', + code: ` + const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); + assert.equal(response.get(ETAG), correctVersion); + assert.equal(response.get('etag'), correctVersion); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get(ETAG), correctVersion); + assert.equal(response.headers.get('etag'), correctVersion); + `, + errors: 1, + }, + { + name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', + code: ` + await fixture.api.get(\`/sample-service/v2/ping\`).expect(200); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, 200); + `, + errors: 1, + }, + { + name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', + code: ` + const createdOn = Date.now().toUTCString(); + await fixture.api.get(\`/sample-service/v2/ping\`).expect(200).expect(validateBody(createdOn)); + `, + output: ` + const createdOn = Date.now().toUTCString(); + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), validateBody(createdOn)); + `, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for body', + code: ` + const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + `, + output: ` + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const responseBody = await response.json(); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + `, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers when body is presented as well', + code: ` + const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + assert(body); + assert.ok(headers2.get(ETAG)); + `, + output: ` + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const body = await response.json(); + const headers2 = response.headers; + assert(body); + assert.ok(headers2.get(ETAG)); + `, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers without body presented but with assertions used', + code: ` + const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + assert.ok(headers.get(ETAG)); + `, + output: ` + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const headers = response.headers; + assert.ok(headers.get(ETAG)); + `, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', + code: ` + const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); + assert.ok(headers.get(ETAG)); + `, + output: ` + const { headers } = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.ok(headers.get(ETAG)); + `, + errors: 1, + }, + { + name: 'avoid response variable name conflict with existing variables in the same scope', + code: ` + async () => { + const response = 'foo'; + const response1 = 'bar'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + } + `, + output: ` + async () => { + const response = 'foo'; + const response1 = 'bar'; + const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + const response3 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response3.status, StatusCodes.OK); + } + `, + errors: 2, + }, + { + name: 'response variable names in different scope do not conflict with each other', + code: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + }); + `, + output: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response2.status, StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + }); + `, + errors: 2, + }, + { + name: 'inline access to response body should be extracted to a variable', + code: ` + export async function validatePin( + fixture, + ) { + const paymentSecurityServicePublicKey = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; + } + `, + output: ` + export async function validatePin( + fixture, + ) { + const response = await fetch(\`\${BASE_PATH}/public-key\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const responseBody = await response.json(); + const paymentSecurityServicePublicKey = responseBody.publicKey; + } + `, + errors: 1, + }, + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: ` + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + await fixture.api + .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) + .set(CREATED_ON_HEADER, createdOn) + .send({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }) + .expect(StatusCodes.NO_CONTENT) + .expect(ETAG_HEADER, '1') + .expect((res) => verifyTemporalHeaders(res, createdOn)); + `, + output: ` + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify({ + key: '71CA52F757D7C0B45A16C6C04EAFD704', + checkValue: '4F35C4', + }), + headers: { + [CREATED_ON_HEADER]: createdOn, + }, + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.ok(verifyTemporalHeaders(response, createdOn)); + `, + errors: 1, + }, + ], + }); +}); diff --git a/src/no-fixture.ts b/src/fixture/no-fixture.ts similarity index 79% rename from src/no-fixture.ts rename to src/fixture/no-fixture.ts index 2d83078..69edbb9 100644 --- a/src/no-fixture.ts +++ b/src/fixture/no-fixture.ts @@ -17,10 +17,11 @@ import type { VariableDeclaration, } from 'estree'; import { type Rule, type Scope, SourceCode } from 'eslint'; -import { getEnclosingScopeNode, getEnclosingStatement, getParent } from './ast/tree'; +import { getEnclosingScopeNode, getEnclosingStatement, getParent } from '../ast/tree'; +import { analyzeResponseReferences } from './response-reference'; import { strict as assert } from 'node:assert'; -import getDocumentationUrl from './get-documentation-url'; -import { getIndentation } from './ast/format'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../ast/format'; export const ruleId = 'no-fixture'; @@ -42,7 +43,7 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo let nextCall; if (parent.type === 'ReturnStatement') { - // direct return, no variable declaration / await + // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; } else if (parent.type === 'AwaitExpression') { @@ -84,103 +85,13 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo nextCall = setRequestHeaderCall; } } else { - throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}`); + throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); } if (nextCall) { analyzeFixtureCall(nextCall, results, sourceCode); } } -// analyze response related variables and their references0 -function analyzeResponseReferences(fixtureInformation: FixtureCallInformation, scopeManager: Scope.ScopeManager) { - const results: { - variable?: Scope.Variable; - bodyReferences: MemberExpression[]; - headersReferences: MemberExpression[]; - statusReferences: MemberExpression[]; - destructuringBodyVariable?: Scope.Variable; - destructuringHeadersVariable?: Scope.Variable; - destructuringHeadersReferences?: MemberExpression[] | undefined; - } = { - bodyReferences: [], - headersReferences: [], - statusReferences: [], - }; - - if (fixtureInformation.variableDeclaration) { - const responseVariables = scopeManager.getDeclaredVariables(fixtureInformation.variableDeclaration); - for (const responseVariable of responseVariables) { - const scope = responseVariable.scope; - const identifier = responseVariable.identifiers[0]; - assert.ok(identifier); - const identifierParent = getParent(identifier); - assert.ok(identifierParent); - if (identifierParent.type === 'VariableDeclarator') { - // e.g. const response = ... - results.variable = responseVariable; - const responseReferences = responseVariable.references.map((responseReference) => - getParent(responseReference.identifier), - ); - // e.g. response.body - results.bodyReferences = responseReferences.filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - node.property.name === 'body', - ); - // e.g. response.headers / response.header / response.get() - results.headersReferences = responseReferences.filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), - ); - // e.g. response.status / response.statusCode - results.statusReferences = responseReferences.filter( - (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - (node.property.name === 'status' || node.property.name === 'statusCode'), - ); - } else if ( - // body reference through destruction/renaming, e.g. "const { body } = ..." - identifierParent.type === 'Property' && - identifierParent.key.type === 'Identifier' && - identifierParent.key.name === 'body' - ) { - results.destructuringBodyVariable = responseVariable; - } else if ( - // header reference through destruction/renaming, e.g. "const { headers } = ..." - identifierParent.type === 'Property' && - identifierParent.key.type === 'Identifier' && - identifierParent.key.name === 'headers' - ) { - results.destructuringHeadersVariable = responseVariable; - results.destructuringHeadersReferences = scope.set - .get(responseVariable.name) - ?.references.map((reference) => reference.identifier) - .map(getParent) - .filter( - (parent): parent is MemberExpression => - parent?.type === 'MemberExpression' && - parent.property.type === 'Identifier' && - parent.property.name !== 'get' && - getParent(parent)?.type !== 'CallExpression', - ); - } else { - throw new Error(`Unknown response variable reference: ${responseVariable.name}`); - } - } - } - return results; -} - // `/sample-service/v1/ping` -> `${BASE_PATH}/ping` function replaceEndpointUrlPrefixWithBasePath(url: string) { // eslint-disable-next-line no-template-curly-in-string @@ -353,8 +264,7 @@ const rule: Rule.RuleModule = { statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, destructuringHeadersVariable: destructuringResponseHeadersVariable, - // destructuringHeadersReferences: destructuringResponseHeadersReferences, - } = analyzeResponseReferences(fixtureCallInformation, scopeManager); + } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); @@ -491,19 +401,6 @@ const rule: Rule.RuleModule = { assert.ok(headerName !== undefined); yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); } - // if (destructuringResponseHeadersVariable !== undefined) { - // for (const destructuringResponseHeadersReference of destructuringResponseHeadersReferences ?? []) { - // const headerNameNode = destructuringResponseHeadersReference.property; - // const headerName = destructuringResponseHeadersReference.computed - // ? sourceCode.getText(headerNameNode) - // : `'${sourceCode.getText(headerNameNode)}'`; - - // yield fixer.replaceText( - // destructuringResponseHeadersReference, - // `${destructuringResponseHeadersVariable.name}.get(${headerName})`, - // ); - // } - // } // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { diff --git a/src/fixture/response-reference.ts b/src/fixture/response-reference.ts new file mode 100644 index 0000000..6e2d633 --- /dev/null +++ b/src/fixture/response-reference.ts @@ -0,0 +1,108 @@ +// no-fixture.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { MemberExpression, VariableDeclaration } from 'estree'; +import { type Scope } from 'eslint'; +import { strict as assert } from 'node:assert'; +import { getParent } from '../ast/tree'; + +/** + * analyze response related variables and their references + * the implementation is for fixture API, but it can be used for fetch API as well since the tree structure is similar + * @param variableDeclaration - variable declaration node + */ +export function analyzeResponseReferences( + variableDeclaration: VariableDeclaration | undefined, + scopeManager: Scope.ScopeManager, +) { + const results: { + variable?: Scope.Variable; + bodyReferences: MemberExpression[]; + headersReferences: MemberExpression[]; + statusReferences: MemberExpression[]; + destructuringBodyVariable?: Scope.Variable; + destructuringHeadersVariable?: Scope.Variable; + destructuringHeadersReferences?: MemberExpression[] | undefined; + } = { + bodyReferences: [], + headersReferences: [], + statusReferences: [], + }; + if (!variableDeclaration) { + return results; + } + + const responseVariables = scopeManager.getDeclaredVariables(variableDeclaration); + for (const responseVariable of responseVariables) { + const identifier = responseVariable.identifiers[0]; + assert.ok(identifier); + const identifierParent = getParent(identifier); + assert.ok(identifierParent); + if (identifierParent.type === 'VariableDeclarator') { + // e.g. const response = ... + results.variable = responseVariable; + const responseReferences = responseVariable.references.map((responseReference) => + getParent(responseReference.identifier), + ); + // e.g. response.body + results.bodyReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + node.property.name === 'body', + ); + // e.g. response.headers / response.header / response.get() + results.headersReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), + ); + // e.g. response.status / response.statusCode + results.statusReferences = responseReferences.filter( + (node): node is MemberExpression => + node !== null && + node !== undefined && + node.type === 'MemberExpression' && + node.property.type === 'Identifier' && + (node.property.name === 'status' || node.property.name === 'statusCode'), + ); + } else if ( + // body reference through destruction/renaming, e.g. "const { body } = ..." + identifierParent.type === 'Property' && + identifierParent.key.type === 'Identifier' && + identifierParent.key.name === 'body' + ) { + results.destructuringBodyVariable = responseVariable; + } else if ( + // header reference through destruction/renaming, e.g. "const { headers } = ..." + identifierParent.type === 'Property' && + identifierParent.key.type === 'Identifier' && + identifierParent.key.name === 'headers' + ) { + results.destructuringHeadersVariable = responseVariable; + results.destructuringHeadersReferences = responseVariable.references + .map((reference) => reference.identifier) + .map(getParent) + .filter( + (parent): parent is MemberExpression => + parent?.type === 'MemberExpression' && + parent.property.type === 'Identifier' && + parent.property.name !== 'get' && + getParent(parent)?.type !== 'CallExpression', + ); + } else { + throw new Error(`Unknown response variable reference: ${responseVariable.name}`); + } + } + return results; +} diff --git a/src/index.ts b/src/index.ts index e97fe7a..bc6d24e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,9 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; -import noFixture, { ruleId as noFixtureRuleId } from './no-fixture'; -import noFixtureHeaders, { ruleId as noFixtureHeadersRuleId } from './no-fixture-headers'; +import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; @@ -34,7 +34,7 @@ export default { [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, - [noFixtureHeadersRuleId]: noFixtureHeaders, + [fetchHeaderGetterRuleId]: fetchHeaderGetter, }, configs: { all: { @@ -51,7 +51,7 @@ export default { [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${noFixtureHeadersRuleId}`]: 'error', + [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', }, }, recommended: { diff --git a/src/no-fixture-headers.spec.ts b/src/no-fixture-headers.spec.ts deleted file mode 100644 index 64ed166..0000000 --- a/src/no-fixture-headers.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -// no-fixture.spec.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import rule, { ruleId } from './no-fixture-headers'; -import createTester from './tester.test'; -import { describe } from '@jest/globals'; - -describe(ruleId, () => { - createTester().run(ruleId, rule, { - valid: [], - invalid: [ - { - name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - code: ` - it.each(temporalHeaders)( - 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', - async () => { - const zoneKeyId = uuid(); - await importTestMultipartZoneKey(fixture, zoneKeyId); - const createdOn = new Date().toISOString(); - - const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - const headers1 = response.headers; - assert.equal(headers1.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers[ETAG_HEADER], '1'); - assert.equal(response.headers.etag, '1'); - assert.ok(verifyTemporalHeaders(response)); - - const updatedOn = new Date().toISOString(); - const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '339923C206BA8B19EEBF995DEE6619F7', - checkValue: '1ADEE9', - }), - headers: { - [IF_MATCH_HEADER]: headers1[ETAG_HEADER], - [CREATED_ON_HEADER]: updatedOn, - }, - }); - assert.equal(response2.status, StatusCodes.NO_CONTENT); - const headers2 = response2.headers; - assert.equal(headers2.get(ETAG_HEADER), '2'); - assert.ok(verifyTemporalHeaders(response2)); - - // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); - // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); - }, - ); - `, - output: ` - it.each(temporalHeaders)( - 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', - async () => { - const zoneKeyId = uuid(); - await importTestMultipartZoneKey(fixture, zoneKeyId); - const createdOn = new Date().toISOString(); - - const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - const headers1 = response.headers; - assert.equal(headers1.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get('etag'), '1'); - assert.ok(verifyTemporalHeaders(response)); - - const updatedOn = new Date().toISOString(); - const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '339923C206BA8B19EEBF995DEE6619F7', - checkValue: '1ADEE9', - }), - headers: { - [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), - [CREATED_ON_HEADER]: updatedOn, - }, - }); - assert.equal(response2.status, StatusCodes.NO_CONTENT); - const headers2 = response2.headers; - assert.equal(headers2.get(ETAG_HEADER), '2'); - assert.ok(verifyTemporalHeaders(response2)); - - // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); - // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); - }, - ); - `, - errors: 2, - only: true, - }, - ], - }); -}); diff --git a/src/no-fixture-headers.ts b/src/no-fixture-headers.ts deleted file mode 100644 index 31442f9..0000000 --- a/src/no-fixture-headers.ts +++ /dev/null @@ -1,134 +0,0 @@ -// no-fixture.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import type { Identifier, MemberExpression, VariableDeclarator } from 'estree'; -import { getEnclosingScopeNode, getParent } from './ast/tree'; -import { type Rule } from 'eslint'; -import { strict as assert } from 'node:assert'; -import getDocumentationUrl from './get-documentation-url'; - -export const ruleId = 'no-fixture-headers'; - -const rule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Prefer native fetch API over customized fixture API.', - url: getDocumentationUrl(ruleId), - }, - messages: { - preferNativeFetch: 'Prefer native fetch API over customized fixture API.', - unknownError: - 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', - }, - fixable: 'code', - schema: [], - }, - // eslint-disable-next-line max-lines-per-function - create(context) { - const sourceCode = context.sourceCode; - const scopeManager = sourceCode.scopeManager; - - return { - // eslint-disable-next-line max-lines-per-function - 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => { - try { - const enclosingScopeNode = getEnclosingScopeNode(fetchCall); - assert.ok(fetchCall.id.type === 'Identifier'); - const fetchVariableName = fetchCall.id.name; /*?*/ - assert.ok(enclosingScopeNode !== undefined, 'enclosing scope node should exist'); - const scope = scopeManager.acquire(enclosingScopeNode); - const responseVariable = scope?.variables.find((variable) => { - const identifier = variable.identifiers[0]; - return identifier?.type === 'Identifier' && identifier.name === fetchVariableName; - }); - if (responseVariable === undefined) { - return; - } - - const headersReferences = responseVariable.references - .map((reference) => getParent(reference.identifier)) - .filter( - (parent): parent is MemberExpression => - parent?.type === 'MemberExpression' && - parent.property.type === 'Identifier' && - parent.property.name === 'headers', - ); - const directHeadersReferences = headersReferences - .map(getParent) - .filter( - (parent): parent is MemberExpression => - parent?.type === 'MemberExpression' && - !(parent.property.type === 'Identifier' && parent.property.name === 'get'), - ); - directHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/ - - const reDeclaredHeadersVariableNames = headersReferences - .map((reference) => getParent(reference)) - .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator') - .map((declarator) => (declarator.id as Identifier).name); - - const indirectHeadersReferences = reDeclaredHeadersVariableNames - .map((variableName) => { - const headersVariable = scope?.variables.find((variable) => { - const identifier = variable.identifiers[0]; - return identifier?.type === 'Identifier' && identifier.name === variableName; - }); - return ( - headersVariable?.references - .map((reference) => getParent(reference.identifier)) - .filter( - (parent): parent is MemberExpression => - parent?.type === 'MemberExpression' && - !(parent.property.type === 'Identifier' && parent.property.name === 'get'), - ) ?? [] - ); - }) - .flat(); - indirectHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/ - - const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].map< - [MemberExpression, string] - >((reference) => { - sourceCode.getText(reference); /*?*/ - const headerNameNode = reference.property; /*?*/ - const headerName = - // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions - reference.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; /*?*/ - const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`; - return [reference, replacementText]; - }); - - context.report({ - node: fetchCall, - messageId: 'preferNativeFetch', - *fix(fixer) { - // handle response headers references - for (const [node, replacementText] of invalidHeadersReferences) { - yield fixer.replaceText(node, replacementText); - } - }, - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); - context.report({ - node: fetchCall, - messageId: 'unknownError', - data: { - fileName: context.filename, - error: error instanceof Error ? error.toString() : JSON.stringify(error), - }, - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/src/no-fixture.spec.ts b/src/no-fixture.spec.ts deleted file mode 100644 index c419320..0000000 --- a/src/no-fixture.spec.ts +++ /dev/null @@ -1,705 +0,0 @@ -// no-fixture.spec.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import rule, { ruleId } from './no-fixture'; -import createTester from './tester.test'; -import { describe } from '@jest/globals'; - -describe(ruleId, () => { - createTester().run(ruleId, rule, { - valid: [], - invalid: [ - { - name: 'assertion with variable declaration', - code: ` - it('GET /ping', async () => { - const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - const body = pingResponse.body; - const timeDifference = Date.now() - new Date(body.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }); - `, - output: ` - it('GET /ping', async () => { - const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingResponse.status, StatusCodes.OK); - const body = await pingResponse.json(); - const timeDifference = Date.now() - new Date(body.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }); - `, - errors: 1, - }, - { - name: 'assertion without variable declaration', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - }); - `, - errors: 1, - }, - { - name: 'assertion without variable declaration', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); - }); - `, - errors: 1, - }, - { - name: 'PUT with request body', - code: ` - it('PUT /card', async () => { - await fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); - }); - `, - output: ` - it('PUT /card', async () => { - const response = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { - method: 'PUT', - body: JSON.stringify(cardCreationData), - }); - assert.equal(response.status, StatusCodes.BAD_REQUEST); - }); - `, - errors: 1, - }, - { - name: 'PUT with request header', - code: ` - it('PUT /card/:cardId/block', async () => { - const noFraudResponse = await fixture.api - .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) - .set(IF_MATCH_HEADER, originalCard.version) - .set('abc', originalCard.name) - .set('x-y-z', '123') - .expect(StatusCodes.NO_CONTENT); - }); - `, - output: ` - it('PUT /card/:cardId/block', async () => { - const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { - method: 'POST', - headers: { - [IF_MATCH_HEADER]: originalCard.version, - abc: originalCard.name, - 'x-y-z': '123', - }, - }); - assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); - }); - `, - errors: 1, - }, - { - name: 'POST without request header/body', - code: ` - it('PUT /card/:cardId/block', async () => { - await fixture.api - .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) - .expect(StatusCodes.NO_CONTENT); - }); - `, - output: ` - it('PUT /card/:cardId/block', async () => { - const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { - method: 'POST', - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - }); - `, - errors: 1, - }, - { - name: 'headers references should use getter', - code: ` - it('GET /ping', async () => { - const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); - assert.ok(response.headers.etag); - assert.ok(response.headers[ETAG]); - assert.ok(response.headers['content-type']); - assert.ok(response.header.etag); - assert.ok(response.header[ETAG]); - assert.ok(response.header['content-type']); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - assert.ok(response.headers.get('etag')); - assert.ok(response.headers.get(ETAG)); - assert.ok(response.headers.get('content-type')); - assert.ok(response.headers.get('etag')); - assert.ok(response.headers.get(ETAG)); - assert.ok(response.headers.get('content-type')); - }); - `, - errors: 1, - }, - { - name: 'response headers assertion should be externalized with new variable declared if necessary', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v2/ping\`) - .expect(StatusCodes.OK) - .expect('etag', '123') - .expect('content-type', 'application/json') - .expect(ETAG, correctVersion) - .expect(ETAG, /1.*/u); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get('etag'), '123'); - assert.equal(response.headers.get('content-type'), 'application/json'); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.ok(response.headers.get(ETAG).match(/1.*/u)); - }); - `, - errors: 1, - }, - { - name: 'response body assertion', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v2/ping\`).expect({message:'pong'}); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.deepEqual(await response.json(), {message:'pong'}); - }); - `, - errors: 1, - }, - { - name: 'response callback assertion', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v2/ping\`) - .expect(validate) - .expect((response)=>console.log(response)); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.ok(validate(response)); - assert.ok(console.log(response)); - }); - `, - errors: 1, - }, - { - name: 'multiple fixture calls in the same test', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingResponse.status, StatusCodes.OK); - const response2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - assert.deepEqual(await response2.json(), {message:'pong'}); - const response3 = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response3.status, StatusCodes.OK); - }); - `, - errors: 4, - }, - { - name: 'directly return (no await) fixture call', - code: ` - it('GET /ping', async () => { - return fixture.api.get(\`/sample-service/v1/ping\`); - }); - `, - output: ` - it('GET /ping', async () => { - return fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - }); - `, - errors: 1, - }, - { - name: 'directly return (no await) fixture call with assertion', - code: ` - it('GET /ping', async () => { - return fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - return response; - }); - `, - errors: 1, - }, - { - name: 'directly return (no await) fixture call with body/headers', - code: ` - it('PUT /card', async () => { - return fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`) - .set(IF_MATCH_HEADER, originalCard.version) - .send({}); - }); - `, - output: ` - it('PUT /card', async () => { - return fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { - method: 'PUT', - body: JSON.stringify({}), - headers: { - [IF_MATCH_HEADER]: originalCard.version, - }, - }); - }); - `, - errors: 1, - }, - { - name: 'replace statusCode with status', - code: ` - it('GET /ping', async () => { - const response = await fixture.api.get(\`/sample-service/v2/ping\`); - assert.equal(response.statusCode, StatusCodes.OK); - console.log('status:', response.statusCode); - const response2 = await fixture.api.get(\`/sample-service/v2/ping\`); - assert.equal(response2.status, StatusCodes.OK); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - console.log('status:', response.status); - const response2 = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - }); - `, - errors: 2, - }, - { - name: 'replace header access through response.get() with response.headers.get()', - code: ` - it('GET /ping', async () => { - const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); - assert.equal(response.get(ETAG), correctVersion); - assert.equal(response.get('etag'), correctVersion); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.equal(response.headers.get('etag'), correctVersion); - }); - `, - errors: 1, - }, - { - name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', - code: ` - it('GET /ping', async () => { - await fixture.api.get(\`/sample-service/v2/ping\`).expect(200); - }); - `, - output: ` - it('GET /ping', async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, 200); - }); - `, - errors: 1, - }, - { - name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', - code: ` - it('GET /ping', async () => { - const createdOn = Date.now().toUTCString(); - await fixture.api.get(\`/sample-service/v2/ping\`).expect(200).expect(validateBody(createdOn)); - }); - `, - output: ` - it('GET /ping', async () => { - const createdOn = Date.now().toUTCString(); - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), validateBody(createdOn)); - }); - `, - errors: 1, - }, - { - name: 'handle spreading variable declaration for body', - code: ` - it('returns current server time', async () => { - const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }); - `, - output: ` - it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }); - `, - errors: 1, - }, - { - name: 'handle spreading variable declaration for headers when body is presented as well', - code: ` - it('returns current server time', async () => { - const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - assert(body); - assert.ok(headers2.get(ETAG)); - }); - `, - output: ` - it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const body = await response.json(); - const headers2 = response.headers; - assert(body); - assert.ok(headers2.get(ETAG)); - }); - `, - errors: 1, - }, - { - name: 'handle spreading variable declaration for headers without body presented but with assertions used', - code: ` - it('returns current server time', async () => { - const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - assert.ok(headers.get(ETAG)); - }); - `, - output: ` - it('returns current server time', async () => { - const response = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const headers = response.headers; - assert.ok(headers.get(ETAG)); - }); - `, - errors: 1, - }, - { - name: 'handle spreading variable declaration for headers without body/assertion presented does not need to change', - code: ` - it('returns current server time', async () => { - const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); - assert.ok(headers.get(ETAG)); - }); - `, - output: ` - it('returns current server time', async () => { - const { headers } = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.ok(headers.get(ETAG)); - }); - `, - errors: 1, - }, - { - name: 'avoid response variable name conflict with existing variables in the same scope', - code: ` - it('returns current server time', async () => { - const response = 'foo'; - const response1 = 'bar'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('returns current server time', async () => { - const response = 'foo'; - const response1 = 'bar'; - const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - const response3 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response3.status, StatusCodes.OK); - }); - `, - errors: 2, - }, - { - name: 'response variable names in different scope do not conflict with each other', - code: ` - it('#1', async () => { - const response = 'foo'; - }); - it('#2', async () => { - const response = 'foo'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - }); - it('#3', async () => { - const response3 = 'foo'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('#1', async () => { - const response = 'foo'; - }); - it('#2', async () => { - const response = 'foo'; - const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - }); - it('#3', async () => { - const response3 = 'foo'; - const response = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - }); - `, - errors: 2, - }, - { - name: 'inline access to response body should be extracted to a variable', - code: ` - export async function validatePin( - fixture, - ) { - const paymentSecurityServicePublicKey = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; - } - `, - output: ` - export async function validatePin( - fixture, - ) { - const response = await fetch(\`\${BASE_PATH}/public-key\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); - const paymentSecurityServicePublicKey = responseBody.publicKey; - } - `, - errors: 1, - }, - { - name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - code: ` - it.each(temporalHeaders)('imports key using a $createdOnHeaderName header', async ({ createdOnHeaderName: _ }) => { - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - await fixture.api - .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) - .set(CREATED_ON_HEADER, createdOn) - .send({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }) - .expect(StatusCodes.NO_CONTENT) - .expect(ETAG_HEADER, '1') - .expect((res) => verifyTemporalHeaders(res, createdOn)); - }); - `, - output: ` - it.each(temporalHeaders)('imports key using a $createdOnHeaderName header', async ({ createdOnHeaderName: _ }) => { - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.ok(verifyTemporalHeaders(response, createdOn)); - }); - `, - errors: 1, - }, - // { - // name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - // code: ` - // it.each(temporalHeaders)( - // 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', - // async () => { - // const zoneKeyId = uuid(); - // await importTestMultipartZoneKey(fixture, zoneKeyId); - // const createdOn = new Date().toISOString(); - - // const keyId = uuid(); - // const { headers: headers1 } = await fixture.api - // .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) - // .set(CREATED_ON_HEADER, createdOn) - // .send({ - // key: '71CA52F757D7C0B45A16C6C04EAFD704', - // checkValue: '4F35C4', - // }) - // .expect(StatusCodes.NO_CONTENT) - // .expect(ETAG_HEADER, '1') - // .expect(verifyTemporalHeaders); - - // const updatedOn = new Date().toISOString(); - // const { headers: headers2 } = await fixture.api - // .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) - // .set(IF_MATCH_HEADER, headers1[ETAG_HEADER]) - // .set(CREATED_ON_HEADER, updatedOn) - // .send({ - // key: '339923C206BA8B19EEBF995DEE6619F7', - // checkValue: '1ADEE9', - // }) - // .expect(StatusCodes.NO_CONTENT) - // .expect(ETAG_HEADER, '2') - // .expect(verifyTemporalHeaders); - - // assert.ok(headers2[UPDATED_ON_HEADER] > headers1[CREATED_ON_HEADER]); - // }, - // ); - // `, - // output: ` - // it.each(temporalHeaders)( - // 'import Key with $createdOnHeaderName header, update key with $createdOnHeaderName header', - // async () => { - // const zoneKeyId = uuid(); - // await importTestMultipartZoneKey(fixture, zoneKeyId); - // const createdOn = new Date().toISOString(); - - // const keyId = uuid(); - // const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - // method: 'PUT', - // body: JSON.stringify({ - // key: '71CA52F757D7C0B45A16C6C04EAFD704', - // checkValue: '4F35C4', - // }), - // headers: { - // [CREATED_ON_HEADER]: createdOn, - // }, - // }); - // assert.equal(response.status, StatusCodes.NO_CONTENT); - // const headers1 = response.headers; - // assert.equal(headers1.get(ETAG_HEADER), '1'); - // assert.ok(verifyTemporalHeaders(response)); - - // const updatedOn = new Date().toISOString(); - // const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - // method: 'PUT', - // body: JSON.stringify({ - // key: '339923C206BA8B19EEBF995DEE6619F7', - // checkValue: '1ADEE9', - // }), - // headers: { - // [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), - // [CREATED_ON_HEADER]: updatedOn, - // }, - // }); - // assert.equal(response2.status, StatusCodes.NO_CONTENT); - // const headers2 = response2.headers; - // assert.equal(headers2.get(ETAG_HEADER), '2'); - // assert.ok(verifyTemporalHeaders(response2)); - - // assert.ok(headers2.get(UPDATED_ON_HEADER) > headers1.get(CREATED_ON_HEADER)); - // }, - // ); - // `, - // errors: 2, - // only: true, - // }, - ], - }); -}); From 604cdb5e2091a2d9db207aaf2e40ae52bca533ad Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sun, 28 Jul 2024 16:51:41 -0400 Subject: [PATCH 026/115] support concurrent fixture calls through Promise.all --- src/fixture/concurrent-promises.spec.ts | 49 +++++ src/fixture/concurrent-promises.ts | 242 ++++++++++++++++++++++++ src/fixture/fetch-header-getter.spec.ts | 2 +- src/fixture/fetch-header-getter.ts | 2 +- src/fixture/fetch.ts | 5 + src/fixture/no-fixture.spec.ts | 2 +- src/fixture/no-fixture.ts | 19 +- src/fixture/response-reference.ts | 2 +- src/fixture/url.ts | 6 + src/fixture/variable.ts | 5 + src/index.ts | 3 + 11 files changed, 318 insertions(+), 19 deletions(-) create mode 100644 src/fixture/concurrent-promises.spec.ts create mode 100644 src/fixture/concurrent-promises.ts create mode 100644 src/fixture/fetch.ts create mode 100644 src/fixture/url.ts create mode 100644 src/fixture/variable.ts diff --git a/src/fixture/concurrent-promises.spec.ts b/src/fixture/concurrent-promises.spec.ts new file mode 100644 index 0000000..d71f5dd --- /dev/null +++ b/src/fixture/concurrent-promises.spec.ts @@ -0,0 +1,49 @@ +// fixture/concurrent-promises.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './concurrent-promises'; +import createTester from '../tester.test'; +import { describe } from '@jest/globals'; + +describe(ruleId, () => { + createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'assertion with variable declaration', + code: ` + const responses = await Promise.all([ + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + ]); + `, + output: ` + const responses = await Promise.all([ + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }).then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }).then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + ]); + `, + errors: 2, + }, + ], + }); +}); diff --git a/src/fixture/concurrent-promises.ts b/src/fixture/concurrent-promises.ts new file mode 100644 index 0000000..40ba1f9 --- /dev/null +++ b/src/fixture/concurrent-promises.ts @@ -0,0 +1,242 @@ +// fixture/concurrent-promises.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import type { CallExpression, Expression, SimpleCallExpression } from 'estree'; +import { type Rule, SourceCode } from 'eslint'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../ast/format'; +import { getParent } from '../ast/tree'; +import { isValidPropertyName } from './variable'; +import { replaceEndpointUrlPrefixWithBasePath } from './url'; + +export const ruleId = 'concurrent-promises'; + +interface FixtureCallInformation { + fixtureNode: SimpleCallExpression; + requestBody?: Expression; + requestHeaders?: { name: Expression; value: Expression }[]; + assertions?: Expression[][]; +} + +// recursively analyze the fixture/supertest call chain to collect information of request/response +function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { + const parent = getParent(call); + if (!parent) { + return; + } + + let nextCall; + if (parent.type === 'ArrayExpression') { + results.fixtureNode = call; + } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + if (parent.property.name === 'expect') { + // supertest assertions + const assertionCall = getParent(parent); + assert.ok(assertionCall && assertionCall.type === 'CallExpression'); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + nextCall = assertionCall; + } else if (parent.property.name === 'send') { + // request body + const sendRequestBodyCall = getParent(parent); + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); + results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + nextCall = sendRequestBodyCall; + } else if (parent.property.name === 'set') { + // request headers + const setRequestHeaderCall = getParent(parent); + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); + const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression]; + results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }]; + nextCall = setRequestHeaderCall; + } + } else { + throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); + } + if (nextCall) { + analyzeFixtureCall(nextCall, results, sourceCode); + } +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function createResponseAssertions( + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + responseVariableName: string, +) { + let statusAssertion: string | undefined; + const nonStatusAssertions: string[] = []; + for (const expectArguments of fixtureCallInformation.assertions ?? []) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + if ( + (assertionArgument.type === 'MemberExpression' && + assertionArgument.object.type === 'Identifier' && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === 'Literal' || + sourceCode.getText(assertionArgument).includes('StatusCodes.') + ) { + // status code assertion + statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; + } else if (assertionArgument.type === 'ArrowFunctionExpression') { + // callback assertion using arrow function + let functionBody = sourceCode.getText(assertionArgument.body); + + const [originalResponseArgument] = assertionArgument.params; + assert.ok(originalResponseArgument?.type === 'Identifier'); + const originalResponseArgumentName = originalResponseArgument.name; + if (originalResponseArgumentName !== responseVariableName) { + functionBody = functionBody.replace( + new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), + responseVariableName, + ); + } + nonStatusAssertions.push(`assert.ok(${functionBody})`); + } else if (assertionArgument.type === 'Identifier') { + // callback assertion using function reference + nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`); + } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + // body deep equal assertion + nonStatusAssertions.push( + `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, + ); + } else { + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); + } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + const headersReference = `${responseVariableName}.headers`; + if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + nonStatusAssertions.push( + `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + nonStatusAssertions.push( + `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } + } + } + return { + statusAssertion, + nonStatusAssertions, + }; +} + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch API over customized fixture API.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + unknownError: + 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', + }, + fixable: 'code', + schema: [], + }, + // eslint-disable-next-line max-lines-per-function + create(context) { + const sourceCode = context.sourceCode; + + return { + // eslint-disable-next-line max-lines-per-function + 'CallExpression[callee.object.name="Promise"] > ArrayExpression CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': + (fixtureCall: CallExpression) => { + try { + assert.ok(fixtureCall.type === 'CallExpression'); + const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get + assert.ok(fixtureFunction.type === 'MemberExpression'); + const indentation = getIndentation(fixtureCall, sourceCode); + + const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` + assert.ok(urlArgumentNode !== undefined); + + const fixtureCallInformation = {} as FixtureCallInformation; + analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); + + // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` + const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); + const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); + + // fetch request argument + const methodNode = fixtureFunction.property; // get/put/etc. + assert.ok(methodNode.type === 'Identifier'); + const fetchRequestArgumentLines = [ + '{', + ` method: '${methodNode.name.toUpperCase()}',`, + ...(fixtureCallInformation.requestBody + ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] + : []), + ...(fixtureCallInformation.requestHeaders + ? [ + ` headers: {`, + ...fixtureCallInformation.requestHeaders.map( + ({ name, value }) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals + ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ), + ` },`, + ] + : []), + '}', + ].join(`\n${indentation}`); + + const responseVariableNameToUse = 'res'; + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + ); + + // add variable declaration if needed + const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method'; + const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; + const appendingAssignmentAndAssertionText = [ + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + const replacementText = [ + disableLintComment, + `${fetchCallText}.then((${responseVariableNameToUse}) => {`, + appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, + ` return ${responseVariableNameToUse};`, + `})`, + ].join(`\n${indentation}`); + + context.report({ + node: fixtureCall, + messageId: 'preferNativeFetch', + fix(fixer) { + return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: fixtureCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/src/fixture/fetch-header-getter.spec.ts b/src/fixture/fetch-header-getter.spec.ts index 6f0f500..4085b33 100644 --- a/src/fixture/fetch-header-getter.spec.ts +++ b/src/fixture/fetch-header-getter.spec.ts @@ -1,4 +1,4 @@ -// no-fixture.spec.ts +// fixture/no-fixture.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/fixture/fetch-header-getter.ts b/src/fixture/fetch-header-getter.ts index c797238..7da8616 100644 --- a/src/fixture/fetch-header-getter.ts +++ b/src/fixture/fetch-header-getter.ts @@ -1,4 +1,4 @@ -// no-fixture.ts +// fixture/no-fixture.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/fixture/fetch.ts b/src/fixture/fetch.ts new file mode 100644 index 0000000..467b2cc --- /dev/null +++ b/src/fixture/fetch.ts @@ -0,0 +1,5 @@ +// fixture/fetch.ts + +export function getResponseBodyRetrievalText(responseVariableName: string) { + return `await ${responseVariableName}.json()`; +} diff --git a/src/fixture/no-fixture.spec.ts b/src/fixture/no-fixture.spec.ts index ce6aefa..b3a30f3 100644 --- a/src/fixture/no-fixture.spec.ts +++ b/src/fixture/no-fixture.spec.ts @@ -1,4 +1,4 @@ -// no-fixture.spec.ts +// fixture/no-fixture.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/fixture/no-fixture.ts b/src/fixture/no-fixture.ts index 69edbb9..4c34283 100644 --- a/src/fixture/no-fixture.ts +++ b/src/fixture/no-fixture.ts @@ -1,4 +1,4 @@ -// no-fixture.ts +// fixture/no-fixture.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -22,6 +22,9 @@ import { analyzeResponseReferences } from './response-reference'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../ast/format'; +import { getResponseBodyRetrievalText } from './fetch'; +import { isValidPropertyName } from './variable'; +import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; @@ -92,16 +95,6 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo } } -// `/sample-service/v1/ping` -> `${BASE_PATH}/ping` -function replaceEndpointUrlPrefixWithBasePath(url: string) { - // eslint-disable-next-line no-template-curly-in-string - return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); -} - -function isValidPropertyName(name: unknown) { - return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); -} - // eslint-disable-next-line sonarjs/cognitive-complexity function createResponseAssertions( fixtureCallInformation: FixtureCallInformation, @@ -215,10 +208,6 @@ function isResponseBodyRedefinition(responseBodyReference: MemberExpression): bo return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier'; } -function getResponseBodyRetrievalText(responseVariableName: string) { - return `await ${responseVariableName}.json()`; -} - const rule: Rule.RuleModule = { meta: { type: 'suggestion', diff --git a/src/fixture/response-reference.ts b/src/fixture/response-reference.ts index 6e2d633..6e32db8 100644 --- a/src/fixture/response-reference.ts +++ b/src/fixture/response-reference.ts @@ -1,4 +1,4 @@ -// no-fixture.ts +// fixture/response-reference.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/fixture/url.ts b/src/fixture/url.ts new file mode 100644 index 0000000..72c50a4 --- /dev/null +++ b/src/fixture/url.ts @@ -0,0 +1,6 @@ +// fixture/url.ts + +export function replaceEndpointUrlPrefixWithBasePath(url: string) { + // eslint-disable-next-line no-template-curly-in-string + return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); +} diff --git a/src/fixture/variable.ts b/src/fixture/variable.ts new file mode 100644 index 0000000..b4e8b70 --- /dev/null +++ b/src/fixture/variable.ts @@ -0,0 +1,5 @@ +// fixture/variable.ts + +export function isValidPropertyName(name: unknown) { + return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); +} diff --git a/src/index.ts b/src/index.ts index bc6d24e..b8e1026 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import concurrentPromises, { ruleId as concurrentPromisesRuleId } from './fixture/concurrent-promises'; import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; @@ -35,6 +36,7 @@ export default { [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, [fetchHeaderGetterRuleId]: fetchHeaderGetter, + [concurrentPromisesRuleId]: concurrentPromises, }, configs: { all: { @@ -52,6 +54,7 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${concurrentPromisesRuleId}`]: 'error', }, }, recommended: { From a7c2d5c76e2352a9ded09080442be6134766dffe Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 29 Jul 2024 10:56:21 -0400 Subject: [PATCH 027/115] handle header access with concurrent api calls --- src/fixture/concurrent-promises.spec.ts | 62 +++++++++++- src/fixture/concurrent-promises.ts | 119 ++++++++++++++++++++++-- src/fixture/fetch-header-getter.ts | 53 +++++------ src/fixture/fetch.ts | 25 +++++ src/fixture/no-fixture.spec.ts | 12 ++- src/fixture/no-fixture.ts | 6 ++ src/fixture/response-reference.ts | 14 +-- 7 files changed, 242 insertions(+), 49 deletions(-) diff --git a/src/fixture/concurrent-promises.spec.ts b/src/fixture/concurrent-promises.spec.ts index d71f5dd..1b71d77 100644 --- a/src/fixture/concurrent-promises.spec.ts +++ b/src/fixture/concurrent-promises.spec.ts @@ -15,7 +15,29 @@ describe(ruleId, () => { valid: [], invalid: [ { - name: 'assertion with variable declaration', + name: 'without assertions', + code: ` + const responses = await Promise.all([ + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), + ]); + `, + output: ` + const responses = await Promise.all([ + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }), + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }), + ]); + `, + errors: 2, + }, + { + name: 'with assertions', code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), @@ -44,6 +66,44 @@ describe(ruleId, () => { `, errors: 2, }, + { + name: 'with assertions', + code: ` + const responses = await Promise.all([ + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + ]); + assert.deepEqual(responses.map((response) => response.headers.etag).sort(), ['1', '1']); + assert.equal(responses[0].headers[LAST_MODIFIED_HEADER], responses[1].headers[LAST_MODIFIED_HEADER]); + assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); + assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + `, + output: ` + const responses = await Promise.all([ + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }).then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }).then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + ]); + assert.deepEqual(responses.map((response) => response.headers.get('etag')).sort(), ['1', '1']); + assert.equal(responses[0].headers.get(LAST_MODIFIED_HEADER), responses[1].headers.get(LAST_MODIFIED_HEADER)); + assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); + assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + `, + errors: 12, + }, ], }); }); diff --git a/src/fixture/concurrent-promises.ts b/src/fixture/concurrent-promises.ts index 40ba1f9..027ae26 100644 --- a/src/fixture/concurrent-promises.ts +++ b/src/fixture/concurrent-promises.ts @@ -6,12 +6,13 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import type { CallExpression, Expression, SimpleCallExpression } from 'estree'; -import { type Rule, SourceCode } from 'eslint'; +import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree'; +import { type Rule, type Scope, SourceCode } from 'eslint'; +import { getEnclosingStatement, getParent } from '../ast/tree'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../ast/format'; -import { getParent } from '../ast/tree'; +import { isInvalidResponseHeadersAccess } from './fetch'; import { isValidPropertyName } from './variable'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; @@ -131,6 +132,54 @@ function createResponseAssertions( }; } +function getResponseHeadersAccesses( + responseVariables: Scope.Variable[], + scopeManager: Scope.ScopeManager, + sourceCode: SourceCode, +) { + const responseHeadersAccesses: MemberExpression[] = []; + for (const responseVariable of responseVariables) { + for (const responseReference of responseVariable.references) { + const responseAccess = getParent(responseReference.identifier); + if (!responseAccess || responseAccess.type !== 'MemberExpression') { + continue; + } + + const responseAccessParent = getParent(responseAccess); + if (!responseAccessParent) { + continue; + } + + if ( + responseAccessParent.type === 'CallExpression' && + responseAccessParent.arguments[0]?.type === 'ArrowFunctionExpression' + ) { + // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) + responseHeadersAccesses.push( + ...getResponseHeadersAccesses( + scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]), + scopeManager, + sourceCode, + ), + ); + continue; + } + + if ( + responseAccess.computed && + responseAccess.property.type === 'Literal' && + responseAccessParent.type === 'MemberExpression' + ) { + // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. + responseHeadersAccesses.push(responseAccessParent); + } else { + responseHeadersAccesses.push(responseAccess); + } + } + } + return responseHeadersAccesses; +} + const rule: Rule.RuleModule = { meta: { type: 'suggestion', @@ -140,6 +189,7 @@ const rule: Rule.RuleModule = { }, messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + shouldUseHeaderGetter: 'Getter should be used to access response headers.', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', }, @@ -149,6 +199,7 @@ const rule: Rule.RuleModule = { // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; return { // eslint-disable-next-line max-lines-per-function @@ -207,13 +258,15 @@ const rule: Rule.RuleModule = { ...(statusAssertion !== undefined ? [statusAssertion] : []), ...nonStatusAssertions, ].join(`;\n${indentation}`); - const replacementText = [ - disableLintComment, - `${fetchCallText}.then((${responseVariableNameToUse}) => {`, - appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, - ` return ${responseVariableNameToUse};`, - `})`, - ].join(`\n${indentation}`); + const replacementText = fixtureCallInformation.assertions + ? [ + disableLintComment, + `${fetchCallText}.then((${responseVariableNameToUse}) => {`, + appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, + ` return ${responseVariableNameToUse};`, + `})`, + ].join(`\n${indentation}`) + : fetchCallText; context.report({ node: fixtureCall, @@ -222,6 +275,52 @@ const rule: Rule.RuleModule = { return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); }, }); + + const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); + if (!responsesVariable) { + return; + } + + const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); + const responseHeadersAccesses = getResponseHeadersAccesses( + responseVariableReferences, + scopeManager, + sourceCode, + ); + for (const responseHeadersAccess of responseHeadersAccesses) { + if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { + const headerAccess = getParent(responseHeadersAccess); + if (headerAccess?.type === 'MemberExpression') { + const headerNameNode = headerAccess.property; + const headerName = headerAccess.computed + ? sourceCode.getText(headerNameNode) + : `'${sourceCode.getText(headerNameNode)}'`; + const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; + + context.report({ + node: headerAccess, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(headerAccess, headerAccessReplacementText); + }, + }); + } else if ( + headerAccess?.type === 'CallExpression' && + responseHeadersAccess.property.type === 'Identifier' && + responseHeadersAccess.property.name === 'get' + ) { + const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; + + context.report({ + node: headerAccess, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(headerAccess, headerAccessReplacementText); + }, + }); + } + } + } } catch (error) { // eslint-disable-next-line no-console console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); diff --git a/src/fixture/fetch-header-getter.ts b/src/fixture/fetch-header-getter.ts index 7da8616..accb0f2 100644 --- a/src/fixture/fetch-header-getter.ts +++ b/src/fixture/fetch-header-getter.ts @@ -6,12 +6,13 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import type { Identifier, MemberExpression, VariableDeclarator } from 'estree'; +import type { Identifier, VariableDeclarator } from 'estree'; import type { Rule } from 'eslint'; import { analyzeResponseReferences } from './response-reference'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getParent } from '../ast/tree'; +import { isInvalidResponseHeadersAccess } from './fetch'; export const ruleId = 'fetch-header-getter'; @@ -42,12 +43,13 @@ const rule: Rule.RuleModule = { ); assert.ok(responseVariable); - const directHeaderReferences = responseHeadersReferences - .map(getParent) - .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression'); + const directHeadersReferences = responseHeadersReferences.filter((headersReference) => { + const headersAccess = getParent(headersReference); + return headersAccess?.type !== 'VariableDeclarator'; + }); - const indirectHeaderReferences = responseHeadersReferences - .map((reference) => getParent(reference)) + const indirectHeadersReferences = responseHeadersReferences + .map(getParent) .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator') .map((declarator) => (declarator.id as Identifier).name) .map((redefinedHeadersVariableName) => { @@ -55,32 +57,31 @@ const rule: Rule.RuleModule = { const identifier = variable.identifiers[0]; return identifier?.type === 'Identifier' && identifier.name === redefinedHeadersVariableName; }); - return ( - headersVariable?.references - .map((reference) => getParent(reference.identifier)) - .filter((parent): parent is MemberExpression => parent?.type === 'MemberExpression') ?? [] - ); + return headersVariable?.references.map((reference) => reference.identifier) ?? []; }) .flat(); - const invalidHeaderReferences = [...directHeaderReferences, ...indirectHeaderReferences].filter( - (reference) => !(reference.property.type === 'Identifier' && reference.property.name === 'get'), + const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].filter( + isInvalidResponseHeadersAccess, ); - invalidHeaderReferences.forEach((reference) => { - const headerNameNode = reference.property; - const headerName = reference.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`; + invalidHeadersReferences.forEach((headersReference) => { + const headerAccess = getParent(headersReference); + if (headerAccess?.type === 'MemberExpression') { + const headerNameNode = headerAccess.property; + const headerName = headerAccess.computed + ? sourceCode.getText(headerNameNode) + : `'${sourceCode.getText(headerNameNode)}'`; + const replacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; - context.report({ - node: reference, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(reference, replacementText); - }, - }); + context.report({ + node: headerAccess, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(headerAccess, replacementText); + }, + }); + } }); }, }; diff --git a/src/fixture/fetch.ts b/src/fixture/fetch.ts index 467b2cc..bbf3926 100644 --- a/src/fixture/fetch.ts +++ b/src/fixture/fetch.ts @@ -1,5 +1,30 @@ // fixture/fetch.ts +import type { Node } from 'estree'; +import { getParent } from '../ast/tree'; + export function getResponseBodyRetrievalText(responseVariableName: string) { return `await ${responseVariableName}.json()`; } + +export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node) { + const responseHeaderAccessParent = getParent(responseHeadersAccess); + if (responseHeaderAccessParent?.type === 'VariableDeclarator') { + return false; + } + + if ( + responseHeaderAccessParent?.type === 'CallExpression' && + responseHeaderAccessParent.callee.type === 'MemberExpression' && + responseHeaderAccessParent.callee.property.type === 'Identifier' && + responseHeaderAccessParent.callee.property.name === 'get' + ) { + return true; + } + + return !( + responseHeaderAccessParent?.type === 'MemberExpression' && + responseHeaderAccessParent.property.type === 'Identifier' && + responseHeaderAccessParent.property.name === 'get' + ); +} diff --git a/src/fixture/no-fixture.spec.ts b/src/fixture/no-fixture.spec.ts index b3a30f3..ad02139 100644 --- a/src/fixture/no-fixture.spec.ts +++ b/src/fixture/no-fixture.spec.ts @@ -12,7 +12,17 @@ import { describe } from '@jest/globals'; describe(ruleId, () => { createTester().run(ruleId, rule, { - valid: [], + valid: [ + { + name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', + code: ` + const responses = await Promise.all([ + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + ]); + `, + }, + ], invalid: [ { name: 'assertion with variable declaration', diff --git a/src/fixture/no-fixture.ts b/src/fixture/no-fixture.ts index 4c34283..3695cda 100644 --- a/src/fixture/no-fixture.ts +++ b/src/fixture/no-fixture.ts @@ -37,6 +37,7 @@ interface FixtureCallInformation { assertions?: Expression[][]; inlineStatementNode?: Node; inlineBodyReference?: MemberExpression; + isConcurrent?: boolean; } // recursively analyze the fixture/supertest call chain to collect information of request/response @@ -49,6 +50,8 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; + } else if (parent.type === 'ArrayExpression') { + results.isConcurrent = true; } else if (parent.type === 'AwaitExpression') { results.fixtureNode = call; const enclosingStatement = getEnclosingStatement(parent); @@ -245,6 +248,9 @@ const rule: Rule.RuleModule = { const fixtureCallInformation = {} as FixtureCallInformation; analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); + if (fixtureCallInformation.isConcurrent === true) { + return; + } const { variable: responseVariable, diff --git a/src/fixture/response-reference.ts b/src/fixture/response-reference.ts index 6e32db8..8a03025 100644 --- a/src/fixture/response-reference.ts +++ b/src/fixture/response-reference.ts @@ -52,27 +52,19 @@ export function analyzeResponseReferences( // e.g. response.body results.bodyReferences = responseReferences.filter( (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - node.property.name === 'body', + node?.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'body', ); // e.g. response.headers / response.header / response.get() results.headersReferences = responseReferences.filter( (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && + node?.type === 'MemberExpression' && node.property.type === 'Identifier' && (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), ); // e.g. response.status / response.statusCode results.statusReferences = responseReferences.filter( (node): node is MemberExpression => - node !== null && - node !== undefined && - node.type === 'MemberExpression' && + node?.type === 'MemberExpression' && node.property.type === 'Identifier' && (node.property.name === 'status' || node.property.name === 'statusCode'), ); From 80f60107e1896aeb7534cecf6762a45d0a9cd2ec Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 29 Jul 2024 15:46:19 -0400 Subject: [PATCH 028/115] generalize concurrent promises cases to used-as-value-but-can-not-use-await scenario --- src/ast/tree.ts | 48 +++- ...nt-promises.spec.ts => fetch-then.spec.ts} | 72 +++-- .../{concurrent-promises.ts => fetch-then.ts} | 258 +++++++++--------- src/fixture/fetch.ts | 24 +- src/fixture/no-fixture.spec.ts | 43 +++ src/fixture/no-fixture.ts | 31 ++- src/index.ts | 6 +- 7 files changed, 320 insertions(+), 162 deletions(-) rename src/fixture/{concurrent-promises.spec.ts => fetch-then.spec.ts} (65%) rename src/fixture/{concurrent-promises.ts => fetch-then.ts} (54%) diff --git a/src/ast/tree.ts b/src/ast/tree.ts index 519506e..e54f114 100644 --- a/src/ast/tree.ts +++ b/src/ast/tree.ts @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import type { Node } from 'estree'; +import type { Expression, Node } from 'estree'; type NodeParent = Node | undefined | null; @@ -38,11 +38,12 @@ export function getAncestor( return getAncestor(parent, matcher, exitMatcher); } +export function isBlockStatement(node: Node) { + return node.type.endsWith('Statement') || node.type.endsWith('Declaration'); +} + export function getEnclosingStatement(node: Node) { - return getAncestor( - node, - (parentNode) => parentNode.type.endsWith('Statement') || parentNode.type.endsWith('Declaration'), - ); + return getAncestor(node, isBlockStatement); } export function getEnclosingScopeNode(node: Node) { @@ -50,3 +51,40 @@ export function getEnclosingScopeNode(node: Node) { ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type), ); } + +export function isUsedInArrayOrAsArgument(node: Node) { + if (isBlockStatement(node)) { + return false; + } + + const parent = getParent(node); + if (!parent) { + return false; + } + + if ( + parent.type === 'ArrayExpression' || + (parent.type === 'CallExpression' && parent.arguments.includes(node as Expression)) + ) { + return true; + } + + // recurse up the tree until hitting a block statement + return isUsedInArrayOrAsArgument(parent); +} + +export function getEnclosingFunction(node: Node) { + if ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + return node; + } + + const parent = getParent(node); + if (!parent) { + return; + } + return getEnclosingFunction(parent); +} diff --git a/src/fixture/concurrent-promises.spec.ts b/src/fixture/fetch-then.spec.ts similarity index 65% rename from src/fixture/concurrent-promises.spec.ts rename to src/fixture/fetch-then.spec.ts index 1b71d77..f569a69 100644 --- a/src/fixture/concurrent-promises.spec.ts +++ b/src/fixture/fetch-then.spec.ts @@ -6,36 +6,24 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import rule, { ruleId } from './concurrent-promises'; +import rule, { ruleId } from './fetch-then'; import createTester from '../tester.test'; import { describe } from '@jest/globals'; describe(ruleId, () => { createTester().run(ruleId, rule, { - valid: [], - invalid: [ + valid: [ { - name: 'without assertions', + name: 'skip regular fixture calls which will be handled in "no-fixture" rule', code: ` - const responses = await Promise.all([ - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), - ]); + const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + const body = pingResponse.body; + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); `, - output: ` - const responses = await Promise.all([ - fetch(\`\${BASE_PATH}/key\`, { - method: 'PUT', - body: JSON.stringify(keyData), - }), - fetch(\`\${BASE_PATH}/key\`, { - method: 'PUT', - body: JSON.stringify(keyData), - }), - ]); - `, - errors: 2, }, + ], + invalid: [ { name: 'with assertions', code: ` @@ -67,7 +55,7 @@ describe(ruleId, () => { errors: 2, }, { - name: 'with assertions', + name: 'adjust header access correctly', code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), @@ -104,6 +92,46 @@ describe(ruleId, () => { `, errors: 12, }, + { + name: 'in non-async arrow function with concurrent promises', + code: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + fixture.api + .put(\`\${BASE_PATH}/zone-key/\${zoneKeyId}\`) + .send(requestWithPropertyMissing) + .expect(StatusCodes.BAD_REQUEST) + ); + }), + ); + `, + output: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/zone-key/\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify(requestWithPropertyMissing), + }).then((res) => { + assert.equal(res.status, StatusCodes.BAD_REQUEST); + return res; + }) + ); + }), + ); + `, + errors: 1, + }, ], }); }); diff --git a/src/fixture/concurrent-promises.ts b/src/fixture/fetch-then.ts similarity index 54% rename from src/fixture/concurrent-promises.ts rename to src/fixture/fetch-then.ts index 027ae26..6ddab3f 100644 --- a/src/fixture/concurrent-promises.ts +++ b/src/fixture/fetch-then.ts @@ -1,4 +1,4 @@ -// fixture/concurrent-promises.ts +// fixture/fetch-then.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -8,15 +8,15 @@ import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree'; import { type Rule, type Scope, SourceCode } from 'eslint'; -import { getEnclosingStatement, getParent } from '../ast/tree'; +import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../ast/tree'; +import { hasAssertions, isInvalidResponseHeadersAccess } from './fetch'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../ast/format'; -import { isInvalidResponseHeadersAccess } from './fetch'; import { isValidPropertyName } from './variable'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; -export const ruleId = 'concurrent-promises'; +export const ruleId = 'fetch-then'; interface FixtureCallInformation { fixtureNode: SimpleCallExpression; @@ -33,9 +33,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo } let nextCall; - if (parent.type === 'ArrayExpression') { + if (parent.type !== 'MemberExpression') { results.fixtureNode = call; - } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + return; + } + + if (parent.property.type === 'Identifier') { if (parent.property.name === 'expect') { // supertest assertions const assertionCall = getParent(parent); @@ -203,137 +206,148 @@ const rule: Rule.RuleModule = { return { // eslint-disable-next-line max-lines-per-function - 'CallExpression[callee.object.name="Promise"] > ArrayExpression CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': - (fixtureCall: CallExpression) => { - try { - assert.ok(fixtureCall.type === 'CallExpression'); - const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get - assert.ok(fixtureFunction.type === 'MemberExpression'); - const indentation = getIndentation(fixtureCall, sourceCode); + 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( + fixtureCall: CallExpression, + // eslint-disable-next-line sonarjs/cognitive-complexity + ) => { + try { + if (!hasAssertions(fixtureCall)) { + // skip if there are no assertions, let "no-fixture" rule to handle the conversion + return; + } - const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` - assert.ok(urlArgumentNode !== undefined); + if (!(isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false)) { + return; + } - const fixtureCallInformation = {} as FixtureCallInformation; - analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); + assert.ok(fixtureCall.type === 'CallExpression'); + const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get + assert.ok(fixtureFunction.type === 'MemberExpression'); + const indentation = getIndentation(fixtureCall, sourceCode); - // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` - const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); - const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); + const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` + assert.ok(urlArgumentNode !== undefined); - // fetch request argument - const methodNode = fixtureFunction.property; // get/put/etc. - assert.ok(methodNode.type === 'Identifier'); - const fetchRequestArgumentLines = [ - '{', - ` method: '${methodNode.name.toUpperCase()}',`, - ...(fixtureCallInformation.requestBody - ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] - : []), - ...(fixtureCallInformation.requestHeaders - ? [ - ` headers: {`, - ...fixtureCallInformation.requestHeaders.map( - ({ name, value }) => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals - ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, - ), - ` },`, - ] - : []), - '}', - ].join(`\n${indentation}`); + const fixtureCallInformation = {} as FixtureCallInformation; + analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); - const responseVariableNameToUse = 'res'; - const { statusAssertion, nonStatusAssertions } = createResponseAssertions( - fixtureCallInformation, - sourceCode, - responseVariableNameToUse, - ); + // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` + const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); + const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); - // add variable declaration if needed - const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method'; - const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; - const appendingAssignmentAndAssertionText = [ - ...(statusAssertion !== undefined ? [statusAssertion] : []), - ...nonStatusAssertions, - ].join(`;\n${indentation}`); - const replacementText = fixtureCallInformation.assertions + // fetch request argument + const methodNode = fixtureFunction.property; // get/put/etc. + assert.ok(methodNode.type === 'Identifier'); + const fetchRequestArgumentLines = [ + '{', + ` method: '${methodNode.name.toUpperCase()}',`, + ...(fixtureCallInformation.requestBody + ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] + : []), + ...(fixtureCallInformation.requestHeaders ? [ - disableLintComment, - `${fetchCallText}.then((${responseVariableNameToUse}) => {`, - appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, - ` return ${responseVariableNameToUse};`, - `})`, - ].join(`\n${indentation}`) - : fetchCallText; + ` headers: {`, + ...fixtureCallInformation.requestHeaders.map( + ({ name, value }) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals + ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ), + ` },`, + ] + : []), + '}', + ].join(`\n${indentation}`); - context.report({ - node: fixtureCall, - messageId: 'preferNativeFetch', - fix(fixer) { - return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); - }, - }); + const responseVariableNameToUse = 'res'; + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + ); - const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); - if (!responsesVariable) { - return; - } + // add variable declaration if needed + const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method'; + const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; + const appendingAssignmentAndAssertionText = [ + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + const replacementText = fixtureCallInformation.assertions + ? [ + disableLintComment, + `${fetchCallText}.then((${responseVariableNameToUse}) => {`, + appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, + ` return ${responseVariableNameToUse};`, + `})`, + ].join(`\n${indentation}`) + : fetchCallText; - const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); - const responseHeadersAccesses = getResponseHeadersAccesses( - responseVariableReferences, - scopeManager, - sourceCode, - ); - for (const responseHeadersAccess of responseHeadersAccesses) { - if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { - const headerAccess = getParent(responseHeadersAccess); - if (headerAccess?.type === 'MemberExpression') { - const headerNameNode = headerAccess.property; - const headerName = headerAccess.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; + context.report({ + node: fixtureCall, + messageId: 'preferNativeFetch', + fix(fixer) { + return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); + }, + }); - context.report({ - node: headerAccess, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(headerAccess, headerAccessReplacementText); - }, - }); - } else if ( - headerAccess?.type === 'CallExpression' && - responseHeadersAccess.property.type === 'Identifier' && - responseHeadersAccess.property.name === 'get' - ) { - const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; + const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); + if (!responsesVariable) { + return; + } - context.report({ - node: headerAccess, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(headerAccess, headerAccessReplacementText); - }, - }); - } + const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); + const responseHeadersAccesses = getResponseHeadersAccesses( + responseVariableReferences, + scopeManager, + sourceCode, + ); + for (const responseHeadersAccess of responseHeadersAccesses) { + if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { + const headerAccess = getParent(responseHeadersAccess); + if (headerAccess?.type === 'MemberExpression') { + const headerNameNode = headerAccess.property; + const headerName = headerAccess.computed + ? sourceCode.getText(headerNameNode) + : `'${sourceCode.getText(headerNameNode)}'`; + const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; + + context.report({ + node: headerAccess, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(headerAccess, headerAccessReplacementText); + }, + }); + } else if ( + headerAccess?.type === 'CallExpression' && + responseHeadersAccess.property.type === 'Identifier' && + responseHeadersAccess.property.name === 'get' + ) { + const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; + + context.report({ + node: headerAccess, + messageId: 'shouldUseHeaderGetter', + fix(fixer) { + return fixer.replaceText(headerAccess, headerAccessReplacementText); + }, + }); } } - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); - context.report({ - node: fixtureCall, - messageId: 'unknownError', - data: { - fileName: context.filename, - error: error instanceof Error ? error.toString() : JSON.stringify(error), - }, - }); } - }, + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: fixtureCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, }; }, }; diff --git a/src/fixture/fetch.ts b/src/fixture/fetch.ts index bbf3926..2cf3212 100644 --- a/src/fixture/fetch.ts +++ b/src/fixture/fetch.ts @@ -1,7 +1,7 @@ // fixture/fetch.ts +import { getParent, isBlockStatement } from '../ast/tree'; import type { Node } from 'estree'; -import { getParent } from '../ast/tree'; export function getResponseBodyRetrievalText(responseVariableName: string) { return `await ${responseVariableName}.json()`; @@ -28,3 +28,25 @@ export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node) { responseHeaderAccessParent.property.name === 'get' ); } + +export function hasAssertions(fixtureCall: Node) { + if (isBlockStatement(fixtureCall)) { + return false; + } + + const parent = getParent(fixtureCall); + if (!parent) { + return false; + } + + if ( + parent.type === 'MemberExpression' && + parent.property.type === 'Identifier' && + parent.property.name === 'expect' && + getParent(parent)?.type === 'CallExpression' + ) { + return true; + } + + return hasAssertions(parent); +} diff --git a/src/fixture/no-fixture.spec.ts b/src/fixture/no-fixture.spec.ts index ad02139..9b33c81 100644 --- a/src/fixture/no-fixture.spec.ts +++ b/src/fixture/no-fixture.spec.ts @@ -24,6 +24,28 @@ describe(ruleId, () => { }, ], invalid: [ + { + name: 'without assertions', + code: ` + const responses = await Promise.all([ + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), + fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), + ]); + `, + output: ` + const responses = await Promise.all([ + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }), + fetch(\`\${BASE_PATH}/key\`, { + method: 'PUT', + body: JSON.stringify(keyData), + }), + ]); + `, + errors: 2, + }, { name: 'assertion with variable declaration', code: ` @@ -506,6 +528,27 @@ describe(ruleId, () => { `, errors: 1, }, + { + name: 'in arrow function without concurrent promises', + code: ` + const delayedCardCreationPromise = new Promise((delayedExecution) => { + setTimeout(() => { + delayedExecution(fixture.api.put(\`\${BASE_PATH}/card/\${cardId}\`).send(otherTestCard)); + }, 600); + }); + `, + output: ` + const delayedCardCreationPromise = new Promise((delayedExecution) => { + setTimeout(() => { + delayedExecution(fetch(\`\${BASE_PATH}/card/\${cardId}\`, { + method: 'PUT', + body: JSON.stringify(otherTestCard), + })); + }, 600); + }); + `, + errors: 1, + }, ], }); }); diff --git a/src/fixture/no-fixture.ts b/src/fixture/no-fixture.ts index 3695cda..6204741 100644 --- a/src/fixture/no-fixture.ts +++ b/src/fixture/no-fixture.ts @@ -17,19 +17,25 @@ import type { VariableDeclaration, } from 'estree'; import { type Rule, type Scope, SourceCode } from 'eslint'; -import { getEnclosingScopeNode, getEnclosingStatement, getParent } from '../ast/tree'; +import { + getEnclosingFunction, + getEnclosingScopeNode, + getEnclosingStatement, + getParent, + isUsedInArrayOrAsArgument, +} from '../ast/tree'; +import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; import { analyzeResponseReferences } from './response-reference'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../ast/format'; -import { getResponseBodyRetrievalText } from './fetch'; import { isValidPropertyName } from './variable'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; interface FixtureCallInformation { - rootNode: AwaitExpression | ReturnStatement | VariableDeclaration; + rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression; fixtureNode: AwaitExpression | SimpleCallExpression; variableDeclaration?: VariableDeclaration; requestBody?: Expression; @@ -37,10 +43,10 @@ interface FixtureCallInformation { assertions?: Expression[][]; inlineStatementNode?: Node; inlineBodyReference?: MemberExpression; - isConcurrent?: boolean; } // recursively analyze the fixture/supertest call chain to collect information of request/response +// eslint-disable-next-line sonarjs/cognitive-complexity function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); @@ -50,8 +56,10 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; - } else if (parent.type === 'ArrayExpression') { - results.isConcurrent = true; + } else if (parent.type === 'ArrayExpression' || parent.type === 'CallExpression') { + // direct return, no variable declaration or await + results.fixtureNode = call; + results.rootNode = call; } else if (parent.type === 'AwaitExpression') { results.fixtureNode = call; const enclosingStatement = getEnclosingStatement(parent); @@ -238,6 +246,14 @@ const rule: Rule.RuleModule = { fixtureCall: CallExpression, ) => { try { + if ( + hasAssertions(fixtureCall) && + (isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false) + ) { + // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here + return; + } + assert.ok(fixtureCall.type === 'CallExpression'); const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get assert.ok(fixtureFunction.type === 'MemberExpression'); @@ -248,9 +264,6 @@ const rule: Rule.RuleModule = { const fixtureCallInformation = {} as FixtureCallInformation; analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); - if (fixtureCallInformation.isConcurrent === true) { - return; - } const { variable: responseVariable, diff --git a/src/index.ts b/src/index.ts index b8e1026..feaf30d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,8 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import concurrentPromises, { ruleId as concurrentPromisesRuleId } from './fixture/concurrent-promises'; import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; +import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; @@ -36,7 +36,7 @@ export default { [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, [fetchHeaderGetterRuleId]: fetchHeaderGetter, - [concurrentPromisesRuleId]: concurrentPromises, + [fetchThenRuleId]: fetchThen, }, configs: { all: { @@ -54,7 +54,7 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${concurrentPromisesRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', }, }, recommended: { From 60eb286aadecc992fd99250db3b466999f2bbbb9 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 31 Jul 2024 16:24:27 -0400 Subject: [PATCH 029/115] basic service wrapper functional with typing support --- package-lock.json | 163 ++++++++------- package.json | 12 +- src/ast/format.ts | 3 +- src/ast/ts-tree.ts | 90 +++++++++ src/fixture/no-service-wrapper.spec.ts | 263 +++++++++++++++++++++++++ src/fixture/no-service-wrapper.ts | 210 ++++++++++++++++++++ src/index.ts | 3 + src/ts-init/file.ts | 0 src/ts-init/react.tsx | 0 9 files changed, 658 insertions(+), 86 deletions(-) create mode 100644 src/ast/ts-tree.ts create mode 100644 src/fixture/no-service-wrapper.spec.ts create mode 100644 src/fixture/no-service-wrapper.ts create mode 100644 src/ts-init/file.ts create mode 100644 src/ts-init/react.tsx diff --git a/package-lock.json b/package-lock.json index 493488b..fcf2df3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,19 @@ "name": "@checkdigit/eslint-plugin", "version": "6.6.0", "license": "MIT", + "dependencies": { + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "ts-api-utils": "^1.3.0" + }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/rule-tester": "7.18.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", "eslint-plugin-import": "^2.29.1", @@ -1995,17 +2001,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", - "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.17.0", - "@typescript-eslint/type-utils": "7.17.0", - "@typescript-eslint/utils": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2029,16 +2035,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -2057,15 +2063,40 @@ } } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", - "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "node_modules/@typescript-eslint/rule-tester": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.18.0.tgz", + "integrity": "sha512-ClrFQlwen9pJcYPIBLuarzBpONQAwjmJ0+YUjAo1TGzoZFJPyUK/A7bb4Mps0u+SMJJnFXbfMN8I9feQDf0O5A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0" + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "ajv": "^6.12.6", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "4.6.2", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@eslint/eslintrc": ">=2", + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2076,14 +2107,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", - "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", - "dev": true, + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.17.0", - "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2104,10 +2134,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", - "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", - "dev": true, + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2118,14 +2147,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", - "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", - "dev": true, + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/visitor-keys": "7.17.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2147,16 +2175,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", - "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", - "dev": true, + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.17.0", - "@typescript-eslint/types": "7.17.0", - "@typescript-eslint/typescript-estree": "7.17.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2170,13 +2197,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", - "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", - "dev": true, + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2233,7 +2259,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2377,7 +2402,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2632,7 +2656,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2642,7 +2665,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3172,7 +3194,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -3999,14 +4020,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4023,7 +4042,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4036,8 +4054,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -4083,7 +4100,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4409,7 +4425,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "license": "MIT", "dependencies": { "array-union": "^2.1.0", @@ -4827,7 +4842,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5747,15 +5761,13 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -5861,8 +5873,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -5942,7 +5953,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -5952,7 +5962,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5977,7 +5986,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -6308,7 +6316,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6339,7 +6346,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6628,7 +6634,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -6885,7 +6890,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6990,7 +6994,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7415,7 +7418,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -7428,7 +7430,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16" @@ -7644,7 +7645,6 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -7716,7 +7716,6 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 7054c21..77d162e 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,9 @@ "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/rule-tester": "7.18.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", "eslint-plugin-import": "^2.29.1", @@ -76,10 +77,15 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-sonarjs": "0.24.0" }, + "dependencies": { + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "ts-api-utils": "^1.3.0" + }, "peerDependencies": { "eslint": ">=8 <9" }, "engines": { "node": ">=20.14" } -} +} \ No newline at end of file diff --git a/src/ast/format.ts b/src/ast/format.ts index 16bfaff..e0bac37 100644 --- a/src/ast/format.ts +++ b/src/ast/format.ts @@ -6,11 +6,12 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type { Node } from 'estree'; import type { SourceCode } from 'eslint'; import { strict as assert } from 'node:assert'; -export function getIndentation(node: Node, sourceCode: SourceCode) { +export function getIndentation(node: Node | TSESTree.Node, sourceCode: SourceCode | TSESLint.SourceCode) { assert.ok(node.loc); const line = sourceCode.lines[node.loc.start.line - 1]; assert.ok(line !== undefined); diff --git a/src/ast/ts-tree.ts b/src/ast/ts-tree.ts new file mode 100644 index 0000000..c5aa0d7 --- /dev/null +++ b/src/ast/ts-tree.ts @@ -0,0 +1,90 @@ +// tree.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +type NodeParent = TSESTree.Node | undefined | null; + +interface NodeParentExtension { + parent: NodeParent; +} + +export function getParent(node: TSESTree.Node): TSESTree.Node | undefined | null { + return (node as unknown as NodeParentExtension).parent; +} + +export function getAncestor( + node: TSESTree.Node, + matcher: AST_NODE_TYPES | ((testNode: TSESTree.Node) => boolean), + exitMatcher?: AST_NODE_TYPES | ((testNode: TSESTree.Node) => boolean), +): TSESTree.Node | undefined { + const parent = getParent(node); + if (!parent) { + return undefined; + } else if (typeof matcher === 'string' && parent.type === matcher) { + return parent; + } else if (typeof matcher === 'function' && matcher(parent)) { + return parent; + } else if (typeof exitMatcher === 'string' && parent.type === exitMatcher) { + return undefined; + } else if (typeof exitMatcher === 'function' && exitMatcher(parent)) { + return undefined; + } + return getAncestor(parent, matcher, exitMatcher); +} + +export function isBlockStatement(node: TSESTree.Node) { + return node.type.endsWith('Statement') || node.type.endsWith('Declaration'); +} + +export function getEnclosingStatement(node: TSESTree.Node) { + return getAncestor(node, isBlockStatement); +} + +export function getEnclosingScopeNode(node: TSESTree.Node) { + return getAncestor(node, (parentNode) => + ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type), + ); +} + +export function isUsedInArrayOrAsArgument(node: TSESTree.Node) { + if (isBlockStatement(node)) { + return false; + } + + const parent = getParent(node); + if (!parent) { + return false; + } + + if ( + parent.type === AST_NODE_TYPES.ArrayExpression || + (parent.type === AST_NODE_TYPES.CallExpression && parent.arguments.includes(node as TSESTree.Expression)) + ) { + return true; + } + + // recurse up the tree until hitting a block statement + return isUsedInArrayOrAsArgument(parent); +} + +export function getEnclosingFunction(node: TSESTree.Node) { + if ( + node.type === AST_NODE_TYPES.FunctionDeclaration || + node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.ArrowFunctionExpression + ) { + return node; + } + + const parent = getParent(node); + if (!parent) { + return; + } + return getEnclosingFunction(parent); +} diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts new file mode 100644 index 0000000..cf5e194 --- /dev/null +++ b/src/fixture/no-service-wrapper.spec.ts @@ -0,0 +1,263 @@ +// fixture/no-service-wrapper.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-service-wrapper'; +import { RuleTester } from '@typescript-eslint/rule-tester'; +// import { dirname } from 'node:path'; +// import { fileURLToPath } from 'node:url'; + +// const currentDirname = dirname(fileURLToPath(import.meta.url)); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../../tsconfig.json', + tsconfigRootDir: `/Users/lcong/workspace/eslint-plugin/src/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'service wrapper passed in as a function argument with type as Endpoint', + code: ` + async function getKey(pingService: Endpoint) { + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey(pingService: Endpoint) { + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'service wrapper passed in as a function argument with type as ResolvedService', + code: ` + async function getKey( + pingService: ResolvedService, + request: InboundContext + ) { + await pingService(request).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey( + pingService: ResolvedService, + request: InboundContext + ) { + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'service configuration passed in as a argument with type as Configuration', + code: ` + async function getKey( + config: Configuration, + ) { + await config.service.ping(EMPTY_CONTEXT).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey( + config: Configuration, + ) { + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'fixture passed in as a argument', + code: ` + async function getKey( + fixture: Fixture, + ) { + await fixture.config.service.ping(EMPTY_CONTEXT).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey( + fixture: Fixture, + ) { + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'url declared as a variable', + code: ` + const url = \`\${PING_BASE_PATH}/key/\${keyId}\`; + await pingService.get(url, { + resolveWithFullResponse: true, + }); + `, + output: ` + const url = \`\${PING_BASE_PATH}/key/\${keyId}\`; + await fetch(url, { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle request with headers', + code: ` + async function getKey( + fixture: Fixture, + ) { + await fixture.config.service.ping(EMPTY_CONTEXT).head(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + `, + output: ` + async function getKey( + fixture: Fixture, + ) { + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'HEAD', + headers: { + 'Content-Type': 'application/json', + }, + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle request with body', + code: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.put(\`\${PING_BASE_PATH}/key/\${keyId}\`, {data:'hi'}, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify({data:'hi'}), + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle request with both body and headers', + code: ` + async function getKey( + fixture: Fixture, + keyRequest: ping.KeyRequest, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.post(\`\${PING_BASE_PATH}/key/\${keyId}\`, keyRequest, { + resolveWithFullResponse: true, + headers: { + etag: '123', + }, + }); + } + `, + output: ` + async function getKey( + fixture: Fixture, + keyRequest: ping.KeyRequest, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'POST', + headers: { + etag: '123', + }, + body: JSON.stringify(keyRequest), + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'initiate and call serve-runtime service in the same function', + code: ` + import type { Configuration, InboundContext } from '@checkdigit/serve-runtime'; + import type { pingV1 as ping } from '../services'; + + export async function createKey( + config: Configuration, + inboundContext: InboundContext, + keyRequest: ping.KeyRequest, + ): Promise { + const pingService = config.service.ping(inboundContext); + const newKeyResponse = await pingService.put( + \`\${PING_BASE_PATH}/key/\${keyId}\`, + keyRequest, + { + resolveWithFullResponse: true, + }, + ); + if (newKeyResponse.statusCode !== StatusCodes.OK) { + throw new Error('failed'); + } + return newKeyResponse.body; + } + `, + output: ` + import type { Configuration, InboundContext } from '@checkdigit/serve-runtime'; + import type { pingV1 as ping } from '../services'; + + export async function createKey( + config: Configuration, + inboundContext: InboundContext, + keyRequest: ping.KeyRequest, + ): Promise { + const pingService = config.service.ping(inboundContext); + const newKeyResponse = await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + body: JSON.stringify(keyRequest), + }); + if (newKeyResponse.statusCode !== StatusCodes.OK) { + throw new Error('failed'); + } + return newKeyResponse.body; + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + ], +}); diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts new file mode 100644 index 0000000..288df3c --- /dev/null +++ b/src/fixture/no-service-wrapper.ts @@ -0,0 +1,210 @@ +// fixture/no-service-wrapper.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { type Scope, ScopeManager } from '@typescript-eslint/scope-manager'; +import { + getEnclosingFunction, + getEnclosingScopeNode, + getEnclosingStatement, + getParent, + isUsedInArrayOrAsArgument, +} from '../ast/ts-tree'; +import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; +import { analyzeResponseReferences } from './response-reference'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../ast/format'; +import { isValidPropertyName } from './variable'; +import { replaceEndpointUrlPrefixWithBasePath } from './url'; + +export const ruleId = 'no-service-wrapper'; + +// interface ServiceCallInformation { +// rootNode: +// | TSESTree.AwaitExpression +// | TSESTree.ReturnStatement +// | TSESTree.VariableDeclaration +// | TSESTree.CallExpression; +// fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; +// variableDeclaration?: TSESTree.VariableDeclaration; +// requestBody?: TSESTree.Expression; +// requestHeaders?: TSESTree.Expression; +// } + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch over customized service wrapper.', + }, + messages: { + preferNativeFetch: 'Prefer native fetch over customized service wrapper.', + unknownError: + 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + // eslint-disable-next-line max-lines-per-function + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + const parserService = ESLintUtils.getParserServices(context); + const typeChecker = parserService.program.getTypeChecker(); + + function isUrlArgumentTemplateLiteral(urlArgument: TSESTree.Node | undefined, scope: Scope) { + return ( + urlArgument?.type === AST_NODE_TYPES.TemplateLiteral || + (urlArgument?.type === AST_NODE_TYPES.Identifier && + scope.variables.some((variable) => variable.name === urlArgument.name)) + ); + } + + function getType(identifier: TSESTree.Identifier) { + const variable = parserService.esTreeNodeToTSNodeMap.get(identifier); + const variableType = typeChecker.getTypeAtLocation(variable); + return typeChecker.typeToString(variableType); + } + + function isServiceLikeName(name: string) { + return /.*[Ss]ervice$/u.test(name); + } + + function isCalleeServiceWrapper(serviceCall: TSESTree.CallExpression) { + const callee = serviceCall.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const endpoint = callee.object; + if (endpoint.type === AST_NODE_TYPES.Identifier) { + return getType(endpoint) === 'Endpoint' || isServiceLikeName(endpoint.name); + } + if (endpoint.type !== AST_NODE_TYPES.CallExpression) { + return false; + } + + const [contextArgument] = endpoint.arguments; + if (contextArgument?.type !== AST_NODE_TYPES.Identifier) { + return false; + } + if (contextArgument.name !== 'EMPTY_CONTEXT' && getType(contextArgument) !== 'InboundContext') { + return false; + } + const service = endpoint.callee; + if (service.type === AST_NODE_TYPES.Identifier) { + return getType(service) === 'ResolvedService'; + } + + if (service.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const services = service.object; + if (services.type === AST_NODE_TYPES.Identifier) { + return getType(services) === 'ResolvedServices'; + } + + if (services.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const configuration = services.object; + if (configuration.type === AST_NODE_TYPES.Identifier) { + return getType(configuration) === 'Configuration'; + } + + // following applies only to test code (fixture) + if (configuration.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const fixture = configuration.object; + if (fixture.type === AST_NODE_TYPES.Identifier) { + return fixture.name === 'fixture' || getType(fixture) === 'Fixture'; + } + + return false; + } + + return { + 'CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]': ( + serviceCall: TSESTree.CallExpression, + ) => { + const enclosingScopeNode = getEnclosingScopeNode(serviceCall); + assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); + const scope = scopeManager?.acquire(enclosingScopeNode); + assert.ok(scope, 'scope is undefined'); + + const urlArgument = serviceCall.arguments[0]; + + if (!isUrlArgumentTemplateLiteral(urlArgument, scope)) { + return; + } + + if (!isCalleeServiceWrapper(serviceCall)) { + return; + } + + assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); + assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); + + const method = serviceCall.callee.property.name; /*?*/ + const optionsArgument = ['get', 'head', 'del'].includes(method) + ? serviceCall.arguments[1] + : serviceCall.arguments[2]; + if (optionsArgument !== undefined && optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { + throw new Error('optionsArgument is not an ObjectExpression'); + } + const resolveWithFullResponse = optionsArgument?.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'resolveWithFullResponse', + ); + if ( + resolveWithFullResponse?.type !== AST_NODE_TYPES.Property || + resolveWithFullResponse.value.type !== AST_NODE_TYPES.Literal || + resolveWithFullResponse.value.value !== true + ) { + throw new Error('resolveWithFullResponse is not true'); + } + + const requestHeadersProperty = optionsArgument?.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'headers', + ); + + const requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined; + + context.report({ + messageId: 'preferNativeFetch', + node: serviceCall, + fix(fixer) { + const url = sourceCode.getText(urlArgument); + const indentation = getIndentation(serviceCall, sourceCode); + const fetchText = [ + `fetch(${url}, {`, + ` method: '${method.toUpperCase()}',`, + ...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []), + ...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []), + '})', + ].join(`\n${indentation}`); + return fixer.replaceText(serviceCall, fetchText); + }, + }); + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index feaf30d..2b0f765 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; +import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './fixture/no-service-wrapper'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -37,6 +38,7 @@ export default { [noFixtureRuleId]: noFixture, [fetchHeaderGetterRuleId]: fetchHeaderGetter, [fetchThenRuleId]: fetchThen, + [noServiceWrapperRuleId]: noServiceWrapper, }, configs: { all: { @@ -55,6 +57,7 @@ export default { [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', }, }, recommended: { diff --git a/src/ts-init/file.ts b/src/ts-init/file.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ts-init/react.tsx b/src/ts-init/react.tsx new file mode 100644 index 0000000..e69de29 From 42ddba5d4775f7aec4fc4a3b8d6b8b78693079ec Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 31 Jul 2024 17:45:05 -0400 Subject: [PATCH 030/115] cleanup config --- package.json | 12 ++--- src/fixture/no-service-wrapper.spec.ts | 8 +--- src/fixture/no-service-wrapper.ts | 63 ++++++++++++++++---------- {src/ts-init => ts-init}/file.ts | 0 {src/ts-init => ts-init}/react.tsx | 0 5 files changed, 46 insertions(+), 37 deletions(-) rename {src/ts-init => ts-init}/file.ts (100%) rename {src/ts-init => ts-init}/react.tsx (100%) diff --git a/package.json b/package.json index 77d162e..152be94 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,11 @@ "jest": { "preset": "@checkdigit/jest-config" }, + "dependencies": { + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "ts-api-utils": "^1.3.0" + }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", @@ -77,15 +82,10 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-sonarjs": "0.24.0" }, - "dependencies": { - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "ts-api-utils": "^1.3.0" - }, "peerDependencies": { "eslint": ">=8 <9" }, "engines": { "node": ">=20.14" } -} \ No newline at end of file +} diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts index cf5e194..45f83e4 100644 --- a/src/fixture/no-service-wrapper.spec.ts +++ b/src/fixture/no-service-wrapper.spec.ts @@ -8,16 +8,12 @@ import rule, { ruleId } from './no-service-wrapper'; import { RuleTester } from '@typescript-eslint/rule-tester'; -// import { dirname } from 'node:path'; -// import { fileURLToPath } from 'node:url'; - -// const currentDirname = dirname(fileURLToPath(import.meta.url)); const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { - project: '../../tsconfig.json', - tsconfigRootDir: `/Users/lcong/workspace/eslint-plugin/src/ts-init`, + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, }, }); diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index 288df3c..2ed15fd 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -7,21 +7,11 @@ */ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import { type Scope, ScopeManager } from '@typescript-eslint/scope-manager'; -import { - getEnclosingFunction, - getEnclosingScopeNode, - getEnclosingStatement, - getParent, - isUsedInArrayOrAsArgument, -} from '../ast/ts-tree'; -import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; -import { analyzeResponseReferences } from './response-reference'; +import { type Scope } from '@typescript-eslint/scope-manager'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; +import { getEnclosingScopeNode } from '../ast/ts-tree'; import { getIndentation } from '../ast/format'; -import { isValidPropertyName } from './variable'; -import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-service-wrapper'; @@ -48,8 +38,10 @@ const rule = createRule({ }, messages: { preferNativeFetch: 'Prefer native fetch over customized service wrapper.', - unknownError: - 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.', + invalidOptions: + '"options" argument should be provided with "resolveWithFullResponse" property set as "true". Otherwise, it indicates that the response body will be obtained without status code assertion which could result in unexpected issue. Please manually convert the usage of customized service wrapper call to native fetch.', + // unknownError: + // 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.', }, fixable: 'code', schema: [], @@ -62,6 +54,14 @@ const rule = createRule({ const parserService = ESLintUtils.getParserServices(context); const typeChecker = parserService.program.getTypeChecker(); + // function reportUnknownError(node: TSESTree.Node, error: string) { + // context.report({ + // node, + // messageId: 'unknownError', + // data: { error, fileName: context.filename }, + // }); + // } + function isUrlArgumentTemplateLiteral(urlArgument: TSESTree.Node | undefined, scope: Scope) { return ( urlArgument?.type === AST_NODE_TYPES.TemplateLiteral || @@ -156,36 +156,49 @@ const rule = createRule({ assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); - const method = serviceCall.callee.property.name; /*?*/ + // method + const method = serviceCall.callee.property.name; + + // body + const requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined; + + // options const optionsArgument = ['get', 'head', 'del'].includes(method) ? serviceCall.arguments[1] : serviceCall.arguments[2]; - if (optionsArgument !== undefined && optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { - throw new Error('optionsArgument is not an ObjectExpression'); + if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { + context.report({ + node: serviceCall, + messageId: 'invalidOptions', + }); + return; } - const resolveWithFullResponse = optionsArgument?.properties.find( + const resolveWithFullResponseProperty = optionsArgument.properties.find( (property) => property.type === AST_NODE_TYPES.Property && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'resolveWithFullResponse', ); if ( - resolveWithFullResponse?.type !== AST_NODE_TYPES.Property || - resolveWithFullResponse.value.type !== AST_NODE_TYPES.Literal || - resolveWithFullResponse.value.value !== true + resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || + resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || + resolveWithFullResponseProperty.value.value !== true ) { - throw new Error('resolveWithFullResponse is not true'); + context.report({ + node: optionsArgument, + messageId: 'invalidOptions', + }); + return; } - const requestHeadersProperty = optionsArgument?.properties.find( + // headers + const requestHeadersProperty = optionsArgument.properties.find( (property) => property.type === AST_NODE_TYPES.Property && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'headers', ); - const requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined; - context.report({ messageId: 'preferNativeFetch', node: serviceCall, diff --git a/src/ts-init/file.ts b/ts-init/file.ts similarity index 100% rename from src/ts-init/file.ts rename to ts-init/file.ts diff --git a/src/ts-init/react.tsx b/ts-init/react.tsx similarity index 100% rename from src/ts-init/react.tsx rename to ts-init/react.tsx From b4ece1678178eb6b4232c474e7576cb9b3ccd798 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 13:02:32 -0400 Subject: [PATCH 031/115] improve url validation and conversion --- src/fixture/no-service-wrapper.spec.ts | 63 +++++++- src/fixture/no-service-wrapper.ts | 205 ++++++++++++++----------- src/fixture/url.ts | 10 +- 3 files changed, 184 insertions(+), 94 deletions(-) diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts index 45f83e4..91df63a 100644 --- a/src/fixture/no-service-wrapper.spec.ts +++ b/src/fixture/no-service-wrapper.spec.ts @@ -18,7 +18,12 @@ const ruleTester = new RuleTester({ }); ruleTester.run(ruleId, rule, { - valid: [], + valid: [ + { + name: 'none service wrapper call will not trigger an error', + code: `response.headers.get('foo');`, + }, + ], invalid: [ { name: 'service wrapper passed in as a function argument with type as Endpoint', @@ -255,5 +260,61 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'convert url to add domain', + code: ` + await pingService.get(\`/ping/v1/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + `, + output: ` + await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`, { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'works with string literal url as well', + code: ` + await pingService.get('/ping/v1/ping', { + resolveWithFullResponse: true, + }); + `, + output: ` + await fetch('https://ping.checkdigit/ping/v1/ping', { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'do not convert url containing BASE_PATH constant for the main service', + code: ` + await service.get(\`\${BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + `, + output: ` + await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'do not convert url containing BASE_PATH like constant for the dependent service', + code: ` + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + `, + output: ` + await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, ], }); diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index 2ed15fd..74ef74a 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -7,7 +7,8 @@ */ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import { type Scope } from '@typescript-eslint/scope-manager'; +import { DefinitionType, type Scope } from '@typescript-eslint/scope-manager'; +import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP, replaceEndpointUrlPrefixWithDomain } from './url'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; import { getEnclosingScopeNode } from '../ast/ts-tree'; @@ -15,18 +16,6 @@ import { getIndentation } from '../ast/format'; export const ruleId = 'no-service-wrapper'; -// interface ServiceCallInformation { -// rootNode: -// | TSESTree.AwaitExpression -// | TSESTree.ReturnStatement -// | TSESTree.VariableDeclaration -// | TSESTree.CallExpression; -// fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; -// variableDeclaration?: TSESTree.VariableDeclaration; -// requestBody?: TSESTree.Expression; -// requestHeaders?: TSESTree.Expression; -// } - const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); const rule = createRule({ @@ -40,8 +29,8 @@ const rule = createRule({ preferNativeFetch: 'Prefer native fetch over customized service wrapper.', invalidOptions: '"options" argument should be provided with "resolveWithFullResponse" property set as "true". Otherwise, it indicates that the response body will be obtained without status code assertion which could result in unexpected issue. Please manually convert the usage of customized service wrapper call to native fetch.', - // unknownError: - // 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.', + unknownError: + 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the usage of customized service wrapper call to native fetch.', }, fixable: 'code', schema: [], @@ -62,12 +51,27 @@ const rule = createRule({ // }); // } - function isUrlArgumentTemplateLiteral(urlArgument: TSESTree.Node | undefined, scope: Scope) { - return ( - urlArgument?.type === AST_NODE_TYPES.TemplateLiteral || - (urlArgument?.type === AST_NODE_TYPES.Identifier && - scope.variables.some((variable) => variable.name === urlArgument.name)) - ); + function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) { + if ( + (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === 'string') || + urlArgument?.type === AST_NODE_TYPES.TemplateLiteral + ) { + const urlText = sourceCode.getText(urlArgument); + return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText); + } + + if (urlArgument?.type === AST_NODE_TYPES.Identifier) { + const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name); + assert.ok(foundVariable, `Variable "${urlArgument.name}" not found in scope`); + const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); + assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); + const variableDefinitionNode = variableDefinition.node; + assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator); + assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); + return isUrlArgumentValid(variableDefinitionNode.init, scope); + } + + return false; } function getType(identifier: TSESTree.Identifier) { @@ -119,7 +123,7 @@ const rule = createRule({ } const configuration = services.object; if (configuration.type === AST_NODE_TYPES.Identifier) { - return getType(configuration) === 'Configuration'; + return ['Configuration', 'Configuration'].includes(getType(configuration)); } // following applies only to test code (fixture) @@ -138,83 +142,100 @@ const rule = createRule({ 'CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]': ( serviceCall: TSESTree.CallExpression, ) => { - const enclosingScopeNode = getEnclosingScopeNode(serviceCall); - assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); - const scope = scopeManager?.acquire(enclosingScopeNode); - assert.ok(scope, 'scope is undefined'); - - const urlArgument = serviceCall.arguments[0]; - - if (!isUrlArgumentTemplateLiteral(urlArgument, scope)) { - return; - } + try { + const enclosingScopeNode = getEnclosingScopeNode(serviceCall); + assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); + const scope = scopeManager?.acquire(enclosingScopeNode); + assert.ok(scope, 'scope is undefined'); + + const urlArgument = serviceCall.arguments[0]; + + if (!isUrlArgumentValid(urlArgument, scope)) { + return; + } + + if (!isCalleeServiceWrapper(serviceCall)) { + return; + } + + assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); + assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); + + // method + const method = serviceCall.callee.property.name; + + // body + const requestBodyProperty = ['put', 'post', 'options'].includes(method) + ? serviceCall.arguments[1] + : undefined; + + // options + const optionsArgument = ['get', 'head', 'del'].includes(method) + ? serviceCall.arguments[1] + : serviceCall.arguments[2]; + if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { + context.report({ + node: serviceCall, + messageId: 'invalidOptions', + }); + return; + } + const resolveWithFullResponseProperty = optionsArgument.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'resolveWithFullResponse', + ); + if ( + resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || + resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || + resolveWithFullResponseProperty.value.value !== true + ) { + context.report({ + node: optionsArgument, + messageId: 'invalidOptions', + }); + return; + } + + // headers + const requestHeadersProperty = optionsArgument.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'headers', + ); - if (!isCalleeServiceWrapper(serviceCall)) { - return; - } - - assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); - assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); - - // method - const method = serviceCall.callee.property.name; - - // body - const requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined; - - // options - const optionsArgument = ['get', 'head', 'del'].includes(method) - ? serviceCall.arguments[1] - : serviceCall.arguments[2]; - if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { context.report({ + messageId: 'preferNativeFetch', node: serviceCall, - messageId: 'invalidOptions', + fix(fixer) { + const url = sourceCode.getText(urlArgument); + const replacedUrl = replaceEndpointUrlPrefixWithDomain(url); + const indentation = getIndentation(serviceCall, sourceCode); + + const fetchText = [ + `fetch(${replacedUrl}, {`, + ` method: '${method.toUpperCase()}',`, + ...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []), + ...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []), + '})', + ].join(`\n${indentation}`); + return fixer.replaceText(serviceCall, fetchText); + }, }); - return; - } - const resolveWithFullResponseProperty = optionsArgument.properties.find( - (property) => - property.type === AST_NODE_TYPES.Property && - property.key.type === AST_NODE_TYPES.Identifier && - property.key.name === 'resolveWithFullResponse', - ); - if ( - resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || - resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || - resolveWithFullResponseProperty.value.value !== true - ) { + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); context.report({ - node: optionsArgument, - messageId: 'invalidOptions', + node: serviceCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, }); - return; } - - // headers - const requestHeadersProperty = optionsArgument.properties.find( - (property) => - property.type === AST_NODE_TYPES.Property && - property.key.type === AST_NODE_TYPES.Identifier && - property.key.name === 'headers', - ); - - context.report({ - messageId: 'preferNativeFetch', - node: serviceCall, - fix(fixer) { - const url = sourceCode.getText(urlArgument); - const indentation = getIndentation(serviceCall, sourceCode); - const fetchText = [ - `fetch(${url}, {`, - ` method: '${method.toUpperCase()}',`, - ...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []), - ...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []), - '})', - ].join(`\n${indentation}`); - return fixer.replaceText(serviceCall, fetchText); - }, - }); }, }; }, diff --git a/src/fixture/url.ts b/src/fixture/url.ts index 72c50a4..ea10115 100644 --- a/src/fixture/url.ts +++ b/src/fixture/url.ts @@ -1,6 +1,14 @@ // fixture/url.ts +export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/.+[`']$/u; +export const TOKENIZED_URL_REGEXP = /^`\$\{(?\w+_)*BASE_PATH\}\/.+`$/u; + export function replaceEndpointUrlPrefixWithBasePath(url: string) { // eslint-disable-next-line no-template-curly-in-string - return url.replace(/`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); + return url.replace(/^`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); +} + +export function replaceEndpointUrlPrefixWithDomain(url: string) { + // eslint-disable-next-line no-template-curly-in-string + return url.replace(/\/(?\w+(?-\w+)*)(?\/v\d+\/.*$)/u, 'https://$1.checkdigit/$1$3'); } From 1f38c233081f76f21bcd8bdd98a2151c5bef8f5f Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 13:10:58 -0400 Subject: [PATCH 032/115] fix codeql alert --- src/fixture/url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixture/url.ts b/src/fixture/url.ts index ea10115..b94a8c8 100644 --- a/src/fixture/url.ts +++ b/src/fixture/url.ts @@ -1,7 +1,7 @@ // fixture/url.ts export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/.+[`']$/u; -export const TOKENIZED_URL_REGEXP = /^`\$\{(?\w+_)*BASE_PATH\}\/.+`$/u; +export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/.+`$/u; export function replaceEndpointUrlPrefixWithBasePath(url: string) { // eslint-disable-next-line no-template-curly-in-string From 203bd2dd6f21a92cdce70132d0fcd7a81fab594d Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 13:11:23 -0400 Subject: [PATCH 033/115] codeql alert --- src/fixture/no-service-wrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index 74ef74a..2f00f1d 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -56,7 +56,7 @@ const rule = createRule({ (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === 'string') || urlArgument?.type === AST_NODE_TYPES.TemplateLiteral ) { - const urlText = sourceCode.getText(urlArgument); + const urlText = sourceCode.getText(urlArgument); return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText); } From 96c5f5bae3d2f9518f61e618b27b990bfddb93dd Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 15:16:09 -0400 Subject: [PATCH 034/115] replace statusCode with status --- src/fixture/no-status-code.spec.ts | 44 ++++++++++++++++++ src/fixture/no-status-code.ts | 72 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/fixture/no-status-code.spec.ts create mode 100644 src/fixture/no-status-code.ts diff --git a/src/fixture/no-status-code.spec.ts b/src/fixture/no-status-code.spec.ts new file mode 100644 index 0000000..1e77528 --- /dev/null +++ b/src/fixture/no-status-code.spec.ts @@ -0,0 +1,44 @@ +// fixture/no-status-code.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-status-code'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'no change if no "status" property is found in response type', + code: ` + const response = {statusCode: 200}; + const status = response.statusCode; + `, + }, + ], + invalid: [ + { + name: 'replace statusCode with status', + code: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + const status = response.statusCode; + `, + output: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + const status = response.status; + `, + errors: [{ messageId: 'replaceStatusCode' }], + }, + ], +}); diff --git a/src/fixture/no-status-code.ts b/src/fixture/no-status-code.ts new file mode 100644 index 0000000..6113578 --- /dev/null +++ b/src/fixture/no-status-code.ts @@ -0,0 +1,72 @@ +// fixture/no-status-code.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'no-status-code'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Access the status code property of the fetch Response using "status" instead of "statusCode".', + }, + messages: { + replaceStatusCode: 'Replacing "statusCode" with "status".', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + // eslint-disable-next-line max-lines-per-function + create(context) { + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + + return { + 'MemberExpression[property.name="statusCode"]': (responseStatusCode: TSESTree.MemberExpression) => { + try { + const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseStatusCode.object); + const responseType = typeChecker.getTypeAtLocation(responseNode); + + const shouldReplace = + responseType.getProperties().some((symbol) => symbol.name === 'status') && + !responseType.getProperties().some((symbol) => symbol.name === 'statusCode'); + + if (shouldReplace) { + context.report({ + messageId: 'replaceStatusCode', + node: responseStatusCode.property, + fix(fixer) { + return fixer.replaceText(responseStatusCode.property, 'status'); + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: responseStatusCode, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; From a2f58c774173d923aed2f4f5d1626ee208a0505f Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 15:30:16 -0400 Subject: [PATCH 035/115] add no-status-code rule in index.ts, fix url argument validation --- src/fixture/no-service-wrapper.ts | 24 ++++++++---------------- src/fixture/no-status-code.ts | 1 - src/index.ts | 3 +++ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index 2f00f1d..0f1188f 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -36,21 +36,12 @@ const rule = createRule({ schema: [], }, defaultOptions: [], - // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; const parserService = ESLintUtils.getParserServices(context); const typeChecker = parserService.program.getTypeChecker(); - // function reportUnknownError(node: TSESTree.Node, error: string) { - // context.report({ - // node, - // messageId: 'unknownError', - // data: { error, fileName: context.filename }, - // }); - // } - function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) { if ( (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === 'string') || @@ -62,13 +53,14 @@ const rule = createRule({ if (urlArgument?.type === AST_NODE_TYPES.Identifier) { const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name); - assert.ok(foundVariable, `Variable "${urlArgument.name}" not found in scope`); - const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); - assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); - const variableDefinitionNode = variableDefinition.node; - assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator); - assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); - return isUrlArgumentValid(variableDefinitionNode.init, scope); + if (foundVariable) { + const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); + assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); + const variableDefinitionNode = variableDefinition.node; + assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator); + assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); + return isUrlArgumentValid(variableDefinitionNode.init, scope); + } } return false; diff --git a/src/fixture/no-status-code.ts b/src/fixture/no-status-code.ts index 6113578..87737c2 100644 --- a/src/fixture/no-status-code.ts +++ b/src/fixture/no-status-code.ts @@ -28,7 +28,6 @@ const rule = createRule({ schema: [], }, defaultOptions: [], - // eslint-disable-next-line max-lines-per-function create(context) { const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); diff --git a/src/index.ts b/src/index.ts index 2b0f765..76354c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './in import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './fixture/no-service-wrapper'; +import noStatusCode, { ruleId as noStatusCodeRuleId } from './fixture/no-status-code'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -39,6 +40,7 @@ export default { [fetchHeaderGetterRuleId]: fetchHeaderGetter, [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, + [noStatusCodeRuleId]: noStatusCode, }, configs: { all: { @@ -58,6 +60,7 @@ export default { [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', }, }, recommended: { From cea4d080e9fe2d3ff2491b70a12844a95c42e303 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 17:10:51 -0400 Subject: [PATCH 036/115] replace "response.body" with "await response.json()" --- src/fixture/fetch-response-body-json.spec.ts | 56 +++++++++++++++ src/fixture/fetch-response-body-json.ts | 73 ++++++++++++++++++++ src/fixture/no-status-code.spec.ts | 2 +- src/fixture/no-status-code.ts | 2 +- src/index.ts | 3 + 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/fixture/fetch-response-body-json.spec.ts create mode 100644 src/fixture/fetch-response-body-json.ts diff --git a/src/fixture/fetch-response-body-json.spec.ts b/src/fixture/fetch-response-body-json.spec.ts new file mode 100644 index 0000000..b423507 --- /dev/null +++ b/src/fixture/fetch-response-body-json.spec.ts @@ -0,0 +1,56 @@ +// fixture/fetch-response-body-json.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './fetch-response-body-json'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'no change if no "json" property is found in the response type', + code: ` + const response = {body: 'foo'}; + const body = response.body; + `, + }, + ], + invalid: [ + { + name: 'replace statusCode with status', + code: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + const body = response.body; + `, + output: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + const body = (await response.json()); + `, + errors: [{ messageId: 'replaceBodyWithJson' }], + }, + { + name: 'replace statusCode with status in chained access', + code: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + return response.body.data; + `, + output: ` + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + return (await response.json()).data; + `, + errors: [{ messageId: 'replaceBodyWithJson' }], + }, + ], +}); diff --git a/src/fixture/fetch-response-body-json.ts b/src/fixture/fetch-response-body-json.ts new file mode 100644 index 0000000..f26e90c --- /dev/null +++ b/src/fixture/fetch-response-body-json.ts @@ -0,0 +1,73 @@ +// fixture/fetch-response-body-json.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'fetch-response-body-json'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Replace "response.body" with "await response.json()".', + }, + messages: { + replaceBodyWithJson: 'Replace "response.body" with "await response.json()".', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + const sourceCode = context.sourceCode; + + return { + 'MemberExpression[property.name="body"]': (responseBody: TSESTree.MemberExpression) => { + try { + const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBody.object); + const responseType = typeChecker.getTypeAtLocation(responseNode); + + const shouldReplace = + responseType.getProperties().some((symbol) => symbol.name === 'body') && + responseType.getProperties().some((symbol) => symbol.name === 'json'); + + if (shouldReplace) { + const responseText = sourceCode.getText(responseBody.object); + context.report({ + messageId: 'replaceBodyWithJson', + node: responseBody, + fix(fixer) { + return fixer.replaceText(responseBody, `(await ${responseText}.json())`); + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: responseBody, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/fixture/no-status-code.spec.ts b/src/fixture/no-status-code.spec.ts index 1e77528..e2fd56b 100644 --- a/src/fixture/no-status-code.spec.ts +++ b/src/fixture/no-status-code.spec.ts @@ -20,7 +20,7 @@ const ruleTester = new RuleTester({ ruleTester.run(ruleId, rule, { valid: [ { - name: 'no change if no "status" property is found in response type', + name: 'no change if no "status" property is found in the response type', code: ` const response = {statusCode: 200}; const status = response.statusCode; diff --git a/src/fixture/no-status-code.ts b/src/fixture/no-status-code.ts index 87737c2..0f56da7 100644 --- a/src/fixture/no-status-code.ts +++ b/src/fixture/no-status-code.ts @@ -21,7 +21,7 @@ const rule = createRule({ description: 'Access the status code property of the fetch Response using "status" instead of "statusCode".', }, messages: { - replaceStatusCode: 'Replacing "statusCode" with "status".', + replaceStatusCode: 'Replace "statusCode" with "status".', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', diff --git a/src/index.ts b/src/index.ts index 76354c9..3560dd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ */ import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; +import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './fixture/fetch-response-body-json'; import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; @@ -41,6 +42,7 @@ export default { [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, + [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, }, configs: { all: { @@ -61,6 +63,7 @@ export default { [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', }, }, recommended: { From d0d70e3e5aea7fdc559365eac0e2f52fce6f27c7 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 18:21:17 -0400 Subject: [PATCH 037/115] handle request body provided as undefined --- src/fixture/no-service-wrapper.spec.ts | 24 ++++++++++++++++++++++++ src/fixture/no-service-wrapper.ts | 12 ++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts index 91df63a..c141dcb 100644 --- a/src/fixture/no-service-wrapper.spec.ts +++ b/src/fixture/no-service-wrapper.spec.ts @@ -180,6 +180,30 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'handle PUT request with undefined body', + code: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.put(\`\${PING_BASE_PATH}/key/\${keyId}\`, undefined, { + resolveWithFullResponse: true, + }); + } + `, + output: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'PUT', + }); + } + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, { name: 'handle request with both body and headers', code: ` diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index 0f1188f..e8dca90 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -157,10 +157,14 @@ const rule = createRule({ const method = serviceCall.callee.property.name; // body - const requestBodyProperty = ['put', 'post', 'options'].includes(method) - ? serviceCall.arguments[1] - : undefined; - + let requestBodyProperty = ['put', 'post', 'options'].includes(method) ? serviceCall.arguments[1] : undefined; + if ( + requestBodyProperty !== undefined && + requestBodyProperty.type === AST_NODE_TYPES.Identifier && + requestBodyProperty.name === 'undefined' + ) { + requestBodyProperty = undefined; + } // options const optionsArgument = ['get', 'head', 'del'].includes(method) ? serviceCall.arguments[1] From 7c6f5f01ec48043f1e767186d421a1f938b512ba Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 21:06:38 -0400 Subject: [PATCH 038/115] handle multi-line url --- src/fixture/no-service-wrapper.spec.ts | 22 ++++++++++++++++++++++ src/fixture/url.ts | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts index c141dcb..3acdb57 100644 --- a/src/fixture/no-service-wrapper.spec.ts +++ b/src/fixture/no-service-wrapper.spec.ts @@ -340,5 +340,27 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'handle multi-line url string literal', + code: ` + await pingService.get(\`/message/v1/picked-request?cardId=\${cardIds.toString()}fromDate={encodeURIComponent( + fromDate, + )}toDate=\${encodeURIComponent( + toDate, + )}fields=ADVICE_RESPONSE,CATEGORIZATION,CREATED_ON,MATCHED_MESSAGE_ID,SETTLEMENT_AMOUNT,MESSAGE_ID,RECEIVED_DATE_TIME\`, { + resolveWithFullResponse: true, + }); + `, + output: ` + await fetch(\`/message/v1/picked-request?cardId=\${cardIds.toString()}fromDate={encodeURIComponent( + fromDate, + )}toDate=\${encodeURIComponent( + toDate, + )}fields=ADVICE_RESPONSE,CATEGORIZATION,CREATED_ON,MATCHED_MESSAGE_ID,SETTLEMENT_AMOUNT,MESSAGE_ID,RECEIVED_DATE_TIME\`, { + method: 'GET', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, ], }); diff --git a/src/fixture/url.ts b/src/fixture/url.ts index b94a8c8..2a66eb1 100644 --- a/src/fixture/url.ts +++ b/src/fixture/url.ts @@ -1,7 +1,7 @@ // fixture/url.ts -export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/.+[`']$/u; -export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/.+`$/u; +export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; +export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; export function replaceEndpointUrlPrefixWithBasePath(url: string) { // eslint-disable-next-line no-template-curly-in-string From 9dd53e89e61fff5a68877a25774071c12467c6c6 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 1 Aug 2024 22:51:25 -0400 Subject: [PATCH 039/115] use get() to access response headers, remove redundant await for response body --- src/fixture/fetch-response-body-json.spec.ts | 16 ++++ src/fixture/fetch-response-body-json.ts | 7 +- .../fetch-response-header-getter-ts.spec.ts | 68 +++++++++++++ .../fetch-response-header-getter-ts.ts | 96 +++++++++++++++++++ src/index.ts | 5 + 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 src/fixture/fetch-response-header-getter-ts.spec.ts create mode 100644 src/fixture/fetch-response-header-getter-ts.ts diff --git a/src/fixture/fetch-response-body-json.spec.ts b/src/fixture/fetch-response-body-json.spec.ts index b423507..1169177 100644 --- a/src/fixture/fetch-response-body-json.spec.ts +++ b/src/fixture/fetch-response-body-json.spec.ts @@ -52,5 +52,21 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'replaceBodyWithJson' }], }, + { + name: 'no redundant "await" for return statement.', + code: ` + async function foo() { + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + return response.body; + } + `, + output: ` + async function foo() { + const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + return response.json(); + } + `, + errors: [{ messageId: 'replaceBodyWithJson' }], + }, ], }); diff --git a/src/fixture/fetch-response-body-json.ts b/src/fixture/fetch-response-body-json.ts index f26e90c..b222d02 100644 --- a/src/fixture/fetch-response-body-json.ts +++ b/src/fixture/fetch-response-body-json.ts @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'fetch-response-body-json'; @@ -45,11 +45,14 @@ const rule = createRule({ if (shouldReplace) { const responseText = sourceCode.getText(responseBody.object); + const needAwait = responseBody.parent.type !== AST_NODE_TYPES.ReturnStatement; + const replacementText = needAwait ? `(await ${responseText}.json())` : `${responseText}.json()`; + context.report({ messageId: 'replaceBodyWithJson', node: responseBody, fix(fixer) { - return fixer.replaceText(responseBody, `(await ${responseText}.json())`); + return fixer.replaceText(responseBody, replacementText); }, }); } diff --git a/src/fixture/fetch-response-header-getter-ts.spec.ts b/src/fixture/fetch-response-header-getter-ts.spec.ts new file mode 100644 index 0000000..71de9f1 --- /dev/null +++ b/src/fixture/fetch-response-header-getter-ts.spec.ts @@ -0,0 +1,68 @@ +// fixture/fetch-response-header-getter-ts.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './fetch-response-header-getter-ts'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'no change if get() method is already used', + code: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers.get(ETAG); + `, + }, + ], + invalid: [ + { + name: 'use get() method to get header value from the headers object if the typing allows.', + code: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers[ETAG]; + `, + output: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers.get(ETAG); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'access using string literal', + code: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers['created-on']; + `, + output: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers.get('created-on'); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'access using Template literal', + code: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers[\`etag\`]; + `, + output: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers.get(\`etag\`); + `, + errors: [{ messageId: 'useGetter' }], + }, + ], +}); diff --git a/src/fixture/fetch-response-header-getter-ts.ts b/src/fixture/fetch-response-header-getter-ts.ts new file mode 100644 index 0000000..8375ff6 --- /dev/null +++ b/src/fixture/fetch-response-header-getter-ts.ts @@ -0,0 +1,96 @@ +// fixture/fetch-response-header-getter-ts.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'fetch-response-header-getter-ts'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Use "get()" method to get header value from the headers object of the fetch response.', + }, + messages: { + useGetter: 'Use "get()" method to get header value from the headers object of the fetch response.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + const sourceCode = context.sourceCode; + + return { + 'MemberExpression[object.property.name="headers"]': (responseHeadersAccess: TSESTree.MemberExpression) => { + try { + if ( + responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && + responseHeadersAccess.property.name === 'get' + ) { + // getter is already being used + return; + } + + const responseHeadersTsNode = parserServices.esTreeNodeToTSNodeMap.get(responseHeadersAccess.object); + const responseType = typeChecker.getTypeAtLocation(responseHeadersTsNode); + + const shouldReplace = responseType.getProperties().some((symbol) => symbol.name === 'get'); + if (!shouldReplace) { + return; + } + + // let replacementText = 'xxx'; + // if (responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier) { + // replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; + // } + let replacementText: string; + if (responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier) { + replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; + } else if (responseHeadersAccess.property.type === AST_NODE_TYPES.TemplateLiteral) { + replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; + } else if (responseHeadersAccess.property.type === AST_NODE_TYPES.Literal) { + replacementText = responseHeadersAccess.computed + ? `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})` + : `${sourceCode.getText(responseHeadersAccess.object)}.get('${sourceCode.getText(responseHeadersAccess.property)}')`; + } else { + throw new Error(`Unexpected property type: ${responseHeadersAccess.property.type}`); + } + + context.report({ + messageId: 'useGetter', + node: responseHeadersAccess.property, + fix(fixer) { + return fixer.replaceText(responseHeadersAccess, replacementText); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: responseHeadersAccess, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 3560dd0..47ba96d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,9 @@ import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './fixture/fetch-response-body-json'; +import fetchResponseHeaderGetterTs, { + ruleId as fetchResponseHeaderGetterTsRuleId, +} from './fixture/fetch-response-header-getter-ts'; import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; @@ -43,6 +46,7 @@ export default { [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, + [fetchResponseHeaderGetterTsRuleId]: fetchResponseHeaderGetterTs, }, configs: { all: { @@ -64,6 +68,7 @@ export default { [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', }, }, recommended: { From 82e31a0a6f67bf416425a20858257addcd537d54 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 2 Aug 2024 00:02:09 -0400 Subject: [PATCH 040/115] convert multiline url --- src/fixture/no-service-wrapper.spec.ts | 2 +- src/fixture/url.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/fixture/no-service-wrapper.spec.ts index 3acdb57..3e468f5 100644 --- a/src/fixture/no-service-wrapper.spec.ts +++ b/src/fixture/no-service-wrapper.spec.ts @@ -352,7 +352,7 @@ ruleTester.run(ruleId, rule, { }); `, output: ` - await fetch(\`/message/v1/picked-request?cardId=\${cardIds.toString()}fromDate={encodeURIComponent( + await fetch(\`https://message.checkdigit/message/v1/picked-request?cardId=\${cardIds.toString()}fromDate={encodeURIComponent( fromDate, )}toDate=\${encodeURIComponent( toDate, diff --git a/src/fixture/url.ts b/src/fixture/url.ts index 2a66eb1..01fb706 100644 --- a/src/fixture/url.ts +++ b/src/fixture/url.ts @@ -10,5 +10,8 @@ export function replaceEndpointUrlPrefixWithBasePath(url: string) { export function replaceEndpointUrlPrefixWithDomain(url: string) { // eslint-disable-next-line no-template-curly-in-string - return url.replace(/\/(?\w+(?-\w+)*)(?\/v\d+\/.*$)/u, 'https://$1.checkdigit/$1$3'); + return url.replace( + /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+\/(?.|\r|\n)+(?[`'])$)/u, + '$1https://$2.checkdigit/$2$4', + ); } From 127a30d0e1b8535d2d2cadd9e750326c1a89cf34 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 2 Aug 2024 00:59:28 -0400 Subject: [PATCH 041/115] auto convert BATH_PATH like variable declaration --- src/fixture/add-url-domain.spec.ts | 47 +++++++++++++++++++ src/fixture/add-url-domain.ts | 75 ++++++++++++++++++++++++++++++ src/fixture/url.ts | 8 +++- src/index.ts | 3 ++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/fixture/add-url-domain.spec.ts create mode 100644 src/fixture/add-url-domain.ts diff --git a/src/fixture/add-url-domain.spec.ts b/src/fixture/add-url-domain.spec.ts new file mode 100644 index 0000000..84fd155 --- /dev/null +++ b/src/fixture/add-url-domain.spec.ts @@ -0,0 +1,47 @@ +// fixture/add-url-domain.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './add-url-domain'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'no change if no "status" property is found in the response type', + code: `export const BASE_PATH = 'https://ping.checkdigit/ping/v1';`, + }, + ], + invalid: [ + { + name: 'add domain to url constant variable BASE_PATH', + code: `export const BASE_PATH = '/ping/v1';`, + output: `export const BASE_PATH = 'https://ping.checkdigit/ping/v1';`, + errors: [{ messageId: 'addDomain' }], + }, + { + name: 'add domain to url constant variable BASE_PATH defined with template literal', + code: `export const BASE_PATH = \`/ping/v1\`;`, + output: `export const BASE_PATH = \`https://ping.checkdigit/ping/v1\`;`, + errors: [{ messageId: 'addDomain' }], + }, + { + name: 'add domain to BASE_PATH like url constant variable', + code: `const FOO_BAR_BASE_PATH = '/foo-bar/v1';`, + output: `const FOO_BAR_BASE_PATH = 'https://foo-bar.checkdigit/foo-bar/v1';`, + errors: [{ messageId: 'addDomain' }], + }, + ], +}); diff --git a/src/fixture/add-url-domain.ts b/src/fixture/add-url-domain.ts new file mode 100644 index 0000000..22707f4 --- /dev/null +++ b/src/fixture/add-url-domain.ts @@ -0,0 +1,75 @@ +// fixture/add-url-domain.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { addBasePathUrlDomain } from './url'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'add-url-domain'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Add HTTP domain to the BASE_PATH like url constant variable.', + }, + messages: { + addDomain: 'Add HTTP domain to the BASE_PATH like url constant variable.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + 'VariableDeclarator[id.name=/^([A-Z]+_)*BASE_PATH$/]': (basePathDeclarator: TSESTree.VariableDeclarator) => { + try { + if ( + basePathDeclarator.init === null || + (basePathDeclarator.init.type !== AST_NODE_TYPES.Literal && + basePathDeclarator.init.type !== AST_NODE_TYPES.TemplateLiteral) + ) { + return; + } + + const urlText = sourceCode.getText(basePathDeclarator.init); /*?*/ + const replacement = addBasePathUrlDomain(urlText); /*?*/ + + if (replacement !== urlText) { + context.report({ + messageId: 'addDomain', + node: basePathDeclarator.init, + fix(fixer) { + return fixer.replaceText(basePathDeclarator.init as TSESTree.Node, replacement); + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: basePathDeclarator, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/fixture/url.ts b/src/fixture/url.ts index 01fb706..0f8cc60 100644 --- a/src/fixture/url.ts +++ b/src/fixture/url.ts @@ -9,9 +9,15 @@ export function replaceEndpointUrlPrefixWithBasePath(url: string) { } export function replaceEndpointUrlPrefixWithDomain(url: string) { - // eslint-disable-next-line no-template-curly-in-string return url.replace( /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+\/(?.|\r|\n)+(?[`'])$)/u, '$1https://$2.checkdigit/$2$4', ); } + +export function addBasePathUrlDomain(url: string) { + return url.replace( + /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+(?[`'])$)/u, + '$1https://$2.checkdigit/$2$4', + ); +} diff --git a/src/index.ts b/src/index.ts index 47ba96d..e1f08ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import addUrlDomain, { ruleId as addUrlDomainRuleId } from './fixture/add-url-domain'; import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './fixture/fetch-response-body-json'; import fetchResponseHeaderGetterTs, { @@ -47,6 +48,7 @@ export default { [noStatusCodeRuleId]: noStatusCode, [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, [fetchResponseHeaderGetterTsRuleId]: fetchResponseHeaderGetterTs, + [addUrlDomainRuleId]: addUrlDomain, }, configs: { all: { @@ -69,6 +71,7 @@ export default { [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', }, }, recommended: { From 670993764f33799dd915b64017e689abdc020f45 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 2 Aug 2024 13:38:00 -0400 Subject: [PATCH 042/115] convert response.get() to response.headers.get() --- .../fetch-response-header-getter-ts.spec.ts | 19 ++++++++++ .../fetch-response-header-getter-ts.ts | 35 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/fixture/fetch-response-header-getter-ts.spec.ts b/src/fixture/fetch-response-header-getter-ts.spec.ts index 71de9f1..ed66a07 100644 --- a/src/fixture/fetch-response-header-getter-ts.spec.ts +++ b/src/fixture/fetch-response-header-getter-ts.spec.ts @@ -26,6 +26,13 @@ ruleTester.run(ruleId, rule, { response.headers.get(ETAG); `, }, + { + name: 'no change of response.get() if the type of response does not include "headers" property', + code: ` + const response : Record = await getResponse(); + response.get(ETAG); + `, + }, ], invalid: [ { @@ -64,5 +71,17 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'useGetter' }], }, + { + name: 'response.get() should be changed to response.headers.get()', + code: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.get(ETAG); + `, + output: ` + const response : { headers: {get: (string)=>string} } = await getResponse(); + response.headers.get(ETAG); + `, + errors: [{ messageId: 'useGetter' }], + }, ], }); diff --git a/src/fixture/fetch-response-header-getter-ts.ts b/src/fixture/fetch-response-header-getter-ts.ts index 8375ff6..62d6444 100644 --- a/src/fixture/fetch-response-header-getter-ts.ts +++ b/src/fixture/fetch-response-header-getter-ts.ts @@ -89,6 +89,41 @@ const rule = createRule({ }); } }, + 'CallExpression[callee.property.name="get"]': (responseHeadersAccess: TSESTree.CallExpression) => { + try { + if (responseHeadersAccess.callee.type !== AST_NODE_TYPES.MemberExpression) { + return; + } + + const responseNode = responseHeadersAccess.callee.object; + const responseHeadersTsNode = parserServices.esTreeNodeToTSNodeMap.get(responseNode); + const responseType = typeChecker.getTypeAtLocation(responseHeadersTsNode); + const hasHeadersProperty = responseType.getProperties().some((symbol) => symbol.name === 'headers'); + if (!hasHeadersProperty) { + return; + } + + const replacementText = `${sourceCode.getText(responseNode)}.headers`; + context.report({ + messageId: 'useGetter', + node: responseHeadersAccess, + fix(fixer) { + return fixer.replaceText(responseNode, replacementText); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: responseHeadersAccess, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, }; }, }); From fae1d6003b58c566b2fd11805e8c69d81c023508 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 2 Aug 2024 16:38:53 -0400 Subject: [PATCH 043/115] handle header getter edge cases --- src/fixture/fetch-response-header-getter-ts.spec.ts | 9 +++++++++ src/fixture/no-service-wrapper.ts | 10 ++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/fixture/fetch-response-header-getter-ts.spec.ts b/src/fixture/fetch-response-header-getter-ts.spec.ts index ed66a07..8be83b5 100644 --- a/src/fixture/fetch-response-header-getter-ts.spec.ts +++ b/src/fixture/fetch-response-header-getter-ts.spec.ts @@ -33,6 +33,15 @@ ruleTester.run(ruleId, rule, { response.get(ETAG); `, }, + { + name: 'no change of request.get() if the type the request is InboundContext', + code: ` + type InboundContext = { get: (string)=>string }; + async function doSomething(request: InboundContext) { + const etagRequestHeader = request.get(ETAG); + } + `, + }, ], invalid: [ { diff --git a/src/fixture/no-service-wrapper.ts b/src/fixture/no-service-wrapper.ts index e8dca90..3a3fb1b 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/fixture/no-service-wrapper.ts @@ -135,21 +135,19 @@ const rule = createRule({ serviceCall: TSESTree.CallExpression, ) => { try { + if (!isCalleeServiceWrapper(serviceCall)) { + return; + } + const enclosingScopeNode = getEnclosingScopeNode(serviceCall); assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); const scope = scopeManager?.acquire(enclosingScopeNode); assert.ok(scope, 'scope is undefined'); - const urlArgument = serviceCall.arguments[0]; - if (!isUrlArgumentValid(urlArgument, scope)) { return; } - if (!isCalleeServiceWrapper(serviceCall)) { - return; - } - assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); From 405bb4c00c72cc894ecb199a1a3ca2db96b7da60 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 2 Aug 2024 18:47:48 -0400 Subject: [PATCH 044/115] don't modify headers getter for request --- .../fetch-response-header-getter-ts.spec.ts | 31 +++++++++++++++++-- .../fetch-response-header-getter-ts.ts | 8 ++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/fixture/fetch-response-header-getter-ts.spec.ts b/src/fixture/fetch-response-header-getter-ts.spec.ts index 8be83b5..82f8458 100644 --- a/src/fixture/fetch-response-header-getter-ts.spec.ts +++ b/src/fixture/fetch-response-header-getter-ts.spec.ts @@ -34,14 +34,39 @@ ruleTester.run(ruleId, rule, { `, }, { - name: 'no change of request.get() if the type the request is InboundContext', + name: 'no change of request.get() if the type the request do not have "headers" property', code: ` - type InboundContext = { get: (string)=>string }; - async function doSomething(request: InboundContext) { + type Context = { get: (string)=>string }; + async function doSomething(req: Context) { + const etagRequestHeader = req.get(ETAG); + } + `, + }, + { + name: 'no change of request.get() if the variable name is "request"', + code: ` + type Context = { get: (string)=>string, headers: Record }; + async function doSomething(request: Context) { const etagRequestHeader = request.get(ETAG); } `, }, + { + name: 'no change of request.get() if the type the request is InboundContext', + code: ` + async function doSomething(req: InboundContext) { + const etagRequestHeader = req.get(ETAG); + } + `, + }, + { + name: 'no change of request.get() if the type the request is xxxRequestType', + code: ` + async function doSomething(fooReq: FooRequestType) { + const etagRequestHeader = fooReq.get(ETAG); + } + `, + }, ], invalid: [ { diff --git a/src/fixture/fetch-response-header-getter-ts.ts b/src/fixture/fetch-response-header-getter-ts.ts index 62d6444..6308cd8 100644 --- a/src/fixture/fetch-response-header-getter-ts.ts +++ b/src/fixture/fetch-response-header-getter-ts.ts @@ -89,7 +89,9 @@ const rule = createRule({ }); } }, - 'CallExpression[callee.property.name="get"]': (responseHeadersAccess: TSESTree.CallExpression) => { + 'CallExpression[callee.property.name="get"]:not([callee.object.name="request"])': ( + responseHeadersAccess: TSESTree.CallExpression, + ) => { try { if (responseHeadersAccess.callee.type !== AST_NODE_TYPES.MemberExpression) { return; @@ -98,6 +100,10 @@ const rule = createRule({ const responseNode = responseHeadersAccess.callee.object; const responseHeadersTsNode = parserServices.esTreeNodeToTSNodeMap.get(responseNode); const responseType = typeChecker.getTypeAtLocation(responseHeadersTsNode); + const typeName = typeChecker.typeToString(responseType); + if (typeName === 'InboundContext' || typeName.endsWith('RequestType')) { + return; + } const hasHeadersProperty = responseType.getProperties().some((symbol) => symbol.name === 'headers'); if (!hasHeadersProperty) { return; From 7a0da28507e1eacfa38be978c41d392b03d66507 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 6 Aug 2024 00:23:16 -0400 Subject: [PATCH 045/115] add FullResponse removal rule --- src/fixture/no-full-response.spec.ts | 98 ++++++++++++++++++++++++++++ src/fixture/no-full-response.ts | 75 +++++++++++++++++++++ src/fixture/ts-tree.ts | 14 ++++ src/index.ts | 3 + 4 files changed, 190 insertions(+) create mode 100644 src/fixture/no-full-response.spec.ts create mode 100644 src/fixture/no-full-response.ts create mode 100644 src/fixture/ts-tree.ts diff --git a/src/fixture/no-full-response.spec.ts b/src/fixture/no-full-response.spec.ts new file mode 100644 index 0000000..acfdcf7 --- /dev/null +++ b/src/fixture/no-full-response.spec.ts @@ -0,0 +1,98 @@ +// fixture/no-full-response.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-full-response'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'remove type annotation from variable declaration', + code: `const responses: FullResponse = await fixture.api.put('\${BASE_PATH}/ping').send(testCard);`, + output: `const responses = await fixture.api.put('\${BASE_PATH}/ping').send(testCard);`, + errors: [{ messageId: 'removeFullResponse' }], + }, + { + name: 'remove type annotation from array variable declaration', + code: ` + const results: FullResponse[] = await Promise.all([ + fetch(\`\${BASE_PATH}/ping\`), + fetch(\`\${BASE_PATH}/ping\`) + ]); + `, + output: ` + const results = await Promise.all([ + fetch(\`\${BASE_PATH}/ping\`), + fetch(\`\${BASE_PATH}/ping\`) + ]); + `, + errors: [{ messageId: 'removeFullResponse' }], + }, + { + name: 'remove type annotation from function return type', + code: ` + export async function putPersonDataEncryptionKey( + configuration: Configuration, + inboundContext: InboundContext, + dataEncryptionKeyId: string, + ): Promise> { + const putDataEncryptionKeyResponse = await configuration.service + .person(inboundContext) + .put(\`/person/v1/data-encryption-key/\${dataEncryptionKeyId}\`, requestBody, { + resolveWithFullResponse: true, + }); + + if (putDataEncryptionKeyResponse.statusCode === StatusCodes.OK) { + return putDataEncryptionKeyResponse; + } + throw new Error(\`Error creating Person data encryption key \${dataEncryptionKeyId}. \`); + } + `, + output: ` + export async function putPersonDataEncryptionKey( + configuration: Configuration, + inboundContext: InboundContext, + dataEncryptionKeyId: string, + ) { + const putDataEncryptionKeyResponse = await configuration.service + .person(inboundContext) + .put(\`/person/v1/data-encryption-key/\${dataEncryptionKeyId}\`, requestBody, { + resolveWithFullResponse: true, + }); + + if (putDataEncryptionKeyResponse.statusCode === StatusCodes.OK) { + return putDataEncryptionKeyResponse; + } + throw new Error(\`Error creating Person data encryption key \${dataEncryptionKeyId}. \`); + } + `, + errors: [{ messageId: 'removeFullResponse' }], + }, + { + name: 'remove type annotation from arrow function argument narrowing', + code: `putResponses.map((putResponse: FullResponse) => putResponse.statusCode)`, + output: `putResponses.map((putResponse) => putResponse.statusCode)`, + errors: [{ messageId: 'removeFullResponse' }], + }, + { + name: 'remove type annotation from "as" type narrowingF', + code: `const fullResponse = response as FullResponse;`, + output: `const fullResponse = response;`, + errors: [{ messageId: 'removeFullResponse' }], + }, + ], +}); diff --git a/src/fixture/no-full-response.ts b/src/fixture/no-full-response.ts new file mode 100644 index 0000000..e6a8f6e --- /dev/null +++ b/src/fixture/no-full-response.ts @@ -0,0 +1,75 @@ +// fixture/no-full-response.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; +import { getTypeParentNode } from './ts-tree'; + +export const ruleId = 'no-full-response'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Remove the usage of FullResponse type.', + }, + messages: { + removeFullResponse: 'Remove the usage of FullResponse type.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + 'TSTypeReference[typeName.name="FullResponse"]': (typeReference: TSESTree.TSTypeReference) => { + try { + const typeParentNode = getTypeParentNode(typeReference); + assert.ok(typeParentNode); + if (typeParentNode.type === TSESTree.AST_NODE_TYPES.TSAsExpression) { + context.report({ + messageId: 'removeFullResponse', + node: typeReference, + fix(fixer) { + return fixer.replaceText(typeParentNode, sourceCode.getText(typeParentNode.expression)); + }, + }); + } else { + context.report({ + messageId: 'removeFullResponse', + node: typeReference, + fix(fixer) { + return fixer.remove(typeParentNode); + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: typeReference, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/fixture/ts-tree.ts b/src/fixture/ts-tree.ts new file mode 100644 index 0000000..da5eb91 --- /dev/null +++ b/src/fixture/ts-tree.ts @@ -0,0 +1,14 @@ +// fixture/ts-tree.ts + +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; + +export function getTypeParentNode( + node: TSESTree.Node | undefined, +): TSESTree.TSTypeAnnotation | TSESTree.TSAsExpression | undefined { + if (!node) { + return undefined; + } + return node.type === AST_NODE_TYPES.TSTypeAnnotation || node.type === AST_NODE_TYPES.TSAsExpression + ? node + : getTypeParentNode(node.parent); +} diff --git a/src/index.ts b/src/index.ts index e1f08ca..9e0225d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import fetchResponseHeaderGetterTs, { import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; +import noFullResponse, { ruleId as noFullResponseRuleId } from './fixture/no-full-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './fixture/no-service-wrapper'; import noStatusCode, { ruleId as noStatusCodeRuleId } from './fixture/no-status-code'; @@ -49,6 +50,7 @@ export default { [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, [fetchResponseHeaderGetterTsRuleId]: fetchResponseHeaderGetterTs, [addUrlDomainRuleId]: addUrlDomain, + [noFullResponseRuleId]: noFullResponse, }, configs: { all: { @@ -72,6 +74,7 @@ export default { [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', }, }, recommended: { From efccbb88afcdace2edd81e7ae73c9619e2acc5d5 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 7 Aug 2024 17:40:13 -0400 Subject: [PATCH 046/115] don't include fetch conversion rules in the "all" config --- src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9e0225d..677856c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,15 +66,15 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFullResponseRuleId}`]: 'error', + // [`@checkdigit/${noFixtureRuleId}`]: 'error', + // [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', + // [`@checkdigit/${fetchThenRuleId}`]: 'error', + // [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + // [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + // [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + // [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', + // [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + // [`@checkdigit/${noFullResponseRuleId}`]: 'error', }, }, recommended: { From b8274e57484dbc8228c6efd5d0bd7535c6c65212 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 7 Aug 2024 21:08:17 -0400 Subject: [PATCH 047/115] create "agent" config for grouping all agent conversion related rules --- src/index.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 677856c..bb71b25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,15 +66,6 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - // [`@checkdigit/${noFixtureRuleId}`]: 'error', - // [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', - // [`@checkdigit/${fetchThenRuleId}`]: 'error', - // [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - // [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - // [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - // [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', - // [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - // [`@checkdigit/${noFullResponseRuleId}`]: 'error', }, }, recommended: { @@ -93,4 +84,17 @@ export default { }, }, }, + agent: { + rules: { + [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + }, + }, }; From ec774ac195b7f52a85adea1e5fe8a108b665470d Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 7 Aug 2024 21:47:13 -0400 Subject: [PATCH 048/115] fix config --- src/index.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index bb71b25..afa462c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,18 +83,18 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', }, }, - }, - agent: { - rules: { - [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFullResponseRuleId}`]: 'error', + agent: { + rules: { + [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + }, }, }, }; From b86f505d4a0ae83310fa981fbb87a5156359cbb4 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 12 Aug 2024 14:39:59 -0400 Subject: [PATCH 049/115] refactoring --- .eslintrc | 1 + package-lock.json | 10 +- package.json | 3 +- src/{fixture => agent}/add-url-domain.spec.ts | 16 +- src/{fixture => agent}/add-url-domain.ts | 4 +- .../fetch-response-body-json.spec.ts | 0 .../fetch-response-body-json.ts | 0 .../fetch-response-header-getter.spec.ts | 186 +++++++++++ .../fetch-response-header-getter.ts} | 53 ++-- src/{fixture => agent}/fetch-then.spec.ts | 0 src/{fixture => agent}/fetch-then.ts | 6 +- src/{fixture => agent}/fetch.ts | 2 +- src/{fixture => agent}/no-fixture.spec.ts | 0 src/{fixture => agent}/no-fixture.ts | 6 +- .../no-full-response.spec.ts | 0 src/{fixture => agent}/no-full-response.ts | 2 +- .../no-service-wrapper.spec.ts | 0 src/{fixture => agent}/no-service-wrapper.ts | 4 +- src/{fixture => agent}/no-status-code.spec.ts | 0 src/{fixture => agent}/no-status-code.ts | 0 src/{fixture => agent}/response-reference.ts | 2 +- src/{fixture => agent}/url.ts | 0 src/fixture/fetch-header-getter.spec.ts | 95 ------ src/fixture/fetch-header-getter.ts | 91 ------ .../fetch-response-header-getter-ts.spec.ts | 121 ------- src/fixture/ts-tree.ts | 14 - src/index.ts | 27 +- src/{ast => library}/format.ts | 0 src/{ast => library}/tree.ts | 0 src/{ast => library}/ts-tree.ts | 11 + src/{fixture => library}/variable.ts | 0 src/ts-tester.test.ts | 19 ++ .../checkdigit/openapi-cli/v1/index.ts | 6 + .../checkdigit/openapi-cli/v1/interface.d.ts | 29 ++ .../checkdigit/openapi-cli/v1/swagger.ts | 295 ++++++++++++++++++ .../checkdigit/openapi-cli/v1/swagger.yml | 209 +++++++++++++ ts-init/services/checkdigit/ping/v1/index.ts | 6 + .../checkdigit/ping/v1/interface.d.ts | 28 ++ .../services/checkdigit/ping/v1/swagger.ts | 223 +++++++++++++ .../services/checkdigit/ping/v1/swagger.yml | 81 +++++ ts-init/services/checkdigit/ping/v2/index.ts | 6 + .../checkdigit/ping/v2/interface.d.ts | 28 ++ .../services/checkdigit/ping/v2/swagger.ts | 223 +++++++++++++ .../services/checkdigit/ping/v2/swagger.yml | 81 +++++ ts-init/services/index-fixture.ts | 67 ++++ ts-init/services/index.ts | 54 ++++ ts-init/services/openapiCli/index.ts | 15 + ts-init/services/openapiCli/v1/index.ts | 5 + ts-init/services/openapiCli/v1/interface.ts | 60 ++++ ts-init/services/openapiCli/v1/swagger.ts | 295 ++++++++++++++++++ ts-init/services/ping/index.ts | 12 + ts-init/services/ping/v1/index.ts | 6 + ts-init/services/ping/v1/interface.ts | 50 +++ ts-init/services/ping/v1/swagger.ts | 223 +++++++++++++ ts-init/services/ping/v1/swagger.yml | 81 +++++ ts-init/services/ping/v2/index.ts | 6 + ts-init/services/ping/v2/interface.ts | 50 +++ ts-init/services/ping/v2/swagger.ts | 223 +++++++++++++ ts-init/services/ping/v2/swagger.yml | 81 +++++ ts-init/services/typing-fixture.d.ts | 12 + ts-init/services/typing.d.ts | 12 + 61 files changed, 2757 insertions(+), 383 deletions(-) rename src/{fixture => agent}/add-url-domain.spec.ts (68%) rename src/{fixture => agent}/add-url-domain.ts (96%) rename src/{fixture => agent}/fetch-response-body-json.spec.ts (100%) rename src/{fixture => agent}/fetch-response-body-json.ts (100%) create mode 100644 src/agent/fetch-response-header-getter.spec.ts rename src/{fixture/fetch-response-header-getter-ts.ts => agent/fetch-response-header-getter.ts} (67%) rename src/{fixture => agent}/fetch-then.spec.ts (100%) rename src/{fixture => agent}/fetch-then.ts (98%) rename src/{fixture => agent}/fetch.ts (95%) rename src/{fixture => agent}/no-fixture.spec.ts (100%) rename src/{fixture => agent}/no-fixture.ts (99%) rename src/{fixture => agent}/no-full-response.spec.ts (100%) rename src/{fixture => agent}/no-full-response.ts (97%) rename src/{fixture => agent}/no-service-wrapper.spec.ts (100%) rename src/{fixture => agent}/no-service-wrapper.ts (98%) rename src/{fixture => agent}/no-status-code.spec.ts (100%) rename src/{fixture => agent}/no-status-code.ts (100%) rename src/{fixture => agent}/response-reference.ts (98%) rename src/{fixture => agent}/url.ts (100%) delete mode 100644 src/fixture/fetch-header-getter.spec.ts delete mode 100644 src/fixture/fetch-header-getter.ts delete mode 100644 src/fixture/fetch-response-header-getter-ts.spec.ts delete mode 100644 src/fixture/ts-tree.ts rename src/{ast => library}/format.ts (100%) rename src/{ast => library}/tree.ts (100%) rename src/{ast => library}/ts-tree.ts (88%) rename src/{fixture => library}/variable.ts (100%) create mode 100644 src/ts-tester.test.ts create mode 100644 ts-init/services/checkdigit/openapi-cli/v1/index.ts create mode 100644 ts-init/services/checkdigit/openapi-cli/v1/interface.d.ts create mode 100644 ts-init/services/checkdigit/openapi-cli/v1/swagger.ts create mode 100644 ts-init/services/checkdigit/openapi-cli/v1/swagger.yml create mode 100644 ts-init/services/checkdigit/ping/v1/index.ts create mode 100644 ts-init/services/checkdigit/ping/v1/interface.d.ts create mode 100644 ts-init/services/checkdigit/ping/v1/swagger.ts create mode 100644 ts-init/services/checkdigit/ping/v1/swagger.yml create mode 100644 ts-init/services/checkdigit/ping/v2/index.ts create mode 100644 ts-init/services/checkdigit/ping/v2/interface.d.ts create mode 100644 ts-init/services/checkdigit/ping/v2/swagger.ts create mode 100644 ts-init/services/checkdigit/ping/v2/swagger.yml create mode 100644 ts-init/services/index-fixture.ts create mode 100644 ts-init/services/index.ts create mode 100644 ts-init/services/openapiCli/index.ts create mode 100644 ts-init/services/openapiCli/v1/index.ts create mode 100644 ts-init/services/openapiCli/v1/interface.ts create mode 100644 ts-init/services/openapiCli/v1/swagger.ts create mode 100644 ts-init/services/ping/index.ts create mode 100644 ts-init/services/ping/v1/index.ts create mode 100644 ts-init/services/ping/v1/interface.ts create mode 100644 ts-init/services/ping/v1/swagger.ts create mode 100644 ts-init/services/ping/v1/swagger.yml create mode 100644 ts-init/services/ping/v2/index.ts create mode 100644 ts-init/services/ping/v2/interface.ts create mode 100644 ts-init/services/ping/v2/swagger.ts create mode 100644 ts-init/services/ping/v2/swagger.yml create mode 100644 ts-init/services/typing-fixture.d.ts create mode 100644 ts-init/services/typing.d.ts diff --git a/.eslintrc b/.eslintrc index 67b46bf..a537fee 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,6 +12,7 @@ "plugin:sonarjs/recommended", "prettier" ], + "ignorePatterns": ["ts-init/**/*.ts"], "rules": { "sort-keys": "off", "capitalized-comments": "off", diff --git a/package-lock.json b/package-lock.json index fcf2df3..8da711a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-no-secrets": "^1.0.2", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-sonarjs": "0.24.0" + "eslint-plugin-sonarjs": "0.24.0", + "http-status-codes": "^2.3.0" }, "engines": { "node": ">=20.14" @@ -4564,6 +4565,13 @@ "license": "MIT", "peer": true }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "dev": true, + "license": "MIT" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", diff --git a/package.json b/package.json index 152be94..35eabc1 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-no-secrets": "^1.0.2", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-sonarjs": "0.24.0" + "eslint-plugin-sonarjs": "0.24.0", + "http-status-codes": "^2.3.0" }, "peerDependencies": { "eslint": ">=8 <9" diff --git a/src/fixture/add-url-domain.spec.ts b/src/agent/add-url-domain.spec.ts similarity index 68% rename from src/fixture/add-url-domain.spec.ts rename to src/agent/add-url-domain.spec.ts index 84fd155..0b003a8 100644 --- a/src/fixture/add-url-domain.spec.ts +++ b/src/agent/add-url-domain.spec.ts @@ -7,32 +7,26 @@ */ import rule, { ruleId } from './add-url-domain'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import createTester from '../ts-tester.test'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); +const ruleTester = createTester(); ruleTester.run(ruleId, rule, { valid: [ { - name: 'no change if no "status" property is found in the response type', + name: 'no change if the url already has domain', code: `export const BASE_PATH = 'https://ping.checkdigit/ping/v1';`, }, ], invalid: [ { - name: 'add domain to url constant variable BASE_PATH', + name: 'add domain to url constant variable BASE_PATH as string', code: `export const BASE_PATH = '/ping/v1';`, output: `export const BASE_PATH = 'https://ping.checkdigit/ping/v1';`, errors: [{ messageId: 'addDomain' }], }, { - name: 'add domain to url constant variable BASE_PATH defined with template literal', + name: 'add domain to url constant variable BASE_PATH as template literal', code: `export const BASE_PATH = \`/ping/v1\`;`, output: `export const BASE_PATH = \`https://ping.checkdigit/ping/v1\`;`, errors: [{ messageId: 'addDomain' }], diff --git a/src/fixture/add-url-domain.ts b/src/agent/add-url-domain.ts similarity index 96% rename from src/fixture/add-url-domain.ts rename to src/agent/add-url-domain.ts index 22707f4..33111ee 100644 --- a/src/fixture/add-url-domain.ts +++ b/src/agent/add-url-domain.ts @@ -43,8 +43,8 @@ const rule = createRule({ return; } - const urlText = sourceCode.getText(basePathDeclarator.init); /*?*/ - const replacement = addBasePathUrlDomain(urlText); /*?*/ + const urlText = sourceCode.getText(basePathDeclarator.init); + const replacement = addBasePathUrlDomain(urlText); if (replacement !== urlText) { context.report({ diff --git a/src/fixture/fetch-response-body-json.spec.ts b/src/agent/fetch-response-body-json.spec.ts similarity index 100% rename from src/fixture/fetch-response-body-json.spec.ts rename to src/agent/fetch-response-body-json.spec.ts diff --git a/src/fixture/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts similarity index 100% rename from src/fixture/fetch-response-body-json.ts rename to src/agent/fetch-response-body-json.ts diff --git a/src/agent/fetch-response-header-getter.spec.ts b/src/agent/fetch-response-header-getter.spec.ts new file mode 100644 index 0000000..86a8a92 --- /dev/null +++ b/src/agent/fetch-response-header-getter.spec.ts @@ -0,0 +1,186 @@ +// fixture/fetch-response-header-getter-ts.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './fetch-response-header-getter'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'no change for fixture.api.get()', + code: ` + fixture.api.get('/ping'); + `, + }, + { + name: 'no change for non-response object', + code: ` + const map = new Map(); + map.get('key'); + + const headers = new Headers(); + headers.get('etag'); + `, + }, + { + name: 'no change for request.get()', + code: ` + const request : { headers: Headers } = await getRequest(); + request.get(ETAG); + `, + }, + { + name: 'no change of response.get() if the type of response does not include "headers" property', + code: ` + const response : Record = await getResponse(); + response.get(ETAG); + `, + }, + { + name: 'no change of request.get() if the variable name is "request"', + code: ` + type Context = { get: (string)=>string, headers: Record }; + async function doSomething(request: Context) { + const etagRequestHeader = request.get(ETAG); + } + `, + }, + { + name: 'no change of request.get() if the type the request is InboundContext', + code: ` + async function doSomething(req: InboundContext) { + const etagRequestHeader = req.get(ETAG); + } + `, + }, + { + name: 'no change of request.get() if the type the request is xxxRequestType', + code: ` + async function doSomething(fooReq: FooRequestType) { + const etagRequestHeader = fooReq.get(ETAG); + } + `, + }, + { + name: 'no change if get() method is already used - with non-typed fetch', + code: ` + const response = await fetch(\`https://example.org\`); + assert.equal(response.headers.get('etag'), '1'); + assert.equal(response.headers.get(ETAG), '1'); + `, + }, + ], + invalid: [ + { + name: 'use get() method to get header value from the headers object if the typing allows.', + code: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers[ETAG]; + `, + output: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers.get(ETAG); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'access using string literal', + code: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers['created-on']; + `, + output: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers.get('created-on'); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'access using Template literal', + code: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers[\`etag\`]; + `, + output: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + response.headers.get(\`etag\`); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'replace the direct headers property access with getter', + code: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + assert.equal(response.headers.etag, '1'); + `, + output: ` + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + assert.equal(response.headers.get('etag'), '1'); + `, + errors: [{ messageId: 'useGetter' }], + }, + { + name: 'still work with status assertion', + code: ` + import { strict as assert } from 'node:assert'; + import { StatusCodes } from 'http-status-codes'; + + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers['updated-on'], '1'); + assert.equal(response.headers.etag, '1'); + `, + output: ` + import { strict as assert } from 'node:assert'; + import { StatusCodes } from 'http-status-codes'; + + const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get('updated-on'), '1'); + assert.equal(response.headers.get('etag'), '1'); + `, + errors: [{ messageId: 'useGetter' }, { messageId: 'useGetter' }], + }, + { + name: 'work with non-typed fetch', + code: ` + const response = await fetch(\`https://example.org\`); + assert.equal(response.headers.etag, '1'); + assert.equal(response.headers['etag'], '1'); + assert.equal(response.headers[ETAG], '1'); + `, + output: ` + const response = await fetch(\`https://example.org\`); + assert.equal(response.headers.get('etag'), '1'); + assert.equal(response.headers.get('etag'), '1'); + assert.equal(response.headers.get(ETAG), '1'); + `, + errors: [{ messageId: 'useGetter' }, { messageId: 'useGetter' }, { messageId: 'useGetter' }], + }, + { + name: 'response.get() should be changed to response.headers.get()', + code: ` + const response : { headers: Headers } = await getResponse(); + response.get(ETAG); + `, + output: ` + const response : { headers: Headers } = await getResponse(); + response.headers.get(ETAG); + `, + errors: [{ messageId: 'useGetter' }], + }, + ], +}); diff --git a/src/fixture/fetch-response-header-getter-ts.ts b/src/agent/fetch-response-header-getter.ts similarity index 67% rename from src/fixture/fetch-response-header-getter-ts.ts rename to src/agent/fetch-response-header-getter.ts index 6308cd8..0641361 100644 --- a/src/fixture/fetch-response-header-getter-ts.ts +++ b/src/agent/fetch-response-header-getter.ts @@ -10,6 +10,7 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils' import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'fetch-response-header-getter-ts'; +const HEADER_BUILTIN_FUNCTIONS = Object.keys(Headers.prototype); const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); @@ -34,37 +35,38 @@ const rule = createRule({ const sourceCode = context.sourceCode; return { - 'MemberExpression[object.property.name="headers"]': (responseHeadersAccess: TSESTree.MemberExpression) => { + MemberExpression: (responseHeadersAccess: TSESTree.MemberExpression) => { try { if ( responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && - responseHeadersAccess.property.name === 'get' + HEADER_BUILTIN_FUNCTIONS.includes(responseHeadersAccess.property.name) ) { - // getter is already being used + // skip Headers's built-in function calls return; } const responseHeadersTsNode = parserServices.esTreeNodeToTSNodeMap.get(responseHeadersAccess.object); - const responseType = typeChecker.getTypeAtLocation(responseHeadersTsNode); - - const shouldReplace = responseType.getProperties().some((symbol) => symbol.name === 'get'); - if (!shouldReplace) { + let responseHeadersType = typeChecker.getTypeAtLocation(responseHeadersTsNode); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + responseHeadersType = responseHeadersType.isUnion() ? responseHeadersType.types[0]! : responseHeadersType; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const responseHeadersTypeName = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (responseHeadersType.symbol ?? responseHeadersType.aliasSymbol)?.escapedName; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (responseHeadersTypeName !== 'Headers' && responseHeadersTypeName !== 'HeaderGetter') { return; } - // let replacementText = 'xxx'; - // if (responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier) { - // replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; - // } let replacementText: string; - if (responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier) { - replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; - } else if (responseHeadersAccess.property.type === AST_NODE_TYPES.TemplateLiteral) { + if (!responseHeadersAccess.computed) { + // e.g. headers.etag + replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get('${sourceCode.getText(responseHeadersAccess.property)}')`; + } else if ( + responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier || + responseHeadersAccess.property.type === AST_NODE_TYPES.Literal || + responseHeadersAccess.property.type === AST_NODE_TYPES.TemplateLiteral + ) { replacementText = `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})`; - } else if (responseHeadersAccess.property.type === AST_NODE_TYPES.Literal) { - replacementText = responseHeadersAccess.computed - ? `${sourceCode.getText(responseHeadersAccess.object)}.get(${sourceCode.getText(responseHeadersAccess.property)})` - : `${sourceCode.getText(responseHeadersAccess.object)}.get('${sourceCode.getText(responseHeadersAccess.property)}')`; } else { throw new Error(`Unexpected property type: ${responseHeadersAccess.property.type}`); } @@ -89,14 +91,21 @@ const rule = createRule({ }); } }, - 'CallExpression[callee.property.name="get"]:not([callee.object.name="request"])': ( - responseHeadersAccess: TSESTree.CallExpression, - ) => { + + // convert response.get() to response.headers.get() + 'CallExpression[callee.property.name="get"]': (responseHeadersAccess: TSESTree.CallExpression) => { try { if (responseHeadersAccess.callee.type !== AST_NODE_TYPES.MemberExpression) { return; } + // skip request-like calls + if ( + responseHeadersAccess.callee.object.type !== AST_NODE_TYPES.Identifier || + responseHeadersAccess.callee.object.name === 'request' + ) { + return; + } const responseNode = responseHeadersAccess.callee.object; const responseHeadersTsNode = parserServices.esTreeNodeToTSNodeMap.get(responseNode); const responseType = typeChecker.getTypeAtLocation(responseHeadersTsNode); @@ -104,6 +113,8 @@ const rule = createRule({ if (typeName === 'InboundContext' || typeName.endsWith('RequestType')) { return; } + + // make sure the response type has "headers" property const hasHeadersProperty = responseType.getProperties().some((symbol) => symbol.name === 'headers'); if (!hasHeadersProperty) { return; diff --git a/src/fixture/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts similarity index 100% rename from src/fixture/fetch-then.spec.ts rename to src/agent/fetch-then.spec.ts diff --git a/src/fixture/fetch-then.ts b/src/agent/fetch-then.ts similarity index 98% rename from src/fixture/fetch-then.ts rename to src/agent/fetch-then.ts index 6ddab3f..2a445f3 100644 --- a/src/fixture/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -8,12 +8,12 @@ import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree'; import { type Rule, type Scope, SourceCode } from 'eslint'; -import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../ast/tree'; +import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/tree'; import { hasAssertions, isInvalidResponseHeadersAccess } from './fetch'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; -import { getIndentation } from '../ast/format'; -import { isValidPropertyName } from './variable'; +import { getIndentation } from '../library/format'; +import { isValidPropertyName } from '../library/variable'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'fetch-then'; diff --git a/src/fixture/fetch.ts b/src/agent/fetch.ts similarity index 95% rename from src/fixture/fetch.ts rename to src/agent/fetch.ts index 2cf3212..270cf09 100644 --- a/src/fixture/fetch.ts +++ b/src/agent/fetch.ts @@ -1,6 +1,6 @@ // fixture/fetch.ts -import { getParent, isBlockStatement } from '../ast/tree'; +import { getParent, isBlockStatement } from '../library/tree'; import type { Node } from 'estree'; export function getResponseBodyRetrievalText(responseVariableName: string) { diff --git a/src/fixture/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts similarity index 100% rename from src/fixture/no-fixture.spec.ts rename to src/agent/no-fixture.spec.ts diff --git a/src/fixture/no-fixture.ts b/src/agent/no-fixture.ts similarity index 99% rename from src/fixture/no-fixture.ts rename to src/agent/no-fixture.ts index 6204741..a43a746 100644 --- a/src/fixture/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -23,13 +23,13 @@ import { getEnclosingStatement, getParent, isUsedInArrayOrAsArgument, -} from '../ast/tree'; +} from '../library/tree'; import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; import { analyzeResponseReferences } from './response-reference'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; -import { getIndentation } from '../ast/format'; -import { isValidPropertyName } from './variable'; +import { getIndentation } from '../library/format'; +import { isValidPropertyName } from '../library/variable'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; diff --git a/src/fixture/no-full-response.spec.ts b/src/agent/no-full-response.spec.ts similarity index 100% rename from src/fixture/no-full-response.spec.ts rename to src/agent/no-full-response.spec.ts diff --git a/src/fixture/no-full-response.ts b/src/agent/no-full-response.ts similarity index 97% rename from src/fixture/no-full-response.ts rename to src/agent/no-full-response.ts index e6a8f6e..6bef609 100644 --- a/src/fixture/no-full-response.ts +++ b/src/agent/no-full-response.ts @@ -9,7 +9,7 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; -import { getTypeParentNode } from './ts-tree'; +import { getTypeParentNode } from '../library/ts-tree'; export const ruleId = 'no-full-response'; diff --git a/src/fixture/no-service-wrapper.spec.ts b/src/agent/no-service-wrapper.spec.ts similarity index 100% rename from src/fixture/no-service-wrapper.spec.ts rename to src/agent/no-service-wrapper.spec.ts diff --git a/src/fixture/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts similarity index 98% rename from src/fixture/no-service-wrapper.ts rename to src/agent/no-service-wrapper.ts index 3a3fb1b..d297aed 100644 --- a/src/fixture/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -11,8 +11,8 @@ import { DefinitionType, type Scope } from '@typescript-eslint/scope-manager'; import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP, replaceEndpointUrlPrefixWithDomain } from './url'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; -import { getEnclosingScopeNode } from '../ast/ts-tree'; -import { getIndentation } from '../ast/format'; +import { getEnclosingScopeNode } from '../library/ts-tree'; +import { getIndentation } from '../library/format'; export const ruleId = 'no-service-wrapper'; diff --git a/src/fixture/no-status-code.spec.ts b/src/agent/no-status-code.spec.ts similarity index 100% rename from src/fixture/no-status-code.spec.ts rename to src/agent/no-status-code.spec.ts diff --git a/src/fixture/no-status-code.ts b/src/agent/no-status-code.ts similarity index 100% rename from src/fixture/no-status-code.ts rename to src/agent/no-status-code.ts diff --git a/src/fixture/response-reference.ts b/src/agent/response-reference.ts similarity index 98% rename from src/fixture/response-reference.ts rename to src/agent/response-reference.ts index 8a03025..ebb41ae 100644 --- a/src/fixture/response-reference.ts +++ b/src/agent/response-reference.ts @@ -9,7 +9,7 @@ import type { MemberExpression, VariableDeclaration } from 'estree'; import { type Scope } from 'eslint'; import { strict as assert } from 'node:assert'; -import { getParent } from '../ast/tree'; +import { getParent } from '../library/tree'; /** * analyze response related variables and their references diff --git a/src/fixture/url.ts b/src/agent/url.ts similarity index 100% rename from src/fixture/url.ts rename to src/agent/url.ts diff --git a/src/fixture/fetch-header-getter.spec.ts b/src/fixture/fetch-header-getter.spec.ts deleted file mode 100644 index 4085b33..0000000 --- a/src/fixture/fetch-header-getter.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -// fixture/no-fixture.spec.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import rule, { ruleId } from './fetch-header-getter'; -import createTester from '../tester.test'; -import { describe } from '@jest/globals'; - -describe(ruleId, () => { - createTester().run(ruleId, rule, { - valid: [], - invalid: [ - { - name: 'replace the access of headers property using getter instead of direct access', - code: `async() => { - const createdOn = new Date().toISOString(); - const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { - method: 'PUT', - body: JSON.stringify({ - checkValue: 'foo', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - const headers1 = response.headers; - assert.equal(headers1.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers[ETAG_HEADER], '1'); - assert.equal(response.headers.etag, '1'); - assert.ok(verifyTemporalHeaders(response)); - - const updatedOn = new Date().toISOString(); - const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { - method: 'PUT', - body: JSON.stringify({ - checkValue: 'bar', - }), - headers: { - [IF_MATCH_HEADER]: headers1[ETAG_HEADER], - [CREATED_ON_HEADER]: updatedOn, - }, - }); - assert.equal(response2.status, StatusCodes.NO_CONTENT); - const headers2 = response2.headers; - assert.equal(headers2[ETAG_HEADER], '2'); - assert.ok(verifyTemporalHeaders(response2)); - }`, - output: `async() => { - const createdOn = new Date().toISOString(); - const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { - method: 'PUT', - body: JSON.stringify({ - checkValue: 'foo', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - const headers1 = response.headers; - assert.equal(headers1.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.equal(response.headers.get('etag'), '1'); - assert.ok(verifyTemporalHeaders(response)); - - const updatedOn = new Date().toISOString(); - const response2 = await fetch(\`\${BASE_PATH}/key/\${keyId}\`, { - method: 'PUT', - body: JSON.stringify({ - checkValue: 'bar', - }), - headers: { - [IF_MATCH_HEADER]: headers1.get(ETAG_HEADER), - [CREATED_ON_HEADER]: updatedOn, - }, - }); - assert.equal(response2.status, StatusCodes.NO_CONTENT); - const headers2 = response2.headers; - assert.equal(headers2.get(ETAG_HEADER), '2'); - assert.ok(verifyTemporalHeaders(response2)); - }`, - errors: 4, - }, - ], - }); -}); diff --git a/src/fixture/fetch-header-getter.ts b/src/fixture/fetch-header-getter.ts deleted file mode 100644 index accb0f2..0000000 --- a/src/fixture/fetch-header-getter.ts +++ /dev/null @@ -1,91 +0,0 @@ -// fixture/no-fixture.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import type { Identifier, VariableDeclarator } from 'estree'; -import type { Rule } from 'eslint'; -import { analyzeResponseReferences } from './response-reference'; -import { strict as assert } from 'node:assert'; -import getDocumentationUrl from '../get-documentation-url'; -import { getParent } from '../ast/tree'; -import { isInvalidResponseHeadersAccess } from './fetch'; - -export const ruleId = 'fetch-header-getter'; - -const rule: Rule.RuleModule = { - meta: { - type: 'problem', - docs: { - description: 'Make sure getter is used to access response headers.', - url: getDocumentationUrl(ruleId), - }, - messages: { - shouldUseHeaderGetter: 'Getter should be used to access response headers.', - }, - fixable: 'code', - schema: [], - }, - create(context) { - const sourceCode = context.sourceCode; - const scopeManager = sourceCode.scopeManager; - - return { - 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => { - const variableDeclaration = getParent(fetchCall); - assert.ok(variableDeclaration?.type === 'VariableDeclaration'); - const { variable: responseVariable, headersReferences: responseHeadersReferences } = analyzeResponseReferences( - variableDeclaration, - scopeManager, - ); - assert.ok(responseVariable); - - const directHeadersReferences = responseHeadersReferences.filter((headersReference) => { - const headersAccess = getParent(headersReference); - return headersAccess?.type !== 'VariableDeclarator'; - }); - - const indirectHeadersReferences = responseHeadersReferences - .map(getParent) - .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator') - .map((declarator) => (declarator.id as Identifier).name) - .map((redefinedHeadersVariableName) => { - const headersVariable = responseVariable.scope.variables.find((variable) => { - const identifier = variable.identifiers[0]; - return identifier?.type === 'Identifier' && identifier.name === redefinedHeadersVariableName; - }); - return headersVariable?.references.map((reference) => reference.identifier) ?? []; - }) - .flat(); - - const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].filter( - isInvalidResponseHeadersAccess, - ); - - invalidHeadersReferences.forEach((headersReference) => { - const headerAccess = getParent(headersReference); - if (headerAccess?.type === 'MemberExpression') { - const headerNameNode = headerAccess.property; - const headerName = headerAccess.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - const replacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; - - context.report({ - node: headerAccess, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(headerAccess, replacementText); - }, - }); - } - }); - }, - }; - }, -}; - -export default rule; diff --git a/src/fixture/fetch-response-header-getter-ts.spec.ts b/src/fixture/fetch-response-header-getter-ts.spec.ts deleted file mode 100644 index 82f8458..0000000 --- a/src/fixture/fetch-response-header-getter-ts.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -// fixture/fetch-response-header-getter-ts.spec.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import rule, { ruleId } from './fetch-response-header-getter-ts'; -import { RuleTester } from '@typescript-eslint/rule-tester'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { - valid: [ - { - name: 'no change if get() method is already used', - code: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers.get(ETAG); - `, - }, - { - name: 'no change of response.get() if the type of response does not include "headers" property', - code: ` - const response : Record = await getResponse(); - response.get(ETAG); - `, - }, - { - name: 'no change of request.get() if the type the request do not have "headers" property', - code: ` - type Context = { get: (string)=>string }; - async function doSomething(req: Context) { - const etagRequestHeader = req.get(ETAG); - } - `, - }, - { - name: 'no change of request.get() if the variable name is "request"', - code: ` - type Context = { get: (string)=>string, headers: Record }; - async function doSomething(request: Context) { - const etagRequestHeader = request.get(ETAG); - } - `, - }, - { - name: 'no change of request.get() if the type the request is InboundContext', - code: ` - async function doSomething(req: InboundContext) { - const etagRequestHeader = req.get(ETAG); - } - `, - }, - { - name: 'no change of request.get() if the type the request is xxxRequestType', - code: ` - async function doSomething(fooReq: FooRequestType) { - const etagRequestHeader = fooReq.get(ETAG); - } - `, - }, - ], - invalid: [ - { - name: 'use get() method to get header value from the headers object if the typing allows.', - code: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers[ETAG]; - `, - output: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers.get(ETAG); - `, - errors: [{ messageId: 'useGetter' }], - }, - { - name: 'access using string literal', - code: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers['created-on']; - `, - output: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers.get('created-on'); - `, - errors: [{ messageId: 'useGetter' }], - }, - { - name: 'access using Template literal', - code: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers[\`etag\`]; - `, - output: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers.get(\`etag\`); - `, - errors: [{ messageId: 'useGetter' }], - }, - { - name: 'response.get() should be changed to response.headers.get()', - code: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.get(ETAG); - `, - output: ` - const response : { headers: {get: (string)=>string} } = await getResponse(); - response.headers.get(ETAG); - `, - errors: [{ messageId: 'useGetter' }], - }, - ], -}); diff --git a/src/fixture/ts-tree.ts b/src/fixture/ts-tree.ts deleted file mode 100644 index da5eb91..0000000 --- a/src/fixture/ts-tree.ts +++ /dev/null @@ -1,14 +0,0 @@ -// fixture/ts-tree.ts - -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; - -export function getTypeParentNode( - node: TSESTree.Node | undefined, -): TSESTree.TSTypeAnnotation | TSESTree.TSAsExpression | undefined { - if (!node) { - return undefined; - } - return node.type === AST_NODE_TYPES.TSTypeAnnotation || node.type === AST_NODE_TYPES.TSAsExpression - ? node - : getTypeParentNode(node.parent); -} diff --git a/src/index.ts b/src/index.ts index afa462c..73283da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,19 +6,18 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import addUrlDomain, { ruleId as addUrlDomainRuleId } from './fixture/add-url-domain'; -import fetchHeaderGetter, { ruleId as fetchHeaderGetterRuleId } from './fixture/fetch-header-getter'; -import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './fixture/fetch-response-body-json'; -import fetchResponseHeaderGetterTs, { - ruleId as fetchResponseHeaderGetterTsRuleId, -} from './fixture/fetch-response-header-getter-ts'; -import fetchThen, { ruleId as fetchThenRuleId } from './fixture/fetch-then'; +import addUrlDomain, { ruleId as addUrlDomainRuleId } from './agent/add-url-domain'; +import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './agent/fetch-response-body-json'; +import fetchResponseHeaderGetter, { + ruleId as fetchResponseHeaderGetterRuleId, +} from './agent/fetch-response-header-getter'; +import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; -import noFixture, { ruleId as noFixtureRuleId } from './fixture/no-fixture'; -import noFullResponse, { ruleId as noFullResponseRuleId } from './fixture/no-full-response'; +import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; +import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; -import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './fixture/no-service-wrapper'; -import noStatusCode, { ruleId as noStatusCodeRuleId } from './fixture/no-status-code'; +import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; +import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -43,12 +42,11 @@ export default { [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, - [fetchHeaderGetterRuleId]: fetchHeaderGetter, [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, - [fetchResponseHeaderGetterTsRuleId]: fetchResponseHeaderGetterTs, + [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, [addUrlDomainRuleId]: addUrlDomain, [noFullResponseRuleId]: noFullResponse, }, @@ -86,12 +84,11 @@ export default { agent: { rules: { [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${fetchHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterTsRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', [`@checkdigit/${noFullResponseRuleId}`]: 'error', }, diff --git a/src/ast/format.ts b/src/library/format.ts similarity index 100% rename from src/ast/format.ts rename to src/library/format.ts diff --git a/src/ast/tree.ts b/src/library/tree.ts similarity index 100% rename from src/ast/tree.ts rename to src/library/tree.ts diff --git a/src/ast/ts-tree.ts b/src/library/ts-tree.ts similarity index 88% rename from src/ast/ts-tree.ts rename to src/library/ts-tree.ts index c5aa0d7..8c37fa8 100644 --- a/src/ast/ts-tree.ts +++ b/src/library/ts-tree.ts @@ -88,3 +88,14 @@ export function getEnclosingFunction(node: TSESTree.Node) { } return getEnclosingFunction(parent); } + +export function getTypeParentNode( + node: TSESTree.Node | undefined, +): TSESTree.TSTypeAnnotation | TSESTree.TSAsExpression | undefined { + if (!node) { + return undefined; + } + return node.type === AST_NODE_TYPES.TSTypeAnnotation || node.type === AST_NODE_TYPES.TSAsExpression + ? node + : getTypeParentNode(node.parent); +} diff --git a/src/fixture/variable.ts b/src/library/variable.ts similarity index 100% rename from src/fixture/variable.ts rename to src/library/variable.ts diff --git a/src/ts-tester.test.ts b/src/ts-tester.test.ts new file mode 100644 index 0000000..76d1018 --- /dev/null +++ b/src/ts-tester.test.ts @@ -0,0 +1,19 @@ +// agent/ts-tester.test.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { RuleTester } from '@typescript-eslint/rule-tester'; + +export default function createTester() { + return new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, + }); +} diff --git a/ts-init/services/checkdigit/openapi-cli/v1/index.ts b/ts-init/services/checkdigit/openapi-cli/v1/index.ts new file mode 100644 index 0000000..daa82c2 --- /dev/null +++ b/ts-init/services/checkdigit/openapi-cli/v1/index.ts @@ -0,0 +1,6 @@ +/* c8 ignore start */ +// services/checkdigit/openapi-cli/v1/index.ts + +export type * as openapiCliV1 from './swagger'; + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/openapi-cli/v1/interface.d.ts b/ts-init/services/checkdigit/openapi-cli/v1/interface.d.ts new file mode 100644 index 0000000..b17dd4b --- /dev/null +++ b/ts-init/services/checkdigit/openapi-cli/v1/interface.d.ts @@ -0,0 +1,29 @@ +/* c8 ignore start */ +// services/checkdigit/openapi-cli/v1/interface.d.ts + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +import type { MappedRequestBody, MappedRequestHeaders, MappedResponse, CaseInsensitive } from '../../../index'; +import type * as types from './swagger'; + +declare global { + function fetch( + url: `https://openapi-cli.checkdigit/sample/v1/tenant/${string}/rsa-key-pair/${string}`, + init: Omit & { + method: CaseInsensitive<'PUT'>; + } & MappedRequestHeaders & + MappedRequestBody, + ): Promise>; + + function fetch( + url: `https://openapi-cli.checkdigit/sample/v1/ping`, + init?: Omit & { + method?: CaseInsensitive<'GET'>; + } & MappedRequestHeaders<{} | undefined>, + ): Promise>; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/openapi-cli/v1/swagger.ts b/ts-init/services/checkdigit/openapi-cli/v1/swagger.ts new file mode 100644 index 0000000..fc2f0c0 --- /dev/null +++ b/ts-init/services/checkdigit/openapi-cli/v1/swagger.ts @@ -0,0 +1,295 @@ +/* c8 ignore start */ +// services/checkdigit/openapi-cli/v1/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +export interface Conflict {} + +/** + * Server error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code: + | 'INVALID_AT' + | 'INVALID_FROM' + | 'INVALID_TO' + | 'TO_LESS_THAN_FROM' + | 'HASH_MISMATCH' + | 'INVALID_SCHEMA' + | 'SCHEMA_VALIDATION_FAILURE' + | 'INVALID_JSON' + | 'INVALID_JSON_OBJECT' + | 'INVALID_ENCRYPTED_DATA' + | 'INVALID_KEY' + | 'KEY_MISMATCH' + | 'INVALID_PUBLIC_KEY_ID' + | 'INVALID_IF_MATCH' + | 'INVALID_CREATED_ON' + | 'INVALID_TENANT_ID' + | 'INVALID_ID'; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Server error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + headers: LowercaseKeys; + body: Ping; +} +export interface PingGetResponseOKHeaders { + 'Created-On': string; // date-time +} +/** + * Public key in PEM format + */ +export type PublicKey = string; +/** + * Request to generate an RSA key-pair + */ +export interface RSAKeyPairRequest { + transmissionKey: /* Public key in PEM format */ PublicKey; +} +/** + * Encrypted RSA key-pair + */ +export interface RSAKeyPairResponse { + /** + * AES-256-CBC secret key data encrypted with the request tranmissionKey, encoded using base-64 + */ + transmissionSecretKey: string; + publicKey: /* Public key in PEM format */ PublicKey; + /** + * Private key part of generated RSA key pair, encrypted with transmissionSecretKey and base-64 encoded + */ + encryptedPrivateKey: string; +} +export interface RsaKeyPairPutContext { + request: RsaKeyPairPutRequestType; + params: { + tenantId: string; + rsaKeyPairId: string; + }; + response: RsaKeyPairPutResponseContext; +} +export interface RsaKeyPairPutRequestContext { + request: RsaKeyPairPutRequestType; + params: { + tenantId: string; + rsaKeyPairId: string; + }; +} +export interface RsaKeyPairPutRequestHeaders { + 'Created-On'?: string; // date-time +} +export interface RsaKeyPairPutRequestType extends InboundContext { + headers?: LowercaseKeys; + body?: /* Request to generate an RSA key-pair */ RSAKeyPairRequest; +} +export interface RsaKeyPairPutResponseConflict { + status: 409; +} +export type RsaKeyPairPutResponseContext = + | RsaKeyPairPutResponseOK + | RsaKeyPairPutResponseConflict + | RsaKeyPairPutResponseDefault; +export interface RsaKeyPairPutResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Server error message */ Error; +} +export interface RsaKeyPairPutResponseOK { + status: 200; + headers: LowercaseKeys; + body: /* Encrypted RSA key-pair */ RSAKeyPairResponse; +} +export interface RsaKeyPairPutResponseOKHeaders { + 'Created-On': string; // date-time + 'Updated-On'?: string; // date-time +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/openapi-cli/v1/swagger.yml b/ts-init/services/checkdigit/openapi-cli/v1/swagger.yml new file mode 100644 index 0000000..66ca70c --- /dev/null +++ b/ts-init/services/checkdigit/openapi-cli/v1/swagger.yml @@ -0,0 +1,209 @@ +openapi: 3.0.0 +info: + title: Sample Service + description: Sample Service + version: 0.0.1 + contact: + name: Check Digit +tags: + - name: API + - name: Health Check + +servers: + - url: /sample/v1 + +x-firehose-logged: false + +paths: + /ping: + get: + description: Test service connectivity + operationId: ping-get + tags: + - Health Check + responses: + '200': + $ref: '#/components/responses/Ping' + default: + $ref: '#/components/responses/ServerError' + + /tenant/{tenantId}/rsa-key-pair/{rsaKeyPairId}: + put: + x-firehose-logged: true + description: Generate a RSA 2048 public/private key pair. This operation will always return the same key pair for the given rsaKeyPairId. The passphrase for privateKey is set to the rsaKeyPairId. + operationId: rsa-key-pair-put + tags: + - API + parameters: + - $ref: '#/components/parameters/tenantId' + - $ref: '#/components/parameters/rsaKeyPairId' + - $ref: '#/components/parameters/createdOn' + requestBody: + $ref: '#/components/requestBodies/RSAKeyPairRequest' + responses: + '200': + $ref: '#/components/responses/RSAKeyPairResponse' + '409': + $ref: '#/components/responses/Conflict' + default: + $ref: '#/components/responses/ServerError' + +components: + schemas: + Ping: + type: object + additionalProperties: false + required: + - serverTime + properties: + serverTime: + type: string + format: date-time + description: Current server time + + Error: + type: object + additionalProperties: false + description: Server error message + required: + - code + properties: + message: + type: string + code: + type: string + enum: + - INVALID_AT + - INVALID_FROM + - INVALID_TO + - TO_LESS_THAN_FROM + - HASH_MISMATCH + - INVALID_SCHEMA + - SCHEMA_VALIDATION_FAILURE + - INVALID_JSON + - INVALID_JSON_OBJECT + - INVALID_ENCRYPTED_DATA + - INVALID_KEY + - KEY_MISMATCH + - INVALID_PUBLIC_KEY_ID + - INVALID_IF_MATCH + - INVALID_CREATED_ON + - INVALID_TENANT_ID + - INVALID_ID + + PublicKey: + description: Public key in PEM format + type: string + + RSAKeyPairRequest: + type: object + additionalProperties: false + description: Request to generate an RSA key-pair + required: + - transmissionKey + properties: + transmissionKey: + $ref: '#/components/schemas/PublicKey' + + RSAKeyPairResponse: + type: object + additionalProperties: false + description: Encrypted RSA key-pair + required: + - publicKey + - encryptedPrivateKey + - transmissionSecretKey + properties: + transmissionSecretKey: + description: AES-256-CBC secret key data encrypted with the request tranmissionKey, encoded using base-64 + type: string + publicKey: + $ref: '#/components/schemas/PublicKey' + encryptedPrivateKey: + description: Private key part of generated RSA key pair, encrypted with transmissionSecretKey and base-64 encoded + type: string + + parameters: + tenantId: + in: path + name: tenantId + description: Specifies tenant of Sentinel service + required: true + schema: + type: string + + rsaKeyPairId: + in: path + name: rsaKeyPairId + description: Parameter used to represent a generated RSA key pair + required: true + schema: + type: string + + createdOn: + name: Created-On + in: header + description: Created-On header + required: false + schema: + type: string + format: date-time + + requestBodies: + RSAKeyPairRequest: + description: Request to generate an RSA key-pair + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RSAKeyPairRequest' + + responses: + RSAKeyPairResponse: + description: Encrypted RSA key-pair + content: + application/json: + schema: + $ref: '#/components/schemas/RSAKeyPairResponse' + headers: + Created-On: + required: true + $ref: '#/components/headers/Created-On' + Updated-On: + required: false + $ref: '#/components/headers/Updated-On' + + Ping: + description: ping successful response + headers: + Created-On: + $ref: '#/components/headers/Created-On' + content: + application/json: + schema: + $ref: '#/components/schemas/Ping' + + Conflict: + description: Conflict + + ServerError: + description: Server Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + headers: + Created-On: + description: CreatedOn timestamp for the newly created record + required: true + schema: + type: string + format: date-time + + Updated-On: + description: UpdatedOn timestamp for the existing record + required: false + schema: + type: string + format: date-time diff --git a/ts-init/services/checkdigit/ping/v1/index.ts b/ts-init/services/checkdigit/ping/v1/index.ts new file mode 100644 index 0000000..417331a --- /dev/null +++ b/ts-init/services/checkdigit/ping/v1/index.ts @@ -0,0 +1,6 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v1/index.ts + +export type * as pingV1 from './swagger'; + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v1/interface.d.ts b/ts-init/services/checkdigit/ping/v1/interface.d.ts new file mode 100644 index 0000000..f90374f --- /dev/null +++ b/ts-init/services/checkdigit/ping/v1/interface.d.ts @@ -0,0 +1,28 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v1/interface.d.ts + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +import type { MappedRequestBody, MappedRequestHeaders, MappedResponse, CaseInsensitive } from '../../../index'; +import type * as types from './swagger'; + +declare global { + function fetch( + url: `https://ping.checkdigit/ping/v1/ping`, + init?: Omit & { + method?: CaseInsensitive<'GET'>; + } & MappedRequestHeaders<{} | undefined>, + ): Promise>; + + function fetch( + url: `https://ping.checkdigit/ping/v1/ping`, + init: Omit & { + method: CaseInsensitive<'HEAD'>; + } & MappedRequestHeaders<{} | undefined>, + ): Promise>; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v1/swagger.ts b/ts-init/services/checkdigit/ping/v1/swagger.ts new file mode 100644 index 0000000..2a18d65 --- /dev/null +++ b/ts-init/services/checkdigit/ping/v1/swagger.ts @@ -0,0 +1,223 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v1/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +/** + * Error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code?: string; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + body: Ping; +} +export interface PingHeadContext { + request: PingHeadRequestType; + response: PingHeadResponseContext; +} +export interface PingHeadRequestContext { + request: PingHeadRequestType; +} +export interface PingHeadRequestType extends InboundContext {} +export type PingHeadResponseContext = PingHeadResponseOK | PingHeadResponseDefault; +export interface PingHeadResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingHeadResponseOK { + status: 200; +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v1/swagger.yml b/ts-init/services/checkdigit/ping/v1/swagger.yml new file mode 100644 index 0000000..07b199b --- /dev/null +++ b/ts-init/services/checkdigit/ping/v1/swagger.yml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + title: Ping Service + description: | + A service that accepts incoming pings. This is only used for testing. + + © Check Digit LLC. 2019-2024 + version: 1.0.0 + contact: + name: Check Digit +servers: + - url: /ping/v1 + +tags: + - name: Service Health + +x-firehoseLogged: false + +paths: + /ping: + get: + description: Test service connectivity + tags: + - Service Health + operationId: ping-get + responses: + '200': + $ref: '#/components/responses/Ping' + default: + $ref: '#/components/responses/ServerError' + head: + description: Test service connectivity + tags: + - Service Health + operationId: ping-head + responses: + '200': + description: ping HEAD successful + content: + application/json: + schema: {} + default: + $ref: '#/components/responses/ServerError' + +components: + schemas: + Ping: + type: object + additionalProperties: false + required: + - serverTime + properties: + serverTime: + type: string + format: date-time + description: Current server time + + Error: + type: object + additionalProperties: false + description: Error message + properties: + message: + type: string + code: + type: string + + responses: + ServerError: + description: Server Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Ping: + description: ping successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Ping' diff --git a/ts-init/services/checkdigit/ping/v2/index.ts b/ts-init/services/checkdigit/ping/v2/index.ts new file mode 100644 index 0000000..0216e3a --- /dev/null +++ b/ts-init/services/checkdigit/ping/v2/index.ts @@ -0,0 +1,6 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v2/index.ts + +export type * as pingV2 from './swagger'; + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v2/interface.d.ts b/ts-init/services/checkdigit/ping/v2/interface.d.ts new file mode 100644 index 0000000..8a84cc4 --- /dev/null +++ b/ts-init/services/checkdigit/ping/v2/interface.d.ts @@ -0,0 +1,28 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v2/interface.d.ts + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +import type { MappedRequestBody, MappedRequestHeaders, MappedResponse, CaseInsensitive } from '../../../index'; +import type * as types from './swagger'; + +declare global { + function fetch( + url: `https://ping.checkdigit/ping/v2/ping`, + init?: Omit & { + method?: CaseInsensitive<'GET'>; + } & MappedRequestHeaders<{} | undefined>, + ): Promise>; + + function fetch( + url: `https://ping.checkdigit/ping/v2/ping`, + init: Omit & { + method: CaseInsensitive<'HEAD'>; + } & MappedRequestHeaders<{} | undefined>, + ): Promise>; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v2/swagger.ts b/ts-init/services/checkdigit/ping/v2/swagger.ts new file mode 100644 index 0000000..c70ece6 --- /dev/null +++ b/ts-init/services/checkdigit/ping/v2/swagger.ts @@ -0,0 +1,223 @@ +/* c8 ignore start */ +// services/checkdigit/ping/v2/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +/** + * Error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code?: string; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + body: Ping; +} +export interface PingHeadContext { + request: PingHeadRequestType; + response: PingHeadResponseContext; +} +export interface PingHeadRequestContext { + request: PingHeadRequestType; +} +export interface PingHeadRequestType extends InboundContext {} +export type PingHeadResponseContext = PingHeadResponseOK | PingHeadResponseDefault; +export interface PingHeadResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingHeadResponseOK { + status: 200; +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/checkdigit/ping/v2/swagger.yml b/ts-init/services/checkdigit/ping/v2/swagger.yml new file mode 100644 index 0000000..c9b6a5c --- /dev/null +++ b/ts-init/services/checkdigit/ping/v2/swagger.yml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + title: Ping Service + description: | + A service that accepts incoming pings. This is only used for testing. + + © Check Digit LLC. 2019-2024 + version: 2.0.6 + contact: + name: Check Digit +servers: + - url: /ping/v2 + +tags: + - name: Service Health + +x-firehose-logged: false + +paths: + /ping: + get: + description: Test service connectivity + tags: + - Service Health + operationId: ping-get + responses: + '200': + $ref: '#/components/responses/Ping' + default: + $ref: '#/components/responses/ServerError' + head: + description: Test service connectivity + tags: + - Service Health + operationId: ping-head + responses: + '200': + description: ping HEAD successful + content: + application/json: + schema: {} + default: + $ref: '#/components/responses/ServerError' + +components: + schemas: + Ping: + type: object + additionalProperties: false + required: + - serverTime + properties: + serverTime: + type: string + format: date-time + description: Current server time + + Error: + type: object + additionalProperties: false + description: Error message + properties: + message: + type: string + code: + type: string + + responses: + ServerError: + description: Server Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Ping: + description: ping successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Ping' diff --git a/ts-init/services/index-fixture.ts b/ts-init/services/index-fixture.ts new file mode 100644 index 0000000..1f8e9a2 --- /dev/null +++ b/ts-init/services/index-fixture.ts @@ -0,0 +1,67 @@ +/* c8 ignore start */ +// services/index-fixture.ts + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +import type { pingApi } from './ping'; +import type { openapiCliApi } from './openapiCli'; + +export interface InboundContext { + get: (field: string) => string; +} +type FunctionFromRecord = (parameter: K) => T[K]; + +type MappedResponseHeaders = undefined extends HeadersType + ? { + header: NonNullable; + headers: NonNullable; + get: FunctionFromRecord>; + } + : { + header: HeadersType; + headers: HeadersType; + get: FunctionFromRecord; + }; + +type MappedResponseBody = undefined extends BodyType + ? { + body: NonNullable; + } + : { + body: BodyType; + }; + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +/** + * Adding backward compatibility using mapped type + * The existing codes might access certain properties available in Koa's Response which may be unavailable in ResponseContext (e.g. 'statusCode' vs. 'status') + * The following mapped type create alias between the Koa's Response and ResponseContext. + * More importantly it maintain both 'status' and 'statusCode' as discriminators to differentiate each corresponding ResponseContexts in the union-ed ResponseContext at the api operation level + */ +export type MappedResponse = ResponseContextUnion extends infer ResponseContext + ? ResponseContext extends ApiResponseContext + ? { + status: ResponseContext['status']; + statusCode: ResponseContext['status']; + } & MappedResponseHeaders & + MappedResponseBody + : never + : never; + +export interface TypedServices { + ping: (context: InboundContext) => pingApi; + _main: openapiCliApi; +} + +export type * from './ping'; +export type * from './openapiCli'; + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/index.ts b/ts-init/services/index.ts new file mode 100644 index 0000000..0c49b91 --- /dev/null +++ b/ts-init/services/index.ts @@ -0,0 +1,54 @@ +/* c8 ignore start */ +// services/index.ts + +export type Stringified = string & { _: T }; + +interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +type ExtensionHeaders = Record<`x-${string}`, string> & { + 'content-type'?: 'application/json'; +}; + +export type MappedRequestHeaders = undefined extends HeadersType + ? { + headers?: ExtensionHeaders & NonNullable; + } + : { + headers: ExtensionHeaders & HeadersType; + }; + +export type MappedRequestBody = undefined extends BodyType + ? { + body?: Stringified>; + } + : { + body: Stringified; + }; + +type HeaderGetter> = Omit & { + get(headerName: HeaderName): HeadersType[HeaderName]; +}; + +export type MappedResponse = ResponseContextUnion extends infer ResponseContext + ? ResponseContext extends ApiResponseContext + ? Omit & { + readonly status: ResponseContext['status']; + readonly headers: HeaderGetter>; + json(): Promise; + } + : never + : never; + +export type CaseInsensitive = T extends `${infer First}${infer Rest}` + ? `${Uppercase}${CaseInsensitive}` | `${Lowercase}${CaseInsensitive}` + : ''; + +export type * from './checkdigit/ping/v2'; +export type * from './checkdigit/ping/v1'; +export type * from './checkdigit/openapi-cli/v1'; + +/* c8 ignore stop */ diff --git a/ts-init/services/openapiCli/index.ts b/ts-init/services/openapiCli/index.ts new file mode 100644 index 0000000..f4e17b8 --- /dev/null +++ b/ts-init/services/openapiCli/index.ts @@ -0,0 +1,15 @@ +/* c8 ignore start */ +// services/openapiCli/index.ts + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +import type * as v1 from './v1/index'; + +export type openapiCliApi = v1.Api; + +export type * as openapiCliV1 from './v1/index'; + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/openapiCli/v1/index.ts b/ts-init/services/openapiCli/v1/index.ts new file mode 100644 index 0000000..64d7754 --- /dev/null +++ b/ts-init/services/openapiCli/v1/index.ts @@ -0,0 +1,5 @@ +/* c8 ignore start */ +// services/openapiCli/v1/index.ts +export type * from './interface'; + +/* c8 ignore stop */ diff --git a/ts-init/services/openapiCli/v1/interface.ts b/ts-init/services/openapiCli/v1/interface.ts new file mode 100644 index 0000000..9dca985 --- /dev/null +++ b/ts-init/services/openapiCli/v1/interface.ts @@ -0,0 +1,60 @@ +/* c8 ignore start */ +// services/openapiCli/v1/interface.ts + +import type * as types from './swagger'; +import type { MappedResponse } from '../../index-fixture.ts'; + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface RsaKeyPairPutTest + extends Promise> { + send(body: types.RsaKeyPairPutRequestType['body']): this; + set(headerName: keyof NonNullable, value: string): this; + set(headers: Partial): this; + + expect( + status: Status, + ): Status extends types.RsaKeyPairPutResponseOK['status'] + ? RsaKeyPairPutTest + : Status extends types.RsaKeyPairPutResponseConflict['status'] + ? RsaKeyPairPutTest + : Status extends types.RsaKeyPairPutResponseDefault['status'] + ? RsaKeyPairPutTest + : never; + expect(checker: (res: Awaited) => any): this; + expect( + headerName: ResponseContext extends { headers: unknown } ? keyof ResponseContext['headers'] : never, + headerValue: string | RegExp, + ): this; + expect(body: string | RegExp | Object): this; +} +export interface PingGetTest + extends Promise> { + set(headerName: string, value: string): this; + set(headers: Record): this; + + expect( + status: Status, + ): Status extends types.PingGetResponseOK['status'] + ? PingGetTest + : Status extends types.PingGetResponseDefault['status'] + ? PingGetTest + : never; + expect(checker: (res: Awaited) => any): this; + expect( + headerName: ResponseContext extends { headers: unknown } ? keyof ResponseContext['headers'] : never, + headerValue: string | RegExp, + ): this; + expect(body: string | RegExp | Object): this; +} + +export interface Api { + put(url: `/sample/v1/tenant/${string}/rsa-key-pair/${string}`): RsaKeyPairPutTest; + + get(url: `/sample/v1/ping`): PingGetTest; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/openapiCli/v1/swagger.ts b/ts-init/services/openapiCli/v1/swagger.ts new file mode 100644 index 0000000..e20b220 --- /dev/null +++ b/ts-init/services/openapiCli/v1/swagger.ts @@ -0,0 +1,295 @@ +/* c8 ignore start */ +// api/v1/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +export interface Conflict {} + +/** + * Server error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code: + | 'INVALID_AT' + | 'INVALID_FROM' + | 'INVALID_TO' + | 'TO_LESS_THAN_FROM' + | 'HASH_MISMATCH' + | 'INVALID_SCHEMA' + | 'SCHEMA_VALIDATION_FAILURE' + | 'INVALID_JSON' + | 'INVALID_JSON_OBJECT' + | 'INVALID_ENCRYPTED_DATA' + | 'INVALID_KEY' + | 'KEY_MISMATCH' + | 'INVALID_PUBLIC_KEY_ID' + | 'INVALID_IF_MATCH' + | 'INVALID_CREATED_ON' + | 'INVALID_TENANT_ID' + | 'INVALID_ID'; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Server error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + headers: LowercaseKeys; + body: Ping; +} +export interface PingGetResponseOKHeaders { + 'Created-On': string; // date-time +} +/** + * Public key in PEM format + */ +export type PublicKey = string; +/** + * Request to generate an RSA key-pair + */ +export interface RSAKeyPairRequest { + transmissionKey: /* Public key in PEM format */ PublicKey; +} +/** + * Encrypted RSA key-pair + */ +export interface RSAKeyPairResponse { + /** + * AES-256-CBC secret key data encrypted with the request tranmissionKey, encoded using base-64 + */ + transmissionSecretKey: string; + publicKey: /* Public key in PEM format */ PublicKey; + /** + * Private key part of generated RSA key pair, encrypted with transmissionSecretKey and base-64 encoded + */ + encryptedPrivateKey: string; +} +export interface RsaKeyPairPutContext { + request: RsaKeyPairPutRequestType; + params: { + tenantId: string; + rsaKeyPairId: string; + }; + response: RsaKeyPairPutResponseContext; +} +export interface RsaKeyPairPutRequestContext { + request: RsaKeyPairPutRequestType; + params: { + tenantId: string; + rsaKeyPairId: string; + }; +} +export interface RsaKeyPairPutRequestHeaders { + 'Created-On'?: string; // date-time +} +export interface RsaKeyPairPutRequestType extends InboundContext { + headers?: LowercaseKeys; + body?: /* Request to generate an RSA key-pair */ RSAKeyPairRequest; +} +export interface RsaKeyPairPutResponseConflict { + status: 409; +} +export type RsaKeyPairPutResponseContext = + | RsaKeyPairPutResponseOK + | RsaKeyPairPutResponseConflict + | RsaKeyPairPutResponseDefault; +export interface RsaKeyPairPutResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Server error message */ Error; +} +export interface RsaKeyPairPutResponseOK { + status: 200; + headers: LowercaseKeys; + body: /* Encrypted RSA key-pair */ RSAKeyPairResponse; +} +export interface RsaKeyPairPutResponseOKHeaders { + 'Created-On': string; // date-time + 'Updated-On'?: string; // date-time +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/ping/index.ts b/ts-init/services/ping/index.ts new file mode 100644 index 0000000..fe615ac --- /dev/null +++ b/ts-init/services/ping/index.ts @@ -0,0 +1,12 @@ +/* c8 ignore start */ +// services/ping/index.ts + +import type * as v1 from './v1'; +import type * as v2 from './v2'; + +export type pingApi = v1.Api & v2.Api; + +export type * as pingV1 from './v1'; +export type * as pingV2 from './v2'; + +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v1/index.ts b/ts-init/services/ping/v1/index.ts new file mode 100644 index 0000000..aae2a92 --- /dev/null +++ b/ts-init/services/ping/v1/index.ts @@ -0,0 +1,6 @@ +/* c8 ignore start */ +// services/ping/v1/index.ts +export type * from './swagger'; +export type { Api } from './interface'; + +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v1/interface.ts b/ts-init/services/ping/v1/interface.ts new file mode 100644 index 0000000..495c25f --- /dev/null +++ b/ts-init/services/ping/v1/interface.ts @@ -0,0 +1,50 @@ +/* c8 ignore start */ +// services/ping/v1/interface.ts + +import type { MappedResponse } from '../../index-fixture.ts'; +import type * as types from './swagger'; + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface Api { + get( + url: `/ping/v1/ping`, + + options?: { + resolveWithFullResponse: false; + headers?: Record; + }, + ): Promise; + + get( + url: `/ping/v1/ping`, + + options?: { + resolveWithFullResponse: true; + headers?: Record; + }, + ): Promise>; + + head( + url: `/ping/v1/ping`, + + options?: { + resolveWithFullResponse: false; + headers?: Record; + }, + ): Promise; + + head( + url: `/ping/v1/ping`, + + options?: { + resolveWithFullResponse: true; + headers?: Record; + }, + ): Promise>; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v1/swagger.ts b/ts-init/services/ping/v1/swagger.ts new file mode 100644 index 0000000..f9cdb2d --- /dev/null +++ b/ts-init/services/ping/v1/swagger.ts @@ -0,0 +1,223 @@ +/* c8 ignore start */ +// services/ping/v1/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +/** + * Error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code?: string; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + body: Ping; +} +export interface PingHeadContext { + request: PingHeadRequestType; + response: PingHeadResponseContext; +} +export interface PingHeadRequestContext { + request: PingHeadRequestType; +} +export interface PingHeadRequestType extends InboundContext {} +export type PingHeadResponseContext = PingHeadResponseOK | PingHeadResponseDefault; +export interface PingHeadResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingHeadResponseOK { + status: 200; +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v1/swagger.yml b/ts-init/services/ping/v1/swagger.yml new file mode 100644 index 0000000..07b199b --- /dev/null +++ b/ts-init/services/ping/v1/swagger.yml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + title: Ping Service + description: | + A service that accepts incoming pings. This is only used for testing. + + © Check Digit LLC. 2019-2024 + version: 1.0.0 + contact: + name: Check Digit +servers: + - url: /ping/v1 + +tags: + - name: Service Health + +x-firehoseLogged: false + +paths: + /ping: + get: + description: Test service connectivity + tags: + - Service Health + operationId: ping-get + responses: + '200': + $ref: '#/components/responses/Ping' + default: + $ref: '#/components/responses/ServerError' + head: + description: Test service connectivity + tags: + - Service Health + operationId: ping-head + responses: + '200': + description: ping HEAD successful + content: + application/json: + schema: {} + default: + $ref: '#/components/responses/ServerError' + +components: + schemas: + Ping: + type: object + additionalProperties: false + required: + - serverTime + properties: + serverTime: + type: string + format: date-time + description: Current server time + + Error: + type: object + additionalProperties: false + description: Error message + properties: + message: + type: string + code: + type: string + + responses: + ServerError: + description: Server Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Ping: + description: ping successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Ping' diff --git a/ts-init/services/ping/v2/index.ts b/ts-init/services/ping/v2/index.ts new file mode 100644 index 0000000..830bde6 --- /dev/null +++ b/ts-init/services/ping/v2/index.ts @@ -0,0 +1,6 @@ +/* c8 ignore start */ +// services/ping/v2/index.ts +export type * from './swagger'; +export type { Api } from './interface'; + +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v2/interface.ts b/ts-init/services/ping/v2/interface.ts new file mode 100644 index 0000000..3cfc200 --- /dev/null +++ b/ts-init/services/ping/v2/interface.ts @@ -0,0 +1,50 @@ +/* c8 ignore start */ +// services/ping/v2/interface.ts + +import type { MappedResponse } from '../../index-fixture.ts'; +import type * as types from './swagger'; + +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface Api { + get( + url: `/ping/v2/ping`, + + options?: { + resolveWithFullResponse: false; + headers?: Record; + }, + ): Promise; + + get( + url: `/ping/v2/ping`, + + options?: { + resolveWithFullResponse: true; + headers?: Record; + }, + ): Promise>; + + head( + url: `/ping/v2/ping`, + + options?: { + resolveWithFullResponse: false; + headers?: Record; + }, + ): Promise; + + head( + url: `/ping/v2/ping`, + + options?: { + resolveWithFullResponse: true; + headers?: Record; + }, + ): Promise>; +} + +/* eslint-enable */ + +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v2/swagger.ts b/ts-init/services/ping/v2/swagger.ts new file mode 100644 index 0000000..f7e6ac0 --- /dev/null +++ b/ts-init/services/ping/v2/swagger.ts @@ -0,0 +1,223 @@ +/* c8 ignore start */ +// services/ping/v2/swagger.ts +/* eslint-disable eslint-comments/no-unlimited-disable */ +/* eslint-disable */ + +export interface InboundContext { + get(key: string): string; +} + +export interface ApiResponseContext { + status: number; + body?: unknown; + headers?: Record; +} + +export interface KoaResponseContext { + status: number; + body?: unknown; + headers?: Record; + set(key: string, value: string): void; +} + +export type LowercaseKeys = { + [K in keyof T as Lowercase]: T[K]; +}; + +type NotErrorInstanceLike = { stack?: never; cause?: never }; + +/** + * Error message + */ +export interface Error extends NotErrorInstanceLike { + message?: string; + code?: string; +} +export interface Ping { + /** + * Current server time + */ + serverTime: string; // date-time +} +export interface PingGetContext { + request: PingGetRequestType; + response: PingGetResponseContext; +} +export interface PingGetRequestContext { + request: PingGetRequestType; +} +export interface PingGetRequestType extends InboundContext {} +export type PingGetResponseContext = PingGetResponseOK | PingGetResponseDefault; +export interface PingGetResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingGetResponseOK { + status: 200; + body: Ping; +} +export interface PingHeadContext { + request: PingHeadRequestType; + response: PingHeadResponseContext; +} +export interface PingHeadRequestContext { + request: PingHeadRequestType; +} +export interface PingHeadRequestType extends InboundContext {} +export type PingHeadResponseContext = PingHeadResponseOK | PingHeadResponseDefault; +export interface PingHeadResponseDefault { + status: + | 100 + | 101 + | 102 + | 103 + | 201 + | 202 + | 203 + | 204 + | 205 + | 206 + | 207 + | 300 + | 301 + | 302 + | 303 + | 304 + | 305 + | 307 + | 308 + | 400 + | 401 + | 402 + | 403 + | 404 + | 405 + | 406 + | 407 + | 408 + | 409 + | 410 + | 411 + | 412 + | 413 + | 414 + | 415 + | 416 + | 417 + | 418 + | 419 + | 420 + | 421 + | 422 + | 423 + | 424 + | 426 + | 428 + | 429 + | 431 + | 451 + | 500 + | 501 + | 502 + | 503 + | 504 + | 505 + | 507 + | 511; + body: /* Error message */ Error; +} +export interface PingHeadResponseOK { + status: 200; +} + +export function setResponse( + response: Response, + responseContext: ResponseContext, +): void { + const koaResponse = response as unknown as KoaResponseContext; + koaResponse.status = responseContext.status; + if (responseContext.body) { + if (responseContext.body instanceof globalThis.Error) { + console.warn( + `Error instance or complex object with non-enumerable properties should not be used as response body directly because it'll cause firehose data inconsistency. Please use plain object literal instead.`, + ); + + const error = responseContext.body as unknown as Error; + const errorProperties: (keyof Error)[] = ['message', 'code']; + const convertedError = Object.fromEntries( + errorProperties + .map((key) => (error[key] === undefined ? undefined : [key, error[key]])) + .filter((entry) => entry !== undefined) as [string, unknown][], + ); + console.warn('The provided Error instance has been converted as a plain object:', convertedError); + koaResponse.body = convertedError; + } else { + koaResponse.body = responseContext.body; + } + } + if (responseContext.headers) { + for (const [key, value] of Object.entries(responseContext.headers)) { + koaResponse.set(key, value); + } + } +} + +/* eslint-enable */ +/* c8 ignore stop */ diff --git a/ts-init/services/ping/v2/swagger.yml b/ts-init/services/ping/v2/swagger.yml new file mode 100644 index 0000000..c9b6a5c --- /dev/null +++ b/ts-init/services/ping/v2/swagger.yml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + title: Ping Service + description: | + A service that accepts incoming pings. This is only used for testing. + + © Check Digit LLC. 2019-2024 + version: 2.0.6 + contact: + name: Check Digit +servers: + - url: /ping/v2 + +tags: + - name: Service Health + +x-firehose-logged: false + +paths: + /ping: + get: + description: Test service connectivity + tags: + - Service Health + operationId: ping-get + responses: + '200': + $ref: '#/components/responses/Ping' + default: + $ref: '#/components/responses/ServerError' + head: + description: Test service connectivity + tags: + - Service Health + operationId: ping-head + responses: + '200': + description: ping HEAD successful + content: + application/json: + schema: {} + default: + $ref: '#/components/responses/ServerError' + +components: + schemas: + Ping: + type: object + additionalProperties: false + required: + - serverTime + properties: + serverTime: + type: string + format: date-time + description: Current server time + + Error: + type: object + additionalProperties: false + description: Error message + properties: + message: + type: string + code: + type: string + + responses: + ServerError: + description: Server Error response + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Ping: + description: ping successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Ping' diff --git a/ts-init/services/typing-fixture.d.ts b/ts-init/services/typing-fixture.d.ts new file mode 100644 index 0000000..d0ed53b --- /dev/null +++ b/ts-init/services/typing-fixture.d.ts @@ -0,0 +1,12 @@ +/* c8 ignore start */ +// services/typing-fixture.d.ts + +import type { TypedServices } from './index-fixture'; + +declare module '@checkdigit/fixture' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface ResolvedServices extends TypedServices { + // + } +} +/* c8 ignore stop */ diff --git a/ts-init/services/typing.d.ts b/ts-init/services/typing.d.ts new file mode 100644 index 0000000..a493af1 --- /dev/null +++ b/ts-init/services/typing.d.ts @@ -0,0 +1,12 @@ +/* c8 ignore start */ +// services/typing.d.ts + +import type { Stringified } from '.'; + +declare global { + interface JSON { + stringify(input: T): Stringified; + } +} + +/* c8 ignore stop */ From 9406dad325b87682f131f86d4cbacf22e87cd137 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 12 Aug 2024 16:58:05 -0400 Subject: [PATCH 050/115] enforce no FullResponse and resolveWithFullResponse in service wrapper calls --- src/agent/fetch-then.ts | 3 +- src/agent/no-fixture.ts | 3 +- src/agent/no-full-response.ts | 2 +- src/index.ts | 6 + src/require-resolve-full-response.spec.ts | 196 +++++++++++++++++++++ src/require-resolve-full-response.ts | 199 ++++++++++++++++++++++ 6 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 src/require-resolve-full-response.spec.ts create mode 100644 src/require-resolve-full-response.ts diff --git a/src/agent/fetch-then.ts b/src/agent/fetch-then.ts index 2a445f3..4aed81b 100644 --- a/src/agent/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -193,8 +193,7 @@ const rule: Rule.RuleModule = { messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', shouldUseHeaderGetter: 'Getter should be used to access response headers.', - unknownError: - 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', schema: [], diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index a43a746..b1e6d22 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -228,8 +228,7 @@ const rule: Rule.RuleModule = { }, messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', - unknownError: - 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', schema: [], diff --git a/src/agent/no-full-response.ts b/src/agent/no-full-response.ts index 6bef609..cddb0d9 100644 --- a/src/agent/no-full-response.ts +++ b/src/agent/no-full-response.ts @@ -23,7 +23,7 @@ const rule = createRule({ description: 'Remove the usage of FullResponse type.', }, messages: { - removeFullResponse: 'Remove the usage of FullResponse type.', + removeFullResponse: 'Removing the usage of FullResponse type.', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', diff --git a/src/index.ts b/src/index.ts index 73283da..e9eeafa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,9 @@ import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full- import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code'; +import requireResolveFullResponse, { + ruleId as requireResolveFullResponseRuleId, +} from './require-resolve-full-response'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -49,6 +52,7 @@ export default { [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, [addUrlDomainRuleId]: addUrlDomain, [noFullResponseRuleId]: noFullResponse, + [requireResolveFullResponseRuleId]: requireResolveFullResponse, }, configs: { all: { @@ -64,6 +68,8 @@ export default { '@checkdigit/no-test-import': 'error', [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', }, }, recommended: { diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts new file mode 100644 index 0000000..2a7fe1c --- /dev/null +++ b/src/require-resolve-full-response.spec.ts @@ -0,0 +1,196 @@ +// require-resolve-full-response.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './require-resolve-full-response'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'none service wrapper call will not trigger an error', + code: `response.headers.get('foo');`, + }, + { + name: 'no error if service wrapper call sets resolveWithFullResponse as true', + code: ` + async function getKey(pingService: Endpoint) { + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + } + `, + }, + ], + invalid: [ + { + name: 'service wrapper passed in as a function argument with type as Endpoint', + code: ` + async function getKey(pingService: Endpoint) { + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'service wrapper passed in as a function argument with type as ResolvedService', + code: ` + async function getKey( + pingService: ResolvedService, + request: InboundContext + ) { + await pingService(request).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'service configuration passed in as a argument with type as Configuration', + code: ` + async function getKey( + config: Configuration, + ) { + await config.service.ping(EMPTY_CONTEXT).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'fixture passed in as a argument', + code: ` + async function getKey( + fixture: Fixture, + ) { + await fixture.config.service.ping(EMPTY_CONTEXT).get(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'url declared as a variable', + code: ` + const url = \`\${PING_BASE_PATH}/key/\${keyId}\`; + await pingService.get(url, { + resolveWithFullResponse: false, + }); + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'handle request with headers', + code: ` + async function getKey( + fixture: Fixture, + ) { + await fixture.config.service.ping(EMPTY_CONTEXT).head(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: false, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'handle request with body', + code: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.put(\`\${PING_BASE_PATH}/key/\${keyId}\`, {data:'hi'}, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'handle PUT request with undefined body', + code: ` + async function getKey( + fixture: Fixture, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.put(\`\${PING_BASE_PATH}/key/\${keyId}\`, undefined, { + resolveWithFullResponse: false, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'handle request with both body and headers', + code: ` + async function getKey( + fixture: Fixture, + keyRequest: ping.KeyRequest, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.post(\`\${PING_BASE_PATH}/key/\${keyId}\`, keyRequest, { + headers: { + etag: '123', + }, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'initiate and call serve-runtime service in the same function', + code: ` + import type { Configuration, InboundContext } from '@checkdigit/serve-runtime'; + import type { pingV1 as ping } from '../services'; + + export async function createKey( + config: Configuration, + inboundContext: InboundContext, + keyRequest: ping.KeyRequest, + ): Promise { + const pingService = config.service.ping(inboundContext); + const newKeyResponse = await pingService.put( + \`\${PING_BASE_PATH}/key/\${keyId}\`, + keyRequest, + ); + if (newKeyResponse.statusCode !== StatusCodes.OK) { + throw new Error('failed'); + } + return newKeyResponse.body; + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, + { + name: 'handle multi-line url string literal', + code: ` + await pingService.get(\`/message/v1/picked-request?cardId=\${cardIds.toString()}fromDate={encodeURIComponent( + fromDate, + )}toDate=\${encodeURIComponent( + toDate, + )}fields=ADVICE_RESPONSE,CATEGORIZATION,CREATED_ON,MATCHED_MESSAGE_ID,SETTLEMENT_AMOUNT,MESSAGE_ID,RECEIVED_DATE_TIME\`); + `, + errors: [{ messageId: 'invalidOptions' }], + }, + ], +}); diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts new file mode 100644 index 0000000..b9a18e9 --- /dev/null +++ b/src/require-resolve-full-response.ts @@ -0,0 +1,199 @@ +// require-resolve-full-response.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { DefinitionType, type Scope } from '@typescript-eslint/scope-manager'; +import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP } from './agent/url'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from './get-documentation-url'; +import { getEnclosingScopeNode } from './library/ts-tree'; + +export const ruleId = 'require-resolve-full-response'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch over customized service wrapper.', + }, + messages: { + invalidOptions: + '"options" argument should be provided with "resolveWithFullResponse" property set as "true". Otherwise, it indicates that the response body will be obtained without status code assertion which could result in unexpected issue.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + const parserService = ESLintUtils.getParserServices(context); + const typeChecker = parserService.program.getTypeChecker(); + + function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) { + if ( + (urlArgument?.type === AST_NODE_TYPES.Literal && typeof urlArgument.value === 'string') || + urlArgument?.type === AST_NODE_TYPES.TemplateLiteral + ) { + const urlText = sourceCode.getText(urlArgument); + return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText); + } + + if (urlArgument?.type === AST_NODE_TYPES.Identifier) { + const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name); + if (foundVariable) { + const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); + assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); + const variableDefinitionNode = variableDefinition.node; + assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator); + assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); + return isUrlArgumentValid(variableDefinitionNode.init, scope); + } + } + + return false; + } + + function getType(identifier: TSESTree.Identifier) { + const variable = parserService.esTreeNodeToTSNodeMap.get(identifier); + const variableType = typeChecker.getTypeAtLocation(variable); + return typeChecker.typeToString(variableType); + } + + function isServiceLikeName(name: string) { + return /.*[Ss]ervice$/u.test(name); + } + + function isCalleeServiceWrapper(serviceCall: TSESTree.CallExpression) { + const callee = serviceCall.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + + const endpoint = callee.object; + if (endpoint.type === AST_NODE_TYPES.Identifier) { + return getType(endpoint) === 'Endpoint' || isServiceLikeName(endpoint.name); + } + if (endpoint.type !== AST_NODE_TYPES.CallExpression) { + return false; + } + + const [contextArgument] = endpoint.arguments; + if (contextArgument?.type !== AST_NODE_TYPES.Identifier) { + return false; + } + if (contextArgument.name !== 'EMPTY_CONTEXT' && getType(contextArgument) !== 'InboundContext') { + return false; + } + const service = endpoint.callee; + if (service.type === AST_NODE_TYPES.Identifier) { + return getType(service) === 'ResolvedService'; + } + + if (service.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const services = service.object; + if (services.type === AST_NODE_TYPES.Identifier) { + return getType(services) === 'ResolvedServices'; + } + + if (services.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const configuration = services.object; + if (configuration.type === AST_NODE_TYPES.Identifier) { + return ['Configuration', 'Configuration'].includes(getType(configuration)); + } + + // following applies only to test code (fixture) + if (configuration.type !== AST_NODE_TYPES.MemberExpression) { + return false; + } + const fixture = configuration.object; + if (fixture.type === AST_NODE_TYPES.Identifier) { + return fixture.name === 'fixture' || getType(fixture) === 'Fixture'; + } + + return false; + } + + return { + 'CallExpression[callee.property.name=/^(head|get|put|post|del|patch)$/]': ( + serviceCall: TSESTree.CallExpression, + ) => { + try { + if (!isCalleeServiceWrapper(serviceCall)) { + return; + } + + const enclosingScopeNode = getEnclosingScopeNode(serviceCall); + assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); + const scope = scopeManager?.acquire(enclosingScopeNode); + assert.ok(scope, 'scope is undefined'); + const urlArgument = serviceCall.arguments[0]; + if (!isUrlArgumentValid(urlArgument, scope)) { + return; + } + + assert.ok(serviceCall.callee.type === AST_NODE_TYPES.MemberExpression); + assert.ok(serviceCall.callee.property.type === AST_NODE_TYPES.Identifier); + + // method + const method = serviceCall.callee.property.name; + + // options + const optionsArgument = ['get', 'head', 'del'].includes(method) + ? serviceCall.arguments[1] + : serviceCall.arguments[2]; + if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { + context.report({ + node: serviceCall, + messageId: 'invalidOptions', + }); + return; + } + + const resolveWithFullResponseProperty = optionsArgument.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'resolveWithFullResponse', + ); + if ( + resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || + resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || + resolveWithFullResponseProperty.value.value !== true + ) { + context.report({ + node: optionsArgument, + messageId: 'invalidOptions', + }); + return; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: serviceCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; From ceccb0ba0e29a70773ba2a1a44824729bbc8c446 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 13 Aug 2024 12:21:47 -0400 Subject: [PATCH 051/115] convert 'del' as 'DELETE', rule config separation --- src/agent/no-fixture.spec.ts | 15 ++++++++++++++ src/agent/no-fixture.ts | 4 +++- src/agent/no-service-wrapper.spec.ts | 20 +++++++++++++++++++ src/agent/no-service-wrapper.ts | 2 +- src/index.ts | 30 ++++++++++++++++++++++++---- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 9b33c81..59b42a5 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -143,6 +143,21 @@ describe(ruleId, () => { `, errors: 1, }, + { + name: 'replace del with DELETE', + code: ` + await fixture.api + .del(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) + .expect(StatusCodes.NO_CONTENT); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + method: 'DELETE', + }); + assert.equal(response.status, StatusCodes.NO_CONTENT); + `, + errors: 1, + }, { name: 'response headers assertion should be externalized with new variable declared if necessary', code: ` diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index b1e6d22..10d7bfe 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -280,9 +280,11 @@ const rule: Rule.RuleModule = { // fetch request argument const methodNode = fixtureFunction.property; // get/put/etc. assert.ok(methodNode.type === 'Identifier'); + const methodName = methodNode.name.toUpperCase(); + const fetchRequestArgumentLines = [ '{', - ` method: '${methodNode.name.toUpperCase()}',`, + ` method: '${methodName === 'DEL' ? 'DELETE' : methodName}',`, ...(fixtureCallInformation.requestBody ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] : []), diff --git a/src/agent/no-service-wrapper.spec.ts b/src/agent/no-service-wrapper.spec.ts index 3e468f5..67c3dc9 100644 --- a/src/agent/no-service-wrapper.spec.ts +++ b/src/agent/no-service-wrapper.spec.ts @@ -23,6 +23,10 @@ ruleTester.run(ruleId, rule, { name: 'none service wrapper call will not trigger an error', code: `response.headers.get('foo');`, }, + { + name: 'no change if already converted to fetch', + code: `fetch(\`https://ping.checkdigit/ping/v1/ping\`);`, + }, ], invalid: [ { @@ -237,6 +241,22 @@ ruleTester.run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'replace del method as DELETE', + code: ` + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.del(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + resolveWithFullResponse: true, + }); + `, + output: ` + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await fetch(\`\${PING_BASE_PATH}/key/\${keyId}\`, { + method: 'DELETE', + }); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, { name: 'initiate and call serve-runtime service in the same function', code: ` diff --git a/src/agent/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts index d297aed..59e9f59 100644 --- a/src/agent/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -210,7 +210,7 @@ const rule = createRule({ const fetchText = [ `fetch(${replacedUrl}, {`, - ` method: '${method.toUpperCase()}',`, + ` method: '${method.toLowerCase() === 'del' ? 'DELETE' : method.toUpperCase()}',`, ...(requestHeadersProperty ? [` ${sourceCode.getText(requestHeadersProperty)},`] : []), ...(requestBodyProperty ? [` body: JSON.stringify(${sourceCode.getText(requestBodyProperty)}),`] : []), '})', diff --git a/src/index.ts b/src/index.ts index e9eeafa..fcc5481 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,13 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', + [`@checkdigit/${noStatusCodeRuleId}`]: 'off', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', + [`@checkdigit/${fetchThenRuleId}`]: 'off', }, }, recommended: { @@ -87,16 +94,31 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', }, }, - agent: { + 'agent-serve-runtime': { + ignorePatterns: ['*.spec.ts', '*.test.ts'], rules: { - [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + }, + }, + 'agent-fixture': { + rules: { [`@checkdigit/${noFullResponseRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', }, }, }, From 0ea4a0d05dc23facb3c379a5afb1c84030e7beb6 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 20 Sep 2024 14:14:06 -0400 Subject: [PATCH 052/115] added rule to replace MappedResponse with FetchResponse --- package.json | 6 +- src/agent/no-mapped-response.spec.ts | 62 ++++++++++++++++++++ src/agent/no-mapped-response.ts | 84 ++++++++++++++++++++++++++++ src/index.ts | 15 +++-- 4 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 src/agent/no-mapped-response.spec.ts create mode 100644 src/agent/no-mapped-response.ts diff --git a/package.json b/package.json index 35eabc1..1868960 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,9 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", - "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@types/eslint": "8.56.10", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@typescript-eslint/rule-tester": "7.18.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", diff --git a/src/agent/no-mapped-response.spec.ts b/src/agent/no-mapped-response.spec.ts new file mode 100644 index 0000000..9d9eb79 --- /dev/null +++ b/src/agent/no-mapped-response.spec.ts @@ -0,0 +1,62 @@ +// fixture/no-mapped-response-type.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-mapped-response'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'import statement', + code: `import type { MappedResponse } from '../../../services';`, + output: `import type { FetchResponse } from '../../../services';`, + errors: [{ messageId: 'replaceFullResponseWithFetchResponse' }], + }, + { + name: 'import statement with multiple imports', + code: `import type { apiV1, MappedResponse, xxx } from '../../../services';`, + output: `import type { apiV1, FetchResponse, xxx } from '../../../services';`, + errors: [{ messageId: 'replaceFullResponseWithFetchResponse' }], + }, + { + name: 'import statement mixing type and value imports', + code: `import { type apiV1, type MappedResponse, xxx } from '../../../services';`, + output: `import { type apiV1, type FetchResponse, xxx } from '../../../services';`, + errors: [{ messageId: 'replaceFullResponseWithFetchResponse' }], + }, + { + name: 'function return type', + code: ` + export async function getSensitiveInformation(): Promise> { + return; + } + `, + output: ` + export async function getSensitiveInformation(): Promise> { + return; + } + `, + errors: [{ messageId: 'replaceFullResponseWithFetchResponse' }], + }, + { + name: 'type casting', + code: `const fullResponse = response as MappedResponse;`, + output: `const fullResponse = response as FetchResponse;`, + errors: [{ messageId: 'replaceFullResponseWithFetchResponse' }], + }, + ], +}); diff --git a/src/agent/no-mapped-response.ts b/src/agent/no-mapped-response.ts new file mode 100644 index 0000000..a9a861e --- /dev/null +++ b/src/agent/no-mapped-response.ts @@ -0,0 +1,84 @@ +// fixture/no-mapped-response-type.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'no-mapped-response'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Replace the usage of MappedResponse type with FetchResponse.', + }, + messages: { + replaceFullResponseWithFetchResponse: 'Replace the usage of FullResponse type with FetchResponse.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + 'TSTypeReference[typeName.name="MappedResponse"]': (typeReference: TSESTree.TSTypeReference) => { + try { + context.report({ + messageId: 'replaceFullResponseWithFetchResponse', + node: typeReference, + fix(fixer) { + const typeParams = sourceCode.getText(typeReference.typeArguments); + return fixer.replaceText(typeReference, `FetchResponse${typeParams || ''}`); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: typeReference, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + 'ImportSpecifier[imported.name="MappedResponse"]': (importSpecifier: TSESTree.ImportSpecifier) => { + try { + context.report({ + messageId: 'replaceFullResponseWithFetchResponse', + node: importSpecifier.imported, + fix(fixer) { + return fixer.replaceText(importSpecifier.imported, 'FetchResponse'); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: importSpecifier.imported, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index fcc5481..cf65105 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-response'; +import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code'; @@ -52,6 +53,7 @@ export default { [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, [addUrlDomainRuleId]: addUrlDomain, [noFullResponseRuleId]: noFullResponse, + [noMappedResponseRuleId]: noMappedResponse, [requireResolveFullResponseRuleId]: requireResolveFullResponse, }, configs: { @@ -94,13 +96,14 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', }, }, - 'agent-serve-runtime': { - ignorePatterns: ['*.spec.ts', '*.test.ts'], + 'agent-phase-1-test': { + files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], rules: { [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', @@ -108,12 +111,14 @@ export default { [`@checkdigit/${fetchThenRuleId}`]: 'error', }, }, - 'agent-fixture': { + 'agent-phase-2-production': { + ignorePatterns: ['*.spec.ts', '*.test.ts'], rules: { [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', From a124eca48a284c195b1ddfffc61455aae9866300 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 20 Sep 2024 14:29:05 -0400 Subject: [PATCH 053/115] npm i --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8da711a..ec73ed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", - "@types/eslint": "^8.56.10", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@types/eslint": "8.56.10", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/parser": "7.18.0", "@typescript-eslint/rule-tester": "7.18.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-eslint-plugin": "^6.2.0", @@ -1891,9 +1891,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.11", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", - "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "dev": true, "license": "MIT", "dependencies": { From 88aa2fd1cf96b58f310cd27fd14e7a95e2742df4 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 20 Sep 2024 15:39:36 -0400 Subject: [PATCH 054/115] correct file inclusion config for ruleset --- src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index cf65105..dcd891d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,7 +97,11 @@ export default { }, }, 'agent-phase-1-test': { - files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], + overrides: [ + { + files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], + }, + ], rules: { [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', From d33efd8981fca262cf06a83054e796ee6f053d0d Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 24 Sep 2024 03:44:46 -0400 Subject: [PATCH 055/115] add rule no-duplicated-imports for merging duplicated import statements with the same 'from' --- src/index.ts | 14 +--- src/no-duplicated-imports.spec.ts | 67 +++++++++++++++++ src/no-duplicated-imports.ts | 118 ++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 src/no-duplicated-imports.spec.ts create mode 100644 src/no-duplicated-imports.ts diff --git a/src/index.ts b/src/index.ts index dcd891d..b2e5a1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import fetchResponseHeaderGetter, { } from './agent/fetch-response-header-getter'; import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; +import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-response'; import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; @@ -55,6 +56,7 @@ export default { [noFullResponseRuleId]: noFullResponse, [noMappedResponseRuleId]: noMappedResponse, [requireResolveFullResponseRuleId]: requireResolveFullResponse, + [noDuplicatedImportsRuleId]: noDuplicatedImports, }, configs: { all: { @@ -72,13 +74,7 @@ export default { [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'off', - [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', - [`@checkdigit/${noStatusCodeRuleId}`]: 'off', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', - [`@checkdigit/${fetchThenRuleId}`]: 'off', + [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', }, }, recommended: { @@ -103,8 +99,6 @@ export default { }, ], rules: { - [`@checkdigit/${noFullResponseRuleId}`]: 'error', - [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', [`@checkdigit/${noMappedResponseRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', @@ -118,8 +112,6 @@ export default { 'agent-phase-2-production': { ignorePatterns: ['*.spec.ts', '*.test.ts'], rules: { - [`@checkdigit/${noFullResponseRuleId}`]: 'error', - [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', [`@checkdigit/${noMappedResponseRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'off', diff --git a/src/no-duplicated-imports.spec.ts b/src/no-duplicated-imports.spec.ts new file mode 100644 index 0000000..1159669 --- /dev/null +++ b/src/no-duplicated-imports.spec.ts @@ -0,0 +1,67 @@ +// fixture/no-duplicated-imports.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-duplicated-imports'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'distinct import statement - type', + code: `import type { TypeOne } from 'abc';`, + }, + { + name: 'distinct import statement - value', + code: `import { ValueOne } from 'abc';`, + }, + { + name: 'distinct import statement - mix of type and value', + code: `import { type TypeOne, ValueOne } from 'abc';`, + }, + ], + invalid: [ + { + name: 'duplicated import from should be merged - values', + code: `import { ValueOne } from 'abc';\nimport { ValueTwo } from 'abc';\n`, + output: `import { ValueOne, ValueTwo } from 'abc';\n`, + errors: [{ messageId: 'mergeDuplicatedImports' }], + }, + { + name: 'duplicated import from should be merged - types', + code: `import type { TypeOne } from 'abc';\nimport type { TypeTwo } from 'abc';\n`, + output: `import type { TypeOne, TypeTwo } from 'abc';\n`, + errors: [{ messageId: 'mergeDuplicatedImports' }], + }, + { + name: 'duplicated import from should be merged - mix of type and value', + code: `import type { TypeOne } from 'abc';\nimport { ValueOne } from 'abc';\n`, + output: `import { type TypeOne, ValueOne } from 'abc';\n`, + errors: [{ messageId: 'mergeDuplicatedImports' }], + }, + { + name: 'works with named imports', + code: `import type { TypeOne as T1 } from 'abc';\nimport { ValueOne as V1 } from 'abc';\n`, + output: `import { type TypeOne as T1, ValueOne as V1 } from 'abc';\n`, + errors: [{ messageId: 'mergeDuplicatedImports' }], + }, + { + name: 'works with default import', + code: `import type { TypeOne as T1 } from 'abc';\nimport { ValueOne as V1 } from 'abc';\nimport abc from 'abc';\n`, + output: `import abc, { type TypeOne as T1, ValueOne as V1 } from 'abc';\n`, + errors: [{ messageId: 'mergeDuplicatedImports' }], + }, + ], +}); diff --git a/src/no-duplicated-imports.ts b/src/no-duplicated-imports.ts new file mode 100644 index 0000000..f8848b0 --- /dev/null +++ b/src/no-duplicated-imports.ts @@ -0,0 +1,118 @@ +// fixture/no-duplicated-imports.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'no-duplicated-imports'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Merge duplicated import statements with the same "from".', + }, + messages: { + mergeDuplicatedImports: 'Merge duplicated import statements with the same "from".', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + const importDeclarations = new Map(); + + return { + ImportDeclaration(node) { + const moduleName = node.source.value; + let declarations = importDeclarations.get(moduleName); + if (declarations === undefined) { + declarations = []; + importDeclarations.set(moduleName, declarations); + } + declarations.push(node); + }, + 'Program:exit'() { + for (const [moduleName, declarations] of importDeclarations.entries()) { + if (declarations.length <= 1) { + continue; + } + + const firstDeclaration = declarations[0]; + assert.ok(firstDeclaration); + + const isAllTypeOnly = declarations.every( + (declaration) => + declaration.importKind === 'type' || + declaration.specifiers.every( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === 'type', + ), + ); /*?*/ + + context.report({ + messageId: 'mergeDuplicatedImports', + node: firstDeclaration, + fix(fixer) { + const fixes = []; + + const defaultSpecifier = declarations + .flatMap((declaration) => + declaration.specifiers.map((specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier ? specifier : undefined, + ), + ) + .filter(Boolean); /*?*/ + const defaultSpecifierText = defaultSpecifier[0] + ? sourceCode.getText(defaultSpecifier[0]) + : undefined; /*?*/ + + const mergedSpecifiers = declarations.flatMap((declaration) => { + const isCurrentDeclarationTypeOnly = + declaration.importKind === 'type' || + declaration.specifiers.every( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === 'type', + ); /*?*/ + return declaration.specifiers + .filter((specifier) => specifier.type !== TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) + .map((specifier) => + // eslint-disable-next-line no-nested-ternary + isAllTypeOnly + ? sourceCode.getText(specifier).replace('type ', '') + : isCurrentDeclarationTypeOnly + ? `type ${sourceCode.getText(specifier)}` + : sourceCode.getText(specifier), + ); + }); + const mergedSpecifiersText = `${isAllTypeOnly ? 'type ' : ''}{ ${[...mergedSpecifiers].join(', ')} }`; + + // Replace the first import with the merged import + const mergedImport = `import ${[defaultSpecifierText, mergedSpecifiersText].filter(Boolean).join(', ')} from '${moduleName}';`; + fixes.push(fixer.replaceText(firstDeclaration, mergedImport)); + + // Remove the remaining imports + declarations.slice(1).forEach((declaration) => { + fixes.push(fixer.removeRange([declaration.range[0], declaration.range[1] + 1])); + }); + + return fixes; + }, + }); + } + }, + }; + }, +}); + +export default rule; From 97082aa55b8e21ee663764cb9df7b1dd4d6af187 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 25 Sep 2024 18:51:09 -0400 Subject: [PATCH 056/115] new rule require-fixed-services-import and require-type-out-of-type-only-imports --- src/agent/add-url-domain.spec.ts | 2 +- src/agent/add-url-domain.ts | 2 +- src/agent/fetch-response-body-json.spec.ts | 2 +- src/agent/fetch-response-body-json.ts | 2 +- .../fetch-response-header-getter.spec.ts | 2 +- src/agent/fetch-response-header-getter.ts | 2 +- src/agent/fetch-then.spec.ts | 2 +- src/agent/fetch-then.ts | 2 +- src/agent/fetch.ts | 2 +- src/agent/no-fixture.spec.ts | 2 +- src/agent/no-fixture.ts | 2 +- src/agent/no-full-response.spec.ts | 2 +- src/agent/no-full-response.ts | 2 +- src/agent/no-mapped-response.spec.ts | 2 +- src/agent/no-mapped-response.ts | 2 +- src/agent/no-service-wrapper.spec.ts | 2 +- src/agent/no-service-wrapper.ts | 2 +- src/agent/no-status-code.spec.ts | 2 +- src/agent/no-status-code.ts | 2 +- src/agent/response-reference.ts | 2 +- src/agent/url.ts | 2 +- src/index.ts | 10 +++ src/library/format.ts | 2 +- src/library/tree.ts | 2 +- src/library/ts-tree.ts | 2 +- src/library/variable.ts | 2 +- src/no-duplicated-imports.spec.ts | 2 +- src/no-duplicated-imports.ts | 14 ++--- src/require-fixed-services-import.spec.ts | 41 ++++++++++++ src/require-fixed-services-import.ts | 52 +++++++++++++++ ...uire-type-out-of-type-only-imports.spec.ts | 59 +++++++++++++++++ src/require-type-out-of-type-only-imports.ts | 63 +++++++++++++++++++ src/ts-tester.test.ts | 2 +- ts-init/react.tsx | 0 34 files changed, 258 insertions(+), 35 deletions(-) create mode 100644 src/require-fixed-services-import.spec.ts create mode 100644 src/require-fixed-services-import.ts create mode 100644 src/require-type-out-of-type-only-imports.spec.ts create mode 100644 src/require-type-out-of-type-only-imports.ts delete mode 100644 ts-init/react.tsx diff --git a/src/agent/add-url-domain.spec.ts b/src/agent/add-url-domain.spec.ts index 0b003a8..6f268e5 100644 --- a/src/agent/add-url-domain.spec.ts +++ b/src/agent/add-url-domain.spec.ts @@ -1,4 +1,4 @@ -// fixture/add-url-domain.spec.ts +// agent/add-url-domain.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/add-url-domain.ts b/src/agent/add-url-domain.ts index 33111ee..448991c 100644 --- a/src/agent/add-url-domain.ts +++ b/src/agent/add-url-domain.ts @@ -1,4 +1,4 @@ -// fixture/add-url-domain.ts +// agent/add-url-domain.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-response-body-json.spec.ts b/src/agent/fetch-response-body-json.spec.ts index 1169177..63f2478 100644 --- a/src/agent/fetch-response-body-json.spec.ts +++ b/src/agent/fetch-response-body-json.spec.ts @@ -1,4 +1,4 @@ -// fixture/fetch-response-body-json.spec.ts +// agent/fetch-response-body-json.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts index b222d02..bee3044 100644 --- a/src/agent/fetch-response-body-json.ts +++ b/src/agent/fetch-response-body-json.ts @@ -1,4 +1,4 @@ -// fixture/fetch-response-body-json.ts +// agent/fetch-response-body-json.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-response-header-getter.spec.ts b/src/agent/fetch-response-header-getter.spec.ts index 86a8a92..f5a6f06 100644 --- a/src/agent/fetch-response-header-getter.spec.ts +++ b/src/agent/fetch-response-header-getter.spec.ts @@ -1,4 +1,4 @@ -// fixture/fetch-response-header-getter-ts.spec.ts +// agent/fetch-response-header-getter-ts.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-response-header-getter.ts b/src/agent/fetch-response-header-getter.ts index 0641361..c62fabb 100644 --- a/src/agent/fetch-response-header-getter.ts +++ b/src/agent/fetch-response-header-getter.ts @@ -1,4 +1,4 @@ -// fixture/fetch-response-header-getter-ts.ts +// agent/fetch-response-header-getter-ts.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts index f569a69..059ec88 100644 --- a/src/agent/fetch-then.spec.ts +++ b/src/agent/fetch-then.spec.ts @@ -1,4 +1,4 @@ -// fixture/concurrent-promises.spec.ts +// agent/concurrent-promises.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch-then.ts b/src/agent/fetch-then.ts index 4aed81b..aed6054 100644 --- a/src/agent/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -1,4 +1,4 @@ -// fixture/fetch-then.ts +// agent/fetch-then.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index 270cf09..1869f83 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -1,4 +1,4 @@ -// fixture/fetch.ts +// agent/fetch.ts import { getParent, isBlockStatement } from '../library/tree'; import type { Node } from 'estree'; diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 59b42a5..171d594 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-fixture.spec.ts +// agent/no-fixture.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 10d7bfe..0b39566 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -1,4 +1,4 @@ -// fixture/no-fixture.ts +// agent/no-fixture.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-full-response.spec.ts b/src/agent/no-full-response.spec.ts index acfdcf7..8eb4fc5 100644 --- a/src/agent/no-full-response.spec.ts +++ b/src/agent/no-full-response.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-full-response.spec.ts +// agent/no-full-response.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-full-response.ts b/src/agent/no-full-response.ts index cddb0d9..848e072 100644 --- a/src/agent/no-full-response.ts +++ b/src/agent/no-full-response.ts @@ -1,4 +1,4 @@ -// fixture/no-full-response.ts +// agent/no-full-response.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-mapped-response.spec.ts b/src/agent/no-mapped-response.spec.ts index 9d9eb79..a4ee5f6 100644 --- a/src/agent/no-mapped-response.spec.ts +++ b/src/agent/no-mapped-response.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-mapped-response-type.ts +// agent/no-mapped-response-type.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-mapped-response.ts b/src/agent/no-mapped-response.ts index a9a861e..3fb5455 100644 --- a/src/agent/no-mapped-response.ts +++ b/src/agent/no-mapped-response.ts @@ -1,4 +1,4 @@ -// fixture/no-mapped-response-type.ts +// agent/no-mapped-response-type.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-service-wrapper.spec.ts b/src/agent/no-service-wrapper.spec.ts index 67c3dc9..1cfbcf3 100644 --- a/src/agent/no-service-wrapper.spec.ts +++ b/src/agent/no-service-wrapper.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-service-wrapper.spec.ts +// agent/no-service-wrapper.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts index 59e9f59..35ac206 100644 --- a/src/agent/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -1,4 +1,4 @@ -// fixture/no-service-wrapper.ts +// agent/no-service-wrapper.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-status-code.spec.ts b/src/agent/no-status-code.spec.ts index e2fd56b..586338a 100644 --- a/src/agent/no-status-code.spec.ts +++ b/src/agent/no-status-code.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-status-code.spec.ts +// agent/no-status-code.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-status-code.ts b/src/agent/no-status-code.ts index 0f56da7..125c516 100644 --- a/src/agent/no-status-code.ts +++ b/src/agent/no-status-code.ts @@ -1,4 +1,4 @@ -// fixture/no-status-code.ts +// agent/no-status-code.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index ebb41ae..eca6af9 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -1,4 +1,4 @@ -// fixture/response-reference.ts +// agent/response-reference.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/url.ts b/src/agent/url.ts index 0f8cc60..ca9eea2 100644 --- a/src/agent/url.ts +++ b/src/agent/url.ts @@ -1,4 +1,4 @@ -// fixture/url.ts +// agent/url.ts export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; diff --git a/src/index.ts b/src/index.ts index b2e5a1c..341173f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,9 +20,15 @@ import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-m import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code'; +import requireFixedServicesImport, { + ruleId as requireFixedServicesImportRuleId, +} from './require-fixed-services-import'; import requireResolveFullResponse, { ruleId as requireResolveFullResponseRuleId, } from './require-resolve-full-response'; +import requireTypeOutOfTypeOnlyImports, { + ruleId as requireTypeOutOfTypeOnlyImportsRuleId, +} from './require-type-out-of-type-only-imports'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -57,6 +63,8 @@ export default { [noMappedResponseRuleId]: noMappedResponse, [requireResolveFullResponseRuleId]: requireResolveFullResponse, [noDuplicatedImportsRuleId]: noDuplicatedImports, + [requireFixedServicesImportRuleId]: requireFixedServicesImport, + [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, }, configs: { all: { @@ -75,6 +83,8 @@ export default { [`@checkdigit/${noFullResponseRuleId}`]: 'error', [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', + [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', + [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', }, }, recommended: { diff --git a/src/library/format.ts b/src/library/format.ts index e0bac37..11ca6be 100644 --- a/src/library/format.ts +++ b/src/library/format.ts @@ -1,4 +1,4 @@ -// format.ts +// library/format.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/library/tree.ts b/src/library/tree.ts index e54f114..86982da 100644 --- a/src/library/tree.ts +++ b/src/library/tree.ts @@ -1,4 +1,4 @@ -// tree.ts +// library/tree.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/library/ts-tree.ts b/src/library/ts-tree.ts index 8c37fa8..3eb2b7d 100644 --- a/src/library/ts-tree.ts +++ b/src/library/ts-tree.ts @@ -1,4 +1,4 @@ -// tree.ts +// library/ts-tree.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/library/variable.ts b/src/library/variable.ts index b4e8b70..305365b 100644 --- a/src/library/variable.ts +++ b/src/library/variable.ts @@ -1,4 +1,4 @@ -// fixture/variable.ts +// library/variable.ts export function isValidPropertyName(name: unknown) { return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); diff --git a/src/no-duplicated-imports.spec.ts b/src/no-duplicated-imports.spec.ts index 1159669..da1cf25 100644 --- a/src/no-duplicated-imports.spec.ts +++ b/src/no-duplicated-imports.spec.ts @@ -1,4 +1,4 @@ -// fixture/no-duplicated-imports.spec.ts +// no-duplicated-imports.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/no-duplicated-imports.ts b/src/no-duplicated-imports.ts index f8848b0..ce9061c 100644 --- a/src/no-duplicated-imports.ts +++ b/src/no-duplicated-imports.ts @@ -1,4 +1,4 @@ -// fixture/no-duplicated-imports.ts +// no-duplicated-imports.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -58,7 +58,7 @@ const rule = createRule({ (specifier) => specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === 'type', ), - ); /*?*/ + ); context.report({ messageId: 'mergeDuplicatedImports', @@ -72,10 +72,8 @@ const rule = createRule({ specifier.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier ? specifier : undefined, ), ) - .filter(Boolean); /*?*/ - const defaultSpecifierText = defaultSpecifier[0] - ? sourceCode.getText(defaultSpecifier[0]) - : undefined; /*?*/ + .filter(Boolean); + const defaultSpecifierText = defaultSpecifier[0] ? sourceCode.getText(defaultSpecifier[0]) : undefined; const mergedSpecifiers = declarations.flatMap((declaration) => { const isCurrentDeclarationTypeOnly = @@ -83,7 +81,7 @@ const rule = createRule({ declaration.specifiers.every( (specifier) => specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === 'type', - ); /*?*/ + ); return declaration.specifiers .filter((specifier) => specifier.type !== TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) .map((specifier) => @@ -95,7 +93,7 @@ const rule = createRule({ : sourceCode.getText(specifier), ); }); - const mergedSpecifiersText = `${isAllTypeOnly ? 'type ' : ''}{ ${[...mergedSpecifiers].join(', ')} }`; + const mergedSpecifiersText = `${isAllTypeOnly ? 'type ' : ''}{ ${mergedSpecifiers.join(', ')} }`; // Replace the first import with the merged import const mergedImport = `import ${[defaultSpecifierText, mergedSpecifiersText].filter(Boolean).join(', ')} from '${moduleName}';`; diff --git a/src/require-fixed-services-import.spec.ts b/src/require-fixed-services-import.spec.ts new file mode 100644 index 0000000..2e818f5 --- /dev/null +++ b/src/require-fixed-services-import.spec.ts @@ -0,0 +1,41 @@ +// require-fixed-services-import.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './require-fixed-services-import'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'correctly import service typing', + code: `import type { Ping } from '../../services';`, + }, + ], + invalid: [ + { + name: 'update service typing import from', + code: `import type { personV1 as person } from '../../services/person';`, + output: `import type { personV1 as person } from '../../services';`, + errors: [{ messageId: 'updateServicesImportFrom' }], + }, + { + name: 'update service typing import from - with deeper path', + code: `import type { personV1 as person } from '../../services/person/v1';`, + output: `import type { personV1 as person } from '../../services';`, + errors: [{ messageId: 'updateServicesImportFrom' }], + }, + ], +}); diff --git a/src/require-fixed-services-import.ts b/src/require-fixed-services-import.ts new file mode 100644 index 0000000..13d8124 --- /dev/null +++ b/src/require-fixed-services-import.ts @@ -0,0 +1,52 @@ +// require-fixed-services-import.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils } from '@typescript-eslint/utils'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'require-fixed-services-import'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const SERVICE_TYPINGS_IMPORT_PATH_PREFIX = /(?\.\.\/)+services\/.*/u; + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Require fixed "from" with service typing imports from "src/services".', + }, + messages: { + updateServicesImportFrom: 'Update service typing imports to be from the fixed "src/services" path.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + ImportDeclaration(node) { + const moduleName = node.source.value; + if (SERVICE_TYPINGS_IMPORT_PATH_PREFIX.test(moduleName)) { + context.report({ + messageId: 'updateServicesImportFrom', + node: node.source, + *fix(fixer) { + yield fixer.replaceText( + node.source, + `'${moduleName.slice(0, moduleName.indexOf('../services') + '../services'.length)}'`, + ); + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/require-type-out-of-type-only-imports.spec.ts b/src/require-type-out-of-type-only-imports.spec.ts new file mode 100644 index 0000000..8e2ba51 --- /dev/null +++ b/src/require-type-out-of-type-only-imports.spec.ts @@ -0,0 +1,59 @@ +// require-type-out-of-type-only-imports.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './require-type-out-of-type-only-imports'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, +}); + +ruleTester.run(ruleId, rule, { + valid: [ + { + name: 'correct import with one type specifier', + code: `import type { TypeOne } from 'abc';`, + }, + { + name: 'correct import with one named type specifier', + code: `import type { TypeOne as T1 } from 'abc';`, + }, + { + name: 'correct import with multiple type specifiers', + code: `import type { TypeOne, TypeTwo } from 'abc';`, + }, + { + name: 'correct import with mixed type and value', + code: `import { type TypeOne, ValueOne } from 'abc';`, + }, + ], + invalid: [ + { + name: 'one type specifier', + code: `import { type TypeOne } from 'abc';`, + output: `import type { TypeOne } from 'abc';`, + errors: [{ messageId: 'moveTypeOutside' }], + }, + { + name: 'multiple type specifier', + code: `import { type TypeOne, type TypeTwo, type TypeThree } from 'abc';`, + output: `import type { TypeOne, TypeTwo, TypeThree } from 'abc';`, + errors: [{ messageId: 'moveTypeOutside' }], + }, + { + name: 'both unnamed and named specifiers', + code: `import { type TypeOne, type TypeTwo as T2 } from 'abc';`, + output: `import type { TypeOne, TypeTwo as T2 } from 'abc';`, + errors: [{ messageId: 'moveTypeOutside' }], + }, + ], +}); diff --git a/src/require-type-out-of-type-only-imports.ts b/src/require-type-out-of-type-only-imports.ts new file mode 100644 index 0000000..905f95f --- /dev/null +++ b/src/require-type-out-of-type-only-imports.ts @@ -0,0 +1,63 @@ +// require-type-out-of-type-only-imports.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'require-type-out-of-type-only-imports'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Require "type" to be out side of type-only imports.', + }, + messages: { + moveTypeOutside: 'Update the type-only imports to use "tpe" outside of the curly braces.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + ImportDeclaration(declaration) { + if ( + declaration.importKind === 'type' || + !declaration.specifiers.every( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === 'type', + ) + ) { + return; + } + + context.report({ + messageId: 'moveTypeOutside', + node: declaration, + *fix(fixer) { + const moduleName = declaration.source.value; + const mergedSpecifiers = declaration.specifiers + .filter((specifier) => specifier.type !== TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) + .map((specifier) => sourceCode.getText(specifier).replace('type ', '')); + const updatedImportDeclaration = `import type { ${mergedSpecifiers.join(', ')} } from '${moduleName}';`; + + yield fixer.replaceText(declaration, updatedImportDeclaration); + }, + }); + }, + }; + }, +}); + +export default rule; diff --git a/src/ts-tester.test.ts b/src/ts-tester.test.ts index 76d1018..bbfa885 100644 --- a/src/ts-tester.test.ts +++ b/src/ts-tester.test.ts @@ -1,4 +1,4 @@ -// agent/ts-tester.test.ts +// ts-tester.test.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/ts-init/react.tsx b/ts-init/react.tsx deleted file mode 100644 index e69de29..0000000 From abee4bc9a9bfbc614aa740ff0d6ac45b6ec48be1 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 25 Sep 2024 22:33:17 -0400 Subject: [PATCH 057/115] add rule: no-unused-function-argument --- src/agent/no-unused-function-argument.spec.ts | 60 ++++++++++++++ src/agent/no-unused-function-argument.ts | 83 +++++++++++++++++++ src/index.ts | 6 ++ 3 files changed, 149 insertions(+) create mode 100644 src/agent/no-unused-function-argument.spec.ts create mode 100644 src/agent/no-unused-function-argument.ts diff --git a/src/agent/no-unused-function-argument.spec.ts b/src/agent/no-unused-function-argument.spec.ts new file mode 100644 index 0000000..076ac1d --- /dev/null +++ b/src/agent/no-unused-function-argument.spec.ts @@ -0,0 +1,60 @@ +// agent/no-unused-function-argument.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-unused-function-argument'; +import createTester from '../ts-tester.test'; + +createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'remove unused function arguments - first argument', + code: `function doSomething(a: string, b: number, c: unknown) { console.log(b,c); }`, + output: `function doSomething(b: number, c: unknown) { console.log(b,c); }`, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + { + name: 'remove unused function arguments - last argument', + code: `function doSomething(a: string, b: number, c: unknown) { console.log(a,b); }`, + output: `function doSomething(a: string, b: number) { console.log(a,b); }`, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + { + name: 'remove unused function arguments - middle argument', + code: ` + function doSomething(a: string, b: number, c: unknown) { + console.log(a,c); + } + `, + output: ` + function doSomething(a: string, c: unknown) { + console.log(a,c); + } + `, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + { + name: 'remove unused function arguments - first and second arguments', + code: `function doSomething(a: string, b: number, c: unknown) { console.log(c); }`, + output: `function doSomething(c: unknown) { console.log(c); }`, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + { + name: 'remove unused function arguments - all arguments', + code: `function doSomething(a: string, b: number, c: unknown) {}`, + output: `function doSomething() {}`, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + { + name: 'remove unused function arguments - first and last arguments', + code: `function doSomething(a: string, b: number, c: unknown) { console.log(b); }`, + output: `function doSomething(b: number) { console.log(b); }`, + errors: [{ messageId: 'removeUnusedFunctionArguments' }], + }, + ], +}); diff --git a/src/agent/no-unused-function-argument.ts b/src/agent/no-unused-function-argument.ts new file mode 100644 index 0000000..df705e7 --- /dev/null +++ b/src/agent/no-unused-function-argument.ts @@ -0,0 +1,83 @@ +// agent/no-unused-function-argument.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'no-unused-function-argument'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Remove unused function arguments.', + }, + messages: { + removeUnusedFunctionArguments: 'Removing unused function arguments.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + // Function to check if a parameter is used in the function body + function isParameterUsed(parameter: TSESTree.Parameter, body: TSESTree.BlockStatement) { + if (parameter.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return true; + } + const parameterName = parameter.name; + return sourceCode.getScope(body).references.some((ref) => ref.identifier.name === parameterName); + } + + return { + FunctionDeclaration(functionDeclaration: TSESTree.FunctionDeclaration) { + try { + const parameters = functionDeclaration.params; + if (parameters.length === 0) { + return; + } + + const body = functionDeclaration.body; + const parametersToKeep = parameters.filter((parameter) => isParameterUsed(parameter, body)); + + const updatedParameters = parametersToKeep.map((parameter) => sourceCode.getText(parameter)).join(', '); + context.report({ + node: functionDeclaration, + messageId: 'removeUnusedFunctionArguments', + fix(fixer) { + const firstParameter = parameters[0]; + const lastParameter = parameters.at(-1); + assert.ok(firstParameter !== undefined && lastParameter !== undefined); + return fixer.replaceTextRange([firstParameter.range[0], lastParameter.range[1]], updatedParameters); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: functionDeclaration, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 341173f..2b128fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,9 @@ import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-m import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-code'; +import noUnusedFunctionArguments, { + ruleId as noUnusedFunctionArgumentsRuleId, +} from './agent/no-unused-function-argument'; import requireFixedServicesImport, { ruleId as requireFixedServicesImportRuleId, } from './require-fixed-services-import'; @@ -65,6 +68,7 @@ export default { [noDuplicatedImportsRuleId]: noDuplicatedImports, [requireFixedServicesImportRuleId]: requireFixedServicesImport, [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, + [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, }, configs: { all: { @@ -117,6 +121,7 @@ export default { [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', }, }, 'agent-phase-2-production': { @@ -130,6 +135,7 @@ export default { [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', }, }, }, From 4d473b0513921e98c907193a5b892167e8404ced Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 25 Sep 2024 22:49:53 -0400 Subject: [PATCH 058/115] fix no-unused-function-argument --- src/agent/no-unused-function-argument.spec.ts | 7 ++++++- src/agent/no-unused-function-argument.ts | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/agent/no-unused-function-argument.spec.ts b/src/agent/no-unused-function-argument.spec.ts index 076ac1d..9f5e85d 100644 --- a/src/agent/no-unused-function-argument.spec.ts +++ b/src/agent/no-unused-function-argument.spec.ts @@ -10,7 +10,12 @@ import rule, { ruleId } from './no-unused-function-argument'; import createTester from '../ts-tester.test'; createTester().run(ruleId, rule, { - valid: [], + valid: [ + { + name: 'all function arguments are used', + code: `function doSomething(a: string, b: number, c: unknown) { console.log(a,b,c); }`, + }, + ], invalid: [ { name: 'remove unused function arguments - first argument', diff --git a/src/agent/no-unused-function-argument.ts b/src/agent/no-unused-function-argument.ts index df705e7..3d4deea 100644 --- a/src/agent/no-unused-function-argument.ts +++ b/src/agent/no-unused-function-argument.ts @@ -51,6 +51,9 @@ const rule = createRule({ const body = functionDeclaration.body; const parametersToKeep = parameters.filter((parameter) => isParameterUsed(parameter, body)); + if (parametersToKeep.length === parameters.length) { + return; + } const updatedParameters = parametersToKeep.map((parameter) => sourceCode.getText(parameter)).join(', '); context.report({ From 89e863a2b23cbdb97ee34b93d1079ed45221425c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 00:30:03 -0400 Subject: [PATCH 059/115] handle deeply reference function parameter --- src/agent/no-unused-function-argument.spec.ts | 32 ++++++++++++++++--- src/agent/no-unused-function-argument.ts | 30 +++++++++++------ 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/agent/no-unused-function-argument.spec.ts b/src/agent/no-unused-function-argument.spec.ts index 9f5e85d..627d5f0 100644 --- a/src/agent/no-unused-function-argument.spec.ts +++ b/src/agent/no-unused-function-argument.spec.ts @@ -15,24 +15,46 @@ createTester().run(ruleId, rule, { name: 'all function arguments are used', code: `function doSomething(a: string, b: number, c: unknown) { console.log(a,b,c); }`, }, + { + name: 'argument referenced not directly in function body can be associated', + code: ` + function doSomething(a: string) { + try { + console.log(a); + } catch (error) { + // + } + } + `, + }, + { + name: 'argument referenced in child function declaration', + code: ` + function doSomething(a: string) { + function doSomethingElse() { + console.log(a); + } + } + `, + }, ], invalid: [ { name: 'remove unused function arguments - first argument', - code: `function doSomething(a: string, b: number, c: unknown) { console.log(b,c); }`, + code: `function doSomething(a: string, b: number, c: unknown,) { console.log(b,c); }`, output: `function doSomething(b: number, c: unknown) { console.log(b,c); }`, errors: [{ messageId: 'removeUnusedFunctionArguments' }], }, { name: 'remove unused function arguments - last argument', - code: `function doSomething(a: string, b: number, c: unknown) { console.log(a,b); }`, + code: `function doSomething(a: string, b: number, c: unknown,) { console.log(a,b); }`, output: `function doSomething(a: string, b: number) { console.log(a,b); }`, errors: [{ messageId: 'removeUnusedFunctionArguments' }], }, { name: 'remove unused function arguments - middle argument', code: ` - function doSomething(a: string, b: number, c: unknown) { + function doSomething(a: string, b: number, c: unknown,) { console.log(a,c); } `, @@ -45,13 +67,13 @@ createTester().run(ruleId, rule, { }, { name: 'remove unused function arguments - first and second arguments', - code: `function doSomething(a: string, b: number, c: unknown) { console.log(c); }`, + code: `function doSomething(a: string, b: number, c: unknown,) { console.log(c); }`, output: `function doSomething(c: unknown) { console.log(c); }`, errors: [{ messageId: 'removeUnusedFunctionArguments' }], }, { name: 'remove unused function arguments - all arguments', - code: `function doSomething(a: string, b: number, c: unknown) {}`, + code: `function doSomething(a: string, b: number, c: unknown,) {}`, output: `function doSomething() {}`, errors: [{ messageId: 'removeUnusedFunctionArguments' }], }, diff --git a/src/agent/no-unused-function-argument.ts b/src/agent/no-unused-function-argument.ts index 3d4deea..5c890df 100644 --- a/src/agent/no-unused-function-argument.ts +++ b/src/agent/no-unused-function-argument.ts @@ -7,6 +7,7 @@ */ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope } from '@typescript-eslint/utils/ts-eslint'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; @@ -32,13 +33,11 @@ const rule = createRule({ create(context) { const sourceCode = context.sourceCode; - // Function to check if a parameter is used in the function body - function isParameterUsed(parameter: TSESTree.Parameter, body: TSESTree.BlockStatement) { - if (parameter.type !== TSESTree.AST_NODE_TYPES.Identifier) { - return true; - } - const parameterName = parameter.name; - return sourceCode.getScope(body).references.some((ref) => ref.identifier.name === parameterName); + function isParameterUsed(parameter: TSESTree.Identifier, scope: Scope.Scope): boolean { + return ( + scope.references.some((ref) => ref.identifier.name === parameter.name) || + scope.childScopes.some((childScope) => isParameterUsed(parameter, childScope)) + ); } return { @@ -49,8 +48,11 @@ const rule = createRule({ return; } - const body = functionDeclaration.body; - const parametersToKeep = parameters.filter((parameter) => isParameterUsed(parameter, body)); + const functionScope = sourceCode.getScope(functionDeclaration); + const parametersToKeep = parameters.filter( + (parameter) => + parameter.type !== TSESTree.AST_NODE_TYPES.Identifier || isParameterUsed(parameter, functionScope), + ); if (parametersToKeep.length === parameters.length) { return; } @@ -63,7 +65,15 @@ const rule = createRule({ const firstParameter = parameters[0]; const lastParameter = parameters.at(-1); assert.ok(firstParameter !== undefined && lastParameter !== undefined); - return fixer.replaceTextRange([firstParameter.range[0], lastParameter.range[1]], updatedParameters); + const tokenAfterParameters = sourceCode.getTokenAfter(lastParameter); + + return fixer.replaceTextRange( + [ + firstParameter.range[0], + tokenAfterParameters?.value === ',' ? tokenAfterParameters.range[1] : lastParameter.range[1], + ], + updatedParameters, + ); }, }); } catch (error) { From d998840148f51c740bd29760abd78bf226239781 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 01:51:18 -0400 Subject: [PATCH 060/115] add rule: no-unused-service-variable --- src/agent/no-unused-service-variable.spec.ts | 34 ++++++++ src/agent/no-unused-service-variable.ts | 92 ++++++++++++++++++++ src/index.ts | 4 + 3 files changed, 130 insertions(+) create mode 100644 src/agent/no-unused-service-variable.spec.ts create mode 100644 src/agent/no-unused-service-variable.ts diff --git a/src/agent/no-unused-service-variable.spec.ts b/src/agent/no-unused-service-variable.spec.ts new file mode 100644 index 0000000..a43d907 --- /dev/null +++ b/src/agent/no-unused-service-variable.spec.ts @@ -0,0 +1,34 @@ +// agent/no-unused-service-variable.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-unused-service-variable'; +import createTester from '../ts-tester.test'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'used service variable', + code: ` + const someService = fixture.config.service.xxx(EMPTY_CONTEXT); + await someService.doSomething(); + `, + }, + { + name: 'non-service variable', + code: `const notService = stuff;`, + }, + ], + invalid: [ + { + name: 'remove unused service variable', + code: `const someService = fixture.config.service.xxx(EMPTY_CONTEXT);`, + output: ``, + errors: [{ messageId: 'removeUnusedServiceVariables' }], + }, + ], +}); diff --git a/src/agent/no-unused-service-variable.ts b/src/agent/no-unused-service-variable.ts new file mode 100644 index 0000000..51557b8 --- /dev/null +++ b/src/agent/no-unused-service-variable.ts @@ -0,0 +1,92 @@ +// agent/no-unused-service-variable.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope } from '@typescript-eslint/utils/ts-eslint'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; +import { getEnclosingScopeNode } from '../library/ts-tree'; + +export const ruleId = 'no-unused-service-variable'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Remove unused service variables.', + }, + messages: { + removeUnusedServiceVariables: 'Removing unused service variables.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + + function isVariableUsed(variableIdentifier: TSESTree.Identifier, scope: Scope.Scope): boolean { + const variable = scope.variables.find((variableToCheck) => variableToCheck.name === variableIdentifier.name); + return variable !== undefined && variable.references.length > 1; + } + + return { + VariableDeclaration(variableDeclaration: TSESTree.VariableDeclaration) { + try { + if ( + variableDeclaration.declarations.length !== 1 || + !sourceCode.getText(variableDeclaration).includes('.service.') + ) { + return; + } + + const enclosingScopeNode = getEnclosingScopeNode(variableDeclaration); + assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); + + const declarator = variableDeclaration.declarations[0]; + assert.ok(declarator, 'variable declaration is undefined'); + if (declarator.id.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return; + } + + const scope = scopeManager?.acquire(enclosingScopeNode); + assert.ok(scope, 'variable declaration is undefined'); + if (isVariableUsed(declarator.id, scope)) { + return; + } + + context.report({ + node: variableDeclaration, + messageId: 'removeUnusedServiceVariables', + fix(fixer) { + return fixer.remove(variableDeclaration); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: variableDeclaration, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 2b128fe..01a8f7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-co import noUnusedFunctionArguments, { ruleId as noUnusedFunctionArgumentsRuleId, } from './agent/no-unused-function-argument'; +import noUnusedServiceVariables, { ruleId as noUnusedServiceVariablesRuleId } from './agent/no-unused-service-variable'; import requireFixedServicesImport, { ruleId as requireFixedServicesImportRuleId, } from './require-fixed-services-import'; @@ -69,6 +70,7 @@ export default { [requireFixedServicesImportRuleId]: requireFixedServicesImport, [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, + [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, }, configs: { all: { @@ -122,6 +124,7 @@ export default { [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', }, }, 'agent-phase-2-production': { @@ -136,6 +139,7 @@ export default { [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', }, }, }, From 2d06df805db0ce1057f5e0b95d36705722533f09 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 08:58:37 -0400 Subject: [PATCH 061/115] add rule: no-unused-imports --- src/agent/no-unused-imports.spec.ts | 94 +++++++++++++++++++++++++ src/agent/no-unused-imports.ts | 102 ++++++++++++++++++++++++++++ src/index.ts | 4 ++ 3 files changed, 200 insertions(+) create mode 100644 src/agent/no-unused-imports.spec.ts create mode 100644 src/agent/no-unused-imports.ts diff --git a/src/agent/no-unused-imports.spec.ts b/src/agent/no-unused-imports.spec.ts new file mode 100644 index 0000000..52aaac7 --- /dev/null +++ b/src/agent/no-unused-imports.spec.ts @@ -0,0 +1,94 @@ +// agent/no-unused-imports.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './no-unused-imports'; +import createTester from '../ts-tester.test'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'import not used but not from desired module', + code: `import { SomeType } from 'some-module';`, + }, + { + name: 'import used on top level', + code: ` + import type { Configuration } from '@checkdigit/serve-runtime'; + let config: Configuration; + `, + }, + { + name: 'multiple imports used on top level', + code: ` + import { type Configuration, EMPTY_CONTEXT } from '@checkdigit/fixtures'; + let config: Configuration; + let context: EMPTY_CONTEXT; + `, + }, + { + name: 'import used in function declaration', + code: ` + import type { Configuration } from '@checkdigit/serve-runtime'; + export default async function(config: Configuration): Promise { + // do something + } + `, + }, + { + name: 'import used in nested scope', + code: ` + import type { Configuration } from '@checkdigit/serve-runtime'; + export default async function(): Promise { + try { + let config: Configuration; + } catch (error) { + // do something + } + } + `, + }, + ], + invalid: [ + { + name: 'remove unused import', + code: `import type { Configuration } from '@checkdigit/serve-runtime';`, + output: ``, + errors: [{ messageId: 'removeUnusedImports' }], + }, + { + name: 'remove multiple unused imports', + code: `import type { Configuration, Fixture } from '@checkdigit/fixture';`, + output: ``, + errors: [{ messageId: 'removeUnusedImports' }], + }, + { + name: 'remove partial unused import - type only', + code: ` + import type { Configuration, Fixture } from '@checkdigit/fixture'; + let config: Configuration; + `, + output: ` + import type { Configuration } from '@checkdigit/fixture'; + let config: Configuration; + `, + errors: [{ messageId: 'removeUnusedImports' }], + }, + { + name: 'remove partial unused import - mixed type and value', + code: ` + import { EMPTY_CONTEXT, type Fixture } from '@checkdigit/fixture'; + let fixture: Fixture; + `, + output: ` + import { type Fixture } from '@checkdigit/fixture'; + let fixture: Fixture; + `, + errors: [{ messageId: 'removeUnusedImports' }], + }, + ], +}); diff --git a/src/agent/no-unused-imports.ts b/src/agent/no-unused-imports.ts new file mode 100644 index 0000000..228028b --- /dev/null +++ b/src/agent/no-unused-imports.ts @@ -0,0 +1,102 @@ +// agent/no-unused-imports.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope } from '@typescript-eslint/utils/ts-eslint'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'no-unused-imports'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Remove unused imports.', + }, + messages: { + removeUnusedImports: 'Removing unused imports.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + function isImportUsed(specifier: TSESTree.ImportClause, scope: Scope.Scope): boolean { + return ( + specifier.type !== TSESTree.AST_NODE_TYPES.ImportSpecifier || + scope.references.some((ref) => ref.identifier.name === specifier.local.name) || + scope.childScopes.some((childScope) => isImportUsed(specifier, childScope)) + ); + } + + return { + ImportDeclaration(importDeclaration) { + try { + const moduleName = importDeclaration.source.value; + if ( + !importDeclaration.specifiers.every( + (specifier) => specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier, + ) || + // [TODO:] move to meta schema + !['@checkdigit/serve-runtime', '@checkdigit/fixture'].includes(moduleName) + ) { + return; + } + + const originalSpecifiers = importDeclaration.specifiers; + const scope = sourceCode.getScope(importDeclaration); + const usedSpecifiers = originalSpecifiers.filter((specifier) => isImportUsed(specifier, scope)); + if (usedSpecifiers.length === originalSpecifiers.length) { + return; + } + + if (usedSpecifiers.length === 0) { + context.report({ + messageId: 'removeUnusedImports', + node: importDeclaration, + *fix(fixer) { + yield fixer.remove(importDeclaration); + }, + }); + return; + } + + const usedSpecifierTexts = usedSpecifiers.map((specifier) => sourceCode.getText(specifier)); + const updatedImportDeclaration = `import ${importDeclaration.importKind === 'type' ? 'type ' : ''}{ ${usedSpecifierTexts.join(', ')} } from '${moduleName}';`; + + context.report({ + messageId: 'removeUnusedImports', + node: importDeclaration, + *fix(fixer) { + yield fixer.replaceText(importDeclaration, updatedImportDeclaration); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: importDeclaration, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 01a8f7d..3d732c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import noStatusCode, { ruleId as noStatusCodeRuleId } from './agent/no-status-co import noUnusedFunctionArguments, { ruleId as noUnusedFunctionArgumentsRuleId, } from './agent/no-unused-function-argument'; +import noUnusedImports, { ruleId as noUnusedImportsRuleId } from './agent/no-unused-imports'; import noUnusedServiceVariables, { ruleId as noUnusedServiceVariablesRuleId } from './agent/no-unused-service-variable'; import requireFixedServicesImport, { ruleId as requireFixedServicesImportRuleId, @@ -71,6 +72,7 @@ export default { [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, + [noUnusedImportsRuleId]: noUnusedImports, }, configs: { all: { @@ -125,6 +127,7 @@ export default { [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', }, }, 'agent-phase-2-production': { @@ -140,6 +143,7 @@ export default { [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', }, }, }, From 320f53c5f56a30caf68c3c96860f5774d8fec379 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 11:19:38 -0400 Subject: [PATCH 062/115] add rule: fix-function-call-arguments --- src/agent/fix-function-call-arguments.spec.ts | 58 +++++++++ src/agent/fix-function-call-arguments.ts | 112 ++++++++++++++++++ src/index.ts | 6 + 3 files changed, 176 insertions(+) create mode 100644 src/agent/fix-function-call-arguments.spec.ts create mode 100644 src/agent/fix-function-call-arguments.ts diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts new file mode 100644 index 0000000..7b8f0c3 --- /dev/null +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -0,0 +1,58 @@ +// agent/fix-function-call-arguments.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './fix-function-call-arguments'; +import createTester from '../ts-tester.test'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'correct function call', + code: ` + function doSomething(id:string, count:number) { + // do something + }; + doSomething('abc', 1); + `, + }, + ], + invalid: [ + { + name: 'remove incompatible function arguments', + code: ` + function doSomething(id:string, count:number) { + // do something + }; + doSomething({}, 'abc', 1); + `, + output: ` + function doSomething(id:string, count:number) { + // do something + }; + doSomething('abc', 1); + `, + errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], + }, + { + name: 'remove incompatible function arguments - handle the ending comma', + code: ` + function doSomething(id:string, count:number) { + // do something + }; + doSomething({},); + `, + output: ` + function doSomething(id:string, count:number) { + // do something + }; + doSomething(); + `, + errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], + }, + ], +}); diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts new file mode 100644 index 0000000..caabc33 --- /dev/null +++ b/src/agent/fix-function-call-arguments.ts @@ -0,0 +1,112 @@ +// agent/fix-function-call-arguments.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'fix-function-call-arguments'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Remove incompatible function arguments.', + }, + messages: { + removeIncompatibleFunctionArguments: 'Removing incompatible function arguments.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + const sourceCode = context.sourceCode; + + return { + CallExpression(callExpression) { + try { + const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee); + const calleeType = typeChecker.getTypeAtLocation(calleeTsNode); + const signature = calleeType.getCallSignatures()[0]; + if (!signature) { + return; + } + + const signatureParameters = signature.getParameters(); + const expectedArgsCount = signatureParameters.length; + const providedArgs = callExpression.arguments; + const providedArgsCount = providedArgs.length; + if (providedArgsCount === 0 || providedArgsCount === expectedArgsCount) { + return; + } + const argsToKeep: TSESTree.CallExpressionArgument[] = []; + + let parameterIndex = 0; + for (const arg of providedArgs) { + const currentExpectedArg = signatureParameters[parameterIndex]; + assert.ok(currentExpectedArg, 'Expected argument not found.'); + + const expectedType = typeChecker.getTypeOfSymbol(currentExpectedArg); + typeChecker.typeToString(expectedType); + const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(arg)); + typeChecker.typeToString(actualType); + // @ts-expect-error: internal API + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { + argsToKeep.push(arg); + parameterIndex++; + } + } + + if (argsToKeep.length === providedArgsCount) { + return; + } + + const firstParameter = providedArgs[0]; + const lastParameter = providedArgs.at(-1); + assert.ok(firstParameter !== undefined && lastParameter !== undefined); + const tokenAfterParameters = sourceCode.getTokenAfter(lastParameter); + + context.report({ + node: callExpression, + messageId: 'removeIncompatibleFunctionArguments', + fix(fixer) { + return fixer.replaceTextRange( + [ + firstParameter.range[0], + tokenAfterParameters?.value === ',' ? tokenAfterParameters.range[1] : lastParameter.range[1], + ], + argsToKeep.map((arg) => sourceCode.getText(arg)).join(', '), + ); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: callExpression, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 3d732c4..9ea15f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,9 @@ import fetchResponseHeaderGetter, { ruleId as fetchResponseHeaderGetterRuleId, } from './agent/fetch-response-header-getter'; import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; +import fixFunctionCallArguments, { + ruleId as fixFunctionCallArgumentsRuleId, +} from './agent/fix-function-call-arguments'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; @@ -73,6 +76,7 @@ export default { [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, [noUnusedImportsRuleId]: noUnusedImports, + [fixFunctionCallArgumentsRuleId]: fixFunctionCallArguments, }, configs: { all: { @@ -128,6 +132,7 @@ export default { [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', }, }, 'agent-phase-2-production': { @@ -144,6 +149,7 @@ export default { [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', }, }, }, From ea1d23260e8dd2ea9a4a93fd8faba39c3af4715e Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 15:55:07 -0400 Subject: [PATCH 063/115] fix fix-function-call-arguments - handle the case that the target function has no argument --- src/agent/fix-function-call-arguments.spec.ts | 16 ++++++++++ src/agent/fix-function-call-arguments.ts | 30 +++++++++++-------- src/index.ts | 12 ++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts index 7b8f0c3..2acc03e 100644 --- a/src/agent/fix-function-call-arguments.spec.ts +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -54,5 +54,21 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, + { + name: 'remove incompatible function arguments - original function call has no arguments', + code: ` + function doSomething() { + // do something + }; + doSomething({},'abc', 1); + `, + output: ` + function doSomething() { + // do something + }; + doSomething(); + `, + errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], + }, ], }); diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index caabc33..5dd9064 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -53,20 +53,24 @@ const rule = createRule({ } const argsToKeep: TSESTree.CallExpressionArgument[] = []; - let parameterIndex = 0; - for (const arg of providedArgs) { - const currentExpectedArg = signatureParameters[parameterIndex]; - assert.ok(currentExpectedArg, 'Expected argument not found.'); + if (expectedArgsCount > 0) { + let parameterIndex = 0; + for (const arg of providedArgs) { + const currentExpectedArg = signatureParameters[parameterIndex]; + assert.ok(currentExpectedArg, 'Expected argument not found.'); - const expectedType = typeChecker.getTypeOfSymbol(currentExpectedArg); - typeChecker.typeToString(expectedType); - const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(arg)); - typeChecker.typeToString(actualType); - // @ts-expect-error: internal API - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { - argsToKeep.push(arg); - parameterIndex++; + const expectedType = typeChecker.getTypeOfSymbol(currentExpectedArg); + // eslint-disable-next-line no-console + console.log(currentExpectedArg.escapedName, typeChecker.typeToString(expectedType)); + typeChecker.typeToString(expectedType); + const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(arg)); + typeChecker.typeToString(actualType); + // @ts-expect-error: internal API + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { + argsToKeep.push(arg); + parameterIndex++; + } } } diff --git a/src/index.ts b/src/index.ts index 9ea15f2..3448e76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,6 +97,18 @@ export default { [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + [`@checkdigit/${noMappedResponseRuleId}`]: 'off', + [`@checkdigit/${addUrlDomainRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', + [`@checkdigit/${noStatusCodeRuleId}`]: 'off', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', + [`@checkdigit/${fetchThenRuleId}`]: 'off', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', }, }, recommended: { From 7f3566bb4639b87c8416883c0f298285aaa60748 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 17:55:55 -0400 Subject: [PATCH 064/115] fix fix-function-call-arguments - ignore complex functions from 3rd party module, added debug info --- package-lock.json | 33 +++++++++++++---- package.json | 2 ++ src/agent/fix-function-call-arguments.spec.ts | 8 +++++ src/agent/fix-function-call-arguments.ts | 35 ++++++++++++++----- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec73ed1..3f65c59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "dependencies": { "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.7", "ts-api-utils": "^1.3.0" }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", + "@types/debug": "^4.1.12", "@types/eslint": "8.56.10", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", @@ -1890,6 +1892,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -1963,6 +1975,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", @@ -3061,12 +3080,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6016,9 +6035,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/natural-compare": { diff --git a/package.json b/package.json index 1868960..dac4bc0 100644 --- a/package.json +++ b/package.json @@ -64,12 +64,14 @@ "dependencies": { "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.7", "ts-api-utils": "^1.3.0" }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", + "@types/debug": "^4.1.12", "@types/eslint": "8.56.10", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts index 2acc03e..92de8a4 100644 --- a/src/agent/fix-function-call-arguments.spec.ts +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -20,6 +20,14 @@ createTester().run(ruleId, rule, { doSomething('abc', 1); `, }, + { + name: 'regular node assertion call should not be affected', + code: ` + import { strict as assert } from 'node:assert'; + const valueA = 'abc'; + assert.equal(valueA, 'abc'); + `, + }, ], invalid: [ { diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 5dd9064..4d66135 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -8,11 +8,14 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { strict as assert } from 'node:assert'; +import debug from 'debug'; import getDocumentationUrl from '../get-documentation-url'; +import { getParent } from '../library/ts-tree'; export const ruleId = 'fix-function-call-arguments'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const log = debug('eslint-plugin:fix-function-call-arguments'); const rule = createRule({ name: ruleId, @@ -36,18 +39,30 @@ const rule = createRule({ return { CallExpression(callExpression) { + // ignore calls like `foo.bar()` which are likely to be 3rd party module calls + // we only focus on calls against local functions or functions imported from the same module + if (getParent(callExpression)?.type === TSESTree.AST_NODE_TYPES.MemberExpression) { + return; + } + + log('==================================='); + log('callExpression:', sourceCode.getText(callExpression)); try { const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee); const calleeType = typeChecker.getTypeAtLocation(calleeTsNode); const signature = calleeType.getCallSignatures()[0]; - if (!signature) { + if ( + !signature || + // ignore complex signatures + (signature.typeParameters !== undefined && signature.typeParameters.length > 0) + ) { return; } const signatureParameters = signature.getParameters(); - const expectedArgsCount = signatureParameters.length; - const providedArgs = callExpression.arguments; - const providedArgsCount = providedArgs.length; + const expectedArgsCount = signatureParameters.length; /*?*/ + const providedArgs = callExpression.arguments; /*?*/ + const providedArgsCount = providedArgs.length; /*?*/ if (providedArgsCount === 0 || providedArgsCount === expectedArgsCount) { return; } @@ -60,16 +75,20 @@ const rule = createRule({ assert.ok(currentExpectedArg, 'Expected argument not found.'); const expectedType = typeChecker.getTypeOfSymbol(currentExpectedArg); - // eslint-disable-next-line no-console - console.log(currentExpectedArg.escapedName, typeChecker.typeToString(expectedType)); - typeChecker.typeToString(expectedType); const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(arg)); - typeChecker.typeToString(actualType); + + // eslint-disable-next-line no-console + log('expected type:', currentExpectedArg.escapedName, typeChecker.typeToString(expectedType)); + // eslint-disable-next-line no-console + log('actual type:', sourceCode.getText(arg), typeChecker.typeToString(actualType)); // @ts-expect-error: internal API // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { argsToKeep.push(arg); parameterIndex++; + log('matched'); + } else { + log('not matched'); } } } From 7976a80e827a7f152ace8b39a49d227c2e4380b2 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 18:22:05 -0400 Subject: [PATCH 065/115] fix fix-function-call-arguments continued --- src/agent/fix-function-call-arguments.spec.ts | 4 ++++ src/agent/fix-function-call-arguments.ts | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts index 92de8a4..ff8ab92 100644 --- a/src/agent/fix-function-call-arguments.spec.ts +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -20,6 +20,10 @@ createTester().run(ruleId, rule, { doSomething('abc', 1); `, }, + { + name: 'regular node library call should not be affected', + code: `Buffer.from('some data', 'base64')`, + }, { name: 'regular node assertion call should not be affected', code: ` diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 4d66135..ed2bb74 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -41,16 +41,25 @@ const rule = createRule({ CallExpression(callExpression) { // ignore calls like `foo.bar()` which are likely to be 3rd party module calls // we only focus on calls against local functions or functions imported from the same module - if (getParent(callExpression)?.type === TSESTree.AST_NODE_TYPES.MemberExpression) { + if (callExpression.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression) { return; } - log('==================================='); + log('===== file name:', context.filename); log('callExpression:', sourceCode.getText(callExpression)); try { const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee); const calleeType = typeChecker.getTypeAtLocation(calleeTsNode); - const signature = calleeType.getCallSignatures()[0]; + + const signatures = calleeType.getCallSignatures(); + if ( + // ignore complex signatures with overloads + signatures.length > 1 + ) { + return; + } + + const signature = signatures[0]; if ( !signature || // ignore complex signatures @@ -60,9 +69,9 @@ const rule = createRule({ } const signatureParameters = signature.getParameters(); - const expectedArgsCount = signatureParameters.length; /*?*/ - const providedArgs = callExpression.arguments; /*?*/ - const providedArgsCount = providedArgs.length; /*?*/ + const expectedArgsCount = signatureParameters.length; + const providedArgs = callExpression.arguments; + const providedArgsCount = providedArgs.length; if (providedArgsCount === 0 || providedArgsCount === expectedArgsCount) { return; } From a289f01e2f97be084ab8a410072d7ee4110ae37e Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 18:33:15 -0400 Subject: [PATCH 066/115] fix fix-function-call-arguments continued - handle the case that the function has less parameters than provided --- src/agent/fix-function-call-arguments.spec.ts | 18 +++++- src/agent/fix-function-call-arguments.ts | 58 ++++++++++++------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts index ff8ab92..de089c1 100644 --- a/src/agent/fix-function-call-arguments.spec.ts +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -67,7 +67,7 @@ createTester().run(ruleId, rule, { errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, { - name: 'remove incompatible function arguments - original function call has no arguments', + name: 'remove incompatible function arguments - original function has no arguments', code: ` function doSomething() { // do something @@ -82,5 +82,21 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, + { + name: 'remove incompatible function arguments - original function has less arguments', + code: ` + function doSomething(id: string) { + // do something + }; + doSomething({},'abc', 1); + `, + output: ` + function doSomething(id: string) { + // do something + }; + doSomething('abc', 1); + `, + errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], + }, ], }); diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index ed2bb74..00f63c1 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -10,7 +10,6 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { strict as assert } from 'node:assert'; import debug from 'debug'; import getDocumentationUrl from '../get-documentation-url'; -import { getParent } from '../library/ts-tree'; export const ruleId = 'fix-function-call-arguments'; @@ -69,32 +68,49 @@ const rule = createRule({ } const signatureParameters = signature.getParameters(); - const expectedArgsCount = signatureParameters.length; - const providedArgs = callExpression.arguments; - const providedArgsCount = providedArgs.length; - if (providedArgsCount === 0 || providedArgsCount === expectedArgsCount) { + const expectedParametersCount = signatureParameters.length; + const actualParameters = callExpression.arguments; + const actualParametersCount = actualParameters.length; + if (actualParametersCount === 0 || actualParametersCount === expectedParametersCount) { return; } - const argsToKeep: TSESTree.CallExpressionArgument[] = []; + const parametersToKeep: TSESTree.CallExpressionArgument[] = []; + + if (expectedParametersCount > 0) { + let expectedParameterIndex = 0; + for (const [actualParameterIndex, actualParameter] of actualParameters.entries()) { + if (expectedParameterIndex >= expectedParametersCount) { + parametersToKeep.push(actualParameter); + continue; + } - if (expectedArgsCount > 0) { - let parameterIndex = 0; - for (const arg of providedArgs) { - const currentExpectedArg = signatureParameters[parameterIndex]; - assert.ok(currentExpectedArg, 'Expected argument not found.'); + const expectedParameter = signatureParameters[expectedParameterIndex]; + assert.ok(expectedParameter, 'Expected parameter not found.'); - const expectedType = typeChecker.getTypeOfSymbol(currentExpectedArg); - const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(arg)); + const expectedType = typeChecker.getTypeOfSymbol(expectedParameter); + const actualType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(actualParameter), + ); // eslint-disable-next-line no-console - log('expected type:', currentExpectedArg.escapedName, typeChecker.typeToString(expectedType)); + log( + 'expected type: #', + expectedParameterIndex, + expectedParameter.escapedName, + typeChecker.typeToString(expectedType), + ); // eslint-disable-next-line no-console - log('actual type:', sourceCode.getText(arg), typeChecker.typeToString(actualType)); + log( + 'actual type: #', + actualParameterIndex, + sourceCode.getText(actualParameter), + typeChecker.typeToString(actualType), + ); // @ts-expect-error: internal API // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { - argsToKeep.push(arg); - parameterIndex++; + parametersToKeep.push(actualParameter); + expectedParameterIndex++; log('matched'); } else { log('not matched'); @@ -102,12 +118,12 @@ const rule = createRule({ } } - if (argsToKeep.length === providedArgsCount) { + if (parametersToKeep.length === actualParametersCount) { return; } - const firstParameter = providedArgs[0]; - const lastParameter = providedArgs.at(-1); + const firstParameter = actualParameters[0]; + const lastParameter = actualParameters.at(-1); assert.ok(firstParameter !== undefined && lastParameter !== undefined); const tokenAfterParameters = sourceCode.getTokenAfter(lastParameter); @@ -120,7 +136,7 @@ const rule = createRule({ firstParameter.range[0], tokenAfterParameters?.value === ',' ? tokenAfterParameters.range[1] : lastParameter.range[1], ], - argsToKeep.map((arg) => sourceCode.getText(arg)).join(', '), + parametersToKeep.map((arg) => sourceCode.getText(arg)).join(', '), ); }, }); From 89b8171ae06f46381b7daddaa5a74d494f39c732 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 22:48:11 -0400 Subject: [PATCH 067/115] fix fix-function-call-arguments continued - custom options to restrict types to check --- src/agent/fix-function-call-arguments.spec.ts | 42 ++++-- src/agent/fix-function-call-arguments.ts | 121 ++++++++++-------- 2 files changed, 105 insertions(+), 58 deletions(-) diff --git a/src/agent/fix-function-call-arguments.spec.ts b/src/agent/fix-function-call-arguments.spec.ts index de089c1..3cac75f 100644 --- a/src/agent/fix-function-call-arguments.spec.ts +++ b/src/agent/fix-function-call-arguments.spec.ts @@ -6,26 +6,32 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import rule, { ruleId } from './fix-function-call-arguments'; +import rule, { type FixFunctionCallArgumentsRuleOptions, ruleId } from './fix-function-call-arguments'; import createTester from '../ts-tester.test'; +const testOptions: FixFunctionCallArgumentsRuleOptions = { typesToCheck: ['string', 'number', 'object'] }; createTester().run(ruleId, rule, { valid: [ { name: 'correct function call', + options: [testOptions], code: ` function doSomething(id:string, count:number) { // do something }; - doSomething('abc', 1); + const param1: string = 'abc'; + const param2: number = 2; + doSomething(param1, param2); `, }, { name: 'regular node library call should not be affected', + options: [testOptions], code: `Buffer.from('some data', 'base64')`, }, { name: 'regular node assertion call should not be affected', + options: [testOptions], code: ` import { strict as assert } from 'node:assert'; const valueA = 'abc'; @@ -36,65 +42,85 @@ createTester().run(ruleId, rule, { invalid: [ { name: 'remove incompatible function arguments', + options: [testOptions], code: ` function doSomething(id:string, count:number) { // do something }; - doSomething({}, 'abc', 1); + const param1: number = 1; + const param2: string = 'abc'; + const param3: number = 2; + doSomething(param1, param2, param3); `, output: ` function doSomething(id:string, count:number) { // do something }; - doSomething('abc', 1); + const param1: number = 1; + const param2: string = 'abc'; + const param3: number = 2; + doSomething(param2, param3); `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, { name: 'remove incompatible function arguments - handle the ending comma', + options: [testOptions], code: ` function doSomething(id:string, count:number) { // do something }; - doSomething({},); + const param1: number = 1; + doSomething(param1,); `, output: ` function doSomething(id:string, count:number) { // do something }; + const param1: number = 1; doSomething(); `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, { name: 'remove incompatible function arguments - original function has no arguments', + options: [testOptions], code: ` function doSomething() { // do something }; - doSomething({},'abc', 1); + const param1: number = 1; + doSomething(param1); `, output: ` function doSomething() { // do something }; + const param1: number = 1; doSomething(); `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, { name: 'remove incompatible function arguments - original function has less arguments', + options: [testOptions], code: ` function doSomething(id: string) { // do something }; - doSomething({},'abc', 1); + const param1: number = 1; + const param2: string = 'abc'; + const param3: number = 2; + doSomething(param1, param2, param3); `, output: ` function doSomething(id: string) { // do something }; - doSomething('abc', 1); + const param1: number = 1; + const param2: string = 'abc'; + const param3: number = 2; + doSomething(param2); `, errors: [{ messageId: 'removeIncompatibleFunctionArguments' }], }, diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 00f63c1..8fd45ad 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -13,6 +13,13 @@ import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'fix-function-call-arguments'; +export interface FixFunctionCallArgumentsRuleOptions { + typesToCheck: string[]; +} +const DEFAULT_OPTIONS = { + typesToCheck: ['Configuration', 'EMPTY_CONTEXT', 'Fixture'], +}; + const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); const log = debug('eslint-plugin:fix-function-call-arguments'); @@ -28,10 +35,25 @@ const rule = createRule({ unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', - schema: [], + schema: [ + { + type: 'object', + properties: { + typesToCheck: { + description: 'Text representation of the types of which the function call parameters will be examine', + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], }, - defaultOptions: [], + defaultOptions: [DEFAULT_OPTIONS], create(context) { + const { typesToCheck } = context.options[0] as FixFunctionCallArgumentsRuleOptions; const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); const sourceCode = context.sourceCode; @@ -51,70 +73,69 @@ const rule = createRule({ const calleeType = typeChecker.getTypeAtLocation(calleeTsNode); const signatures = calleeType.getCallSignatures(); - if ( + if (signatures.length > 1) { // ignore complex signatures with overloads - signatures.length > 1 - ) { return; } const signature = signatures[0]; - if ( - !signature || - // ignore complex signatures - (signature.typeParameters !== undefined && signature.typeParameters.length > 0) - ) { + assert.ok(signature, 'Signature not found.'); + if (signature.typeParameters !== undefined && signature.typeParameters.length > 0) { + // ignore complex signatures with type parameters return; } - const signatureParameters = signature.getParameters(); - const expectedParametersCount = signatureParameters.length; + log('signature:', signature.getDeclaration().getText()); + const expectedParameters = signature.getParameters(); + log( + 'expected parameters:', + expectedParameters.map((expectedParameter) => + typeChecker.typeToString(typeChecker.getTypeOfSymbol(expectedParameter)), + ), + ); + const expectedParametersCount = expectedParameters.length; const actualParameters = callExpression.arguments; const actualParametersCount = actualParameters.length; if (actualParametersCount === 0 || actualParametersCount === expectedParametersCount) { return; } - const parametersToKeep: TSESTree.CallExpressionArgument[] = []; - - if (expectedParametersCount > 0) { - let expectedParameterIndex = 0; - for (const [actualParameterIndex, actualParameter] of actualParameters.entries()) { - if (expectedParameterIndex >= expectedParametersCount) { - parametersToKeep.push(actualParameter); - continue; - } - - const expectedParameter = signatureParameters[expectedParameterIndex]; - assert.ok(expectedParameter, 'Expected parameter not found.'); - const expectedType = typeChecker.getTypeOfSymbol(expectedParameter); - const actualType = typeChecker.getTypeAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(actualParameter), - ); + const parametersToKeep: TSESTree.CallExpressionArgument[] = []; + let expectedParameterIndex = 0; + for (const [actualParameterIndex, actualParameter] of actualParameters.entries()) { + // eslint-disable-next-line max-depth + if (expectedParameterIndex >= expectedParametersCount) { + break; + } - // eslint-disable-next-line no-console - log( - 'expected type: #', - expectedParameterIndex, - expectedParameter.escapedName, - typeChecker.typeToString(expectedType), - ); - // eslint-disable-next-line no-console - log( - 'actual type: #', - actualParameterIndex, - sourceCode.getText(actualParameter), - typeChecker.typeToString(actualType), - ); - // @ts-expect-error: internal API + const expectedParameter = expectedParameters[expectedParameterIndex]; + assert.ok(expectedParameter, 'Expected parameter not found.'); + + const expectedType = typeChecker.getTypeOfSymbol(expectedParameter); + const actualType = typeChecker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(actualParameter)); + const actualTypeString = typeChecker.typeToString(actualType); + log( + 'expected type: #', + expectedParameterIndex, + expectedParameter.escapedName, + typeChecker.typeToString(expectedType), + ); + log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString); + + if (!typesToCheck.includes(actualTypeString)) { + // skip the parameter type checking if it's not in the candidate types + parametersToKeep.push(actualParameter); + log('skipped'); + } else if ( + // @ts-expect-error: this is typescript's internal API // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (typeChecker.isTypeAssignableTo(actualType, expectedType) === true) { - parametersToKeep.push(actualParameter); - expectedParameterIndex++; - log('matched'); - } else { - log('not matched'); - } + typeChecker.isTypeAssignableTo(actualType, expectedType) === true + ) { + parametersToKeep.push(actualParameter); + log('matched'); + expectedParameterIndex++; + } else { + log('not matched'); } } From 105b126e3a88fa9e3a8c4b0e3947f555f3c6e333 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 26 Sep 2024 23:52:48 -0400 Subject: [PATCH 068/115] fix fix-function-call-arguments continued, adjust ruleset overrides --- src/agent/fix-function-call-arguments.ts | 9 +++- src/index.ts | 65 +++++++++++++----------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 8fd45ad..e8d6397 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -17,7 +17,12 @@ export interface FixFunctionCallArgumentsRuleOptions { typesToCheck: string[]; } const DEFAULT_OPTIONS = { - typesToCheck: ['Configuration', 'EMPTY_CONTEXT', 'Fixture'], + typesToCheck: [ + 'Configuration', + 'Fixture', + 'InboundContext', + '{ get: () => string; }', + ], }; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); @@ -53,7 +58,7 @@ const rule = createRule({ }, defaultOptions: [DEFAULT_OPTIONS], create(context) { - const { typesToCheck } = context.options[0] as FixFunctionCallArgumentsRuleOptions; + const { typesToCheck } = context.options[0] ?? DEFAULT_OPTIONS; const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); const sourceCode = context.sourceCode; diff --git a/src/index.ts b/src/index.ts index 3448e76..1689a8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -97,6 +97,7 @@ export default { [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + // --- agent rules BEGIN --- [`@checkdigit/${noMappedResponseRuleId}`]: 'off', [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', @@ -109,6 +110,7 @@ export default { [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', + // --- agent rules END --- }, }, recommended: { @@ -130,39 +132,44 @@ export default { overrides: [ { files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], + rules: { + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', + }, }, ], - rules: { - [`@checkdigit/${noMappedResponseRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', - }, }, 'agent-phase-2-production': { - ignorePatterns: ['*.spec.ts', '*.test.ts'], - rules: { - [`@checkdigit/${noMappedResponseRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', - }, + overrides: [ + { + files: ['*.ts'], + excludedFiles: ['*.spec.ts', '*.test.ts'], + rules: { + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', + }, + }, + ], }, }, }; From 8c4854440a917f471e84bc6cd6370916cfbb6558 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 27 Sep 2024 02:54:52 -0400 Subject: [PATCH 069/115] add rule: agent-test-wiring --- src/agent/agent-test-wiring.spec.ts | 79 +++++++++++ src/agent/agent-test-wiring.ts | 204 ++++++++++++++++++++++++++++ src/index.ts | 9 ++ 3 files changed, 292 insertions(+) create mode 100644 src/agent/agent-test-wiring.spec.ts create mode 100644 src/agent/agent-test-wiring.ts diff --git a/src/agent/agent-test-wiring.spec.ts b/src/agent/agent-test-wiring.spec.ts new file mode 100644 index 0000000..836848c --- /dev/null +++ b/src/agent/agent-test-wiring.spec.ts @@ -0,0 +1,79 @@ +// agent/agent-test-wiring.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import rule, { ruleId } from './agent-test-wiring'; +import createTester from '../ts-tester.test'; + +createTester().run(ruleId, rule, { + valid: [], + invalid: [ + { + name: 'update test wiring', + code: ` +import { strict as assert } from 'node:assert'; + +import { beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + beforeAll(async () => { + await fixture.reset(); + }, 15_000); + + it('returns current server time', async () => { + // + }); +}); + `, + output: ` +import { strict as assert } from 'node:assert'; + +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../plugin/fixture.test'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + let agent: Agent; +beforeAll(async () => { + agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); + }, 15_000); +afterAll(async () => { + await agent[Symbol.asyncDispose](); +}); + + it('returns current server time', async () => { + // + }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + ], +}); diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts new file mode 100644 index 0000000..b5083f7 --- /dev/null +++ b/src/agent/agent-test-wiring.ts @@ -0,0 +1,204 @@ +// agent/agent-test-wiring.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_TOKEN_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; +import { strict as assert } from 'node:assert'; +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'agent-test-wiring'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Update test wiring.', + }, + messages: { + updateTestWiring: 'Updating test wiring.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + const importDeclarations = new Map(); + let beforeAllCallExpression: TSESTree.CallExpression | undefined; + let afterAllCallExpression: TSESTree.CallExpression | undefined; + + return { + ImportDeclaration(importDeclaration) { + const moduleName = importDeclaration.source.value; + importDeclarations.set(moduleName, importDeclaration); + }, + 'CallExpression[callee.name="beforeAll"]': (callExpression: TSESTree.CallExpression) => { + beforeAllCallExpression = callExpression; + }, + 'CallExpression[callee.name="afterAll"]': (callExpression: TSESTree.CallExpression) => { + afterAllCallExpression = callExpression; + }, + 'Program:exit'(program) { + try { + let jestImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let agentImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let fixturePluginImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let agentDeclarationFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let beforeAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let afterAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + + const lastImportDeclaration = [...importDeclarations.values()].at(-1); + assert.ok(lastImportDeclaration); + + const jestImportDeclaration = importDeclarations.get('@jest/globals'); + if ( + jestImportDeclaration && + !jestImportDeclaration.specifiers.some( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.imported.name === 'afterAll', + ) + ) { + const firstImportSpecifier = jestImportDeclaration.specifiers[0]; + assert.ok(firstImportSpecifier); + jestImportFixer = (fixer: RuleFixer) => fixer.insertTextBefore(firstImportSpecifier, 'afterAll, '); + } + + const agentImportDeclaration = importDeclarations.get('@checkdigit/agent'); + if (!agentImportDeclaration) { + agentImportFixer = (fixer: RuleFixer) => + fixer.insertTextAfter( + lastImportDeclaration, + `\nimport createAgent, { type Agent } from '@checkdigit/agent';`, + ); + } + + const fixturePluginImportDeclaration = importDeclarations.get('../../plugin/fixture.test'); + if (!fixturePluginImportDeclaration) { + fixturePluginImportFixer = (fixer: RuleFixer) => + fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '../../plugin/fixture.test';`); + } + + if (beforeAllCallExpression !== undefined) { + const beforeAllArrowFunctionExpression = beforeAllCallExpression.arguments[0]; + assert.ok( + beforeAllArrowFunctionExpression !== undefined && + beforeAllArrowFunctionExpression.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression, + ); + const arrowFunctionBody = beforeAllArrowFunctionExpression.body; + assert.ok(arrowFunctionBody.type === TSESTree.AST_NODE_TYPES.BlockStatement); + + const targetStatement = arrowFunctionBody.body.find( + (statement) => sourceCode.getText(statement) /*?*/ === 'await fixture.reset();', + ); + if (targetStatement !== undefined) { + const beforeAllBodyText = sourceCode.getText(arrowFunctionBody); + if (!beforeAllBodyText.includes('agent = await createAgent();')) { + beforeAllFixer = (fixer: RuleFixer) => + fixer.replaceText( + targetStatement, + [ + 'agent = await createAgent();', + 'agent.register(await fixturePlugin(fixture));', + 'agent.enable();', + 'await fixture.reset();', + ].join('\n'), + ); + } + if (!beforeAllBodyText.includes('let agent: Agent;')) { + agentDeclarationFixer = (fixer: RuleFixer) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fixer.insertTextBefore(beforeAllCallExpression!, 'let agent: Agent;\n'); + } + } + } + + if (afterAllCallExpression !== undefined) { + const afterAllArrowFunctionExpression = afterAllCallExpression.arguments[0]; + assert.ok( + afterAllArrowFunctionExpression !== undefined && + afterAllArrowFunctionExpression.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression, + ); + const arrowFunctionBody = afterAllArrowFunctionExpression.body; + assert.ok(arrowFunctionBody.type === TSESTree.AST_NODE_TYPES.BlockStatement); + + const afterAllBodyText = sourceCode.getText(arrowFunctionBody); + if (!afterAllBodyText.includes('await agent[Symbol.asyncDispose]();')) { + const lastStatement = arrowFunctionBody.body.at(-1); + assert.ok(lastStatement); + afterAllFixer = (fixer: RuleFixer) => + fixer.insertTextAfter(lastStatement, 'await agent[Symbol.asyncDispose]();'); + } + } else if (beforeAllCallExpression !== undefined) { + const nextToken = sourceCode.getTokenAfter(beforeAllCallExpression); + afterAllFixer = (fixer: RuleFixer) => + fixer.insertTextAfter( + nextToken !== null && nextToken.type === AST_TOKEN_TYPES.Punctuator + ? nextToken + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + beforeAllCallExpression!, + `\nafterAll(async () => { + await agent[Symbol.asyncDispose](); +});`, + ); + } + + if ( + jestImportFixer !== undefined || + agentImportFixer !== undefined || + fixturePluginImportFixer !== undefined || + agentDeclarationFixer !== undefined || + beforeAllFixer !== undefined || + afterAllFixer !== undefined + ) { + context.report({ + messageId: 'updateTestWiring', + node: program, + *fix(fixer) { + if (jestImportFixer !== undefined) { + yield jestImportFixer(fixer); + } + if (agentImportFixer !== undefined) { + yield agentImportFixer(fixer); + } + if (fixturePluginImportFixer !== undefined) { + yield fixturePluginImportFixer(fixer); + } + if (agentDeclarationFixer !== undefined) { + yield agentDeclarationFixer(fixer); + } + if (beforeAllFixer !== undefined) { + yield beforeAllFixer(fixer); + } + if (afterAllFixer !== undefined) { + yield afterAllFixer(fixer); + } + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: program, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 1689a8c..610b83f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ */ import addUrlDomain, { ruleId as addUrlDomainRuleId } from './agent/add-url-domain'; +import agentTestWiring, { ruleId as agentTestWiringRuleId } from './agent/agent-test-wiring'; import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './agent/fetch-response-body-json'; import fetchResponseHeaderGetter, { ruleId as fetchResponseHeaderGetterRuleId, @@ -77,6 +78,7 @@ export default { [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, [noUnusedImportsRuleId]: noUnusedImports, [fixFunctionCallArgumentsRuleId]: fixFunctionCallArguments, + [agentTestWiringRuleId]: agentTestWiring, }, configs: { all: { @@ -110,6 +112,7 @@ export default { [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', + [`@checkdigit/${agentTestWiringRuleId}`]: 'off', // --- agent rules END --- }, }, @@ -147,6 +150,12 @@ export default { [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', }, }, + { + files: ['*.spec.ts'], + rules: { + [`@checkdigit/${agentTestWiringRuleId}`]: 'error', + }, + }, ], }, 'agent-phase-2-production': { From 15b96d97913f50eb7b2c8da7ceb1f1bcc3ba8078 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 27 Sep 2024 03:08:47 -0400 Subject: [PATCH 070/115] fix duplicated agent declaration --- src/agent/agent-test-wiring.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index b5083f7..9c7dde5 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -112,8 +112,6 @@ const rule = createRule({ 'await fixture.reset();', ].join('\n'), ); - } - if (!beforeAllBodyText.includes('let agent: Agent;')) { agentDeclarationFixer = (fixer: RuleFixer) => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fixer.insertTextBefore(beforeAllCallExpression!, 'let agent: Agent;\n'); From 7b6dc7df6e2d0c73cbf7392b10b5572c37d6c9e5 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 3 Oct 2024 15:41:26 -0400 Subject: [PATCH 071/115] enhance function signature refactoring --- package.json | 2 +- src/agent/fix-function-call-arguments.ts | 2 +- src/index.ts | 34 ++++++++++-------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index dac4bc0..f22f44f 100644 --- a/package.json +++ b/package.json @@ -91,4 +91,4 @@ "engines": { "node": ">=20.14" } -} +} \ No newline at end of file diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index e8d6397..3debb22 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -127,7 +127,7 @@ const rule = createRule({ ); log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString); - if (!typesToCheck.includes(actualTypeString)) { + if (!typesToCheck.includes(actualTypeString) && !actualTypeString.endsWith('RequestType')) { // skip the parameter type checking if it's not in the candidate types parametersToKeep.push(actualParameter); log('skipped'); diff --git a/src/index.ts b/src/index.ts index 610b83f..b099cc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -159,26 +159,20 @@ export default { ], }, 'agent-phase-2-production': { - overrides: [ - { - files: ['*.ts'], - excludedFiles: ['*.spec.ts', '*.test.ts'], - rules: { - [`@checkdigit/${noMappedResponseRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', - }, - }, - ], + rules: { + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', + }, }, }, }; From 255b4d456ed5e5c40d1732eba048d920225bd4f6 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 3 Oct 2024 15:52:17 -0400 Subject: [PATCH 072/115] merge main --- docs/rules/require-resolve-full-response.md | 15 +++++++++++++++ package-lock.json | 8 +++----- package.json | 6 +++--- src/require-resolve-full-response.spec.ts | 12 ++---------- src/require-resolve-full-response.ts | 3 ++- 5 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 docs/rules/require-resolve-full-response.md diff --git a/docs/rules/require-resolve-full-response.md b/docs/rules/require-resolve-full-response.md new file mode 100644 index 0000000..56415b4 --- /dev/null +++ b/docs/rules/require-resolve-full-response.md @@ -0,0 +1,15 @@ +# When handling response from service warpper, the response status code should always be asserted before accessing the response body. In order to do this, resolveWithFullResponse should be set as `true` as part of the service wrapper's options parameter. + +## Fail + +```js +const responseBody = await pingService.get(`${PING_BASE_PATH}/ping`) as Ping; +``` + +## Pass + +```js +const response = await pingService.get(`${PING_BASE_PATH}/ping`); +assert.equal(response.status, StatusCodes.OK); +const responseBody: Ping = response.body +``` diff --git a/package-lock.json b/package-lock.json index 3f65c59..7c47ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,12 @@ "dependencies": { "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.7", "ts-api-utils": "^1.3.0" }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.0", "@checkdigit/typescript-config": "6.0.0", - "@types/debug": "^4.1.12", "@types/eslint": "8.56.10", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", @@ -6914,9 +6912,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index f22f44f..252c36c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.6.0", + "version": "6.7.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", @@ -42,7 +42,7 @@ "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs", + "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", "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", @@ -91,4 +91,4 @@ "engines": { "node": ">=20.14" } -} \ No newline at end of file +} diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts index 2a7fe1c..196fe54 100644 --- a/src/require-resolve-full-response.spec.ts +++ b/src/require-resolve-full-response.spec.ts @@ -7,17 +7,9 @@ */ import rule, { ruleId } from './require-resolve-full-response'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import createTester from './ts-tester.test'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'none service wrapper call will not trigger an error', diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index b9a18e9..75d85da 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -8,12 +8,13 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { DefinitionType, type Scope } from '@typescript-eslint/scope-manager'; -import { PLAIN_URL_REGEXP, TOKENIZED_URL_REGEXP } from './agent/url'; import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; import { getEnclosingScopeNode } from './library/ts-tree'; export const ruleId = 'require-resolve-full-response'; +export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; +export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); From 2f292785ac0f83a1aaaf5225b1ba7f66fee52fdf Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 28 Oct 2024 11:28:05 -0400 Subject: [PATCH 073/115] merge with main --- .eslintrc | 70 - .../workflows/check-published-scheduled.yml | 2 +- .github/workflows/ci.yml | 8 +- .github/workflows/codeql-analysis.yml | 4 +- .github/workflows/publish-beta.yml | 4 + README.md | 8 +- docs/rules/no-duplicated-imports.md | 14 + docs/rules/require-fixed-services-import.md | 13 + eslint.config.mjs | 142 ++ package-lock.json | 1976 +++++++++++------ package.json | 44 +- src/agent/agent-test-wiring.ts | 8 +- src/agent/fetch-response-body-json.spec.ts | 12 +- .../fetch-response-header-getter.spec.ts | 12 +- src/agent/fix-function-call-arguments.ts | 16 +- src/agent/no-full-response.spec.ts | 59 +- src/agent/no-full-response.ts | 48 +- src/agent/no-mapped-response.spec.ts | 12 +- src/agent/no-service-wrapper.spec.ts | 12 +- src/agent/no-status-code.spec.ts | 12 +- src/agent/url.ts | 12 +- src/file-path-comment.spec.ts | 8 +- src/index.ts | 244 +- src/invalid-json-stringify.spec.ts | 19 +- src/invalid-json-stringify.ts | 2 +- src/library/format.ts | 6 +- src/library/tree.ts | 16 +- src/library/ts-tree.ts | 16 +- src/library/variable.ts | 2 +- src/no-card-numbers.spec.ts | 16 +- src/no-duplicated-imports.spec.ts | 12 +- src/no-duplicated-imports.ts | 6 +- src/no-promise-instance-method.spec.ts | 2 +- src/no-test-import.spec.ts | 18 +- src/no-uuid.spec.ts | 6 +- src/no-wallaby-comment.spec.ts | 6 +- src/no-wallaby-comment.ts | 5 +- src/object-literal-response.spec.ts | 11 +- src/regular-expression-comment.spec.ts | 6 +- ...re-assert-predicate-rejects-throws.spec.ts | 6 +- ...require-assert-predicate-rejects-throws.ts | 9 +- src/require-fixed-services-import.spec.ts | 12 +- src/require-fixed-services-import.ts | 2 +- src/require-resolve-full-response.spec.ts | 10 +- src/require-resolve-full-response.ts | 11 +- src/require-strict-assert.spec.ts | 6 +- ...uire-type-out-of-type-only-imports.spec.ts | 12 +- src/require-type-out-of-type-only-imports.ts | 2 +- src/tester.test.ts | 10 +- src/ts-tester.test.ts | 11 +- 50 files changed, 1836 insertions(+), 1144 deletions(-) delete mode 100644 .eslintrc create mode 100644 docs/rules/no-duplicated-imports.md create mode 100644 docs/rules/require-fixed-services-import.md create mode 100644 eslint.config.mjs diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a537fee..0000000 --- a/.eslintrc +++ /dev/null @@ -1,70 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": true - }, - "plugins": ["@typescript-eslint", "sonarjs", "import", "no-only-tests", "no-secrets", "eslint-plugin"], - "extends": [ - "eslint:all", - "plugin:@typescript-eslint/strict-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - "plugin:eslint-plugin-eslint-plugin/recommended", - "plugin:sonarjs/recommended", - "prettier" - ], - "ignorePatterns": ["ts-init/**/*.ts"], - "rules": { - "sort-keys": "off", - "capitalized-comments": "off", - "func-style": ["error", "declaration", { "allowArrowFunctions": true }], - "no-negated-condition": "off", - "multiline-comment-style": "off", - "no-magic-numbers": ["error", { "ignore": [-1, 0, 1, 2, 10, 16, 60] }], - "no-ternary": "off", - "max-params": ["error", 8], - "max-statements": "off", - "consistent-return": "off", - "no-undef": "off", - "init-declarations": "off", - "no-inline-comments": "off", - "line-comment-position": "off", - "prefer-destructuring": "off", - "no-useless-return": "off", - "complexity": "off", - "max-lines": ["error", { "max": 500, "skipBlankLines": true, "skipComments": true }], - "id-length": ["error", { "properties": "never", "exceptions": ["_"] }], - "no-plusplus": "off", - "default-case": "off", - "no-continue": "off", - "callback-return": ["error", ["callback", "cb"]], - "new-cap": ["error", { "capIsNew": false }], - "dot-notation": "off", - "no-undefined": "off", - "one-var": ["error", "never"], - "max-lines-per-function": ["error", 200], - "sonarjs/cognitive-complexity": ["error", 24], - "no-only-tests/no-only-tests": "error", - "curly": "error", - "@typescript-eslint/strict-boolean-expressions": "error" - }, - "overrides": [ - { - "files": ["*.spec.ts", "*.test.ts"], - "rules": { - "no-magic-numbers": "off", - "no-undefined": "off", - "max-lines-per-function": "off", - "sonarjs/no-duplicate-string": "off", - "sonarjs/no-identical-functions": "off", - "sonarjs/cognitive-complexity": "off", - "max-lines": "off", - "no-await-in-loop": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/restrict-template-expressions": "off" - } - } - ] -} diff --git a/.github/workflows/check-published-scheduled.yml b/.github/workflows/check-published-scheduled.yml index c78293b..c4ba566 100644 --- a/.github/workflows/check-published-scheduled.yml +++ b/.github/workflows/check-published-scheduled.yml @@ -18,6 +18,6 @@ jobs: uses: checkdigit/github-actions/check-published@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN_PUBLISH }} DEBUG: '*' SLACK_PUBLISH_MISMATCH: ${{ secrets.SLACK_PUBLISH_MISMATCH }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c703bad..3cdf6e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: ['20.x', '22.x'] + node-version: ['20.x', '22.x', '23.x'] steps: - name: Checkout Code uses: actions/checkout@v4 @@ -27,6 +27,8 @@ jobs: run: npm ci --ignore-scripts env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Prepare + run: npm run prepare - name: Compile run: npm run ci:compile - name: Check Code Style @@ -41,7 +43,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: ['20.x', '22.x'] + node-version: ['20.x', '22.x', '23.x'] steps: - name: Checkout Code uses: actions/checkout@v4 @@ -56,6 +58,8 @@ jobs: run: npm ci --ignore-scripts env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Prepare + run: npm run prepare - name: Compile run: npm run ci:compile - name: Check Code Style diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 66a40ba..dd078b5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,7 +6,7 @@ on: pull_request: branches: ['main'] schedule: - - cron: '44 8 * * 1' + - cron: '22 7 * * 1' jobs: analyze: @@ -21,7 +21,7 @@ jobs: matrix: language: ['javascript'] steps: - - name: Checkout repository + - name: Checkout Code uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml index fe3b172..22eab1a 100644 --- a/.github/workflows/publish-beta.yml +++ b/.github/workflows/publish-beta.yml @@ -14,6 +14,10 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + allow-licenses: ${{ vars.ALLOW_LICENSES }} - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index 07e0807..6b987f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# checkdigit/eslint-plugin +# @checkdigit/eslint-plugin -Copyright (c) 2021-2023 [Check Digit, LLC](https://checkdigit.com) +Copyright (c) 2021-2024 [Check Digit, LLC](https://checkdigit.com) ## Rules @@ -14,6 +14,10 @@ Copyright (c) 2021-2023 [Check Digit, LLC](https://checkdigit.com) - `@checkdigit/object-literal-response` - `@checkdigit/no-test-import` - `@checkdigit/no-promise-instance-method` +- `@checkdigit/invalid-json-stringify` +- `@checkdigit/no-full-response` +- `@checkdigit/require-resolve-full-response` +- `@checkdigit/require-type-out-of-type-only-imports` ## Configurations diff --git a/docs/rules/no-duplicated-imports.md b/docs/rules/no-duplicated-imports.md new file mode 100644 index 0000000..d472e9b --- /dev/null +++ b/docs/rules/no-duplicated-imports.md @@ -0,0 +1,14 @@ +# import statement from the same module should be merged into a single import statement + +## Fail + +```js +import { ValueOne } from 'abc'; +import { ValueTwo } from 'abc'; +``` + +## Pass + +```js +import { ValueOne, ValueTwo } from 'abc'; +``` diff --git a/docs/rules/require-fixed-services-import.md b/docs/rules/require-fixed-services-import.md new file mode 100644 index 0000000..2126810 --- /dev/null +++ b/docs/rules/require-fixed-services-import.md @@ -0,0 +1,13 @@ +# service typing import is restricted to be from `src/services' and not deeper + +## Fail + +```js +import type { personV1 as person } from '../../services/person' +``` + +## Pass + +```js +import type { personV1 as person } from '../../services' +``` diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..674867f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,142 @@ +import { promises as fs } from 'node:fs'; +import ts from 'typescript-eslint'; +import sonarjs from 'eslint-plugin-sonarjs'; +import importPlugin from 'eslint-plugin-import'; +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import noSecrets from 'eslint-plugin-no-secrets'; +import eslintPlugin from 'eslint-plugin-eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; + +const ignores = [ + ...(await fs.readFile('.gitignore', 'utf-8')).split('\n').filter((path) => path.trim() !== ''), + 'eslint.config.mjs', + 'ts-init/**/*', +]; + +export default [ + { ignores }, + js.configs.all, + ...ts.configs.strictTypeChecked, + ...ts.configs.stylisticTypeChecked, + sonarjs.configs.recommended, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + prettier, + eslintPlugin.configs['flat/recommended'], + { + plugins: { + 'no-only-tests': noOnlyTests, + 'no-secrets': noSecrets, + }, + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + projectService: true, + }, + }, + settings: { + 'import/resolver': { + typescript: true, + node: true, + }, + }, + rules: { + 'sort-keys': 'off', + 'capitalized-comments': 'off', + 'func-style': [ + 'error', + 'declaration', + { + allowArrowFunctions: true, + }, + ], + 'no-negated-condition': 'off', + 'multiline-comment-style': 'off', + 'no-magic-numbers': [ + 'error', + { + ignore: [-1, 0, 1, 2, 10, 16, 60], + }, + ], + 'no-ternary': 'off', + 'max-params': ['error', 8], + 'max-statements': 'off', + 'consistent-return': 'off', + 'no-undef': 'off', + 'init-declarations': 'off', + 'no-inline-comments': 'off', + 'line-comment-position': 'off', + 'prefer-destructuring': 'off', + 'no-useless-return': 'off', + complexity: 'off', + 'max-lines': [ + 'error', + { + max: 500, + skipBlankLines: true, + skipComments: true, + }, + ], + 'id-length': [ + 'error', + { + properties: 'never', + exceptions: ['_'], + }, + ], + 'no-plusplus': 'off', + 'default-case': 'off', + 'no-continue': 'off', + 'callback-return': ['error', ['callback', 'cb']], + 'new-cap': [ + 'error', + { + capIsNew: false, + }, + ], + 'dot-notation': 'off', + 'no-undefined': 'off', + 'one-var': ['error', 'never'], + 'max-lines-per-function': ['error', 200], + 'sonarjs/cognitive-complexity': ['error', 24], + 'no-only-tests/no-only-tests': 'error', + curly: 'error', + '@typescript-eslint/strict-boolean-expressions': 'error', + 'sort-imports': [ + 'error', + { + ignoreCase: true, + ignoreDeclarationSort: true, + }, + ], + 'import/order': [ + 'error', + { + 'newlines-between': 'ignore', + }, + ], + }, + }, + { + files: ['**/*.spec.ts', '**/*.test.ts'], + rules: { + 'no-magic-numbers': 'off', + 'no-undefined': 'off', + 'max-lines-per-function': 'off', + 'sonarjs/no-duplicate-string': 'off', + 'sonarjs/no-identical-functions': 'off', + 'sonarjs/cognitive-complexity': 'off', + 'max-lines': 'off', + 'no-await-in-loop': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index baeae51..3cdf7c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,42 +1,60 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.8.0", + "version": "7.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "6.8.0", + "version": "7.1.0", "license": "MIT", "dependencies": { +<<<<<<< HEAD "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.7", +======= + "@typescript-eslint/type-utils": "^8.10.0", + "@typescript-eslint/utils": "^8.10.0", +>>>>>>> main "ts-api-utils": "^1.3.0" }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", +<<<<<<< HEAD "@checkdigit/typescript-config": "6.0.0", "@types/debug": "^4.1.12", "@types/eslint": "8.56.10", "@typescript-eslint/eslint-plugin": "7.18.0", "@typescript-eslint/parser": "7.18.0", "@typescript-eslint/rule-tester": "7.18.0", +======= + "@checkdigit/typescript-config": "^8.0.0", + "@eslint/js": "^9.13.0", + "@types/eslint": "^9.6.1", + "@types/eslint-config-prettier": "^6.11.3", + "@typescript-eslint/parser": "^8.10.0", + "@typescript-eslint/rule-tester": "^8.10.0", + "eslint": "^9.13.0", +>>>>>>> main "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-eslint-plugin": "^6.2.0", "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": "0.24.0", - "http-status-codes": "^2.3.0" + "eslint-plugin-sonarjs": "1.0.4", + "http-status-codes": "^2.3.0", + "rimraf": "^6.0.1", + "typescript-eslint": "^8.10.0" }, "engines": { - "node": ">=20.14" + "node": ">=20.17" }, "peerDependencies": { - "eslint": ">=8 <9" + "eslint": ">=9 <10" } }, "node_modules/@ampproject/remapping": { @@ -55,14 +73,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -70,9 +88,15 @@ } }, "node_modules/@babel/compat-data": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.0.tgz", "integrity": "sha512-P4fwKI2mjEb3ZU5cnMJzvRsRKGBUcs8jvxIoRmr6ufAY9Xk2Bz7JubRTTivkw55c7WQJfTECeqYVa+HZ0FzREg==", +======= + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, @@ -81,14 +105,21 @@ } }, "node_modules/@babel/core": { +<<<<<<< HEAD "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", +======= + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", +<<<<<<< HEAD "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.24.9", "@babel/helper-compilation-targets": "^7.24.8", @@ -98,6 +129,17 @@ "@babel/template": "^7.24.7", "@babel/traverse": "^7.24.8", "@babel/types": "^7.24.9", +======= + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", +>>>>>>> main "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -124,33 +166,55 @@ } }, "node_modules/@babel/generator": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/types": "^7.25.0", +======= + "@babel/types": "^7.25.7", +>>>>>>> main "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { +<<<<<<< HEAD "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/compat-data": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", "browserslist": "^4.23.1", +======= + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", +>>>>>>> main "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -170,32 +234,45 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.0.tgz", "integrity": "sha512-bIkOa2ZJYn7FHnepzr5iX9Kmz8FjIz4UKzJ9zhX3dnYuVW0xul9RuR3skBfoLu+FPTQw90EHW9rJsSZhyLQ3fQ==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", "@babel/helper-validator-identifier": "^7.24.7", "@babel/traverse": "^7.25.0" +======= + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" +>>>>>>> main }, "engines": { "node": ">=6.9.0" @@ -205,9 +282,15 @@ } }, "node_modules/@babel/helper-plugin-utils": { +<<<<<<< HEAD "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, @@ -216,24 +299,35 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" +======= + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" +>>>>>>> main }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { +<<<<<<< HEAD "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, @@ -242,9 +336,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, "license": "MIT", "peer": true, @@ -253,9 +347,15 @@ } }, "node_modules/@babel/helper-validator-option": { +<<<<<<< HEAD "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, @@ -264,29 +364,40 @@ } }, "node_modules/@babel/helpers": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/template": "^7.25.0", "@babel/types": "^7.25.0" +======= + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" +>>>>>>> main }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -381,12 +492,21 @@ } }, "node_modules/@babel/parser": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", +======= + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -436,6 +556,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -465,14 +619,14 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -565,6 +719,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", @@ -583,14 +754,14 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -600,34 +771,60 @@ } }, "node_modules/@babel/template": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/code-frame": "^7.24.7", "@babel/parser": "^7.25.0", "@babel/types": "^7.25.0" +======= + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" +>>>>>>> main }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.0.tgz", "integrity": "sha512-ubALThHQy4GCf6mbb+5ZRNmLLCI7bJ3f8Q6LHBSRlSKSWj5a7dSUzJBLv3VuIhFrFPgjF4IzPF567YG/HSCdZA==", +======= + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.0", "@babel/template": "^7.25.0", "@babel/types": "^7.25.0", +======= + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", +>>>>>>> main "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -647,15 +844,26 @@ } }, "node_modules/@babel/types": { +<<<<<<< HEAD "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.0.tgz", "integrity": "sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==", +======= + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", +======= + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", +>>>>>>> main "to-fast-properties": "^2.0.0" }, "engines": { @@ -703,27 +911,45 @@ } }, "node_modules/@checkdigit/typescript-config": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-6.0.0.tgz", - "integrity": "sha512-J/+6vv9y2lHdPscJl0kQrq8bDG5k8MBgfgxJEl2tEwyTdUYyOjqkgm+QiGinVfx35NRt/N1cfkZFnO3aLnudUQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@checkdigit/typescript-config/-/typescript-config-8.0.0.tgz", + "integrity": "sha512-7Hp1GTkrpR9qVzL7yIBcJu1R3LrI+9DTW/LfuH9/QOQZh+MBw9a4Nn+878UZMjuC9+6ERhMN1YluSyf23EL6AA==", "dev": true, "license": "MIT", "bin": { "builder": "bin/builder.mjs" }, "engines": { - "node": ">=20.9" + "node": ">=20.17" }, "peerDependencies": { - "@types/node": ">=20.9", - "esbuild": "0.19.8", - "typescript": ">=5.3.2 <5.4.0" + "@types/node": ">=20.17", + "esbuild": "0.24.0", + "typescript": ">=5.6.3 <5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -735,13 +961,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -753,13 +979,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -771,13 +997,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -789,13 +1015,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -807,13 +1033,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -825,13 +1051,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -843,13 +1069,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -861,13 +1087,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -879,13 +1105,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -897,13 +1123,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -915,13 +1141,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -933,13 +1159,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -951,13 +1177,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -969,13 +1195,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -987,13 +1213,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -1005,13 +1231,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -1023,13 +1249,31 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -1041,13 +1285,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -1059,13 +1303,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -1077,13 +1321,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -1095,13 +1339,13 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -1113,7 +1357,7 @@ ], "peer": true, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1132,25 +1376,69 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1158,7 +1446,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1169,7 +1457,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1180,7 +1467,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1189,53 +1475,55 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", "license": "MIT", - "peer": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", "license": "Apache-2.0", - "peer": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "levn": "^0.4.1" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "peer": true, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": "*" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1243,7 +1531,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -1252,13 +1539,121 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "license": "BSD-3-Clause", - "peer": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -1785,6 +2180,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1910,9 +2315,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "dependencies": { @@ -1920,13 +2325,19 @@ "@types/json-schema": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "node_modules/@types/eslint-config-prettier": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz", + "integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==", "dev": true, "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1972,7 +2383,6 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -1990,14 +2400,20 @@ "license": "MIT" }, "node_modules/@types/node": { +<<<<<<< HEAD "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", +======= + "version": "22.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", + "integrity": "sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==", +>>>>>>> main "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/stack-utils": { @@ -2009,9 +2425,9 @@ "peer": true }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "license": "MIT", "peer": true, @@ -2028,32 +2444,32 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz", + "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/type-utils": "8.10.0", + "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2062,27 +2478,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz", + "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2091,42 +2507,41 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.18.0.tgz", - "integrity": "sha512-ClrFQlwen9pJcYPIBLuarzBpONQAwjmJ0+YUjAo1TGzoZFJPyUK/A7bb4Mps0u+SMJJnFXbfMN8I9feQDf0O5A==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.10.0.tgz", + "integrity": "sha512-/+Cms6eddJv4UW1wzxbRYeaZKJOlwWrfzuPQCGtzMsiZMTn5SaABS/wyCSZ+po+nUXc86OtP5QajUfsZGH/tSg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/utils": "8.10.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", "semver": "^7.6.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@eslint/eslintrc": ">=2", - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", + "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2134,26 +2549,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz", + "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/typescript-estree": "8.10.0", + "@typescript-eslint/utils": "8.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -2161,12 +2573,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", + "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2174,22 +2586,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", + "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/visitor-keys": "8.10.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -2202,51 +2614,44 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", + "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" + "@typescript-eslint/scope-manager": "8.10.0", + "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/typescript-estree": "8.10.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", + "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/types": "8.10.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "license": "ISC", - "peer": true - }, "node_modules/@xml-tools/parser": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@xml-tools/parser/-/parser-1.0.11.tgz", @@ -2259,11 +2664,10 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2276,11 +2680,21 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2314,26 +2728,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2343,7 +2743,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2373,18 +2772,17 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "peer": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/array-buffer-byte-length": { @@ -2422,16 +2820,7 @@ "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "license": "MIT", - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.findlastindex": { @@ -2631,25 +3020,28 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -2701,9 +3093,15 @@ } }, "node_modules/browserslist": { +<<<<<<< HEAD "version": "4.23.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", +======= + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", +>>>>>>> main "dev": true, "funding": [ { @@ -2722,10 +3120,17 @@ "license": "MIT", "peer": true, "dependencies": { +<<<<<<< HEAD "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", "update-browserslist-db": "^1.1.0" +======= + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" +>>>>>>> main }, "bin": { "browserslist": "cli.js" @@ -2792,7 +3197,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2809,9 +3213,15 @@ } }, "node_modules/caniuse-lite": { +<<<<<<< HEAD "version": "1.0.30001643", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", +======= + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", +>>>>>>> main "dev": true, "funding": [ { @@ -2835,7 +3245,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2887,9 +3296,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true, "license": "MIT", "peer": true @@ -2922,21 +3331,6 @@ "node": ">= 0.12.0" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -2950,7 +3344,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2962,8 +3355,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", @@ -3007,7 +3399,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3017,21 +3408,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3123,8 +3499,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -3173,17 +3548,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-indent": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", @@ -3221,7 +3585,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "path-type": "^4.0.0" }, @@ -3230,22 +3596,35 @@ } }, "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esutils": "^2.0.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { +<<<<<<< HEAD "version": "1.5.2", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", +======= + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", +>>>>>>> main "dev": true, "license": "ISC", "peer": true @@ -3269,8 +3648,21 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } }, "node_modules/error-ex": { "version": "1.3.2", @@ -3424,9 +3816,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3435,37 +3827,39 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "peer": true, @@ -3478,7 +3872,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3487,59 +3880,63 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -3577,6 +3974,42 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", @@ -3697,19 +4130,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3813,30 +4233,29 @@ } }, "node_modules/eslint-plugin-sonarjs": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.24.0.tgz", - "integrity": "sha512-87zp50mbbNrSTuoEOebdRQBPa0mdejA5UEjyuScyIw8hEpEjfWP89Qhkq5xVZfVyVSRQKZc9alVm7yRKQvvUmg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-1.0.4.tgz", + "integrity": "sha512-jF0eGCUsq/HzMub4ExAyD8x1oEgjOyB9XVytYGyWgSFvdiJQJp6IuP7RmtauCf06o6N/kZErh+zW4b10y1WZ+Q==", "dev": true, "license": "LGPL-3.0-only", "engines": { "node": ">=16" }, "peerDependencies": { - "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.0.0 || ^9.0.0" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3885,18 +4304,28 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3904,19 +4333,38 @@ "node": "*" } }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3942,7 +4390,6 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3950,12 +4397,23 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", + "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3972,17 +4430,6 @@ "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4089,8 +4536,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", @@ -4113,16 +4559,15 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "license": "MIT", - "peer": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -4142,7 +4587,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4155,26 +4599,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.3", @@ -4186,10 +4627,41 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC", "peer": true }, @@ -4347,6 +4819,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/git-hooks-list": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz", @@ -4363,6 +4848,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "peer": true, "dependencies": { @@ -4385,7 +4871,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4397,6 +4882,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -4408,6 +4894,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "peer": true, "dependencies": { @@ -4418,16 +4905,12 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4451,20 +4934,35 @@ } }, "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", "merge2": "^1.4.1", - "slash": "^3.0.0" + "slash": "^4.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4488,13 +4986,13 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, "license": "MIT" }, "node_modules/has-bigints": { @@ -4512,7 +5010,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4612,9 +5109,9 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", "engines": { "node": ">= 4" @@ -4625,7 +5122,6 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "license": "MIT", - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -4663,7 +5159,6 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -4673,6 +5168,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "peer": true, "dependencies": { @@ -4684,6 +5180,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC", "peer": true }, @@ -4757,6 +5254,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", + "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -4833,7 +5340,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4899,16 +5405,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -5053,8 +5549,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -5132,6 +5627,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -5756,7 +6267,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5765,9 +6275,9 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "license": "MIT", "peer": true, @@ -5775,15 +6285,14 @@ "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5824,7 +6333,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5856,7 +6364,6 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5886,7 +6393,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -5923,14 +6429,14 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -5969,14 +6475,6 @@ "tmpl": "1.0.5" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true, - "license": "CC0-1.0", - "peer": true - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5995,9 +6493,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6043,6 +6541,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6194,6 +6702,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "peer": true, "dependencies": { @@ -6222,7 +6731,6 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -6240,7 +6748,6 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6256,7 +6763,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -6278,12 +6784,18 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -6316,7 +6828,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6325,6 +6836,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -6336,7 +6848,6 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6348,32 +6859,48 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", "dev": true, "license": "MIT", "peer": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" + "engines": { + "node": ">=8" } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC", "peer": true @@ -6490,7 +7017,6 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6729,16 +7255,16 @@ "peer": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -6819,11 +7345,20 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", - "peer": true, "engines": { "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -6846,17 +7381,60 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, "license": "ISC", - "peer": true, "dependencies": { - "glob": "^7.1.3" + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { - "rimraf": "bin.js" + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6923,9 +7501,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6973,7 +7551,6 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6986,7 +7563,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7030,7 +7606,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -7078,41 +7656,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sort-package-json/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sort-package-json/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7124,17 +7667,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -7201,7 +7733,22 @@ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7267,8 +7814,22 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7303,7 +7864,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -7316,7 +7876,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7338,30 +7897,29 @@ } }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.5.tgz", + "integrity": "sha512-f4WBlP5g8W6pEoDfx741lewMlemy+LIGpEqjGPWqnHVP92wqlQXl87U5O5Bi2tkSUrO95OxOoqwU8qlqiHmFKA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "esm-env": "^1.0.0", + "esrap": "^1.2.2", + "is-reference": "^3.0.2", "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/synckit": { @@ -7382,6 +7940,16 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7428,8 +7996,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tmpl": { "version": "1.0.5", @@ -7556,9 +8123,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "dev": true, "license": "0BSD", "peer": true @@ -7568,7 +8135,6 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7588,9 +8154,10 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, "license": "(MIT OR CC0-1.0)", "peer": true, "engines": { @@ -7678,9 +8245,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", "peer": true, "bin": { @@ -7691,6 +8258,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz", + "integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.10.0", + "@typescript-eslint/parser": "8.10.0", + "@typescript-eslint/utils": "8.10.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7708,17 +8299,17 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -7737,8 +8328,8 @@ "license": "MIT", "peer": true, "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -7788,7 +8379,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -7841,7 +8431,6 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7865,10 +8454,30 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC", "peer": true }, @@ -7942,13 +8551,20 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT", + "peer": true } } } diff --git a/package.json b/package.json index 61da46e..f2603c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "6.8.0", + "version": "7.1.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", @@ -21,7 +21,6 @@ "exports": { ".": { "types": "./dist-types/index.d.ts", - "require": "./dist-cjs/index.cjs", "import": "./dist-mjs/index.mjs", "default": "./dist-mjs/index.mjs" } @@ -29,20 +28,19 @@ "files": [ "src", "dist-types", - "dist-cjs", "dist-mjs", + "!src/**/test/**", "!src/**/*.test.ts", "!src/**/*.spec.ts", + "!dist-types/**/test/**", "!dist-types/**/*.test.d.ts", "!dist-types/**/*.spec.d.ts", - "!dist-cjs/**/*.test.cjs", - "!dist-cjs/**/*.spec.cjs", + "!dist-mjs/**/test/**", "!dist-mjs/**/*.test.mjs", "!dist-mjs/**/*.spec.mjs", "SECURITY.md" ], "scripts": { - "build:dist-cjs": "rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs --external=espree && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs", "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", @@ -50,9 +48,10 @@ "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 --ignore-path .gitignore .", - "lint:fix": "eslint --ignore-path .gitignore . --fix", - "prepublishOnly": "npm run build:dist-types && npm run build:dist-cjs && npm run build:dist-mjs", + "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" @@ -62,33 +61,38 @@ "preset": "@checkdigit/jest-config" }, "dependencies": { - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/type-utils": "^8.10.0", + "@typescript-eslint/utils": "^8.10.0", "debug": "^4.3.7", "ts-api-utils": "^1.3.0" }, "devDependencies": { "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", - "@checkdigit/typescript-config": "6.0.0", + "@checkdigit/typescript-config": "^8.0.0", + "@eslint/js": "^9.13.0", "@types/debug": "^4.1.12", - "@types/eslint": "8.56.10", - "@typescript-eslint/eslint-plugin": "7.18.0", - "@typescript-eslint/parser": "7.18.0", - "@typescript-eslint/rule-tester": "7.18.0", + "@types/eslint": "^9.6.1", + "@types/eslint-config-prettier": "^6.11.3", + "@typescript-eslint/parser": "^8.10.0", + "@typescript-eslint/rule-tester": "^8.10.0", + "eslint": "^9.13.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-eslint-plugin": "^6.2.0", "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": "0.24.0", - "http-status-codes": "^2.3.0" + "eslint-plugin-sonarjs": "1.0.4", + "http-status-codes": "^2.3.0", + "rimraf": "^6.0.1", + "typescript-eslint": "^8.10.0" }, "peerDependencies": { - "eslint": ">=8 <9" + "eslint": ">=9 <10" }, "engines": { - "node": ">=20.14" + "node": ">=20.17" } } diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index 9c7dde5..ba47067 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -6,16 +6,16 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { strict as assert } from 'node:assert'; import { AST_TOKEN_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; -import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'agent-test-wiring'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = createRule({ name: ruleId, meta: { type: 'suggestion', @@ -64,7 +64,9 @@ const rule = createRule({ jestImportDeclaration && !jestImportDeclaration.specifiers.some( (specifier) => - specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && specifier.imported.name === 'afterAll', + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && + specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier && + specifier.imported.name === 'afterAll', ) ) { const firstImportSpecifier = jestImportDeclaration.specifiers[0]; diff --git a/src/agent/fetch-response-body-json.spec.ts b/src/agent/fetch-response-body-json.spec.ts index 63f2478..77897f5 100644 --- a/src/agent/fetch-response-body-json.spec.ts +++ b/src/agent/fetch-response-body-json.spec.ts @@ -6,18 +6,10 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './fetch-response-body-json'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'no change if no "json" property is found in the response type', diff --git a/src/agent/fetch-response-header-getter.spec.ts b/src/agent/fetch-response-header-getter.spec.ts index f5a6f06..ac06541 100644 --- a/src/agent/fetch-response-header-getter.spec.ts +++ b/src/agent/fetch-response-header-getter.spec.ts @@ -6,18 +6,10 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './fetch-response-header-getter'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'no change for fixture.api.get()', diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 3debb22..6ab3182 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -6,8 +6,8 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { strict as assert } from 'node:assert'; +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import debug from 'debug'; import getDocumentationUrl from '../get-documentation-url'; @@ -28,7 +28,10 @@ const DEFAULT_OPTIONS = { const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); const log = debug('eslint-plugin:fix-function-call-arguments'); -const rule = createRule({ +const rule: ESLintUtils.RuleModule< + 'removeIncompatibleFunctionArguments' | 'unknownError', + [FixFunctionCallArgumentsRuleOptions] +> = createRule({ name: ruleId, meta: { type: 'suggestion', @@ -58,7 +61,7 @@ const rule = createRule({ }, defaultOptions: [DEFAULT_OPTIONS], create(context) { - const { typesToCheck } = context.options[0] ?? DEFAULT_OPTIONS; + const { typesToCheck } = context.options[0]; const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); const sourceCode = context.sourceCode; @@ -108,7 +111,6 @@ const rule = createRule({ const parametersToKeep: TSESTree.CallExpressionArgument[] = []; let expectedParameterIndex = 0; for (const [actualParameterIndex, actualParameter] of actualParameters.entries()) { - // eslint-disable-next-line max-depth if (expectedParameterIndex >= expectedParametersCount) { break; } @@ -131,11 +133,7 @@ const rule = createRule({ // skip the parameter type checking if it's not in the candidate types parametersToKeep.push(actualParameter); log('skipped'); - } else if ( - // @ts-expect-error: this is typescript's internal API - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - typeChecker.isTypeAssignableTo(actualType, expectedType) === true - ) { + } else if (typeChecker.isTypeAssignableTo(actualType, expectedType)) { parametersToKeep.push(actualParameter); log('matched'); expectedParameterIndex++; diff --git a/src/agent/no-full-response.spec.ts b/src/agent/no-full-response.spec.ts index 8eb4fc5..2feed5a 100644 --- a/src/agent/no-full-response.spec.ts +++ b/src/agent/no-full-response.spec.ts @@ -6,44 +6,29 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-full-response'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [], invalid: [ { - name: 'remove type annotation from variable declaration', + name: 'report type annotation from variable declaration', code: `const responses: FullResponse = await fixture.api.put('\${BASE_PATH}/ping').send(testCard);`, - output: `const responses = await fixture.api.put('\${BASE_PATH}/ping').send(testCard);`, - errors: [{ messageId: 'removeFullResponse' }], + errors: [{ messageId: 'noFullResponse' }], }, { - name: 'remove type annotation from array variable declaration', + name: 'report type annotation from array variable declaration', code: ` const results: FullResponse[] = await Promise.all([ fetch(\`\${BASE_PATH}/ping\`), fetch(\`\${BASE_PATH}/ping\`) ]); `, - output: ` - const results = await Promise.all([ - fetch(\`\${BASE_PATH}/ping\`), - fetch(\`\${BASE_PATH}/ping\`) - ]); - `, - errors: [{ messageId: 'removeFullResponse' }], + errors: [{ messageId: 'noFullResponse' }], }, { - name: 'remove type annotation from function return type', + name: 'report type annotation from function return type', code: ` export async function putPersonDataEncryptionKey( configuration: Configuration, @@ -62,37 +47,17 @@ ruleTester.run(ruleId, rule, { throw new Error(\`Error creating Person data encryption key \${dataEncryptionKeyId}. \`); } `, - output: ` - export async function putPersonDataEncryptionKey( - configuration: Configuration, - inboundContext: InboundContext, - dataEncryptionKeyId: string, - ) { - const putDataEncryptionKeyResponse = await configuration.service - .person(inboundContext) - .put(\`/person/v1/data-encryption-key/\${dataEncryptionKeyId}\`, requestBody, { - resolveWithFullResponse: true, - }); - - if (putDataEncryptionKeyResponse.statusCode === StatusCodes.OK) { - return putDataEncryptionKeyResponse; - } - throw new Error(\`Error creating Person data encryption key \${dataEncryptionKeyId}. \`); - } - `, - errors: [{ messageId: 'removeFullResponse' }], + errors: [{ messageId: 'noFullResponse' }], }, { - name: 'remove type annotation from arrow function argument narrowing', + name: 'report type annotation from arrow function argument narrowing', code: `putResponses.map((putResponse: FullResponse) => putResponse.statusCode)`, - output: `putResponses.map((putResponse) => putResponse.statusCode)`, - errors: [{ messageId: 'removeFullResponse' }], + errors: [{ messageId: 'noFullResponse' }], }, { - name: 'remove type annotation from "as" type narrowingF', + name: 'report type annotation from "as" type narrowing', code: `const fullResponse = response as FullResponse;`, - output: `const fullResponse = response;`, - errors: [{ messageId: 'removeFullResponse' }], + errors: [{ messageId: 'noFullResponse' }], }, ], }); diff --git a/src/agent/no-full-response.ts b/src/agent/no-full-response.ts index 848e072..c6b4c35 100644 --- a/src/agent/no-full-response.ts +++ b/src/agent/no-full-response.ts @@ -7,66 +7,32 @@ */ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import { strict as assert } from 'node:assert'; import getDocumentationUrl from '../get-documentation-url'; -import { getTypeParentNode } from '../library/ts-tree'; export const ruleId = 'no-full-response'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'noFullResponse'> = createRule({ name: ruleId, meta: { type: 'suggestion', docs: { - description: 'Remove the usage of FullResponse type.', + description: 'FullResponse type should not be used.', }, messages: { - removeFullResponse: 'Removing the usage of FullResponse type.', - unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + noFullResponse: 'Please remove the usage of FullResponse type.', }, - fixable: 'code', schema: [], }, defaultOptions: [], create(context) { - const sourceCode = context.sourceCode; - return { 'TSTypeReference[typeName.name="FullResponse"]': (typeReference: TSESTree.TSTypeReference) => { - try { - const typeParentNode = getTypeParentNode(typeReference); - assert.ok(typeParentNode); - if (typeParentNode.type === TSESTree.AST_NODE_TYPES.TSAsExpression) { - context.report({ - messageId: 'removeFullResponse', - node: typeReference, - fix(fixer) { - return fixer.replaceText(typeParentNode, sourceCode.getText(typeParentNode.expression)); - }, - }); - } else { - context.report({ - messageId: 'removeFullResponse', - node: typeReference, - fix(fixer) { - return fixer.remove(typeParentNode); - }, - }); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); - context.report({ - node: typeReference, - messageId: 'unknownError', - data: { - fileName: context.filename, - error: error instanceof Error ? error.toString() : JSON.stringify(error), - }, - }); - } + context.report({ + messageId: 'noFullResponse', + node: typeReference, + }); }, }; }, diff --git a/src/agent/no-mapped-response.spec.ts b/src/agent/no-mapped-response.spec.ts index a4ee5f6..db413dc 100644 --- a/src/agent/no-mapped-response.spec.ts +++ b/src/agent/no-mapped-response.spec.ts @@ -6,18 +6,10 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-mapped-response'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [], invalid: [ { diff --git a/src/agent/no-service-wrapper.spec.ts b/src/agent/no-service-wrapper.spec.ts index 1cfbcf3..5334131 100644 --- a/src/agent/no-service-wrapper.spec.ts +++ b/src/agent/no-service-wrapper.spec.ts @@ -6,18 +6,10 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-service-wrapper'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'none service wrapper call will not trigger an error', diff --git a/src/agent/no-status-code.spec.ts b/src/agent/no-status-code.spec.ts index 586338a..3369bee 100644 --- a/src/agent/no-status-code.spec.ts +++ b/src/agent/no-status-code.spec.ts @@ -6,18 +6,10 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-status-code'; -import { RuleTester } from '@typescript-eslint/rule-tester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'no change if no "status" property is found in the response type', diff --git a/src/agent/url.ts b/src/agent/url.ts index ca9eea2..cd73e1a 100644 --- a/src/agent/url.ts +++ b/src/agent/url.ts @@ -1,21 +1,23 @@ // agent/url.ts -export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; -export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const PLAIN_URL_REGEXP: RegExp = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const TOKENIZED_URL_REGEXP: RegExp = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; -export function replaceEndpointUrlPrefixWithBasePath(url: string) { +export function replaceEndpointUrlPrefixWithBasePath(url: string): string { // eslint-disable-next-line no-template-curly-in-string return url.replace(/^`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); } -export function replaceEndpointUrlPrefixWithDomain(url: string) { +export function replaceEndpointUrlPrefixWithDomain(url: string): string { return url.replace( /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+\/(?.|\r|\n)+(?[`'])$)/u, '$1https://$2.checkdigit/$2$4', ); } -export function addBasePathUrlDomain(url: string) { +export function addBasePathUrlDomain(url: string): string { return url.replace( /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+(?[`'])$)/u, '$1https://$2.checkdigit/$2$4', diff --git a/src/file-path-comment.spec.ts b/src/file-path-comment.spec.ts index 090aa37..e042021 100644 --- a/src/file-path-comment.spec.ts +++ b/src/file-path-comment.spec.ts @@ -12,15 +12,17 @@ import { describe } from '@jest/globals'; import rule from './file-path-comment'; describe('file-path-comment', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + const ruleTester = new RuleTester({ languageOptions: { parserOptions: { ecmaVersion: 2020 } } }); ruleTester.run('file-path-comment', rule, { valid: [ { filename: 'src/world/hello.ts', code: `// world/hello.ts`, - parserOptions: { - project: './tsconfig.json', + languageOptions: { + parserOptions: { + project: './tsconfig.json', + }, }, }, { diff --git a/src/index.ts b/src/index.ts index b099cc2..e5a65b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import type { TSESLint } from '@typescript-eslint/utils'; + import addUrlDomain, { ruleId as addUrlDomainRuleId } from './agent/add-url-domain'; import agentTestWiring, { ruleId as agentTestWiringRuleId } from './agent/agent-test-wiring'; import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './agent/fetch-response-body-json'; @@ -48,121 +50,109 @@ import regexComment from './regular-expression-comment'; import requireAssertPredicateRejectsThrows from './require-assert-predicate-rejects-throws'; import requireStrictAssert from './require-strict-assert'; -export default { - rules: { - 'file-path-comment': filePathComment, - 'no-card-numbers': noCardNumbers, - 'no-uuid': noUuid, - 'require-strict-assert': requireStrictAssert, - 'no-test-import': noTestImport, - 'no-wallaby-comment': noWallabyComment, - 'regular-expression-comment': regexComment, - 'require-assert-predicate-rejects-throws': requireAssertPredicateRejectsThrows, - 'object-literal-response': objectLiteralResponse, - [invalidJsonStringifyRuleId]: invalidJsonStringify, - [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, - [noFixtureRuleId]: noFixture, - [fetchThenRuleId]: fetchThen, - [noServiceWrapperRuleId]: noServiceWrapper, - [noStatusCodeRuleId]: noStatusCode, - [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, - [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, - [addUrlDomainRuleId]: addUrlDomain, - [noFullResponseRuleId]: noFullResponse, - [noMappedResponseRuleId]: noMappedResponse, - [requireResolveFullResponseRuleId]: requireResolveFullResponse, - [noDuplicatedImportsRuleId]: noDuplicatedImports, - [requireFixedServicesImportRuleId]: requireFixedServicesImport, - [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, - [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, - [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, - [noUnusedImportsRuleId]: noUnusedImports, - [fixFunctionCallArgumentsRuleId]: fixFunctionCallArguments, - [agentTestWiringRuleId]: agentTestWiring, - }, - configs: { - all: { - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'error', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'error', - '@checkdigit/regular-expression-comment': 'error', - '@checkdigit/require-assert-predicate-rejects-throws': 'error', - '@checkdigit/object-literal-response': 'error', - '@checkdigit/no-test-import': 'error', - [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', - [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - [`@checkdigit/${noFullResponseRuleId}`]: 'error', - [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', - [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', - [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', - [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', - // --- agent rules BEGIN --- - [`@checkdigit/${noMappedResponseRuleId}`]: 'off', - [`@checkdigit/${addUrlDomainRuleId}`]: 'off', - [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', - [`@checkdigit/${noStatusCodeRuleId}`]: 'off', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', - [`@checkdigit/${fetchThenRuleId}`]: 'off', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', - [`@checkdigit/${agentTestWiringRuleId}`]: 'off', - // --- agent rules END --- - }, +const rules = { + 'file-path-comment': filePathComment, + 'no-card-numbers': noCardNumbers, + 'no-uuid': noUuid, + 'require-strict-assert': requireStrictAssert, + 'no-test-import': noTestImport, + 'no-wallaby-comment': noWallabyComment, + 'regular-expression-comment': regexComment, + 'require-assert-predicate-rejects-throws': requireAssertPredicateRejectsThrows, + 'object-literal-response': objectLiteralResponse, + [invalidJsonStringifyRuleId]: invalidJsonStringify, + [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, + [noFixtureRuleId]: noFixture, + [fetchThenRuleId]: fetchThen, + [noServiceWrapperRuleId]: noServiceWrapper, + [noStatusCodeRuleId]: noStatusCode, + [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, + [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, + [addUrlDomainRuleId]: addUrlDomain, + [noFullResponseRuleId]: noFullResponse, + [noMappedResponseRuleId]: noMappedResponse, + [requireResolveFullResponseRuleId]: requireResolveFullResponse, + [noDuplicatedImportsRuleId]: noDuplicatedImports, + [requireFixedServicesImportRuleId]: requireFixedServicesImport, + [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, + [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, + [noUnusedServiceVariablesRuleId]: noUnusedServiceVariables, + [noUnusedImportsRuleId]: noUnusedImports, + [fixFunctionCallArgumentsRuleId]: fixFunctionCallArguments, + [agentTestWiringRuleId]: agentTestWiring, +}; + +const plugin: TSESLint.FlatConfig.Plugin = { + rules, +}; + +const configs: Record = { + all: { + plugins: { + '@checkdigit': plugin, }, - recommended: { - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'off', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'off', - '@checkdigit/regular-expression-comment': 'error', - '@checkdigit/require-assert-predicate-rejects-throws': 'error', - '@checkdigit/object-literal-response': 'error', - '@checkdigit/no-test-import': 'error', - [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', - [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - }, + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'error', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'error', + '@checkdigit/regular-expression-comment': 'error', + '@checkdigit/require-assert-predicate-rejects-throws': 'error', + '@checkdigit/object-literal-response': 'error', + '@checkdigit/no-test-import': 'error', + [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', + [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', + [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', + [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + // --- agent rules BEGIN --- + [`@checkdigit/${noMappedResponseRuleId}`]: 'off', + [`@checkdigit/${addUrlDomainRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', + [`@checkdigit/${noStatusCodeRuleId}`]: 'off', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', + [`@checkdigit/${fetchThenRuleId}`]: 'off', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', + [`@checkdigit/${agentTestWiringRuleId}`]: 'off', + // --- agent rules END --- + }, + }, + recommended: { + plugins: { + '@checkdigit': plugin, }, - 'agent-phase-1-test': { - overrides: [ - { - files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], - rules: { - [`@checkdigit/${noMappedResponseRuleId}`]: 'error', - [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', - [`@checkdigit/${noStatusCodeRuleId}`]: 'error', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', - }, - }, - { - files: ['*.spec.ts'], - rules: { - [`@checkdigit/${agentTestWiringRuleId}`]: 'error', - }, - }, - ], + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'off', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'off', + '@checkdigit/regular-expression-comment': 'error', + '@checkdigit/require-assert-predicate-rejects-throws': 'error', + '@checkdigit/object-literal-response': 'error', + '@checkdigit/no-test-import': 'error', + [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', + [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', }, - 'agent-phase-2-production': { + }, + 'agent-phase-1-test': [ + { + files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], + plugins: { + '@checkdigit': plugin, + }, rules: { [`@checkdigit/${noMappedResponseRuleId}`]: 'error', [`@checkdigit/${addUrlDomainRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', [`@checkdigit/${noStatusCodeRuleId}`]: 'error', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', @@ -174,5 +164,41 @@ export default { [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', }, }, + { + files: ['*.spec.ts'], + plugins: { + '@checkdigit': plugin, + }, + rules: { + [`@checkdigit/${agentTestWiringRuleId}`]: 'error', + }, + }, + ], + 'agent-phase-2-production': { + plugins: { + '@checkdigit': plugin, + }, + rules: { + [`@checkdigit/${noMappedResponseRuleId}`]: 'error', + [`@checkdigit/${addUrlDomainRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'error', + [`@checkdigit/${noStatusCodeRuleId}`]: 'error', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', + [`@checkdigit/${fetchThenRuleId}`]: 'error', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'error', + }, }, }; + +const defaultToExport: Exclude & { + configs: Record; +} = { + ...plugin, + configs, +}; +export default defaultToExport; diff --git a/src/invalid-json-stringify.spec.ts b/src/invalid-json-stringify.spec.ts index dd4308b..e961540 100644 --- a/src/invalid-json-stringify.spec.ts +++ b/src/invalid-json-stringify.spec.ts @@ -6,15 +6,17 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import rule, { ruleId } from './invalid-json-stringify'; import { RuleTester } from 'eslint'; import { describe } from '@jest/globals'; +import rule, { INVALID_JSON_STRINGIFY, ruleId } from './invalid-json-stringify'; describe(ruleId, () => { new RuleTester({ - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', + languageOptions: { + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, }, }).run(ruleId, rule, { valid: [`console.log(error);`, `JSON.stringify(body);`, `JSON.parse(error);`], @@ -23,8 +25,10 @@ describe(ruleId, () => { code: `JSON.stringify(error);`, errors: [ { + messageId: INVALID_JSON_STRINGIFY, suggestions: [ { + messageId: INVALID_JSON_STRINGIFY, output: 'String(error);', }, ], @@ -35,8 +39,10 @@ describe(ruleId, () => { code: `JSON.stringify(error, null, 2);`, errors: [ { + messageId: INVALID_JSON_STRINGIFY, suggestions: [ { + messageId: INVALID_JSON_STRINGIFY, output: 'String(error);', }, ], @@ -48,8 +54,10 @@ describe(ruleId, () => { code: 'console.log(`got an error: ${JSON.stringify(error)}`);', errors: [ { + messageId: INVALID_JSON_STRINGIFY, suggestions: [ { + messageId: INVALID_JSON_STRINGIFY, // eslint-disable-next-line no-template-curly-in-string output: 'console.log(`got an error: ${String(error)}`);', }, @@ -61,9 +69,10 @@ describe(ruleId, () => { code: `JSON.stringify(responseError);`, errors: [ { + messageId: INVALID_JSON_STRINGIFY, suggestions: [ { - // eslint-disable-next-line no-template-curly-in-string + messageId: INVALID_JSON_STRINGIFY, output: 'String(responseError);', }, ], diff --git a/src/invalid-json-stringify.ts b/src/invalid-json-stringify.ts index 4a9100f..c483cea 100644 --- a/src/invalid-json-stringify.ts +++ b/src/invalid-json-stringify.ts @@ -10,7 +10,7 @@ import getDocumentationUrl from './get-documentation-url'; */ export const ruleId = 'invalid-json-stringify'; -const INVALID_JSON_STRINGIFY = 'INVALID_JSON_STRINGIFY'; +export const INVALID_JSON_STRINGIFY = 'INVALID_JSON_STRINGIFY'; const DEFAULT_OPTIONS = ['error|.*Error']; export default { diff --git a/src/library/format.ts b/src/library/format.ts index 11ca6be..32067fc 100644 --- a/src/library/format.ts +++ b/src/library/format.ts @@ -6,15 +6,15 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { strict as assert } from 'node:assert'; import { TSESLint, TSESTree } from '@typescript-eslint/utils'; import type { Node } from 'estree'; import type { SourceCode } from 'eslint'; -import { strict as assert } from 'node:assert'; -export function getIndentation(node: Node | TSESTree.Node, sourceCode: SourceCode | TSESLint.SourceCode) { +export function getIndentation(node: Node | TSESTree.Node, sourceCode: SourceCode | TSESLint.SourceCode): string { assert.ok(node.loc); const line = sourceCode.lines[node.loc.start.line - 1]; assert.ok(line !== undefined); - const indentMatch = line.match(/^\s*/u); + const indentMatch = /^\s*/u.exec(line); return indentMatch ? indentMatch[0] : ''; } diff --git a/src/library/tree.ts b/src/library/tree.ts index 86982da..ced5a36 100644 --- a/src/library/tree.ts +++ b/src/library/tree.ts @@ -38,21 +38,21 @@ export function getAncestor( return getAncestor(parent, matcher, exitMatcher); } -export function isBlockStatement(node: Node) { +export function isBlockStatement(node: Node): boolean { return node.type.endsWith('Statement') || node.type.endsWith('Declaration'); } -export function getEnclosingStatement(node: Node) { +export function getEnclosingStatement(node: Node): Node | undefined { return getAncestor(node, isBlockStatement); } -export function getEnclosingScopeNode(node: Node) { +export function getEnclosingScopeNode(node: Node): Node | undefined { return getAncestor(node, (parentNode) => ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type), ); } -export function isUsedInArrayOrAsArgument(node: Node) { +export function isUsedInArrayOrAsArgument(node: Node): boolean { if (isBlockStatement(node)) { return false; } @@ -73,7 +73,13 @@ export function isUsedInArrayOrAsArgument(node: Node) { return isUsedInArrayOrAsArgument(parent); } -export function getEnclosingFunction(node: Node) { +export function getEnclosingFunction( + node: Node, +): + | import('estree').ArrowFunctionExpression + | import('estree').FunctionExpression + | import('estree').FunctionDeclaration + | undefined { if ( node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || diff --git a/src/library/ts-tree.ts b/src/library/ts-tree.ts index 3eb2b7d..c187098 100644 --- a/src/library/ts-tree.ts +++ b/src/library/ts-tree.ts @@ -38,21 +38,21 @@ export function getAncestor( return getAncestor(parent, matcher, exitMatcher); } -export function isBlockStatement(node: TSESTree.Node) { +export function isBlockStatement(node: TSESTree.Node): boolean { return node.type.endsWith('Statement') || node.type.endsWith('Declaration'); } -export function getEnclosingStatement(node: TSESTree.Node) { +export function getEnclosingStatement(node: TSESTree.Node): TSESTree.Node | undefined { return getAncestor(node, isBlockStatement); } -export function getEnclosingScopeNode(node: TSESTree.Node) { +export function getEnclosingScopeNode(node: TSESTree.Node): TSESTree.Node | undefined { return getAncestor(node, (parentNode) => ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression', 'Program'].includes(parentNode.type), ); } -export function isUsedInArrayOrAsArgument(node: TSESTree.Node) { +export function isUsedInArrayOrAsArgument(node: TSESTree.Node): boolean { if (isBlockStatement(node)) { return false; } @@ -73,7 +73,13 @@ export function isUsedInArrayOrAsArgument(node: TSESTree.Node) { return isUsedInArrayOrAsArgument(parent); } -export function getEnclosingFunction(node: TSESTree.Node) { +export function getEnclosingFunction( + node: TSESTree.Node, +): + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclarationWithOptionalName + | TSESTree.FunctionExpression + | undefined { if ( node.type === AST_NODE_TYPES.FunctionDeclaration || node.type === AST_NODE_TYPES.FunctionExpression || diff --git a/src/library/variable.ts b/src/library/variable.ts index 305365b..f174b8c 100644 --- a/src/library/variable.ts +++ b/src/library/variable.ts @@ -1,5 +1,5 @@ // library/variable.ts -export function isValidPropertyName(name: unknown) { +export function isValidPropertyName(name: unknown): boolean { return typeof name === 'string' && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/u.test(name); } diff --git a/src/no-card-numbers.spec.ts b/src/no-card-numbers.spec.ts index d934650..693dfad 100644 --- a/src/no-card-numbers.spec.ts +++ b/src/no-card-numbers.spec.ts @@ -37,11 +37,11 @@ const CONTAINS_A_PASSING_CARD_NUMBER = ` const foo = 4111111111111111; const fuz = 123111111111111111567; const bar = 111111111111111; -const baz = 000000000000000; -const far = 0000000000000000; -const faz = 00000000000000000; -const doo = 000000000000000000; -const dar = 0000000000000000000; +const baz = '000000000000000'; +const far = '0000000000000000'; +const faz = '00000000000000000'; +const doo = '000000000000000000'; +const dar = '0000000000000000000'; `; const CONTAINS_A_PASSING_CARD_NUMBER_IN_COMMENT = ` @@ -94,7 +94,11 @@ const foo = '9118724531442999'; `; describe('no-card-numbers', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020 }, + }, + }); ruleTester.run('no-card-numbers', rule, { valid: [ { diff --git a/src/no-duplicated-imports.spec.ts b/src/no-duplicated-imports.spec.ts index da1cf25..bdd2a21 100644 --- a/src/no-duplicated-imports.spec.ts +++ b/src/no-duplicated-imports.spec.ts @@ -7,17 +7,9 @@ */ import rule, { ruleId } from './no-duplicated-imports'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import createTester from './ts-tester.test'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'distinct import statement - type', diff --git a/src/no-duplicated-imports.ts b/src/no-duplicated-imports.ts index ce9061c..2f9e991 100644 --- a/src/no-duplicated-imports.ts +++ b/src/no-duplicated-imports.ts @@ -6,15 +6,17 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { strict as assert } from 'node:assert'; + +import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; + import getDocumentationUrl from './get-documentation-url'; export const ruleId = 'no-duplicated-imports'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'mergeDuplicatedImports'> = createRule({ name: ruleId, meta: { type: 'suggestion', diff --git a/src/no-promise-instance-method.spec.ts b/src/no-promise-instance-method.spec.ts index e993603..e27d9ce 100644 --- a/src/no-promise-instance-method.spec.ts +++ b/src/no-promise-instance-method.spec.ts @@ -6,13 +6,13 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { describe } from '@jest/globals'; import rule, { NO_PROMISE_INSTANCE_METHOD_CATCH_FINALLY, NO_PROMISE_INSTANCE_METHOD_THEN, ruleId, } from './no-promise-instance-method'; import createTester from './tester.test'; -import { describe } from '@jest/globals'; describe(ruleId, () => { createTester().run(ruleId, rule, { diff --git a/src/no-test-import.spec.ts b/src/no-test-import.spec.ts index 6731909..43aa0fe 100644 --- a/src/no-test-import.spec.ts +++ b/src/no-test-import.spec.ts @@ -6,15 +6,17 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import rule, { NO_TEST_IMPORT, type NoTestImportRuleOptions } from './no-test-import'; import { RuleTester } from 'eslint'; import { describe } from '@jest/globals'; +import rule, { NO_TEST_IMPORT, type NoTestImportRuleOptions } from './no-test-import'; describe('no-test-import', () => { new RuleTester({ - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', + languageOptions: { + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, }, }).run('no-test-import with default configuration', rule, { valid: [ @@ -85,9 +87,11 @@ describe('no-test-import', () => { const overwrittenConfiguration: NoTestImportRuleOptions = { testFilePattern: '\\.test\\.xyz$' }; new RuleTester({ - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', + languageOptions: { + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, }, }).run('no-test-import with overwritten configuration', rule, { valid: [ diff --git a/src/no-uuid.spec.ts b/src/no-uuid.spec.ts index 43760ff..5fdb13c 100644 --- a/src/no-uuid.spec.ts +++ b/src/no-uuid.spec.ts @@ -52,7 +52,11 @@ const foo = 'nothing wrong here'; `; describe('no-uuid', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020 }, + }, + }); ruleTester.run('no-uuid', rule, { valid: [ { diff --git a/src/no-wallaby-comment.spec.ts b/src/no-wallaby-comment.spec.ts index f3ad07b..5cc9cbb 100644 --- a/src/no-wallaby-comment.spec.ts +++ b/src/no-wallaby-comment.spec.ts @@ -208,7 +208,11 @@ const TEST = "this isn't secret"; // testing with ? here and there ??. `; describe('no-wallaby-comment', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020 }, + }, + }); ruleTester.run('no-wallaby-comment', rule, { valid: [ diff --git a/src/no-wallaby-comment.ts b/src/no-wallaby-comment.ts index 8aed30d..dc2078e 100644 --- a/src/no-wallaby-comment.ts +++ b/src/no-wallaby-comment.ts @@ -43,9 +43,10 @@ function processBlockComment(context: Rule.RuleContext, sourceCode: SourceCode, let startLine = comment.loc?.start.line ?? 0; const endLine = comment.loc?.end.line ?? 0; let match; + + let start; + let end; while (comment.loc && (match = wallabyRegex.exec(commentValue)) !== null) { - let start = 0; - let end = 0; const removeEntireComment = blockCommentRegex.test(comment.value.trim()); if (removeEntireComment) { start = sourceCode.getIndexFromLoc({ line: comment.loc.start.line, column: comment.loc.start.column }); diff --git a/src/object-literal-response.spec.ts b/src/object-literal-response.spec.ts index 00f8d55..34790ba 100644 --- a/src/object-literal-response.spec.ts +++ b/src/object-literal-response.spec.ts @@ -6,15 +6,14 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { RuleTester } from 'eslint'; +import { describe } from '@jest/globals'; import rule, { REQUIRE_OBJECT_LITERAL_FOR_ERROR_RESPONSE_MESSAGE_ID, REQUIRE_OBJECT_LITERAL_FOR_HEADERS_MESSAGE_ID, REQUIRE_OBJECT_LITERAL_MESSAGE_ID, } from './object-literal-response'; -import { RuleTester } from 'eslint'; -import { describe } from '@jest/globals'; - const RESPONSE_200_OBJECT_LITERAL = ` setResponse(response, {status: StatusCodes.OK, body: {foo: 'bar'}}); `; @@ -61,7 +60,11 @@ setResponse(response, { `; describe('object-literal-response', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020, project: true } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020, project: true }, + }, + }); ruleTester.run('object-literal-response', rule, { valid: [ { diff --git a/src/regular-expression-comment.spec.ts b/src/regular-expression-comment.spec.ts index 8b78879..9c134ab 100644 --- a/src/regular-expression-comment.spec.ts +++ b/src/regular-expression-comment.spec.ts @@ -96,7 +96,11 @@ const testRegex2 = /created test file for the new file TEST\\.123456789\\./gmu; const INVALID_TEST_7 = `const testRegex1 = /error processing x:test\\.test-xyz\\.test\\.xyz\\.abc/gmu;`; describe('regular-expression-comment', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020 }, + }, + }); ruleTester.run('regular-expression-comment', rule, { valid: [ { diff --git a/src/require-assert-predicate-rejects-throws.spec.ts b/src/require-assert-predicate-rejects-throws.spec.ts index b6c0065..bb86ca8 100644 --- a/src/require-assert-predicate-rejects-throws.spec.ts +++ b/src/require-assert-predicate-rejects-throws.spec.ts @@ -12,7 +12,11 @@ import { describe } from '@jest/globals'; import rule from './require-assert-predicate-rejects-throws'; describe('require-assert-predicate-rejects-throws', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + }); ruleTester.run('require-assert-predicate-rejects-throws', rule, { valid: [ { diff --git a/src/require-assert-predicate-rejects-throws.ts b/src/require-assert-predicate-rejects-throws.ts index 331179f..0cef5c2 100644 --- a/src/require-assert-predicate-rejects-throws.ts +++ b/src/require-assert-predicate-rejects-throws.ts @@ -21,9 +21,12 @@ export default { return { ImportDeclaration(node) { if (node.source.value === 'node:assert') { - const strictImportSpecifier = node.specifiers.filter( - (specifier) => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'strict', - )[0]; + const strictImportSpecifier = node.specifiers.find( + (specifier) => + specifier.type === 'ImportSpecifier' && + specifier.imported.type === 'Identifier' && + specifier.imported.name === 'strict', + ); if (strictImportSpecifier !== undefined) { assertIdentifier = strictImportSpecifier.local.name; } diff --git a/src/require-fixed-services-import.spec.ts b/src/require-fixed-services-import.spec.ts index 2e818f5..8d28e11 100644 --- a/src/require-fixed-services-import.spec.ts +++ b/src/require-fixed-services-import.spec.ts @@ -7,17 +7,9 @@ */ import rule, { ruleId } from './require-fixed-services-import'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import createTester from './ts-tester.test'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'correctly import service typing', diff --git a/src/require-fixed-services-import.ts b/src/require-fixed-services-import.ts index 13d8124..70ba17c 100644 --- a/src/require-fixed-services-import.ts +++ b/src/require-fixed-services-import.ts @@ -14,7 +14,7 @@ export const ruleId = 'require-fixed-services-import'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); const SERVICE_TYPINGS_IMPORT_PATH_PREFIX = /(?\.\.\/)+services\/.*/u; -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'updateServicesImportFrom'> = createRule({ name: ruleId, meta: { type: 'suggestion', diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts index 196fe54..717cff0 100644 --- a/src/require-resolve-full-response.spec.ts +++ b/src/require-resolve-full-response.spec.ts @@ -81,10 +81,12 @@ createTester().run(ruleId, rule, { { name: 'url declared as a variable', code: ` - const url = \`\${PING_BASE_PATH}/key/\${keyId}\`; - await pingService.get(url, { - resolveWithFullResponse: false, - }); + async function doSomething() { + const url = \`\${PING_BASE_PATH}/key/\${keyId}\`; + await pingService.get(url, { + resolveWithFullResponse: false, + }); + } `, errors: [{ messageId: 'invalidOptions' }], }, diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index 75d85da..f4873a5 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -6,19 +6,21 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { strict as assert } from 'node:assert'; import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import { DefinitionType, type Scope } from '@typescript-eslint/scope-manager'; -import { strict as assert } from 'node:assert'; import getDocumentationUrl from './get-documentation-url'; import { getEnclosingScopeNode } from './library/ts-tree'; export const ruleId = 'require-resolve-full-response'; -export const PLAIN_URL_REGEXP = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; -export const TOKENIZED_URL_REGEXP = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const PLAIN_URL_REGEXP: RegExp = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const TOKENIZED_URL_REGEXP: RegExp = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRule({ name: ruleId, meta: { type: 'suggestion', @@ -54,7 +56,6 @@ const rule = createRule({ const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); const variableDefinitionNode = variableDefinition.node; - assert.ok(variableDefinitionNode.type === AST_NODE_TYPES.VariableDeclarator); assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); return isUrlArgumentValid(variableDefinitionNode.init, scope); } diff --git a/src/require-strict-assert.spec.ts b/src/require-strict-assert.spec.ts index 3d6ca8b..b7aa186 100644 --- a/src/require-strict-assert.spec.ts +++ b/src/require-strict-assert.spec.ts @@ -11,7 +11,11 @@ import { describe } from '@jest/globals'; import rule from './require-strict-assert'; describe('require-strict-assert', () => { - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }); + const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + }); ruleTester.run('require-strict-assert', rule, { valid: [ { diff --git a/src/require-type-out-of-type-only-imports.spec.ts b/src/require-type-out-of-type-only-imports.spec.ts index 5bdf4b2..5b2cd9e 100644 --- a/src/require-type-out-of-type-only-imports.spec.ts +++ b/src/require-type-out-of-type-only-imports.spec.ts @@ -7,17 +7,9 @@ */ import rule, { ruleId } from './require-type-out-of-type-only-imports'; -import { RuleTester } from '@typescript-eslint/rule-tester'; +import createTester from './ts-tester.test'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, - }, -}); - -ruleTester.run(ruleId, rule, { +createTester().run(ruleId, rule, { valid: [ { name: 'correct import with one type specifier', diff --git a/src/require-type-out-of-type-only-imports.ts b/src/require-type-out-of-type-only-imports.ts index 46e4c4c..3d2fff4 100644 --- a/src/require-type-out-of-type-only-imports.ts +++ b/src/require-type-out-of-type-only-imports.ts @@ -13,7 +13,7 @@ export const ruleId = 'require-type-out-of-type-only-imports'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule = createRule({ +const rule: ESLintUtils.RuleModule<'moveTypeOutside'> = createRule({ name: ruleId, meta: { type: 'suggestion', diff --git a/src/tester.test.ts b/src/tester.test.ts index cb1cc70..ec229d9 100644 --- a/src/tester.test.ts +++ b/src/tester.test.ts @@ -8,11 +8,13 @@ import { RuleTester } from 'eslint'; -export default function createTester() { +export default function createTester(): RuleTester { return new RuleTester({ - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', + languageOptions: { + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, }, }); } diff --git a/src/ts-tester.test.ts b/src/ts-tester.test.ts index bbfa885..cd96b29 100644 --- a/src/ts-tester.test.ts +++ b/src/ts-tester.test.ts @@ -8,12 +8,13 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; -export default function createTester() { +export default function createTester(): RuleTester { return new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - project: '../tsconfig.json', - tsconfigRootDir: `${process.cwd()}/ts-init`, + languageOptions: { + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: `${process.cwd()}/ts-init`, + }, }, }); } From 95d4477dec168dca7d10b53072a56896c0874b95 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 28 Oct 2024 17:57:51 -0400 Subject: [PATCH 074/115] specify stronger files filter --- src/index.ts | 159 +++++++++++++++++++++++++++------------------------ 1 file changed, 84 insertions(+), 75 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1881bfb..ab8267d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,66 +89,72 @@ const plugin: TSESLint.FlatConfig.Plugin = { }; const configs: Record = { - all: { - plugins: { - '@checkdigit': plugin, - }, - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'error', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'error', - '@checkdigit/regular-expression-comment': 'error', - '@checkdigit/require-assert-predicate-rejects-throws': 'error', - '@checkdigit/object-literal-response': 'error', - '@checkdigit/no-test-import': 'error', - [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', - [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', - [`@checkdigit/${noFullResponseRuleId}`]: 'error', - [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', - [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', - [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', - [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', - [`@checkdigit/${noServeRuntimeRuleId}`]: 'error', - // --- agent rules BEGIN --- - [`@checkdigit/${noMappedResponseRuleId}`]: 'off', - [`@checkdigit/${addUrlDomainRuleId}`]: 'off', - [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', - [`@checkdigit/${noStatusCodeRuleId}`]: 'off', - [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', - [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', - [`@checkdigit/${fetchThenRuleId}`]: 'off', - [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', - [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', - [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', - [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', - [`@checkdigit/${agentTestWiringRuleId}`]: 'off', - // --- agent rules END --- - }, - }, - recommended: { - plugins: { - '@checkdigit': plugin, + all: [ + { + files: ['**/*.ts'], + plugins: { + '@checkdigit': plugin, + }, + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'error', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'error', + '@checkdigit/regular-expression-comment': 'error', + '@checkdigit/require-assert-predicate-rejects-throws': 'error', + '@checkdigit/object-literal-response': 'error', + '@checkdigit/no-test-import': 'error', + [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', + [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + [`@checkdigit/${noFullResponseRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', + [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', + [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + [`@checkdigit/${noServeRuntimeRuleId}`]: 'error', + // --- agent rules BEGIN --- + [`@checkdigit/${noMappedResponseRuleId}`]: 'off', + [`@checkdigit/${addUrlDomainRuleId}`]: 'off', + [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', + [`@checkdigit/${noStatusCodeRuleId}`]: 'off', + [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', + [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', + [`@checkdigit/${fetchThenRuleId}`]: 'off', + [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', + [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', + [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', + [`@checkdigit/${fixFunctionCallArgumentsRuleId}`]: 'off', + [`@checkdigit/${agentTestWiringRuleId}`]: 'off', + // --- agent rules END --- + }, }, - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'off', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'off', - '@checkdigit/regular-expression-comment': 'error', - '@checkdigit/require-assert-predicate-rejects-throws': 'error', - '@checkdigit/object-literal-response': 'error', - '@checkdigit/no-test-import': 'error', - [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', - [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + ], + recommended: [ + { + files: ['**/*.ts'], + plugins: { + '@checkdigit': plugin, + }, + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'off', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'off', + '@checkdigit/regular-expression-comment': 'error', + '@checkdigit/require-assert-predicate-rejects-throws': 'error', + '@checkdigit/object-literal-response': 'error', + '@checkdigit/no-test-import': 'error', + [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error', + [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error', + }, }, - }, + ], 'agent-phase-1-test': [ { - files: ['*.spec.ts', '*.test.ts', 'src/api/v*/index.ts'], + files: ['**/*.spec.ts', '**/*.test.ts', 'src/api/v*/index.ts'], plugins: { '@checkdigit': plugin, }, @@ -168,7 +174,7 @@ const configs: Record & { From ee68da5a57845aeb5e86fe43b39dd503e8f67baa Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 28 Oct 2024 18:24:23 -0400 Subject: [PATCH 075/115] handle default option --- src/agent/fix-function-call-arguments.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 6ab3182..c281904 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -61,7 +61,8 @@ const rule: ESLintUtils.RuleModule< }, defaultOptions: [DEFAULT_OPTIONS], create(context) { - const { typesToCheck } = context.options[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const { typesToCheck } = context.options[0] ?? DEFAULT_OPTIONS; const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); const sourceCode = context.sourceCode; From 83710c808d8f1040651e3b352735ed1715d687b6 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 30 Oct 2024 18:19:46 -0400 Subject: [PATCH 076/115] support agent test wiring for more variety of beforeAll coding style --- eslint.config.mjs | 5 + src/agent/agent-test-wiring.spec.ts | 196 +++++++++++++++++++++++++++- src/agent/agent-test-wiring.ts | 109 ++++++++++------ src/index.ts | 4 + 4 files changed, 269 insertions(+), 45 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 674867f..03feb3c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,6 +17,11 @@ const ignores = [ export default [ { ignores }, + { + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + }, js.configs.all, ...ts.configs.strictTypeChecked, ...ts.configs.stylisticTypeChecked, diff --git a/src/agent/agent-test-wiring.spec.ts b/src/agent/agent-test-wiring.spec.ts index b0c7a0b..b05463b 100644 --- a/src/agent/agent-test-wiring.spec.ts +++ b/src/agent/agent-test-wiring.spec.ts @@ -10,13 +10,26 @@ import createTester from '../ts-tester.test'; import rule, { ruleId } from './agent-test-wiring'; createTester().run(ruleId, rule, { - valid: [], - invalid: [ + valid: [ { - name: 'update test wiring', + name: 'no test wiring needed', code: ` -import { strict as assert } from 'node:assert'; +describe('/ping', () => { + beforeAll(async () => { + // + }); + it('test something', async () => { + // + }); +}); + `, + }, + ], + invalid: [ + { + name: 'update test wiring - async arrow function with body block', + code: ` import { beforeAll, describe, it } from '@jest/globals'; import { StatusCodes } from 'http-status-codes'; @@ -40,8 +53,6 @@ describe('/ping', () => { }); `, output: ` -import { strict as assert } from 'node:assert'; - import { afterAll, beforeAll, describe, it } from '@jest/globals'; import { StatusCodes } from 'http-status-codes'; @@ -65,7 +76,178 @@ agent.enable(); await fixture.reset(); }, 15_000); afterAll(async () => { - await agent[Symbol.asyncDispose](); +await agent[Symbol.asyncDispose](); +}); + + it('returns current server time', async () => { + // + }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'update test wiring - function reference instead of arrow function', + code: ` +import { beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + beforeAll(fixture.reset); + + it('returns current server time', async () => { + // + }); +}); + `, + output: ` +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../plugin/fixture.test'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); +}); +afterAll(async () => { +await agent[Symbol.asyncDispose](); +}); + + it('returns current server time', async () => { + // + }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'update test wiring - function call instead of block - async', + code: ` +import { beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + beforeAll(async () => fixture.reset()); + + it('returns current server time', async () => { + // + }); +}); + `, + output: ` +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../plugin/fixture.test'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); +}); +afterAll(async () => { +await agent[Symbol.asyncDispose](); +}); + + it('returns current server time', async () => { + // + }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'update test wiring - function call instead of block - not async', + code: ` +import { beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + beforeAll(() => fixture.reset()); + + it('returns current server time', async () => { + // + }); +}); + `, + output: ` +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { StatusCodes } from 'http-status-codes'; + +import { amazonSetup } from '@checkdigit/amazon'; +import awsNock from '@checkdigit/aws-nock'; +import { createFixture } from '@checkdigit/fixture'; + +import { BASE_PATH } from './index'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../plugin/fixture.test'; + +describe('/ping', () => { + awsNock(); + const fixture = createFixture(amazonSetup); + + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); +}); +afterAll(async () => { +await agent[Symbol.asyncDispose](); }); it('returns current server time', async () => { diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index 87bcd25..621b197 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -7,14 +7,24 @@ */ import { strict as assert } from 'node:assert'; + import { AST_TOKEN_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; + import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'agent-test-wiring'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const STATEMENT_FIXTURE_RESET = 'fixture.reset()'; +const STATEMENT_FIXTURE_RESET_AWAITED = `await ${STATEMENT_FIXTURE_RESET};`; +const STATEMENT_AGENT_DECLARATION = 'let agent: Agent;'; +const STATEMENT_AGENT_CREATION = 'agent = await createAgent();'; +const STATEMENT_AGENT_REGISTER = 'agent.register(await fixturePlugin(fixture));'; +const STATEMENT_AGENT_ENABLE = 'agent.enable();'; +const STATEMENT_AGENT_DISPOSE = 'await agent[Symbol.asyncDispose]();'; + const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = createRule({ name: ruleId, meta: { @@ -33,21 +43,37 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create create(context) { const sourceCode = context.sourceCode; const importDeclarations = new Map(); - let beforeAllCallExpression: TSESTree.CallExpression | undefined; - let afterAllCallExpression: TSESTree.CallExpression | undefined; + let isFixtureUsed = false; + let beforeAll: TSESTree.CallExpression | undefined; + let afterAll: TSESTree.CallExpression | undefined; return { ImportDeclaration(importDeclaration) { const moduleName = importDeclaration.source.value; importDeclarations.set(moduleName, importDeclaration); + if ( + moduleName === '@checkdigit/fixture' && + importDeclaration.specifiers.some( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && + specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier && + specifier.imported.name === 'createFixture', + ) + ) { + isFixtureUsed = true; + } }, 'CallExpression[callee.name="beforeAll"]': (callExpression: TSESTree.CallExpression) => { - beforeAllCallExpression = callExpression; + beforeAll = callExpression; }, 'CallExpression[callee.name="afterAll"]': (callExpression: TSESTree.CallExpression) => { - afterAllCallExpression = callExpression; + afterAll = callExpression; }, 'Program:exit'(program) { + if (!isFixtureUsed) { + return; + } + try { let jestImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; let agentImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; @@ -89,40 +115,50 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '../../plugin/fixture.test';`); } - if (beforeAllCallExpression !== undefined) { - const beforeAllArrowFunctionExpression = beforeAllCallExpression.arguments[0]; - assert.ok( - beforeAllArrowFunctionExpression !== undefined && - beforeAllArrowFunctionExpression.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression, - ); - const arrowFunctionBody = beforeAllArrowFunctionExpression.body; - assert.ok(arrowFunctionBody.type === TSESTree.AST_NODE_TYPES.BlockStatement); - - const targetStatement = arrowFunctionBody.body.find( - (statement) => sourceCode.getText(statement) === 'await fixture.reset();', - ); - if (targetStatement !== undefined) { - const beforeAllBodyText = sourceCode.getText(arrowFunctionBody); - if (!beforeAllBodyText.includes('agent = await createAgent();')) { + if (beforeAll !== undefined) { + const beforeAllArgument = beforeAll.arguments[0]; + assert.ok(beforeAllArgument !== undefined); + if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) { + if ( + beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression && + beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement + ) { + const fixtureResetStatement = beforeAllArgument.body.body.find( + (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED, + ); + assert.ok(fixtureResetStatement !== undefined); + beforeAllFixer = (fixer: RuleFixer) => + fixer.replaceText( + fixtureResetStatement, + [ + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + ].join('\n'), + ); + } else { beforeAllFixer = (fixer: RuleFixer) => fixer.replaceText( - targetStatement, + beforeAllArgument, [ - 'agent = await createAgent();', - 'agent.register(await fixturePlugin(fixture));', - 'agent.enable();', - 'await fixture.reset();', + `async () => {`, + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + `}`, ].join('\n'), ); - agentDeclarationFixer = (fixer: RuleFixer) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fixer.insertTextBefore(beforeAllCallExpression!, 'let agent: Agent;\n'); } + agentDeclarationFixer = (fixer: RuleFixer) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`); } } - if (afterAllCallExpression !== undefined) { - const afterAllArrowFunctionExpression = afterAllCallExpression.arguments[0]; + if (afterAll !== undefined) { + const afterAllArrowFunctionExpression = afterAll.arguments[0]; assert.ok( afterAllArrowFunctionExpression !== undefined && afterAllArrowFunctionExpression.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression, @@ -131,23 +167,20 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create assert.ok(arrowFunctionBody.type === TSESTree.AST_NODE_TYPES.BlockStatement); const afterAllBodyText = sourceCode.getText(arrowFunctionBody); - if (!afterAllBodyText.includes('await agent[Symbol.asyncDispose]();')) { + if (!afterAllBodyText.includes(STATEMENT_AGENT_DISPOSE)) { const lastStatement = arrowFunctionBody.body.at(-1); assert.ok(lastStatement); - afterAllFixer = (fixer: RuleFixer) => - fixer.insertTextAfter(lastStatement, 'await agent[Symbol.asyncDispose]();'); + afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastStatement, STATEMENT_AGENT_DISPOSE); } - } else if (beforeAllCallExpression !== undefined) { - const nextToken = sourceCode.getTokenAfter(beforeAllCallExpression); + } else if (beforeAll !== undefined) { + const nextToken = sourceCode.getTokenAfter(beforeAll); afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter( nextToken !== null && nextToken.type === AST_TOKEN_TYPES.Punctuator ? nextToken : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - beforeAllCallExpression!, - `\nafterAll(async () => { - await agent[Symbol.asyncDispose](); -});`, + beforeAll!, + ['', `afterAll(async () => {`, STATEMENT_AGENT_DISPOSE, `});`].join('\n'), ); } diff --git a/src/index.ts b/src/index.ts index ab8267d..b0f68a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,6 +155,8 @@ const configs: Record Date: Wed, 30 Oct 2024 21:28:00 -0400 Subject: [PATCH 077/115] fix: update function call argument check to handle optional parameters --- src/agent/fix-function-call-arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index c281904..73bcf5e 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -105,7 +105,7 @@ const rule: ESLintUtils.RuleModule< const expectedParametersCount = expectedParameters.length; const actualParameters = callExpression.arguments; const actualParametersCount = actualParameters.length; - if (actualParametersCount === 0 || actualParametersCount === expectedParametersCount) { + if (actualParametersCount === 0) { return; } From add96785922d334f84532d2f286c250319094e14 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 31 Oct 2024 00:58:23 -0400 Subject: [PATCH 078/115] enhance test wiring and fixture.api response destructure --- src/agent/agent-test-wiring.ts | 85 +++++++++++++++++---------------- src/agent/no-fixture.spec.ts | 16 +++++++ src/agent/no-fixture.ts | 5 +- src/agent/response-reference.ts | 19 ++++++-- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index 621b197..96cd4a7 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -70,7 +70,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create afterAll = callExpression; }, 'Program:exit'(program) { - if (!isFixtureUsed) { + if (!isFixtureUsed || beforeAll === undefined) { return; } @@ -85,6 +85,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create const lastImportDeclaration = [...importDeclarations.values()].at(-1); assert.ok(lastImportDeclaration); + // check if afterAll is already imported from jest const jestImportDeclaration = importDeclarations.get('@jest/globals'); if ( jestImportDeclaration && @@ -100,6 +101,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create jestImportFixer = (fixer: RuleFixer) => fixer.insertTextBefore(firstImportSpecifier, 'afterAll, '); } + // check if agent is already imported const agentImportDeclaration = importDeclarations.get('@checkdigit/agent'); if (!agentImportDeclaration) { agentImportFixer = (fixer: RuleFixer) => @@ -109,54 +111,55 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create ); } + // check if fixture plugin is already imported const fixturePluginImportDeclaration = importDeclarations.get('../../plugin/fixture.test'); if (!fixturePluginImportDeclaration) { fixturePluginImportFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '../../plugin/fixture.test';`); } - if (beforeAll !== undefined) { - const beforeAllArgument = beforeAll.arguments[0]; - assert.ok(beforeAllArgument !== undefined); - if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) { - if ( - beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression && - beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement - ) { - const fixtureResetStatement = beforeAllArgument.body.body.find( - (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED, + // inject agent declaration and initialization to `beforeAll` block + const beforeAllArgument = beforeAll.arguments[0]; + assert.ok(beforeAllArgument !== undefined); + if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) { + if ( + beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression && + beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement + ) { + const fixtureResetStatement = beforeAllArgument.body.body.find( + (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED, + ); + assert.ok(fixtureResetStatement !== undefined); + beforeAllFixer = (fixer: RuleFixer) => + fixer.replaceText( + fixtureResetStatement, + [ + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + ].join('\n'), + ); + } else { + beforeAllFixer = (fixer: RuleFixer) => + fixer.replaceText( + beforeAllArgument, + [ + `async () => {`, + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + `}`, + ].join('\n'), ); - assert.ok(fixtureResetStatement !== undefined); - beforeAllFixer = (fixer: RuleFixer) => - fixer.replaceText( - fixtureResetStatement, - [ - STATEMENT_AGENT_CREATION, - STATEMENT_AGENT_REGISTER, - STATEMENT_AGENT_ENABLE, - STATEMENT_FIXTURE_RESET_AWAITED, - ].join('\n'), - ); - } else { - beforeAllFixer = (fixer: RuleFixer) => - fixer.replaceText( - beforeAllArgument, - [ - `async () => {`, - STATEMENT_AGENT_CREATION, - STATEMENT_AGENT_REGISTER, - STATEMENT_AGENT_ENABLE, - STATEMENT_FIXTURE_RESET_AWAITED, - `}`, - ].join('\n'), - ); - } - agentDeclarationFixer = (fixer: RuleFixer) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`); } + agentDeclarationFixer = (fixer: RuleFixer) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`); } + // inject agent disposal to `afterAll` block if (afterAll !== undefined) { const afterAllArrowFunctionExpression = afterAll.arguments[0]; assert.ok( @@ -172,7 +175,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create assert.ok(lastStatement); afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastStatement, STATEMENT_AGENT_DISPOSE); } - } else if (beforeAll !== undefined) { + } else { const nextToken = sourceCode.getTokenAfter(beforeAll); afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter( @@ -194,7 +197,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create ) { context.report({ messageId: 'updateTestWiring', - node: program, + node: beforeAll, *fix(fixer) { if (jestImportFixer !== undefined) { yield jestImportFixer(fixer); diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 46571b5..b2bff7f 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -368,6 +368,22 @@ describe(ruleId, () => { `, errors: 1, }, + { + name: 'handle destructuring variable declaration for body - with nested destructuring', + code: ` + const { body: { pgpPublicKey: firstPgpPublicKey } } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + `, + output: ` + const response = await fetch(\`$\{BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const { pgpPublicKey: firstPgpPublicKey } = await response.json(); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + `, + errors: 1, + }, { name: 'handle destructuring variable declaration for headers when body is presented as well', code: ` diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index a9dd801..9f00306 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -14,6 +14,7 @@ import type { Expression, MemberExpression, Node, + ObjectPattern, ReturnStatement, SimpleCallExpression, VariableDeclaration, @@ -245,6 +246,7 @@ const rule: Rule.RuleModule = { // eslint-disable-next-line max-lines-per-function 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( fixtureCall: CallExpression, + // eslint-disable-next-line sonarjs/cognitive-complexity ) => { try { if ( @@ -325,7 +327,8 @@ const rule: Rule.RuleModule = { // eslint-disable-next-line no-nested-ternary ...(destructuringResponseBodyVariable ? [ - `const ${destructuringResponseBodyVariable.name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `const ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, ] : isResponseBodyVariableRedefinitionNeeded ? [ diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index 581c1e4..edeba73 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -7,11 +7,14 @@ */ import { strict as assert } from 'node:assert'; -import type { MemberExpression, VariableDeclaration } from 'estree'; +import type { MemberExpression, ObjectPattern, VariableDeclaration } from 'estree'; import { type Scope } from 'eslint'; +import debug from 'debug'; import { getParent } from '../library/tree'; +const log = debug('eslint-plugin:response-reference'); + /** * analyze response related variables and their references * the implementation is for fixture API, but it can be used for fetch API as well since the tree structure is similar @@ -25,7 +28,7 @@ export function analyzeResponseReferences( bodyReferences: MemberExpression[]; headersReferences: MemberExpression[]; statusReferences: MemberExpression[]; - destructuringBodyVariable?: Scope.Variable; + destructuringBodyVariable?: Scope.Variable | ObjectPattern; destructuringHeadersVariable?: Scope.Variable; destructuringHeadersReferences?: MemberExpression[] | undefined; } { @@ -34,7 +37,7 @@ export function analyzeResponseReferences( bodyReferences: MemberExpression[]; headersReferences: MemberExpression[]; statusReferences: MemberExpression[]; - destructuringBodyVariable?: Scope.Variable; + destructuringBodyVariable?: Scope.Variable | ObjectPattern; destructuringHeadersVariable?: Scope.Variable; destructuringHeadersReferences?: MemberExpression[] | undefined; } = { @@ -101,7 +104,17 @@ export function analyzeResponseReferences( parent.property.name !== 'get' && getParent(parent)?.type !== 'CallExpression', ); + } else if (identifierParent.type === 'Property') { + // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..." + const parent = getParent(identifierParent); + if (parent?.type === 'ObjectPattern') { + const parent2 = getParent(parent); + if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') { + results.destructuringBodyVariable = parent; + } + } } else { + log('+++++++ can not handle identifierParent', identifierParent); throw new Error(`Unknown response variable reference: ${responseVariable.name}`); } } From fb96df8480f6ce75f83ccf8913685c75b6e18f78 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 31 Oct 2024 11:48:47 -0400 Subject: [PATCH 079/115] improve agent-test-wiring to handle different folder structure --- src/agent/agent-test-wiring.spec.ts | 168 ++++++++++++--------------- src/agent/agent-test-wiring.ts | 20 ++-- ts-init/src/api/v1/ping.spec.ts | 0 ts-init/src/api/v1/test/util.spec.ts | 0 ts-init/src/service/pgp.spec.ts | 0 5 files changed, 85 insertions(+), 103 deletions(-) create mode 100644 ts-init/src/api/v1/ping.spec.ts create mode 100644 ts-init/src/api/v1/test/util.spec.ts create mode 100644 ts-init/src/service/pgp.spec.ts diff --git a/src/agent/agent-test-wiring.spec.ts b/src/agent/agent-test-wiring.spec.ts index b05463b..9bcc79d 100644 --- a/src/agent/agent-test-wiring.spec.ts +++ b/src/agent/agent-test-wiring.spec.ts @@ -29,45 +29,26 @@ describe('/ping', () => { invalid: [ { name: 'update test wiring - async arrow function with body block', + filename: `src/api/v1/ping.spec.ts`, code: ` import { beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - beforeAll(async () => { await fixture.reset(); }, 15_000); - - it('returns current server time', async () => { - // - }); }); `, output: ` import { afterAll, beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; import createAgent, { type Agent } from '@checkdigit/agent'; import fixturePlugin from '../../plugin/fixture.test'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - let agent: Agent; beforeAll(async () => { agent = await createAgent(); @@ -78,53 +59,30 @@ await fixture.reset(); afterAll(async () => { await agent[Symbol.asyncDispose](); }); - - it('returns current server time', async () => { - // - }); }); `, errors: [{ messageId: 'updateTestWiring' }], }, { name: 'update test wiring - function reference instead of arrow function', + filename: `src/api/v1/ping.spec.ts`, code: ` import { beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - beforeAll(fixture.reset); - - it('returns current server time', async () => { - // - }); }); `, output: ` import { afterAll, beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; import createAgent, { type Agent } from '@checkdigit/agent'; import fixturePlugin from '../../plugin/fixture.test'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - let agent: Agent; beforeAll(async () => { agent = await createAgent(); @@ -135,53 +93,30 @@ await fixture.reset(); afterAll(async () => { await agent[Symbol.asyncDispose](); }); - - it('returns current server time', async () => { - // - }); }); `, errors: [{ messageId: 'updateTestWiring' }], }, { name: 'update test wiring - function call instead of block - async', + filename: `src/api/v1/ping.spec.ts`, code: ` import { beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - beforeAll(async () => fixture.reset()); - - it('returns current server time', async () => { - // - }); }); `, output: ` import { afterAll, beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; import createAgent, { type Agent } from '@checkdigit/agent'; import fixturePlugin from '../../plugin/fixture.test'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - let agent: Agent; beforeAll(async () => { agent = await createAgent(); @@ -192,53 +127,30 @@ await fixture.reset(); afterAll(async () => { await agent[Symbol.asyncDispose](); }); - - it('returns current server time', async () => { - // - }); }); `, errors: [{ messageId: 'updateTestWiring' }], }, { name: 'update test wiring - function call instead of block - not async', + filename: `src/api/v1/ping.spec.ts`, code: ` import { beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - beforeAll(() => fixture.reset()); - - it('returns current server time', async () => { - // - }); }); `, output: ` import { afterAll, beforeAll, describe, it } from '@jest/globals'; -import { StatusCodes } from 'http-status-codes'; - import { amazonSetup } from '@checkdigit/amazon'; -import awsNock from '@checkdigit/aws-nock'; import { createFixture } from '@checkdigit/fixture'; - -import { BASE_PATH } from './index'; import createAgent, { type Agent } from '@checkdigit/agent'; import fixturePlugin from '../../plugin/fixture.test'; - describe('/ping', () => { - awsNock(); const fixture = createFixture(amazonSetup); - let agent: Agent; beforeAll(async () => { agent = await createAgent(); @@ -249,10 +161,74 @@ await fixture.reset(); afterAll(async () => { await agent[Symbol.asyncDispose](); }); - - it('returns current server time', async () => { - // - }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'update test wiring - deeper folder structure', + filename: `src/api/v1/test/util.spec.ts`, + code: ` +import { beforeAll, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + beforeAll(() => fixture.reset()); +}); + `, + output: ` +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../../plugin/fixture.test'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); +}); +afterAll(async () => { +await agent[Symbol.asyncDispose](); +}); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'update test wiring - shallower folder structure', + filename: `src/service/pgp.spec.ts`, + code: ` +import { beforeAll, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + beforeAll(() => fixture.reset()); +}); + `, + output: ` +import { afterAll, beforeAll, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../plugin/fixture.test'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +await fixture.reset(); +}); +afterAll(async () => { +await agent[Symbol.asyncDispose](); +}); }); `, errors: [{ messageId: 'updateTestWiring' }], diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index 96cd4a7..562408d 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -10,12 +10,13 @@ import { strict as assert } from 'node:assert'; import { AST_TOKEN_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; +import debug from 'debug'; import getDocumentationUrl from '../get-documentation-url'; export const ruleId = 'agent-test-wiring'; - const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const log = debug('eslint-plugin:agent:agent-test-wiring'); const STATEMENT_FIXTURE_RESET = 'fixture.reset()'; const STATEMENT_FIXTURE_RESET_AWAITED = `await ${STATEMENT_FIXTURE_RESET};`; @@ -41,6 +42,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create }, defaultOptions: [], create(context) { + log('Processing file:', context.filename); const sourceCode = context.sourceCode; const importDeclarations = new Map(); let isFixtureUsed = false; @@ -85,7 +87,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create const lastImportDeclaration = [...importDeclarations.values()].at(-1); assert.ok(lastImportDeclaration); - // check if afterAll is already imported from jest + // make sure that afterAll is imported from jest const jestImportDeclaration = importDeclarations.get('@jest/globals'); if ( jestImportDeclaration && @@ -101,7 +103,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create jestImportFixer = (fixer: RuleFixer) => fixer.insertTextBefore(firstImportSpecifier, 'afterAll, '); } - // check if agent is already imported + // make sure that agent is imported const agentImportDeclaration = importDeclarations.get('@checkdigit/agent'); if (!agentImportDeclaration) { agentImportFixer = (fixer: RuleFixer) => @@ -111,11 +113,15 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create ); } - // check if fixture plugin is already imported - const fixturePluginImportDeclaration = importDeclarations.get('../../plugin/fixture.test'); - if (!fixturePluginImportDeclaration) { + // make sure that fixture plugin is imported + const pathLets = context.filename.split('/'); + const currentFileIndex = pathLets.length - 1; + const pluginFolderIndex = pathLets.lastIndexOf('src') + 1; + // it should be safe to assume that the test code is always at least one level deeper than the plugin folder + const fixturePluginImportPath = `${'../'.repeat(currentFileIndex - pluginFolderIndex)}plugin/fixture.test`; + if (!importDeclarations.get(fixturePluginImportPath)) { fixturePluginImportFixer = (fixer: RuleFixer) => - fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '../../plugin/fixture.test';`); + fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '${fixturePluginImportPath}';`); } // inject agent declaration and initialization to `beforeAll` block diff --git a/ts-init/src/api/v1/ping.spec.ts b/ts-init/src/api/v1/ping.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/ts-init/src/api/v1/test/util.spec.ts b/ts-init/src/api/v1/test/util.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/ts-init/src/service/pgp.spec.ts b/ts-init/src/service/pgp.spec.ts new file mode 100644 index 0000000..e69de29 From ebf93782cb91840e704ca86f2ac969a262c9111c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 1 Nov 2024 13:58:18 -0400 Subject: [PATCH 080/115] handle BASE_PATH constant declaration and import --- src/agent/add-base-path-const.spec.ts | 32 ++ src/agent/add-base-path-const.ts | 85 +++++ src/agent/file.spec.ts | 50 +++ src/agent/file.ts | 40 +++ src/agent/no-fixture.spec.ts | 429 +++++++++++++++----------- src/agent/no-fixture.ts | 29 ++ src/index.ts | 5 + ts-init/package.json | 3 + ts-init/src/api/v1/index.ts | 0 ts-init/src/api/v1/swagger.yml | 81 +++++ 10 files changed, 568 insertions(+), 186 deletions(-) create mode 100644 src/agent/add-base-path-const.spec.ts create mode 100644 src/agent/add-base-path-const.ts create mode 100644 src/agent/file.spec.ts create mode 100644 src/agent/file.ts create mode 100644 ts-init/package.json create mode 100644 ts-init/src/api/v1/index.ts create mode 100644 ts-init/src/api/v1/swagger.yml diff --git a/src/agent/add-base-path-const.spec.ts b/src/agent/add-base-path-const.spec.ts new file mode 100644 index 0000000..970e5ae --- /dev/null +++ b/src/agent/add-base-path-const.spec.ts @@ -0,0 +1,32 @@ +// agent/add-url-domain.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './add-base-path-const'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'no change if the BASE_PATH const is already declared', + filename: 'src/api/v1/index.ts', + code: `import ping from './ping'; + export const BASE_PATH = 'https://ping.checkdigit/ping/v1';`, + }, + ], + invalid: [ + { + name: 'add BASE_PATH const', + filename: 'src/api/v1/index.ts', + code: `import ping from './ping';`, + output: `import ping from './ping'; +export const BASE_PATH = 'https://ping.checkdigit/ping/v1'; +`, + errors: [{ messageId: 'addBasePathConst' }], + }, + ], +}); diff --git a/src/agent/add-base-path-const.ts b/src/agent/add-base-path-const.ts new file mode 100644 index 0000000..0fc7981 --- /dev/null +++ b/src/agent/add-base-path-const.ts @@ -0,0 +1,85 @@ +// agent/add-url-domain.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; + +import getDocumentationUrl from '../get-documentation-url'; +import { + getProjectRootFolder, + getSwaggerPathByIndexFile, + isApiIndexFile, + loadPackageJson, + loadSwagger, +} from './file'; + +export const ruleId = 'add-base-path-const'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'addBasePathConst'> = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Add HTTP domain to the BASE_PATH like url constant variable.', + }, + messages: { + addBasePathConst: 'Add HTTP domain to the BASE_PATH like url constant variable.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + Program: (program: TSESTree.Program) => { + if (!isApiIndexFile(context.filename)) { + return; + } + + const scope = sourceCode.getScope(program).childScopes[0]; + assert(scope); + + const foundBasePathConst = scope.variables.find((variable) => variable.name === 'BASE_PATH'); + if (foundBasePathConst) { + return; + } + + const swaggerPath = getSwaggerPathByIndexFile(context.filename); + const swaggerFileContents = loadSwagger(swaggerPath); + const baseUrlLine = swaggerFileContents.split('\n').find((line) => /^\s*-\s*url:\s*\/.*$/u.test(line)); + const baseUrl = baseUrlLine?.split(':')[1]?.trim(); + assert(baseUrl !== undefined); + + const packageRoot = getProjectRootFolder(context.filename); + const packageJson = JSON.parse(loadPackageJson(packageRoot)) as { name: string }; + const serviceName = packageJson.name.split('/')[1]; + assert(serviceName !== undefined); + + const domain = `https://${serviceName}.checkdigit${baseUrl}`; + + const lastImportStatement = program.body.findLast((node) => node.type === AST_NODE_TYPES.ImportDeclaration); + assert(lastImportStatement); + + context.report({ + messageId: 'addBasePathConst', + node: program, + fix(fixer) { + return fixer.insertTextAfter(lastImportStatement, `\nexport const BASE_PATH = '${domain}';\n`); + }, + }); + }, + }; + }, +}); + +export default rule; diff --git a/src/agent/file.spec.ts b/src/agent/file.spec.ts new file mode 100644 index 0000000..d9f411a --- /dev/null +++ b/src/agent/file.spec.ts @@ -0,0 +1,50 @@ +// agent/file.spec.ts + +import { strict as assert } from 'node:assert'; +import { describe, it } from '@jest/globals'; +import { + getApiFolder, + getApiIndexPathByFilename, + getProjectRootFolder, + getSwaggerPathByIndexFile, + isApiIndexFile, +} from './file'; + +describe('file utility functions', () => { + it('isApiIndexFile', () => { + assert(isApiIndexFile('/Users/xxx/workspace/src/api/v1/index.ts')); + assert(!isApiIndexFile('/Users/xxx/workspace/src/api/v1/ping.ts')); + assert(!isApiIndexFile('/Users/xxx/workspace/src/api/v1/test/index.ts')); + }); + + it('getProjectRootFolder', () => { + assert.equal(getProjectRootFolder('/Users/xxx/workspace/src/index.ts'), '/Users/xxx/workspace'); + assert.equal(getProjectRootFolder('/Users/xxx/workspace/src/util/pgp.ts'), '/Users/xxx/workspace'); + assert.equal(getProjectRootFolder('/Users/xxx/workspace/src/api/v1/index.ts'), '/Users/xxx/workspace'); + assert.equal(getProjectRootFolder('/Users/xxx/workspace/src/api/v1/ping.ts'), '/Users/xxx/workspace'); + assert.equal(getProjectRootFolder('/Users/xxx/workspace'), ''); + }); + + it('getSwaggerPathByIndexFile', () => { + assert.equal( + getSwaggerPathByIndexFile('/Users/xxx/workspace/src/api/v1/index.ts'), + '/Users/xxx/workspace/src/api/v1/swagger.yml', + ); + }); + + it('getApiFolder', () => { + assert.equal(getApiFolder('src/api/v1/index.ts'), 'src/api/v1'); + assert.equal(getApiFolder('src/api/v1/ping.ts'), 'src/api/v1'); + assert.equal(getApiFolder('src/api/v1/service/abc.ts'), 'src/api/v1'); + + assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/index.ts'), '/Users/xxx/workspace/src/api/v1'); + assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/ping.ts'), '/Users/xxx/workspace/src/api/v1'); + assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/service/abc.ts'), '/Users/xxx/workspace/src/api/v1'); + }); + + it('getApiIndexPathByFilename', () => { + assert.equal(getApiIndexPathByFilename('/Users/xxx/workspace/src/api/v1/ping.ts'), './index'); + assert.equal(getApiIndexPathByFilename('/Users/xxx/workspace/src/api/v1/service/abc.ts'), '../index'); + assert.equal(getApiIndexPathByFilename('/Users/xxx/workspace/src/api/v1/service/util/abc.ts'), '../../index'); + }); +}); diff --git a/src/agent/file.ts b/src/agent/file.ts new file mode 100644 index 0000000..6c7cd97 --- /dev/null +++ b/src/agent/file.ts @@ -0,0 +1,40 @@ +// agent/file.ts + +import { strict as assert } from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; + +export function isApiIndexFile(filename: string): boolean { + return /.*\/src\/api\/v\d+\/index.ts/u.test(filename); +} + +export function getProjectRootFolder(indexFilename: string): string { + return indexFilename.substring(0, indexFilename.lastIndexOf('/src/')); +} + +export function getSwaggerPathByIndexFile(indexFilename: string): string { + return indexFilename.replace(/index\.ts$/u, 'swagger.yml'); +} + +export function loadSwagger(filename: string): string { + return fs.readFileSync(filename, 'utf8'); +} + +export function loadPackageJson(projectRoot: string): string { + return fs.readFileSync(`${projectRoot}/package.json`, 'utf8'); +} + +export function getApiFolder(folder: string): string | undefined { + if (/^(?.*\/)*src\/api\/v\d+$/u.test(folder)) { + return folder; + } + const upperFolder = folder.substring(0, folder.lastIndexOf('/')); + return upperFolder.trim() === '' ? undefined : getApiFolder(upperFolder); +} + +export function getApiIndexPathByFilename(filename: string): string { + const apiFolder = getApiFolder(filename); + assert(apiFolder !== undefined, `Cannot find api folder for ${filename}`); + const relativePath = path.relative(path.dirname(filename), `${apiFolder}/index`); + return relativePath.startsWith('../') ? relativePath : `./${relativePath}`; +} diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index b2bff7f..9df623e 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -6,33 +6,31 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import { describe } from '@jest/globals'; import createTester from '../tester.test'; import rule, { ruleId } from './no-fixture'; -describe(ruleId, () => { - createTester().run(ruleId, rule, { - valid: [ - { - name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', - code: ` +createTester().run(ruleId, rule, { + valid: [ + { + name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', + code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), ]); `, - }, - ], - invalid: [ - { - name: 'without assertions', - code: ` + }, + ], + invalid: [ + { + name: 'without assertions', + code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), ]); `, - output: ` + output: ` const responses = await Promise.all([ fetch(\`\${BASE_PATH}/key\`, { method: 'PUT', @@ -44,17 +42,19 @@ describe(ruleId, () => { }), ]); `, - errors: 2, - }, - { - name: 'assertion with variable declaration', - code: ` + errors: 2, + }, + { + name: 'assertion with variable declaration', + code: ` + import { BASE_PATH } from './index'; const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); const body = pingResponse.body; const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); @@ -63,51 +63,58 @@ describe(ruleId, () => { const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - errors: 1, - }, - { - name: 'assertion without variable declaration', - code: ` + errors: 1, + }, + { + name: 'assertion without variable declaration', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.equal(response.status, StatusCodes.OK); `, - errors: 1, - }, - { - name: 'assertion without variable declaration', - code: ` + errors: 1, + }, + { + name: 'assertion without variable declaration', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); `, - errors: 1, - }, - { - name: 'PUT with request body', - code: ` + errors: 1, + }, + { + name: 'PUT with request body', + code: ` + import { BASE_PATH } from './index'; await fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { method: 'PUT', body: JSON.stringify(cardCreationData), }); assert.equal(response.status, StatusCodes.BAD_REQUEST); `, - errors: 1, - }, - { - name: 'PUT with request header', - code: ` + errors: 1, + }, + { + name: 'PUT with request header', + code: ` + import { BASE_PATH } from './index'; const noFraudResponse = await fixture.api .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .set(IF_MATCH_HEADER, originalCard.version) @@ -115,7 +122,8 @@ describe(ruleId, () => { .set('x-y-z', '123') .expect(StatusCodes.NO_CONTENT); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'POST', headers: { @@ -126,41 +134,46 @@ describe(ruleId, () => { }); assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); `, - errors: 1, - }, - { - name: 'POST without request header/body', - code: ` + errors: 1, + }, + { + name: 'POST without request header/body', + code: ` + import { BASE_PATH } from './index'; await fixture.api .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .expect(StatusCodes.NO_CONTENT); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'POST', }); assert.equal(response.status, StatusCodes.NO_CONTENT); `, - errors: 1, - }, - { - name: 'replace del with DELETE', - code: ` + errors: 1, + }, + { + name: 'replace del with DELETE', + code: ` + import { BASE_PATH } from './index'; await fixture.api .del(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .expect(StatusCodes.NO_CONTENT); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'DELETE', }); assert.equal(response.status, StatusCodes.NO_CONTENT); `, - errors: 1, - }, - { - name: 'response headers assertion should be externalized with new variable declared if necessary', - code: ` + errors: 1, + }, + { + name: 'response headers assertion should be externalized with new variable declared if necessary', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v2/ping\`) .expect(StatusCodes.OK) .expect('etag', '123') @@ -168,7 +181,8 @@ describe(ruleId, () => { .expect(ETAG, correctVersion) .expect(ETAG, /1.*/u); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); @@ -178,46 +192,52 @@ describe(ruleId, () => { assert.equal(response.headers.get(ETAG), correctVersion); assert.ok(response.headers.get(ETAG).match(/1.*/u)); `, - errors: 1, - }, - { - name: 'response body assertion', - code: ` + errors: 1, + }, + { + name: 'response body assertion', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v2/ping\`).expect({message:'pong'}); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.deepEqual(await response.json(), {message:'pong'}); `, - errors: 1, - }, - { - name: 'response callback assertion', - code: ` + errors: 1, + }, + { + name: 'response callback assertion', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v2/ping\`) .expect(validate) .expect((response)=>console.log(response)); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.ok(validate(response)); assert.ok(console.log(response)); `, - errors: 1, - }, - { - name: 'multiple fixture calls in the same test', - code: ` + errors: 1, + }, + { + name: 'multiple fixture calls in the same test', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); @@ -236,42 +256,54 @@ describe(ruleId, () => { }); assert.equal(response3.status, StatusCodes.OK); `, - errors: 4, - }, - { - name: 'directly return (no await) fixture call', - code: `() => { + errors: 4, + }, + { + name: 'directly return (no await) fixture call', + code: ` + import { BASE_PATH } from './index'; + () => { return fixture.api.get(\`/sample-service/v1/ping\`); }`, - output: `() => { + output: ` + import { BASE_PATH } from './index'; + () => { return fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); }`, - errors: 1, - }, - { - name: 'directly return (no await) fixture call with assertion', - code: `async () => { + errors: 1, + }, + { + name: 'directly return (no await) fixture call with assertion', + code: ` + import { BASE_PATH } from './index'; + async () => { return fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); }`, - output: `async () => { + output: ` + import { BASE_PATH } from './index'; + async () => { const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.equal(response.status, StatusCodes.OK); return response; }`, - errors: 1, - }, - { - name: 'directly return (no await) fixture call with body/headers', - code: `() => { + errors: 1, + }, + { + name: 'directly return (no await) fixture call with body/headers', + code: ` + import { BASE_PATH } from './index'; + () => { return fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`) .set(IF_MATCH_HEADER, originalCard.version) .send({}); }`, - output: `() => { + output: ` + import { BASE_PATH } from './index'; + () => { return fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { method: 'PUT', body: JSON.stringify({}), @@ -280,18 +312,20 @@ describe(ruleId, () => { }, }); }`, - errors: 1, - }, - { - name: 'replace statusCode with status', - code: ` + errors: 1, + }, + { + name: 'replace statusCode with status', + code: ` + import { BASE_PATH } from './index'; const response = await fixture.api.get(\`/sample-service/v2/ping\`); assert.equal(response.statusCode, StatusCodes.OK); console.log('status:', response.statusCode); const response2 = await fixture.api.get(\`/sample-service/v2/ping\`); assert.equal(response2.status, StatusCodes.OK); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); @@ -302,16 +336,18 @@ describe(ruleId, () => { }); assert.equal(response2.status, StatusCodes.OK); `, - errors: 2, - }, - { - name: 'replace header access through response.get() with response.headers.get()', - code: ` + errors: 2, + }, + { + name: 'replace header access through response.get() with response.headers.get()', + code: ` + import { BASE_PATH } from './index'; const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); assert.equal(response.get(ETAG), correctVersion); assert.equal(response.get('etag'), correctVersion); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); @@ -319,28 +355,32 @@ describe(ruleId, () => { assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); `, - errors: 1, - }, - { - name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', - code: ` + errors: 1, + }, + { + name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', + code: ` + import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v2/ping\`).expect(200); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); assert.equal(response.status, 200); `, - errors: 1, - }, - { - name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', - code: ` + errors: 1, + }, + { + name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', + code: ` + import { BASE_PATH } from './index'; const createdOn = Date.now().toUTCString(); await fixture.api.get(\`/sample-service/v2/ping\`).expect(200).expect(validateBody(createdOn)); `, - output: ` + output: ` + import { BASE_PATH } from './index'; const createdOn = Date.now().toUTCString(); const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', @@ -348,16 +388,16 @@ describe(ruleId, () => { assert.equal(response.status, 200); assert.deepEqual(await response.json(), validateBody(createdOn)); `, - errors: 1, - }, - { - name: 'handle destructuring variable declaration for body', - code: ` + errors: 1, + }, + { + name: 'handle destructuring variable declaration for body', + code: ` const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - output: ` + output: ` const response = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); @@ -366,15 +406,15 @@ describe(ruleId, () => { const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - errors: 1, - }, - { - name: 'handle destructuring variable declaration for body - with nested destructuring', - code: ` + errors: 1, + }, + { + name: 'handle destructuring variable declaration for body - with nested destructuring', + code: ` const { body: { pgpPublicKey: firstPgpPublicKey } } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); `, - output: ` + output: ` const response = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); @@ -382,16 +422,16 @@ describe(ruleId, () => { const { pgpPublicKey: firstPgpPublicKey } = await response.json(); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); `, - errors: 1, - }, - { - name: 'handle destructuring variable declaration for headers when body is presented as well', - code: ` + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers when body is presented as well', + code: ` const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); assert(body); assert.ok(headers2.get(ETAG)); `, - output: ` + output: ` const response = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); @@ -401,15 +441,15 @@ describe(ruleId, () => { assert(body); assert.ok(headers2.get(ETAG)); `, - errors: 1, - }, - { - name: 'handle destructuring variable declaration for headers without body presented but with assertions used', - code: ` + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers without body presented but with assertions used', + code: ` const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); assert.ok(headers.get(ETAG)); `, - output: ` + output: ` const response = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); @@ -417,25 +457,25 @@ describe(ruleId, () => { const headers = response.headers; assert.ok(headers.get(ETAG)); `, - errors: 1, - }, - { - name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', - code: ` + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', + code: ` const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); assert.ok(headers.get(ETAG)); `, - output: ` + output: ` const { headers } = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); assert.ok(headers.get(ETAG)); `, - errors: 1, - }, - { - name: 'avoid response variable name conflict with existing variables in the same scope', - code: ` + errors: 1, + }, + { + name: 'avoid response variable name conflict with existing variables in the same scope', + code: ` async () => { const response = 'foo'; const response1 = 'bar'; @@ -443,7 +483,7 @@ describe(ruleId, () => { await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); } `, - output: ` + output: ` async () => { const response = 'foo'; const response1 = 'bar'; @@ -457,11 +497,11 @@ describe(ruleId, () => { assert.equal(response3.status, StatusCodes.OK); } `, - errors: 2, - }, - { - name: 'response variable names in different scope do not conflict with each other', - code: ` + errors: 2, + }, + { + name: 'response variable names in different scope do not conflict with each other', + code: ` it('#1', async () => { const response = 'foo'; }); @@ -474,7 +514,7 @@ describe(ruleId, () => { await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); }); `, - output: ` + output: ` it('#1', async () => { const response = 'foo'; }); @@ -493,18 +533,18 @@ describe(ruleId, () => { assert.equal(response.status, StatusCodes.OK); }); `, - errors: 2, - }, - { - name: 'inline access to response body should be extracted to a variable', - code: ` + errors: 2, + }, + { + name: 'inline access to response body should be extracted to a variable', + code: ` export async function validatePin( fixture, ) { const paymentSecurityServicePublicKey = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; } `, - output: ` + output: ` export async function validatePin( fixture, ) { @@ -516,11 +556,11 @@ describe(ruleId, () => { const paymentSecurityServicePublicKey = responseBody.publicKey; } `, - errors: 1, - }, - { - name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - code: ` + errors: 1, + }, + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: ` const createdOn = new Date().toISOString(); const zoneKeyId = uuid(); @@ -537,7 +577,7 @@ describe(ruleId, () => { .expect(ETAG_HEADER, '1') .expect((res) => verifyTemporalHeaders(res, createdOn)); `, - output: ` + output: ` const createdOn = new Date().toISOString(); const zoneKeyId = uuid(); @@ -557,18 +597,18 @@ describe(ruleId, () => { assert.equal(response.headers.get(ETAG_HEADER), '1'); assert.ok(verifyTemporalHeaders(response, createdOn)); `, - errors: 1, - }, - { - name: 'in arrow function without concurrent promises', - code: ` + errors: 1, + }, + { + name: 'in arrow function without concurrent promises', + code: ` const delayedCardCreationPromise = new Promise((delayedExecution) => { setTimeout(() => { delayedExecution(fixture.api.put(\`\${BASE_PATH}/card/\${cardId}\`).send(otherTestCard)); }, 600); }); `, - output: ` + output: ` const delayedCardCreationPromise = new Promise((delayedExecution) => { setTimeout(() => { delayedExecution(fetch(\`\${BASE_PATH}/card/\${cardId}\`, { @@ -578,8 +618,25 @@ describe(ruleId, () => { }, 600); }); `, - errors: 1, - }, - ], - }); + errors: 1, + }, + { + name: 'add missing import of BASE_PATH', + filename: 'src/api/v1/ping.spec.ts', + code: ` + import { strict as assert } from 'node:assert'; + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + `, + output: ` + import { strict as assert } from 'node:assert'; +import { BASE_PATH } from './index'; + + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + errors: [{ messageId: 'addBasePathImport' }, { messageId: 'preferNativeFetch' }], + }, + ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 9f00306..0110204 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -34,6 +34,7 @@ import { isValidPropertyName } from '../library/variable'; import { analyzeResponseReferences } from './response-reference'; import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; +import { getApiIndexPathByFilename } from './file'; export const ruleId = 'no-fixture'; @@ -231,6 +232,7 @@ const rule: Rule.RuleModule = { }, messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + addBasePathImport: 'Add BASE_PATH import statement to the file.', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', @@ -241,6 +243,7 @@ const rule: Rule.RuleModule = { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; const scopeVariablesMap = new Map(); + let isUrlReplacementNeeded = false; return { // eslint-disable-next-line max-lines-per-function @@ -280,6 +283,9 @@ const rule: Rule.RuleModule = { // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); + if (fetchUrlArgumentText !== originalUrlArgumentText) { + isUrlReplacementNeeded = true; + } // fetch request argument const methodNode = fixtureFunction.property; // get/put/etc. @@ -451,6 +457,29 @@ const rule: Rule.RuleModule = { }); } }, + + 'Program:exit': (program) => { + if (isUrlReplacementNeeded) { + const topScope = sourceCode.getScope(program).childScopes[0]; + assert(topScope); + if (topScope.variables.some((variable) => variable.name === 'BASE_PATH')) { + return; + } + + const lastImportStatement = program.body.findLast((statement) => statement.type === 'ImportDeclaration'); + assert(lastImportStatement); + + const apiIndexPath = getApiIndexPathByFilename(context.filename); + const basePathImportStatement = `\nimport { BASE_PATH } from '${apiIndexPath}';\n`; + context.report({ + node: program, + messageId: 'addBasePathImport', + fix(fixer) { + return fixer.insertTextAfter(lastImportStatement, basePathImportStatement); + }, + }); + } + }, }; }, }; diff --git a/src/index.ts b/src/index.ts index b0f68a2..7195d1d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,6 +41,7 @@ import requireTypeOutOfTypeOnlyImports, { ruleId as requireTypeOutOfTypeOnlyImportsRuleId, } from './require-type-out-of-type-only-imports'; import noServeRuntime, { ruleId as noServeRuntimeRuleId } from './agent/no-serve-runtime'; +import addBasePathConst, { ruleId as addBasePathConstRuleId } from './agent/add-base-path-const'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -75,6 +76,7 @@ const rules: Record = { [requireResolveFullResponseRuleId]: requireResolveFullResponse, [noDuplicatedImportsRuleId]: noDuplicatedImports, [noServeRuntimeRuleId]: noServeRuntime, + [addBasePathConstRuleId]: addBasePathConst, [requireFixedServicesImportRuleId]: requireFixedServicesImport, [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, @@ -127,6 +129,7 @@ const configs: Record Date: Fri, 1 Nov 2024 14:00:18 -0400 Subject: [PATCH 081/115] prettier --- src/agent/add-base-path-const.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/agent/add-base-path-const.ts b/src/agent/add-base-path-const.ts index 0fc7981..1e3bf99 100644 --- a/src/agent/add-base-path-const.ts +++ b/src/agent/add-base-path-const.ts @@ -11,13 +11,7 @@ import { strict as assert } from 'node:assert'; import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import getDocumentationUrl from '../get-documentation-url'; -import { - getProjectRootFolder, - getSwaggerPathByIndexFile, - isApiIndexFile, - loadPackageJson, - loadSwagger, -} from './file'; +import { getProjectRootFolder, getSwaggerPathByIndexFile, isApiIndexFile, loadPackageJson, loadSwagger } from './file'; export const ruleId = 'add-base-path-const'; From e15f84f5803cbabb37475ce13badc4ec113658b4 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 1 Nov 2024 15:11:07 -0400 Subject: [PATCH 082/115] handle spec file not in nested folder of api/v* --- src/agent/file.spec.ts | 2 ++ src/agent/file.ts | 8 +++++--- src/agent/no-fixture.spec.ts | 14 ++++++++++++++ src/agent/no-fixture.ts | 24 +++++++++++++----------- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/agent/file.spec.ts b/src/agent/file.spec.ts index d9f411a..4fbfe9f 100644 --- a/src/agent/file.spec.ts +++ b/src/agent/file.spec.ts @@ -40,6 +40,8 @@ describe('file utility functions', () => { assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/index.ts'), '/Users/xxx/workspace/src/api/v1'); assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/ping.ts'), '/Users/xxx/workspace/src/api/v1'); assert.equal(getApiFolder('/Users/xxx/workspace/src/api/v1/service/abc.ts'), '/Users/xxx/workspace/src/api/v1'); + + assert.equal(getApiFolder('/Users/xxx/workspace/src/abc.ts'), undefined); }); it('getApiIndexPathByFilename', () => { diff --git a/src/agent/file.ts b/src/agent/file.ts index 6c7cd97..1db9113 100644 --- a/src/agent/file.ts +++ b/src/agent/file.ts @@ -1,6 +1,5 @@ // agent/file.ts -import { strict as assert } from 'node:assert'; import fs from 'node:fs'; import path from 'node:path'; @@ -32,9 +31,12 @@ export function getApiFolder(folder: string): string | undefined { return upperFolder.trim() === '' ? undefined : getApiFolder(upperFolder); } -export function getApiIndexPathByFilename(filename: string): string { +export function getApiIndexPathByFilename(filename: string): string | undefined { const apiFolder = getApiFolder(filename); - assert(apiFolder !== undefined, `Cannot find api folder for ${filename}`); + if (apiFolder === undefined) { + return undefined; + } + const relativePath = path.relative(path.dirname(filename), `${apiFolder}/index`); return relativePath.startsWith('../') ? relativePath : `./${relativePath}`; } diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 9df623e..ddf09d6 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -638,5 +638,19 @@ import { BASE_PATH } from './index'; `, errors: [{ messageId: 'addBasePathImport' }, { messageId: 'preferNativeFetch' }], }, + { + name: 'do not add missing import of BASE_PATH if api folder can not be determined', + filename: 'src/abc.spec.ts', + code: ` + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + errors: 1, + }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 0110204..74bee50 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -466,18 +466,20 @@ const rule: Rule.RuleModule = { return; } - const lastImportStatement = program.body.findLast((statement) => statement.type === 'ImportDeclaration'); - assert(lastImportStatement); - const apiIndexPath = getApiIndexPathByFilename(context.filename); - const basePathImportStatement = `\nimport { BASE_PATH } from '${apiIndexPath}';\n`; - context.report({ - node: program, - messageId: 'addBasePathImport', - fix(fixer) { - return fixer.insertTextAfter(lastImportStatement, basePathImportStatement); - }, - }); + if (apiIndexPath !== undefined) { + const lastImportStatement = program.body.findLast((statement) => statement.type === 'ImportDeclaration'); + assert(lastImportStatement); + + const basePathImportStatement = `\nimport { BASE_PATH } from '${apiIndexPath}';\n`; + context.report({ + node: program, + messageId: 'addBasePathImport', + fix(fixer) { + return fixer.insertTextAfter(lastImportStatement, basePathImportStatement); + }, + }); + } } }, }; From 58b76e30c97aa7d8f8143c8655bdf0d821d83160 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sun, 3 Nov 2024 14:30:13 -0500 Subject: [PATCH 083/115] add rule to import BASE_PATH if used but not declared; update related tests and documentation --- src/agent/add-base-path-const.ts | 4 +- src/agent/add-base-path-import.spec.ts | 54 +++++++++++++++++++ src/agent/add-base-path-import.ts | 69 ++++++++++++++++++++++++ src/agent/fix-function-call-arguments.ts | 9 ++-- src/agent/no-fixture.spec.ts | 32 ----------- src/agent/no-fixture.ts | 31 ----------- src/index.ts | 5 ++ 7 files changed, 135 insertions(+), 69 deletions(-) create mode 100644 src/agent/add-base-path-import.spec.ts create mode 100644 src/agent/add-base-path-import.ts diff --git a/src/agent/add-base-path-const.ts b/src/agent/add-base-path-const.ts index 1e3bf99..e501149 100644 --- a/src/agent/add-base-path-const.ts +++ b/src/agent/add-base-path-const.ts @@ -22,10 +22,10 @@ const rule: ESLintUtils.RuleModule<'addBasePathConst'> = createRule({ meta: { type: 'suggestion', docs: { - description: 'Add HTTP domain to the BASE_PATH like url constant variable.', + description: 'Add BASE_PATH const variable.', }, messages: { - addBasePathConst: 'Add HTTP domain to the BASE_PATH like url constant variable.', + addBasePathConst: 'Add BASE_PATH const variable.', }, fixable: 'code', schema: [], diff --git a/src/agent/add-base-path-import.spec.ts b/src/agent/add-base-path-import.spec.ts new file mode 100644 index 0000000..245c7ca --- /dev/null +++ b/src/agent/add-base-path-import.spec.ts @@ -0,0 +1,54 @@ +// agent/add-url-domain.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './add-base-path-import'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'no change if the BASE_PATH const is not used', + code: `const abc = '';`, + }, + { + name: 'no change if the BASE_PATH const is already declared', + filename: 'src/api/v1/index.ts', + code: ` + export const BASE_PATH = 'https://ping.checkdigit/ping/v1'; + await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + `, + }, + { + name: 'do not add missing import of BASE_PATH if api folder can not be determined', + code: `await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK);`, + }, + ], + invalid: [ + { + name: 'add missing import of BASE_PATH', + filename: 'src/api/v1/ping.spec.ts', + code: ` + import { strict as assert } from 'node:assert'; + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + output: ` + import { strict as assert } from 'node:assert'; +import { BASE_PATH } from './index'; + + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + errors: [{ messageId: 'addBasePathImport' }], + }, + ], +}); diff --git a/src/agent/add-base-path-import.ts b/src/agent/add-base-path-import.ts new file mode 100644 index 0000000..1901563 --- /dev/null +++ b/src/agent/add-base-path-import.ts @@ -0,0 +1,69 @@ +// agent/add-url-domain.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; + +import getDocumentationUrl from '../get-documentation-url'; +import { getApiIndexPathByFilename } from './file'; + +export const ruleId = 'add-base-path-import'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'addBasePathImport'> = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Add import of BASE_PATH if it is used but not imported.', + }, + messages: { + addBasePathImport: 'Add import of BASE_PATH.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + Program: (program) => { + const isBasePathUsed = sourceCode.text.includes(`$\{BASE_PATH}`); + if (isBasePathUsed) { + const topScope = sourceCode.getScope(program).childScopes[0]; + assert(topScope); + if (topScope.variables.some((variable) => variable.name === 'BASE_PATH')) { + return; + } + + const apiIndexPath = getApiIndexPathByFilename(context.filename); + if (apiIndexPath !== undefined) { + const lastImportStatement = program.body.findLast( + (statement) => statement.type === AST_NODE_TYPES.ImportDeclaration, + ); + assert(lastImportStatement); + + const basePathImportStatement = `\nimport { BASE_PATH } from '${apiIndexPath}';\n`; + context.report({ + node: program, + messageId: 'addBasePathImport', + fix(fixer) { + return fixer.insertTextAfter(lastImportStatement, basePathImportStatement); + }, + }); + } + } + }, + }; + }, +}); + +export default rule; diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 73bcf5e..e0f60d4 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -71,9 +71,9 @@ const rule: ESLintUtils.RuleModule< CallExpression(callExpression) { // ignore calls like `foo.bar()` which are likely to be 3rd party module calls // we only focus on calls against local functions or functions imported from the same module - if (callExpression.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression) { - return; - } + // if (callExpression.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression) { + // return; + // } log('===== file name:', context.filename); log('callExpression:', sourceCode.getText(callExpression)); @@ -133,11 +133,12 @@ const rule: ESLintUtils.RuleModule< if (!typesToCheck.includes(actualTypeString) && !actualTypeString.endsWith('RequestType')) { // skip the parameter type checking if it's not in the candidate types parametersToKeep.push(actualParameter); + expectedParameterIndex++; log('skipped'); } else if (typeChecker.isTypeAssignableTo(actualType, expectedType)) { parametersToKeep.push(actualParameter); - log('matched'); expectedParameterIndex++; + log('matched'); } else { log('not matched'); } diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index ddf09d6..b036273 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -620,37 +620,5 @@ createTester().run(ruleId, rule, { `, errors: 1, }, - { - name: 'add missing import of BASE_PATH', - filename: 'src/api/v1/ping.spec.ts', - code: ` - import { strict as assert } from 'node:assert'; - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - `, - output: ` - import { strict as assert } from 'node:assert'; -import { BASE_PATH } from './index'; - - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - `, - errors: [{ messageId: 'addBasePathImport' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'do not add missing import of BASE_PATH if api folder can not be determined', - filename: 'src/abc.spec.ts', - code: ` - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - `, - output: ` - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - `, - errors: 1, - }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 74bee50..9f00306 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -34,7 +34,6 @@ import { isValidPropertyName } from '../library/variable'; import { analyzeResponseReferences } from './response-reference'; import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; -import { getApiIndexPathByFilename } from './file'; export const ruleId = 'no-fixture'; @@ -232,7 +231,6 @@ const rule: Rule.RuleModule = { }, messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', - addBasePathImport: 'Add BASE_PATH import statement to the file.', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', @@ -243,7 +241,6 @@ const rule: Rule.RuleModule = { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; const scopeVariablesMap = new Map(); - let isUrlReplacementNeeded = false; return { // eslint-disable-next-line max-lines-per-function @@ -283,9 +280,6 @@ const rule: Rule.RuleModule = { // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); - if (fetchUrlArgumentText !== originalUrlArgumentText) { - isUrlReplacementNeeded = true; - } // fetch request argument const methodNode = fixtureFunction.property; // get/put/etc. @@ -457,31 +451,6 @@ const rule: Rule.RuleModule = { }); } }, - - 'Program:exit': (program) => { - if (isUrlReplacementNeeded) { - const topScope = sourceCode.getScope(program).childScopes[0]; - assert(topScope); - if (topScope.variables.some((variable) => variable.name === 'BASE_PATH')) { - return; - } - - const apiIndexPath = getApiIndexPathByFilename(context.filename); - if (apiIndexPath !== undefined) { - const lastImportStatement = program.body.findLast((statement) => statement.type === 'ImportDeclaration'); - assert(lastImportStatement); - - const basePathImportStatement = `\nimport { BASE_PATH } from '${apiIndexPath}';\n`; - context.report({ - node: program, - messageId: 'addBasePathImport', - fix(fixer) { - return fixer.insertTextAfter(lastImportStatement, basePathImportStatement); - }, - }); - } - } - }, }; }, }; diff --git a/src/index.ts b/src/index.ts index 7195d1d..3d3e2f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ import requireTypeOutOfTypeOnlyImports, { } from './require-type-out-of-type-only-imports'; import noServeRuntime, { ruleId as noServeRuntimeRuleId } from './agent/no-serve-runtime'; import addBasePathConst, { ruleId as addBasePathConstRuleId } from './agent/add-base-path-const'; +import addBasePathImport, { ruleId as addBasePathImportRuleId } from './agent/add-base-path-import'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -77,6 +78,7 @@ const rules: Record = { [noDuplicatedImportsRuleId]: noDuplicatedImports, [noServeRuntimeRuleId]: noServeRuntime, [addBasePathConstRuleId]: addBasePathConst, + [addBasePathImportRuleId]: addBasePathImport, [requireFixedServicesImportRuleId]: requireFixedServicesImport, [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, @@ -130,6 +132,7 @@ const configs: Record Date: Sun, 3 Nov 2024 19:47:07 -0500 Subject: [PATCH 084/115] add rule to automatically import assert module if used but not declared; include tests --- package-lock.json | 197 ++++++++++++----------- package.json | 18 +-- src/agent/add-assert-import.spec.ts | 54 +++++++ src/agent/add-assert-import.ts | 74 +++++++++ src/agent/fix-function-call-arguments.ts | 6 +- src/agent/no-service-wrapper.ts | 4 +- src/agent/url.spec.ts | 66 ++++++++ src/agent/url.ts | 15 +- src/index.ts | 5 + 9 files changed, 330 insertions(+), 109 deletions(-) create mode 100644 src/agent/add-assert-import.spec.ts create mode 100644 src/agent/add-assert-import.ts create mode 100644 src/agent/url.spec.ts diff --git a/package-lock.json b/package-lock.json index b81aa41..cd062f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,25 +9,25 @@ "version": "7.3.0", "license": "MIT", "dependencies": { - "@typescript-eslint/type-utils": "8.10.0", - "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "debug": "^4.3.7", - "ts-api-utils": "^1.3.0" + "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.13.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.10.0", - "@typescript-eslint/rule-tester": "8.10.0", - "eslint": "^9.13.0", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/rule-tester": "8.12.2", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-eslint-plugin": "^6.2.0", + "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", @@ -35,7 +35,7 @@ "eslint-plugin-sonarjs": "1.0.4", "http-status-codes": "^2.3.0", "rimraf": "^6.0.1", - "typescript-eslint": "8.10.0" + "typescript-eslint": "8.12.2" }, "engines": { "node": ">=20.17" @@ -1212,9 +1212,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", - "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2175,17 +2175,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz", - "integrity": "sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", + "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.10.0", - "@typescript-eslint/type-utils": "8.10.0", - "@typescript-eslint/utils": "8.10.0", - "@typescript-eslint/visitor-keys": "8.10.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2209,16 +2209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.10.0.tgz", - "integrity": "sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", + "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.10.0", - "@typescript-eslint/types": "8.10.0", - "@typescript-eslint/typescript-estree": "8.10.0", - "@typescript-eslint/visitor-keys": "8.10.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4" }, "engines": { @@ -2238,14 +2238,14 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.10.0.tgz", - "integrity": "sha512-/+Cms6eddJv4UW1wzxbRYeaZKJOlwWrfzuPQCGtzMsiZMTn5SaABS/wyCSZ+po+nUXc86OtP5QajUfsZGH/tSg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.12.2.tgz", + "integrity": "sha512-aggjJT+aZj/LJVUx/qX+97tYGGqpML6vnuLwjmNrjpRP047cuSlYutG1zX8fr3ibr9tzHxiwc03dlKFsLMd12g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.10.0", - "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -2263,13 +2263,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz", - "integrity": "sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.10.0", - "@typescript-eslint/visitor-keys": "8.10.0" + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2280,13 +2280,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz", - "integrity": "sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", + "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.10.0", - "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2304,9 +2304,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.10.0.tgz", - "integrity": "sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2317,13 +2317,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz", - "integrity": "sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.10.0", - "@typescript-eslint/visitor-keys": "8.10.0", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2345,15 +2345,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.10.0.tgz", - "integrity": "sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.10.0", - "@typescript-eslint/types": "8.10.0", - "@typescript-eslint/typescript-estree": "8.10.0" + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2367,12 +2367,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz", - "integrity": "sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.10.0", + "@typescript-eslint/types": "8.12.2", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3586,21 +3586,21 @@ } }, "node_modules/eslint": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", - "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", + "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.13.0", + "@eslint/js": "9.14.0", "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3608,9 +3608,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3952,9 +3952,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -4005,6 +4005,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@humanwhocodes/retry": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", + "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4016,9 +4029,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4048,14 +4061,14 @@ "peer": true }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4065,9 +4078,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7725,9 +7738,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", "license": "MIT", "engines": { "node": ">=16" @@ -7954,15 +7967,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.10.0.tgz", - "integrity": "sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==", + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.12.2.tgz", + "integrity": "sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.10.0", - "@typescript-eslint/parser": "8.10.0", - "@typescript-eslint/utils": "8.10.0" + "@typescript-eslint/eslint-plugin": "8.12.2", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/utils": "8.12.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index a2bae54..b6ba861 100644 --- a/package.json +++ b/package.json @@ -61,25 +61,25 @@ "preset": "@checkdigit/jest-config" }, "dependencies": { - "@typescript-eslint/type-utils": "8.10.0", - "@typescript-eslint/utils": "8.10.0", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", "debug": "^4.3.7", - "ts-api-utils": "^1.3.0" + "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.13.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.10.0", - "@typescript-eslint/rule-tester": "8.10.0", - "eslint": "^9.13.0", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/rule-tester": "8.12.2", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-eslint-plugin": "^6.2.0", + "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", @@ -87,7 +87,7 @@ "eslint-plugin-sonarjs": "1.0.4", "http-status-codes": "^2.3.0", "rimraf": "^6.0.1", - "typescript-eslint": "8.10.0" + "typescript-eslint": "8.12.2" }, "peerDependencies": { "eslint": ">=9 <10" diff --git a/src/agent/add-assert-import.spec.ts b/src/agent/add-assert-import.spec.ts new file mode 100644 index 0000000..50bb05e --- /dev/null +++ b/src/agent/add-assert-import.spec.ts @@ -0,0 +1,54 @@ +// agent/add-url-domain.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './add-assert-import'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'no change if the assert is not used', + code: `const name='abc';`, + }, + { + name: 'no change if the assert is used and imported', + code: `import { strict as assert } from 'node:assert'; + assert(true);`, + }, + { + name: 'no change if the assert is used and imported - not using node: prefix in the module name', + code: `import { strict as assert } from 'assert'; + assert(true);`, + }, + ], + invalid: [ + { + name: 'add assert import if assert is used but not imported', + code: `assert(true);`, + output: `import { strict as assert } from 'node:assert'; +assert(true);`, + errors: [{ messageId: 'addAssertImport' }], + }, + { + name: 'add assert import if assert is used but not imported - using assert.ok', + code: `assert.ok(true);`, + output: `import { strict as assert } from 'node:assert'; +assert.ok(true);`, + errors: [{ messageId: 'addAssertImport' }], + }, + { + name: 'add assert import if assert is used but not imported - with first line as comment', + code: `// api/v1/ping.spec.ts + assert.ok(true);`, + output: `// api/v1/ping.spec.ts + import { strict as assert } from 'node:assert'; +assert.ok(true);`, + errors: [{ messageId: 'addAssertImport' }], + }, + ], +}); diff --git a/src/agent/add-assert-import.ts b/src/agent/add-assert-import.ts new file mode 100644 index 0000000..d50b905 --- /dev/null +++ b/src/agent/add-assert-import.ts @@ -0,0 +1,74 @@ +// agent/add-url-domain.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; + +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'add-assert-import'; + +const ASSERT_IMPORT_STATEMENT = "import { strict as assert } from 'node:assert';"; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'addAssertImport'> = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Add import of assert module of node.', + }, + messages: { + addAssertImport: 'Add import of assert module of node.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + let isAssertImported = false; + let isAssertUsed = false; + + return { + ImportDeclaration: (node) => { + if (node.source.value === 'assert' || node.source.value === 'node:assert') { + isAssertImported = true; + } + }, + CallExpression: (callExpression) => { + // detect if assert is used + if ( + (callExpression.callee.type === AST_NODE_TYPES.Identifier && callExpression.callee.name === 'assert') || + (callExpression.callee.type === AST_NODE_TYPES.MemberExpression && + callExpression.callee.object.type === AST_NODE_TYPES.Identifier && + callExpression.callee.object.name === 'assert') + ) { + isAssertUsed = true; + } + }, + 'Program:exit': (program) => { + // add assert import if necessary + if (isAssertUsed && !isAssertImported) { + const firstStatement = program.body[0]; + assert(firstStatement); + context.report({ + node: program, + messageId: 'addAssertImport', + fix(fixer) { + return fixer.insertTextBefore(firstStatement, `${ASSERT_IMPORT_STATEMENT}\n`); + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index e0f60d4..09cdb1e 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -88,8 +88,10 @@ const rule: ESLintUtils.RuleModule< } const signature = signatures[0]; - assert.ok(signature, 'Signature not found.'); - if (signature.typeParameters !== undefined && signature.typeParameters.length > 0) { + if ( + signature === undefined || + (signature.typeParameters !== undefined && signature.typeParameters.length > 0) + ) { // ignore complex signatures with type parameters return; } diff --git a/src/agent/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts index fa5228c..c59e979 100644 --- a/src/agent/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -14,7 +14,7 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils' import getDocumentationUrl from '../get-documentation-url'; import { getEnclosingScopeNode } from '../library/ts-tree'; import { getIndentation } from '../library/format'; -import { PLAIN_URL_REGEXP, replaceEndpointUrlPrefixWithDomain, TOKENIZED_URL_REGEXP } from './url'; +import { isServiceApiCallUrl, replaceEndpointUrlPrefixWithDomain } from './url'; export const ruleId = 'no-service-wrapper'; @@ -50,7 +50,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'inval urlArgument?.type === AST_NODE_TYPES.TemplateLiteral ) { const urlText = sourceCode.getText(urlArgument); - return PLAIN_URL_REGEXP.test(urlText) || TOKENIZED_URL_REGEXP.test(urlText); + return isServiceApiCallUrl(urlText); } if (urlArgument?.type === AST_NODE_TYPES.Identifier) { diff --git a/src/agent/url.spec.ts b/src/agent/url.spec.ts new file mode 100644 index 0000000..6050a1b --- /dev/null +++ b/src/agent/url.spec.ts @@ -0,0 +1,66 @@ +// src/agent/url.test.ts + +import { strict as assert } from 'node:assert'; +import { describe, it } from '@jest/globals'; +import { + addBasePathUrlDomain, + isServiceApiCallUrl, + replaceEndpointUrlPrefixWithBasePath, + replaceEndpointUrlPrefixWithDomain, +} from './url'; + +describe('URL Utility Functions', () => { + it('PLAIN_URL_REGEXP should match plain URLs - string', () => { + const url = "'/service-name/v1/endpoint'"; + assert(isServiceApiCallUrl(url)); + }); + + it('PLAIN_URL_REGEXP should match tokenized URLs - template literal', () => { + const url = '`/service-name/v1/endpoint`'; + assert(isServiceApiCallUrl(url)); + }); + + it('TOKENIZED_URL_REGEXP should match tokenized URLs - string', () => { + // eslint-disable-next-line no-template-curly-in-string + const url = '`${BASE_PATH}/endpoint`'; + assert(isServiceApiCallUrl(url)); + }); + + it('replaceEndpointUrlPrefixWithBasePath should replace URL prefix with BASE_PATH - string', () => { + const url = "'/service-name/v1/endpoint'"; + // eslint-disable-next-line no-template-curly-in-string + const expected = '`${BASE_PATH}/endpoint`'; + assert.equal(replaceEndpointUrlPrefixWithBasePath(url), expected); + }); + + it('replaceEndpointUrlPrefixWithBasePath should replace URL prefix with BASE_PATH - template literal', () => { + const url = '`/service-name/v1/endpoint`'; + // eslint-disable-next-line no-template-curly-in-string + const expected = '`${BASE_PATH}/endpoint`'; + assert.equal(replaceEndpointUrlPrefixWithBasePath(url), expected); + }); + + it('replaceEndpointUrlPrefixWithDomain should replace URL prefix with domain - string', () => { + const url = "'/service-name/v1/endpoint'"; + const expected = "'https://service-name.checkdigit/service-name/v1/endpoint'"; + assert.equal(replaceEndpointUrlPrefixWithDomain(url), expected); + }); + + it('replaceEndpointUrlPrefixWithDomain should replace URL prefix with domain - template literal', () => { + const url = '`/service-name/v1/endpoint`'; + const expected = '`https://service-name.checkdigit/service-name/v1/endpoint`'; + assert.equal(replaceEndpointUrlPrefixWithDomain(url), expected); + }); + + it('addBasePathUrlDomain should add domain to base path URL - string', () => { + const url = "'/service-name/v1'"; + const expected = "'https://service-name.checkdigit/service-name/v1'"; + assert.equal(addBasePathUrlDomain(url), expected); + }); + + it('addBasePathUrlDomain should add domain to base path URL - template literal', () => { + const url = '`/service-name/v1`'; + const expected = '`https://service-name.checkdigit/service-name/v1`'; + assert.equal(addBasePathUrlDomain(url), expected); + }); +}); diff --git a/src/agent/url.ts b/src/agent/url.ts index cd73e1a..7224d8c 100644 --- a/src/agent/url.ts +++ b/src/agent/url.ts @@ -1,13 +1,20 @@ // agent/url.ts // eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const PLAIN_URL_REGEXP: RegExp = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; +const PLAIN_URL_REGEXP: RegExp = /^[`']\/\w+(?-\w+)*\/v\d+\/(?.|\r|\n)+[`']$/u; // eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const TOKENIZED_URL_REGEXP: RegExp = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; +const TOKENIZED_URL_REGEXP: RegExp = /^`\$\{(?[A-Z]+_)*BASE_PATH\}\/(?.|\r|\n)+`$/u; + +export function isServiceApiCallUrl(url: string): boolean { + return PLAIN_URL_REGEXP.test(url) || TOKENIZED_URL_REGEXP.test(url); +} export function replaceEndpointUrlPrefixWithBasePath(url: string): string { - // eslint-disable-next-line no-template-curly-in-string - return url.replace(/^`\/\w+(?-\w+)*\/v\d+\//u, '`${BASE_PATH}/'); + return url.replace( + /^(?[`'])\/(?\w+(?-\w+)*)(?\/v\d+\/)(?(?.|\r|\n)+)(?[`'])$/u, + // eslint-disable-next-line no-template-curly-in-string + '`${BASE_PATH}/$5`', + ); } export function replaceEndpointUrlPrefixWithDomain(url: string): string { diff --git a/src/index.ts b/src/index.ts index 3d3e2f0..ccee127 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ import requireTypeOutOfTypeOnlyImports, { import noServeRuntime, { ruleId as noServeRuntimeRuleId } from './agent/no-serve-runtime'; import addBasePathConst, { ruleId as addBasePathConstRuleId } from './agent/add-base-path-const'; import addBasePathImport, { ruleId as addBasePathImportRuleId } from './agent/add-base-path-import'; +import addAssertImport, { ruleId as addAssertImportRuleId } from './agent/add-assert-import'; import filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noTestImport from './no-test-import'; @@ -79,6 +80,7 @@ const rules: Record = { [noServeRuntimeRuleId]: noServeRuntime, [addBasePathConstRuleId]: addBasePathConst, [addBasePathImportRuleId]: addBasePathImport, + [addAssertImportRuleId]: addAssertImport, [requireFixedServicesImportRuleId]: requireFixedServicesImport, [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noUnusedFunctionArgumentsRuleId]: noUnusedFunctionArguments, @@ -133,6 +135,7 @@ const configs: Record Date: Mon, 4 Nov 2024 16:18:14 -0500 Subject: [PATCH 085/115] don't report error if service-wrapper's url is passed in as function argument --- src/require-resolve-full-response.spec.ts | 18 ++++++++++++++++++ src/require-resolve-full-response.ts | 12 +++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts index 717cff0..ab6bfde 100644 --- a/src/require-resolve-full-response.spec.ts +++ b/src/require-resolve-full-response.spec.ts @@ -186,5 +186,23 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'invalidOptions' }], }, + { + name: 'handle url provided as a function argument', + code: ` + async function getKey( + fixture: Fixture, + keyRequest: ping.KeyRequest, + url: string, + ) { + const pingService = fixture.config.service.ping(EMPTY_CONTEXT); + const response = await pingService.post(url, keyRequest, { + headers: { + etag: '123', + }, + }); + } + `, + errors: [{ messageId: 'invalidOptions' }], + }, ], }); diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index f4873a5..b71243e 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -54,10 +54,12 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name); if (foundVariable) { const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); - assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); - const variableDefinitionNode = variableDefinition.node; - assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); - return isUrlArgumentValid(variableDefinitionNode.init, scope); + if (variableDefinition) { + const variableDefinitionNode = variableDefinition.node; + assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); + return isUrlArgumentValid(variableDefinitionNode.init, scope); + } + return true; } } @@ -139,7 +141,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu const enclosingScopeNode = getEnclosingScopeNode(serviceCall); assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); - const scope = scopeManager?.acquire(enclosingScopeNode); + const scope = scopeManager?.acquire(enclosingScopeNode); /*?*/ assert.ok(scope, 'scope is undefined'); const urlArgument = serviceCall.arguments[0]; if (!isUrlArgumentValid(urlArgument, scope)) { From b1c000e312a734b05a115c7f4f6466a869114bf8 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 4 Nov 2024 17:02:27 -0500 Subject: [PATCH 086/115] handle legacy service typing Endpoint similar to FullResponse, refactor --- src/agent/no-full-response.ts | 41 ---------------- src/index.ts | 8 +-- ...ec.ts => no-legacy-service-typing.spec.ts} | 21 +++++--- src/no-legacy-service-typing.ts | 49 +++++++++++++++++++ src/{agent => }/no-serve-runtime.spec.ts | 4 +- src/{agent => }/no-serve-runtime.ts | 4 +- 6 files changed, 70 insertions(+), 57 deletions(-) delete mode 100644 src/agent/no-full-response.ts rename src/{agent/no-full-response.spec.ts => no-legacy-service-typing.spec.ts} (75%) create mode 100644 src/no-legacy-service-typing.ts rename src/{agent => }/no-serve-runtime.spec.ts (88%) rename src/{agent => }/no-serve-runtime.ts (92%) diff --git a/src/agent/no-full-response.ts b/src/agent/no-full-response.ts deleted file mode 100644 index c6b4c35..0000000 --- a/src/agent/no-full-response.ts +++ /dev/null @@ -1,41 +0,0 @@ -// agent/no-full-response.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import getDocumentationUrl from '../get-documentation-url'; - -export const ruleId = 'no-full-response'; - -const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); - -const rule: ESLintUtils.RuleModule<'noFullResponse'> = createRule({ - name: ruleId, - meta: { - type: 'suggestion', - docs: { - description: 'FullResponse type should not be used.', - }, - messages: { - noFullResponse: 'Please remove the usage of FullResponse type.', - }, - schema: [], - }, - defaultOptions: [], - create(context) { - return { - 'TSTypeReference[typeName.name="FullResponse"]': (typeReference: TSESTree.TSTypeReference) => { - context.report({ - messageId: 'noFullResponse', - node: typeReference, - }); - }, - }; - }, -}); - -export default rule; diff --git a/src/index.ts b/src/index.ts index ccee127..2d43357 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ import fixFunctionCallArguments, { import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; -import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-response'; +import noLegacyServiceTyping, { ruleId as noLegacyServiceTypingRuleId } from './no-legacy-service-typing'; import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import noServiceWrapper, { ruleId as noServiceWrapperRuleId } from './agent/no-service-wrapper'; @@ -40,7 +40,7 @@ import requireResolveFullResponse, { import requireTypeOutOfTypeOnlyImports, { ruleId as requireTypeOutOfTypeOnlyImportsRuleId, } from './require-type-out-of-type-only-imports'; -import noServeRuntime, { ruleId as noServeRuntimeRuleId } from './agent/no-serve-runtime'; +import noServeRuntime, { ruleId as noServeRuntimeRuleId } from './no-serve-runtime'; import addBasePathConst, { ruleId as addBasePathConstRuleId } from './agent/add-base-path-const'; import addBasePathImport, { ruleId as addBasePathImportRuleId } from './agent/add-base-path-import'; import addAssertImport, { ruleId as addAssertImportRuleId } from './agent/add-assert-import'; @@ -73,7 +73,7 @@ const rules: Record = { [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, [addUrlDomainRuleId]: addUrlDomain, - [noFullResponseRuleId]: noFullResponse, + [noLegacyServiceTypingRuleId]: noLegacyServiceTyping, [noMappedResponseRuleId]: noMappedResponse, [requireResolveFullResponseRuleId]: requireResolveFullResponse, [noDuplicatedImportsRuleId]: noDuplicatedImports, @@ -113,7 +113,7 @@ const configs: Record = await fixture.api.put('\${BASE_PATH}/ping').send(testCard);`, - errors: [{ messageId: 'noFullResponse' }], + errors: [{ messageId: 'noLegacyServiceTyping' }], }, { name: 'report type annotation from array variable declaration', @@ -25,7 +25,7 @@ createTester().run(ruleId, rule, { fetch(\`\${BASE_PATH}/ping\`) ]); `, - errors: [{ messageId: 'noFullResponse' }], + errors: [{ messageId: 'noLegacyServiceTyping' }], }, { name: 'report type annotation from function return type', @@ -47,17 +47,22 @@ createTester().run(ruleId, rule, { throw new Error(\`Error creating Person data encryption key \${dataEncryptionKeyId}. \`); } `, - errors: [{ messageId: 'noFullResponse' }], + errors: [{ messageId: 'noLegacyServiceTyping' }], }, { name: 'report type annotation from arrow function argument narrowing', code: `putResponses.map((putResponse: FullResponse) => putResponse.statusCode)`, - errors: [{ messageId: 'noFullResponse' }], + errors: [{ messageId: 'noLegacyServiceTyping' }], }, { name: 'report type annotation from "as" type narrowing', code: `const fullResponse = response as FullResponse;`, - errors: [{ messageId: 'noFullResponse' }], + errors: [{ messageId: 'noLegacyServiceTyping' }], + }, + { + name: 'report Endpoint similar to FullResponse', + code: `const service: Endpoint = someDependentService;`, + errors: [{ messageId: 'noLegacyServiceTyping' }], }, ], }); diff --git a/src/no-legacy-service-typing.ts b/src/no-legacy-service-typing.ts new file mode 100644 index 0000000..d7b5f86 --- /dev/null +++ b/src/no-legacy-service-typing.ts @@ -0,0 +1,49 @@ +// no-full-response.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import getDocumentationUrl from './get-documentation-url'; + +export const ruleId = 'no-legacy-service-typing'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const DISALLOWED_SERVICE_TYPINGS: string[] | undefined = ['FullResponse', 'Endpoint']; + +const rule: ESLintUtils.RuleModule<'noLegacyServiceTyping', [typeof DISALLOWED_SERVICE_TYPINGS]> = createRule({ + name: ruleId, + meta: { + type: 'problem', + docs: { + description: 'Legacy service typings should not be used.', + }, + messages: { + noLegacyServiceTyping: 'Please remove the usage of legacy service typings.', + }, + schema: [{ type: 'array', items: { type: 'string' } }], + }, + defaultOptions: [DISALLOWED_SERVICE_TYPINGS], + create(context) { + return { + TSTypeReference: (typeReference: TSESTree.TSTypeReference) => { + if ( + typeReference.typeName.type === AST_NODE_TYPES.Identifier && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (context.options[0] ?? DISALLOWED_SERVICE_TYPINGS).includes(typeReference.typeName.name) + ) { + context.report({ + messageId: 'noLegacyServiceTyping', + node: typeReference, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/agent/no-serve-runtime.spec.ts b/src/no-serve-runtime.spec.ts similarity index 88% rename from src/agent/no-serve-runtime.spec.ts rename to src/no-serve-runtime.spec.ts index 70c598f..30d4cbd 100644 --- a/src/agent/no-serve-runtime.spec.ts +++ b/src/no-serve-runtime.spec.ts @@ -1,4 +1,4 @@ -// agent/no-full-response.spec.ts +// no-full-response.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import createTester from '../ts-tester.test'; +import createTester from './ts-tester.test'; import rule, { ruleId } from './no-serve-runtime'; createTester().run(ruleId, rule, { diff --git a/src/agent/no-serve-runtime.ts b/src/no-serve-runtime.ts similarity index 92% rename from src/agent/no-serve-runtime.ts rename to src/no-serve-runtime.ts index 6e5d65a..754f6b5 100644 --- a/src/agent/no-serve-runtime.ts +++ b/src/no-serve-runtime.ts @@ -1,4 +1,4 @@ -// agent/no-serve-runtime.ts +// no-serve-runtime.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -7,7 +7,7 @@ */ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import getDocumentationUrl from '../get-documentation-url'; +import getDocumentationUrl from './get-documentation-url'; export const ruleId = 'no-serve-runtime'; From d9159030eb7290bbdb30717255390117a21357fa Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 4 Nov 2024 17:17:29 -0500 Subject: [PATCH 087/115] upgrade dependencies --- package-lock.json | 116 +++++++++++++++++++++++----------------------- package.json | 10 ++-- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd062f1..6d45037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "7.3.0", "license": "MIT", "dependencies": { - "@typescript-eslint/type-utils": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/type-utils": "8.13.0", + "@typescript-eslint/utils": "8.13.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -22,8 +22,8 @@ "@types/debug": "^4.1.12", "@types/eslint": "^9.6.1", "@types/eslint-config-prettier": "^6.11.3", - "@typescript-eslint/parser": "8.12.2", - "@typescript-eslint/rule-tester": "8.12.2", + "@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", @@ -35,7 +35,7 @@ "eslint-plugin-sonarjs": "1.0.4", "http-status-codes": "^2.3.0", "rimraf": "^6.0.1", - "typescript-eslint": "8.12.2" + "typescript-eslint": "8.13.0" }, "engines": { "node": ">=20.17" @@ -2175,17 +2175,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", - "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", + "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/type-utils": "8.12.2", - "@typescript-eslint/utils": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/type-utils": "8.13.0", + "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2209,16 +2209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", - "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", + "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0", "debug": "^4.3.4" }, "engines": { @@ -2238,14 +2238,14 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.12.2.tgz", - "integrity": "sha512-aggjJT+aZj/LJVUx/qX+97tYGGqpML6vnuLwjmNrjpRP047cuSlYutG1zX8fr3ibr9tzHxiwc03dlKFsLMd12g==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.13.0.tgz", + "integrity": "sha512-VBS9EJ/W3x3XPhqZPbfnVCBL0SXaToLvZzTnfo5JhGLEFNVmY8AMT9m/A7R/VM+TL+ecuDRIPPjkC3asFrPFAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/utils": "8.13.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -2263,13 +2263,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", - "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", + "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2" + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2280,13 +2280,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", - "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", + "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/utils": "8.13.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2304,9 +2304,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", - "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", + "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2317,13 +2317,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", - "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", + "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2345,15 +2345,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", - "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", + "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2" + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/typescript-estree": "8.13.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2367,12 +2367,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", - "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", + "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/types": "8.13.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7967,15 +7967,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.12.2.tgz", - "integrity": "sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", + "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.12.2", - "@typescript-eslint/parser": "8.12.2", - "@typescript-eslint/utils": "8.12.2" + "@typescript-eslint/eslint-plugin": "8.13.0", + "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/utils": "8.13.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index b6ba861..6dffea2 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "preset": "@checkdigit/jest-config" }, "dependencies": { - "@typescript-eslint/type-utils": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/type-utils": "8.13.0", + "@typescript-eslint/utils": "8.13.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -74,8 +74,8 @@ "@types/debug": "^4.1.12", "@types/eslint": "^9.6.1", "@types/eslint-config-prettier": "^6.11.3", - "@typescript-eslint/parser": "8.12.2", - "@typescript-eslint/rule-tester": "8.12.2", + "@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", @@ -87,7 +87,7 @@ "eslint-plugin-sonarjs": "1.0.4", "http-status-codes": "^2.3.0", "rimraf": "^6.0.1", - "typescript-eslint": "8.12.2" + "typescript-eslint": "8.13.0" }, "peerDependencies": { "eslint": ">=9 <10" From 99d491ac17dfcc09c9f372b8ae379844580488ec Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 4 Nov 2024 18:30:31 -0500 Subject: [PATCH 088/115] support full response options type on top of object value check --- src/require-resolve-full-response.spec.ts | 18 +++++++++ src/require-resolve-full-response.ts | 49 +++++++++++++++-------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts index ab6bfde..b242855 100644 --- a/src/require-resolve-full-response.spec.ts +++ b/src/require-resolve-full-response.spec.ts @@ -25,6 +25,24 @@ createTester().run(ruleId, rule, { } `, }, + { + name: 'no error if options is an identifier with type of FullResponseOptions', + code: ` + async function getKey(pingService: Endpoint) { + const options: FullResponseOptions = { resolveWithFullResponse: true }; + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, options); + } + `, + }, + { + name: 'no error if options is an identifier with FullResponseOptions-ish type', + code: ` + async function getKey(pingService: Endpoint) { + const options = { resolveWithFullResponse: true }; + await pingService.get(\`\${PING_BASE_PATH}/key/\${keyId}\`, options); + } + `, + }, ], invalid: [ { diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index b71243e..c0d9975 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -158,7 +158,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu const optionsArgument = ['get', 'head', 'del'].includes(method) ? serviceCall.arguments[1] : serviceCall.arguments[2]; - if (optionsArgument === undefined || optionsArgument.type !== AST_NODE_TYPES.ObjectExpression) { + if (optionsArgument === undefined) { context.report({ node: serviceCall, messageId: 'invalidOptions', @@ -166,23 +166,38 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu return; } - const resolveWithFullResponseProperty = optionsArgument.properties.find( - (property) => - property.type === AST_NODE_TYPES.Property && - property.key.type === AST_NODE_TYPES.Identifier && - property.key.name === 'resolveWithFullResponse', - ); - if ( - resolveWithFullResponseProperty?.type !== AST_NODE_TYPES.Property || - resolveWithFullResponseProperty.value.type !== AST_NODE_TYPES.Literal || - resolveWithFullResponseProperty.value.value !== true - ) { - context.report({ - node: optionsArgument, - messageId: 'invalidOptions', - }); - return; + if (optionsArgument.type === AST_NODE_TYPES.Identifier) { + const optionsTypeString = getType(optionsArgument); + if (optionsTypeString === 'FullResponseOptions') { + return; + } + + const variable = parserService.esTreeNodeToTSNodeMap.get(optionsArgument); + const optionType = typeChecker.getTypeAtLocation(variable); + const resolveWithFullResponseProperty = optionType.getProperty('resolveWithFullResponse'); + if (resolveWithFullResponseProperty?.declarations?.[0]?.getText() === 'resolveWithFullResponse: true') { + return; + } + } else if (optionsArgument.type === AST_NODE_TYPES.ObjectExpression) { + const resolveWithFullResponseProperty = optionsArgument.properties.find( + (property) => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'resolveWithFullResponse', + ); + if ( + resolveWithFullResponseProperty?.type === AST_NODE_TYPES.Property && + resolveWithFullResponseProperty.value.type === AST_NODE_TYPES.Literal && + resolveWithFullResponseProperty.value.value === true + ) { + return; + } } + + context.report({ + node: optionsArgument, + messageId: 'invalidOptions', + }); } catch (error) { // eslint-disable-next-line no-console console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); From 6d73482e9fb7ddd4ebce1a5e67d6b07b8adf1077 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 4 Nov 2024 21:00:01 -0500 Subject: [PATCH 089/115] fix bugs --- src/agent/add-base-path-const.spec.ts | 9 +++++++++ src/agent/add-base-path-const.ts | 4 +++- src/agent/no-fixture.ts | 6 +++++- src/agent/no-service-wrapper.ts | 10 ++++++---- src/library/tree.ts | 1 + ts-init/src/api/v2/index.ts | 0 ts-init/src/api/v2/swagger.yml | 2 ++ 7 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 ts-init/src/api/v2/index.ts create mode 100644 ts-init/src/api/v2/swagger.yml diff --git a/src/agent/add-base-path-const.spec.ts b/src/agent/add-base-path-const.spec.ts index 970e5ae..c1c64e8 100644 --- a/src/agent/add-base-path-const.spec.ts +++ b/src/agent/add-base-path-const.spec.ts @@ -25,6 +25,15 @@ createTester().run(ruleId, rule, { code: `import ping from './ping';`, output: `import ping from './ping'; export const BASE_PATH = 'https://ping.checkdigit/ping/v1'; +`, + errors: [{ messageId: 'addBasePathConst' }], + }, + { + name: 'add BASE_PATH const for swagger 2.0', + filename: 'src/api/v2/index.ts', + code: `import ping from './ping';`, + output: `import ping from './ping'; +export const BASE_PATH = 'https://ping.checkdigit/ping/v2'; `, errors: [{ messageId: 'addBasePathConst' }], }, diff --git a/src/agent/add-base-path-const.ts b/src/agent/add-base-path-const.ts index e501149..3e2efbe 100644 --- a/src/agent/add-base-path-const.ts +++ b/src/agent/add-base-path-const.ts @@ -50,7 +50,9 @@ const rule: ESLintUtils.RuleModule<'addBasePathConst'> = createRule({ const swaggerPath = getSwaggerPathByIndexFile(context.filename); const swaggerFileContents = loadSwagger(swaggerPath); - const baseUrlLine = swaggerFileContents.split('\n').find((line) => /^\s*-\s*url:\s*\/.*$/u.test(line)); + const baseUrlLine = swaggerFileContents + .split('\n') + .find((line) => /^\s*-\s*url:\s*\/.*$/u.test(line) || /^basePath:.*/u.test(line)); const baseUrl = baseUrlLine?.split(':')[1]?.trim(); assert(baseUrl !== undefined); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 9f00306..6df0f90 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -59,7 +59,11 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; - } else if (parent.type === 'ArrayExpression' || parent.type === 'CallExpression') { + } else if ( + parent.type === 'ArrayExpression' || + parent.type === 'CallExpression' || + parent.type === 'ArrowFunctionExpression' + ) { // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = call; diff --git a/src/agent/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts index c59e979..81f5aac 100644 --- a/src/agent/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -57,10 +57,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'inval const foundVariable = scope.variables.find((variable) => variable.name === urlArgument.name); if (foundVariable) { const variableDefinition = foundVariable.defs.find((def) => def.type === DefinitionType.Variable); - assert.ok(variableDefinition, `Variable "${urlArgument.name}" not defined in scope`); - const variableDefinitionNode = variableDefinition.node; - assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); - return isUrlArgumentValid(variableDefinitionNode.init, scope); + if (variableDefinition !== undefined) { + const variableDefinitionNode = variableDefinition.node; + assert.ok(variableDefinitionNode.init, 'Variable definition node has no init property'); + return isUrlArgumentValid(variableDefinitionNode.init, scope); + } + return true; } } diff --git a/src/library/tree.ts b/src/library/tree.ts index ced5a36..f7ac9ab 100644 --- a/src/library/tree.ts +++ b/src/library/tree.ts @@ -64,6 +64,7 @@ export function isUsedInArrayOrAsArgument(node: Node): boolean { if ( parent.type === 'ArrayExpression' || + parent.type === 'ArrowFunctionExpression' || (parent.type === 'CallExpression' && parent.arguments.includes(node as Expression)) ) { return true; diff --git a/ts-init/src/api/v2/index.ts b/ts-init/src/api/v2/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/ts-init/src/api/v2/swagger.yml b/ts-init/src/api/v2/swagger.yml new file mode 100644 index 0000000..fb0741d --- /dev/null +++ b/ts-init/src/api/v2/swagger.yml @@ -0,0 +1,2 @@ +swagger: '2.0' +basePath: /ping/v2 From d179f9981c353f4473e79682d9dff5004c42a730 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 5 Nov 2024 22:23:41 -0500 Subject: [PATCH 090/115] - avoid double read of response body - rename no-full-response files to no-legacy-service-typing --- README.md | 2 +- src/agent/fetch-response-body-json.spec.ts | 227 ++++++++++++++++++--- src/agent/fetch-response-body-json.ts | 131 ++++++++++-- src/no-legacy-service-typing.spec.ts | 2 +- src/no-legacy-service-typing.ts | 2 +- src/no-serve-runtime.spec.ts | 2 +- 6 files changed, 324 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6b987f4..20c64c1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Copyright (c) 2021-2024 [Check Digit, LLC](https://checkdigit.com) - `@checkdigit/no-test-import` - `@checkdigit/no-promise-instance-method` - `@checkdigit/invalid-json-stringify` -- `@checkdigit/no-full-response` +- `@checkdigit/no-legacy-service-typing` - `@checkdigit/require-resolve-full-response` - `@checkdigit/require-type-out-of-type-only-imports` diff --git a/src/agent/fetch-response-body-json.spec.ts b/src/agent/fetch-response-body-json.spec.ts index 77897f5..18dba1d 100644 --- a/src/agent/fetch-response-body-json.spec.ts +++ b/src/agent/fetch-response-body-json.spec.ts @@ -21,44 +21,225 @@ createTester().run(ruleId, rule, { ], invalid: [ { - name: 'replace statusCode with status', - code: ` - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + name: 'first body access is inside variable declaration', + code: `() => { + const response = await fetch(url); const body = response.body; - `, - output: ` - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); - const body = (await response.json()); - `, + }`, + output: `() => { + const response = await fetch(url); + const body = await response.json(); + }`, errors: [{ messageId: 'replaceBodyWithJson' }], }, { - name: 'replace statusCode with status in chained access', - code: ` - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); - return response.body.data; - `, - output: ` - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); - return (await response.json()).data; - `, - errors: [{ messageId: 'replaceBodyWithJson' }], + name: 'first body access along with nested property access is inside variable declaration', + code: `() => { + const response = await fetch(url); + const data = response.body.data; + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +const data = responseBody.data; + }`, + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], + }, + { + name: 'first body access is not inside of variable declaration', + code: `() => { + const response = await fetch(url); + assert(response.body); + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +assert(responseBody); + }`, + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], + }, + { + name: 'first body access along with nested property access is not inside of variable declaration', + code: `() => { + const response = await fetch(url); + assert(response.body.data); + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +assert(responseBody.data); + }`, + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], }, { - name: 'no redundant "await" for return statement.', + name: 'body access is inside of return statement', code: ` async function foo() { - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); + const response = await fetch(url); return response.body; } `, output: ` async function foo() { - const response = await fetch(\`https://ping.checkdigit/ping/v1/key/\${keyId}\`); - return response.json(); + const response = await fetch(url); + const responseBody = await response.json(); +return responseBody; } `, - errors: [{ messageId: 'replaceBodyWithJson' }], + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], + }, + { + name: 'body access along with nested property access is inside of return statement', + code: `() => { + const response = await fetch(url); + return response.body.data; + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +return responseBody.data; + }`, + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], + }, + { + name: 'multiple body access in the same function', + code: `() => { + const response = await fetch(url); + assert(response.body); + assert(response.body.data); + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +assert(responseBody); + assert(responseBody.data); + }`, + errors: [ + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + ], + }, + { + name: 'body access againt multiple responses in the same function', + code: `() => { + const response = await fetch(url); + assert(response.body); + const response2 = await fetch(url2); + assert(response2.body); + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +assert(responseBody); + const response2 = await fetch(url2); + const response2Body = await response2.json(); +assert(response2Body); + }`, + errors: [ + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + ], + }, + { + name: 'multiple body accesses againt multiple responses in the same function', + code: `() => { + const response = await fetch(url); + assert(response.body); + assert(response.body.data); + const response2 = await fetch(url2); + assert(response2.body); + assert(response2.body.data); + }`, + output: `() => { + const response = await fetch(url); + const responseBody = await response.json(); +assert(responseBody); + assert(responseBody.data); + const response2 = await fetch(url2); + const response2Body = await response2.json(); +assert(response2Body); + assert(response2Body.data); + }`, + errors: [ + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + ], + }, + { + name: 'multiple body accesses againt multiple responses in the same function with mixed ordering', + code: `() => { + const response = await fetch(url); + const response2 = await fetch(url2); + assert(response2.body.data); + assert(response2.body); + assert(response.body); + assert(response.body.data); + }`, + output: `() => { + const response = await fetch(url); + const response2 = await fetch(url2); + const response2Body = await response2.json(); +assert(response2Body.data); + assert(response2Body); + const responseBody = await response.json(); +assert(responseBody); + assert(responseBody.data); + }`, + errors: [ + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + ], + }, + { + name: 'multiple body accesses againt multiple responses in multiple functions with mixed ordering', + code: `() => { + const response = await fetch(url); + const response2 = await fetch(url2); + assert(response2.body.data); + assert(response2.body); + assert(response.body); + assert(response.body.data); + } + () => { + const response = await fetch(url3); + return response.body; + }`, + output: `() => { + const response = await fetch(url); + const response2 = await fetch(url2); + const response2Body = await response2.json(); +assert(response2Body.data); + assert(response2Body); + const responseBody = await response.json(); +assert(responseBody); + assert(responseBody.data); + } + () => { + const response = await fetch(url3); + const responseBody = await response.json(); +return responseBody; + }`, + errors: [ + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + { messageId: 'replaceBodyWithJson' }, + ], }, ], }); diff --git a/src/agent/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts index fdad711..244efed 100644 --- a/src/agent/fetch-response-body-json.ts +++ b/src/agent/fetch-response-body-json.ts @@ -6,14 +6,28 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ +import { strict as assert } from 'node:assert'; + import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import getDocumentationUrl from '../get-documentation-url'; +import { getAncestor } from '../library/ts-tree'; export const ruleId = 'fetch-response-body-json'; const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +interface Change { + enclosingFunction: TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration; + enclosingStatement: TSESTree.VariableDeclaration | TSESTree.ExpressionStatement | TSESTree.ReturnStatement; + enclosingStatementIndex: number; + responseBodyNode: TSESTree.MemberExpression; + responseVariableName: string; + responseBodyVariableName: string; + isResponseBodyVariableDeclared: boolean; + // replacementText: string; +} + const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = createRule({ name: ruleId, meta: { @@ -32,12 +46,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre create(context) { const parserServices = ESLintUtils.getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); - const sourceCode = context.sourceCode; + const allChanges = new Map>(); return { - 'MemberExpression[property.name="body"]': (responseBody: TSESTree.MemberExpression) => { + 'MemberExpression[property.name="body"]': (responseBodyNode: TSESTree.MemberExpression) => { try { - const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBody.object); + const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBodyNode.object); const responseType = typeChecker.getTypeAtLocation(responseNode); const shouldReplace = @@ -45,23 +59,54 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre responseType.getProperties().some((symbol) => symbol.name === 'json'); if (shouldReplace) { - const responseText = sourceCode.getText(responseBody.object); - const needAwait = responseBody.parent.type !== AST_NODE_TYPES.ReturnStatement; - const replacementText = needAwait ? `(await ${responseText}.json())` : `${responseText}.json()`; - - context.report({ - messageId: 'replaceBodyWithJson', - node: responseBody, - fix(fixer) { - return fixer.replaceText(responseBody, replacementText); - }, - }); + const enclosingFunction = getAncestor( + responseBodyNode, + (node: TSESTree.Node) => + node.type === AST_NODE_TYPES.ArrowFunctionExpression || + node.type === AST_NODE_TYPES.FunctionExpression || + node.type === AST_NODE_TYPES.FunctionDeclaration, + ) as TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration; + const enclosingStatement = getAncestor( + responseBodyNode, + (node: TSESTree.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, + ) as TSESTree.VariableDeclaration | TSESTree.ExpressionStatement | TSESTree.ReturnStatement; + const enclosingStatementIndex = (enclosingFunction.body as TSESTree.BlockStatement).body.indexOf( + enclosingStatement, + ); + const responseVariableName = (responseBodyNode.object as TSESTree.Identifier).name; + const isResponseBodyVariableDeclared = + enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration && + enclosingStatement.declarations.some((declaration) => declaration.init === responseBodyNode); + const responseBodyVariableName = isResponseBodyVariableDeclared + ? (enclosingStatement.declarations.find((declaration) => declaration.init === responseBodyNode) + ?.id as unknown as string) + : `${(responseBodyNode.object as TSESTree.Identifier).name}Body`; + + const change: Change = { + enclosingFunction, + enclosingStatement, + enclosingStatementIndex, + responseVariableName, + responseBodyNode, + responseBodyVariableName, + isResponseBodyVariableDeclared, + }; + + const changesByFunction = allChanges.get(enclosingFunction) ?? new Map(); + const changesByResponse = changesByFunction.get(responseVariableName) ?? []; + changesByResponse.push(change); + changesByFunction.set(responseVariableName, changesByResponse); + allChanges.set(enclosingFunction, changesByFunction); } } catch (error) { // eslint-disable-next-line no-console console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); context.report({ - node: responseBody, + node: responseBodyNode, messageId: 'unknownError', data: { fileName: context.filename, @@ -70,6 +115,62 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre }); } }, + + 'Program:exit': () => { + if (allChanges.size === 0) { + return; + } + + const fixes: { node: TSESTree.Node; text: string; insert: boolean }[] = []; + for (const changesByFunction of allChanges.values()) { + for (const changesByResponse of changesByFunction.values()) { + const orderedChanges = changesByResponse.sort( + (changeA, changeB) => changeA.enclosingStatementIndex - changeB.enclosingStatementIndex, + ); + const firstChange = orderedChanges[0]; + assert(firstChange); + + const { + responseBodyNode, + responseVariableName, + responseBodyVariableName, + isResponseBodyVariableDeclared, + enclosingStatement, + } = firstChange; + + let remainingChanges; + if (!isResponseBodyVariableDeclared) { + fixes.push({ + node: enclosingStatement, + text: `const ${responseBodyVariableName} = await ${responseVariableName}.json();\n`, + insert: true, + }); + remainingChanges = orderedChanges; + } else { + fixes.push({ + node: responseBodyNode, + text: `await ${responseVariableName}.json()`, + insert: false, + }); + remainingChanges = orderedChanges.slice(1); + } + + for (const change of remainingChanges) { + fixes.push({ node: change.responseBodyNode, text: responseBodyVariableName, insert: false }); + } + } + } + + for (const fix of fixes) { + context.report({ + node: fix.node, + messageId: 'replaceBodyWithJson', + fix(fixer) { + return fix.insert ? fixer.insertTextBefore(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text); + }, + }); + } + }, }; }, }); diff --git a/src/no-legacy-service-typing.spec.ts b/src/no-legacy-service-typing.spec.ts index 4741ec1..3b84f9c 100644 --- a/src/no-legacy-service-typing.spec.ts +++ b/src/no-legacy-service-typing.spec.ts @@ -1,4 +1,4 @@ -// no-full-response.spec.ts +// no-legacy-service-typing.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/no-legacy-service-typing.ts b/src/no-legacy-service-typing.ts index d7b5f86..cb1fdd3 100644 --- a/src/no-legacy-service-typing.ts +++ b/src/no-legacy-service-typing.ts @@ -1,4 +1,4 @@ -// no-full-response.ts +// no-legacy-service-typing.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/no-serve-runtime.spec.ts b/src/no-serve-runtime.spec.ts index 30d4cbd..9efc925 100644 --- a/src/no-serve-runtime.spec.ts +++ b/src/no-serve-runtime.spec.ts @@ -1,4 +1,4 @@ -// no-full-response.spec.ts +// no-serve-runtime.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC From e46e8846c944bddf5b4b6de121f6cd123f7dee08 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 6 Nov 2024 13:06:52 -0500 Subject: [PATCH 091/115] enhance fixture call analysis to support assignment statements and improve response variable handling --- src/agent/no-fixture.spec.ts | 18 ++++++++++++++++++ src/agent/no-fixture.ts | 22 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index b036273..59a7942 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -620,5 +620,23 @@ createTester().run(ruleId, rule, { `, errors: 1, }, + { + name: 'assignment statement instead of variable declaration used for subsequent fixture calls', + code: ` + let response = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + response = await fixture.api.get(\`\${BASE_PATH}/ping2\`).expect(StatusCodes.OK); + `, + output: ` + let response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + response = await fetch(\`\${BASE_PATH}/ping2\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + `, + errors: 2, + }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 6df0f90..f8c5552 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -12,6 +12,7 @@ import type { AwaitExpression, CallExpression, Expression, + ExpressionStatement, MemberExpression, Node, ObjectPattern, @@ -38,9 +39,10 @@ import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; interface FixtureCallInformation { - rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression; + rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression | ExpressionStatement; fixtureNode: AwaitExpression | SimpleCallExpression; variableDeclaration?: VariableDeclaration; + variableAssignment?: ExpressionStatement; requestBody?: Expression; requestHeaders?: { name: Expression; value: Expression }[]; assertions?: Expression[][]; @@ -81,6 +83,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo } else if (enclosingStatement.type === 'VariableDeclaration') { results.variableDeclaration = enclosingStatement; results.rootNode = enclosingStatement; + } else if ( + enclosingStatement.type === 'ExpressionStatement' && + enclosingStatement.expression.type === 'AssignmentExpression' + ) { + results.variableAssignment = enclosingStatement; + results.rootNode = enclosingStatement; } else { results.rootNode = parent; } @@ -190,6 +198,14 @@ function getResponseVariableNameToUse( fixtureCallInformation: FixtureCallInformation, scopeVariablesMap: Map, ) { + if (fixtureCallInformation.variableAssignment) { + assert.ok( + fixtureCallInformation.variableAssignment.expression.type === 'AssignmentExpression' && + fixtureCallInformation.variableAssignment.expression.left.type === 'Identifier', + ); + return fixtureCallInformation.variableAssignment.expression.left.name; + } + if (fixtureCallInformation.variableDeclaration) { const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { @@ -323,7 +339,9 @@ const rule: Rule.RuleModule = { const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; const isResponseVariableRedefinitionNeeded = - (responseVariable === undefined && fixtureCallInformation.assertions !== undefined) || + (fixtureCallInformation.variableAssignment === undefined && + responseVariable === undefined && + fixtureCallInformation.assertions !== undefined) || isResponseBodyVariableRedefinitionNeeded; const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded From b68df81a01ed4aa82a28217ced83fdc50213ea2e Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 7 Nov 2024 12:08:13 -0500 Subject: [PATCH 092/115] fix and support edge cases --- src/agent/fetch-response-body-json.spec.ts | 80 ++++++++++++++-------- src/agent/fetch-response-body-json.ts | 41 ++++++++--- src/agent/fix-function-call-arguments.ts | 50 +++++++++----- src/agent/no-fixture.ts | 4 +- 4 files changed, 113 insertions(+), 62 deletions(-) diff --git a/src/agent/fetch-response-body-json.spec.ts b/src/agent/fetch-response-body-json.spec.ts index 18dba1d..e2397ec 100644 --- a/src/agent/fetch-response-body-json.spec.ts +++ b/src/agent/fetch-response-body-json.spec.ts @@ -40,8 +40,8 @@ createTester().run(ruleId, rule, { }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -const data = responseBody.data; +const responseBody = await response.json(); + const data = responseBody.data; }`, errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], }, @@ -53,8 +53,8 @@ const data = responseBody.data; }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); }`, errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], }, @@ -66,8 +66,8 @@ assert(responseBody); }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -assert(responseBody.data); +const responseBody = await response.json(); + assert(responseBody.data); }`, errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], }, @@ -82,8 +82,8 @@ assert(responseBody.data); output: ` async function foo() { const response = await fetch(url); - const responseBody = await response.json(); -return responseBody; +const responseBody = await response.json(); + return responseBody; } `, errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], @@ -96,8 +96,8 @@ return responseBody; }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -return responseBody.data; +const responseBody = await response.json(); + return responseBody.data; }`, errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], }, @@ -110,8 +110,8 @@ return responseBody.data; }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); assert(responseBody.data); }`, errors: [ @@ -130,11 +130,11 @@ assert(responseBody); }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); const response2 = await fetch(url2); - const response2Body = await response2.json(); -assert(response2Body); +const response2Body = await response2.json(); + assert(response2Body); }`, errors: [ { messageId: 'replaceBodyWithJson' }, @@ -155,12 +155,12 @@ assert(response2Body); }`, output: `() => { const response = await fetch(url); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); assert(responseBody.data); const response2 = await fetch(url2); - const response2Body = await response2.json(); -assert(response2Body); +const response2Body = await response2.json(); + assert(response2Body); assert(response2Body.data); }`, errors: [ @@ -185,11 +185,11 @@ assert(response2Body); output: `() => { const response = await fetch(url); const response2 = await fetch(url2); - const response2Body = await response2.json(); -assert(response2Body.data); +const response2Body = await response2.json(); + assert(response2Body.data); assert(response2Body); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); assert(responseBody.data); }`, errors: [ @@ -218,17 +218,17 @@ assert(responseBody); output: `() => { const response = await fetch(url); const response2 = await fetch(url2); - const response2Body = await response2.json(); -assert(response2Body.data); +const response2Body = await response2.json(); + assert(response2Body.data); assert(response2Body); - const responseBody = await response.json(); -assert(responseBody); +const responseBody = await response.json(); + assert(responseBody); assert(responseBody.data); } () => { const response = await fetch(url3); - const responseBody = await response.json(); -return responseBody; +const responseBody = await response.json(); + return responseBody; }`, errors: [ { messageId: 'replaceBodyWithJson' }, @@ -241,5 +241,25 @@ return responseBody; { messageId: 'replaceBodyWithJson' }, ], }, + { + name: 'work with expression like body.forEach', + code: `() => { + const response = await fetch(url); + response.body.forEach(() => {}); + }`, + output: `() => { + const response = await fetch(url); +const responseBody = await response.json(); + responseBody.forEach(() => {}); + }`, + errors: [{ messageId: 'replaceBodyWithJson' }, { messageId: 'replaceBodyWithJson' }], + }, + { + name: 'report error for inline fetch call', + code: `() => { + (await fetch(url)).body; + }`, + errors: [{ messageId: 'refactorNeeded' }], + }, ], }); diff --git a/src/agent/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts index 244efed..831751e 100644 --- a/src/agent/fetch-response-body-json.ts +++ b/src/agent/fetch-response-body-json.ts @@ -28,7 +28,7 @@ interface Change { // replacementText: string; } -const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = createRule({ +const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'refactorNeeded'> = createRule({ name: ruleId, meta: { type: 'suggestion', @@ -36,6 +36,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre description: 'Replace "response.body" with "await response.json()".', }, messages: { + refactorNeeded: + 'Please extract the fetch call and check its reponse status code before accessing its response body.', replaceBodyWithJson: 'Replace "response.body" with "await response.json()".', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, @@ -59,6 +61,14 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre responseType.getProperties().some((symbol) => symbol.name === 'json'); if (shouldReplace) { + if (responseBodyNode.object.type !== AST_NODE_TYPES.Identifier) { + context.report({ + node: responseBodyNode, + messageId: 'refactorNeeded', + }); + return; + } + const enclosingFunction = getAncestor( responseBodyNode, (node: TSESTree.Node) => @@ -77,14 +87,23 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre const enclosingStatementIndex = (enclosingFunction.body as TSESTree.BlockStatement).body.indexOf( enclosingStatement, ); - const responseVariableName = (responseBodyNode.object as TSESTree.Identifier).name; + const responseVariableName = responseBodyNode.object.name; const isResponseBodyVariableDeclared = enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration && - enclosingStatement.declarations.some((declaration) => declaration.init === responseBodyNode); + enclosingStatement.declarations.some( + (declaration) => + declaration.init === responseBodyNode || + (declaration.init?.type === AST_NODE_TYPES.TSAsExpression && + declaration.init.expression === responseBodyNode), + ); const responseBodyVariableName = isResponseBodyVariableDeclared - ? (enclosingStatement.declarations.find((declaration) => declaration.init === responseBodyNode) - ?.id as unknown as string) - : `${(responseBodyNode.object as TSESTree.Identifier).name}Body`; + ? (enclosingStatement.declarations.find( + (declaration) => + declaration.init === responseBodyNode || + (declaration.init?.type === AST_NODE_TYPES.TSAsExpression && + declaration.init.expression === responseBodyNode), + )?.id as unknown as string) + : `${responseBodyNode.object.name}Body`; const change: Change = { enclosingFunction, @@ -121,7 +140,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre return; } - const fixes: { node: TSESTree.Node; text: string; insert: boolean }[] = []; + const fixes: { node: TSESTree.Node | TSESTree.Token; text: string; insert: boolean }[] = []; for (const changesByFunction of allChanges.values()) { for (const changesByResponse of changesByFunction.values()) { const orderedChanges = changesByResponse.sort( @@ -141,8 +160,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre let remainingChanges; if (!isResponseBodyVariableDeclared) { fixes.push({ - node: enclosingStatement, - text: `const ${responseBodyVariableName} = await ${responseVariableName}.json();\n`, + node: context.sourceCode.getTokenBefore(enclosingStatement) as TSESTree.Token, + text: `\nconst ${responseBodyVariableName} = await ${responseVariableName}.json();`, insert: true, }); remainingChanges = orderedChanges; @@ -161,12 +180,12 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson'> = cre } } - for (const fix of fixes) { + for (const fix of fixes.reverse()) { context.report({ node: fix.node, messageId: 'replaceBodyWithJson', fix(fixer) { - return fix.insert ? fixer.insertTextBefore(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text); + return fix.insert ? fixer.insertTextAfter(fix.node, fix.text) : fixer.replaceText(fix.node, fix.text); }, }); } diff --git a/src/agent/fix-function-call-arguments.ts b/src/agent/fix-function-call-arguments.ts index 09cdb1e..bb3cb75 100644 --- a/src/agent/fix-function-call-arguments.ts +++ b/src/agent/fix-function-call-arguments.ts @@ -22,6 +22,7 @@ const DEFAULT_OPTIONS = { 'Fixture', 'InboundContext', '{ get: () => string; }', + 'Api', ], }; @@ -77,7 +78,21 @@ const rule: ESLintUtils.RuleModule< log('===== file name:', context.filename); log('callExpression:', sourceCode.getText(callExpression)); + try { + const actualParameters = callExpression.arguments; + if ( + !actualParameters.some((actualParameter) => { + const actualType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(actualParameter), + ); + const actualTypeString = typeChecker.typeToString(actualType); + return typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType'); + }) + ) { + return; + } + const calleeTsNode = parserServices.esTreeNodeToTSNodeMap.get(callExpression.callee); const calleeType = typeChecker.getTypeAtLocation(calleeTsNode); @@ -88,13 +103,14 @@ const rule: ESLintUtils.RuleModule< } const signature = signatures[0]; - if ( - signature === undefined || - (signature.typeParameters !== undefined && signature.typeParameters.length > 0) - ) { - // ignore complex signatures with type parameters - return; - } + assert(signature); + // if ( + // signature === undefined || + // (signature.typeParameters !== undefined && signature.typeParameters.length > 0) + // ) { + // // ignore complex signatures with type parameters + // return; + // } log('signature:', signature.getDeclaration().getText()); const expectedParameters = signature.getParameters(); @@ -105,7 +121,6 @@ const rule: ESLintUtils.RuleModule< ), ); const expectedParametersCount = expectedParameters.length; - const actualParameters = callExpression.arguments; const actualParametersCount = actualParameters.length; if (actualParametersCount === 0) { return; @@ -132,18 +147,15 @@ const rule: ESLintUtils.RuleModule< ); log('actual type: #', actualParameterIndex, sourceCode.getText(actualParameter), actualTypeString); - if (!typesToCheck.includes(actualTypeString) && !actualTypeString.endsWith('RequestType')) { - // skip the parameter type checking if it's not in the candidate types - parametersToKeep.push(actualParameter); - expectedParameterIndex++; - log('skipped'); - } else if (typeChecker.isTypeAssignableTo(actualType, expectedType)) { - parametersToKeep.push(actualParameter); - expectedParameterIndex++; - log('matched'); - } else { - log('not matched'); + if ( + (typesToCheck.includes(actualTypeString) || actualTypeString.endsWith('RequestType')) && + !typeChecker.isTypeAssignableTo(actualType, expectedType) + ) { + log('removing un-matched parameter', sourceCode.getText(actualParameter)); + continue; } + parametersToKeep.push(actualParameter); + expectedParameterIndex++; } if (parametersToKeep.length === actualParametersCount) { diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index f8c5552..a2e3efd 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -350,7 +350,7 @@ const rule: Rule.RuleModule = { ...(destructuringResponseBodyVariable ? [ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `const ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, ] : isResponseBodyVariableRedefinitionNeeded ? [ @@ -374,7 +374,7 @@ const rule: Rule.RuleModule = { const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; const fetchStatementText = !isResponseVariableRedefinitionNeeded ? fetchCallText - : `const ${responseVariableNameToUse} = await ${fetchCallText}`; + : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`; const nodeToReplace = isResponseVariableRedefinitionNeeded ? fixtureCallInformation.rootNode From 7c79c2a72074e22543c10a870e7450c6da269604 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 18 Nov 2024 09:21:48 -0500 Subject: [PATCH 093/115] add functions for retrieving response headers and improve destructuring handling --- package.json | 6 +- src/agent/agent-test-wiring.spec.ts | 34 +++++++ src/agent/agent-test-wiring.ts | 133 ++++++++++++++++----------- src/agent/fetch-then.spec.ts | 58 ++++++------ src/agent/fetch-then.ts | 6 +- src/agent/fetch.ts | 4 + src/agent/no-fixture.spec.ts | 20 +++- src/agent/no-fixture.ts | 39 ++++++-- src/agent/response-reference.ts | 13 ++- src/require-resolve-full-response.ts | 2 +- 10 files changed, 212 insertions(+), 103 deletions(-) diff --git a/package.json b/package.json index 6dffea2..b578a62 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,8 @@ "@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", + "@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", @@ -87,7 +87,7 @@ "eslint-plugin-sonarjs": "1.0.4", "http-status-codes": "^2.3.0", "rimraf": "^6.0.1", - "typescript-eslint": "8.13.0" + "typescript-eslint": "^8.13.0" }, "peerDependencies": { "eslint": ">=9 <10" diff --git a/src/agent/agent-test-wiring.spec.ts b/src/agent/agent-test-wiring.spec.ts index 9bcc79d..a88577e 100644 --- a/src/agent/agent-test-wiring.spec.ts +++ b/src/agent/agent-test-wiring.spec.ts @@ -229,6 +229,40 @@ await fixture.reset(); afterAll(async () => { await agent[Symbol.asyncDispose](); }); +}); + `, + errors: [{ messageId: 'updateTestWiring' }], + }, + { + name: 'only beforeEach is presented', + filename: `src/api/v1/ping.spec.ts`, + code: ` +import { beforeEach, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + beforeEach(() => fixture.reset()); +}); + `, + output: ` +import { afterAll, beforeAll, beforeEach, describe, it } from '@jest/globals'; +import { amazonSetup } from '@checkdigit/amazon'; +import { createFixture } from '@checkdigit/fixture'; +import createAgent, { type Agent } from '@checkdigit/agent'; +import fixturePlugin from '../../plugin/fixture.test'; +describe('/ping', () => { + const fixture = createFixture(amazonSetup); + let agent: Agent; +beforeAll(async () => { +agent = await createAgent(); +agent.register(await fixturePlugin(fixture)); +agent.enable(); +}); +beforeEach(() => fixture.reset()); +afterAll(async () => { +await agent[Symbol.asyncDispose](); +}); }); `, errors: [{ messageId: 'updateTestWiring' }], diff --git a/src/agent/agent-test-wiring.ts b/src/agent/agent-test-wiring.ts index 562408d..1495ff7 100644 --- a/src/agent/agent-test-wiring.ts +++ b/src/agent/agent-test-wiring.ts @@ -41,12 +41,14 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create schema: [], }, defaultOptions: [], + // eslint-disable-next-line max-lines-per-function create(context) { log('Processing file:', context.filename); const sourceCode = context.sourceCode; const importDeclarations = new Map(); let isFixtureUsed = false; let beforeAll: TSESTree.CallExpression | undefined; + let beforeEach: TSESTree.CallExpression | undefined; let afterAll: TSESTree.CallExpression | undefined; return { @@ -68,11 +70,16 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create 'CallExpression[callee.name="beforeAll"]': (callExpression: TSESTree.CallExpression) => { beforeAll = callExpression; }, + 'CallExpression[callee.name="beforeEach"]': (callExpression: TSESTree.CallExpression) => { + beforeEach = callExpression; + }, 'CallExpression[callee.name="afterAll"]': (callExpression: TSESTree.CallExpression) => { afterAll = callExpression; }, + // eslint-disable-next-line sonarjs/cognitive-complexity 'Program:exit'(program) { - if (!isFixtureUsed || beforeAll === undefined) { + if (!isFixtureUsed || (beforeAll === undefined && beforeEach === undefined)) { + // only update test wiring if fixture is used return; } @@ -81,7 +88,7 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create let agentImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; let fixturePluginImportFixer: ((fixer: RuleFixer) => RuleFix) | undefined; let agentDeclarationFixer: ((fixer: RuleFixer) => RuleFix) | undefined; - let beforeAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined; + let beforeAllOrEachFixer: ((fixer: RuleFixer) => RuleFix) | undefined; let afterAllFixer: ((fixer: RuleFixer) => RuleFix) | undefined; const lastImportDeclaration = [...importDeclarations.values()].at(-1); @@ -89,18 +96,21 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create // make sure that afterAll is imported from jest const jestImportDeclaration = importDeclarations.get('@jest/globals'); - if ( - jestImportDeclaration && - !jestImportDeclaration.specifiers.some( - (specifier) => - specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && - specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier && - specifier.imported.name === 'afterAll', - ) - ) { + assert.ok(jestImportDeclaration); + const importsToAdd = ['afterAll', 'beforeAll'].filter( + (jestHook) => + !jestImportDeclaration.specifiers.some( + (specifier) => + specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier && + specifier.imported.type === TSESTree.AST_NODE_TYPES.Identifier && + specifier.imported.name === jestHook, + ), + ); + if (importsToAdd.length > 0) { const firstImportSpecifier = jestImportDeclaration.specifiers[0]; assert.ok(firstImportSpecifier); - jestImportFixer = (fixer: RuleFixer) => fixer.insertTextBefore(firstImportSpecifier, 'afterAll, '); + jestImportFixer = (fixer: RuleFixer) => + fixer.insertTextBefore(firstImportSpecifier, `${importsToAdd.join(', ')}, `); } // make sure that agent is imported @@ -124,45 +134,60 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create fixer.insertTextAfter(lastImportDeclaration, `\nimport fixturePlugin from '${fixturePluginImportPath}';`); } - // inject agent declaration and initialization to `beforeAll` block - const beforeAllArgument = beforeAll.arguments[0]; - assert.ok(beforeAllArgument !== undefined); - if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) { - if ( - beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression && - beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement - ) { - const fixtureResetStatement = beforeAllArgument.body.body.find( - (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED, + // inject agent declaration and initialization + if (beforeAll === undefined) { + // create `beforeAll` block if it doesn't exist + beforeAllOrEachFixer = (fixer: RuleFixer) => + fixer.insertTextBefore( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + beforeEach!, + [ + STATEMENT_AGENT_DECLARATION, + `beforeAll(async () => {`, + [STATEMENT_AGENT_CREATION, STATEMENT_AGENT_REGISTER, STATEMENT_AGENT_ENABLE].join('\n'), + `});\n`, + ].join('\n'), ); - assert.ok(fixtureResetStatement !== undefined); - beforeAllFixer = (fixer: RuleFixer) => - fixer.replaceText( - fixtureResetStatement, - [ - STATEMENT_AGENT_CREATION, - STATEMENT_AGENT_REGISTER, - STATEMENT_AGENT_ENABLE, - STATEMENT_FIXTURE_RESET_AWAITED, - ].join('\n'), - ); - } else { - beforeAllFixer = (fixer: RuleFixer) => - fixer.replaceText( - beforeAllArgument, - [ - `async () => {`, - STATEMENT_AGENT_CREATION, - STATEMENT_AGENT_REGISTER, - STATEMENT_AGENT_ENABLE, - STATEMENT_FIXTURE_RESET_AWAITED, - `}`, - ].join('\n'), + } else { + const beforeAllArgument = beforeAll.arguments[0]; + assert.ok(beforeAllArgument !== undefined); + if (!sourceCode.getText(beforeAllArgument).includes(STATEMENT_AGENT_CREATION)) { + if ( + beforeAllArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression && + beforeAllArgument.body.type === TSESTree.AST_NODE_TYPES.BlockStatement + ) { + const fixtureResetStatement = beforeAllArgument.body.body.find( + (statement) => sourceCode.getText(statement) === STATEMENT_FIXTURE_RESET_AWAITED, ); + assert.ok(fixtureResetStatement !== undefined); + beforeAllOrEachFixer = (fixer: RuleFixer) => + fixer.replaceText( + fixtureResetStatement, + [ + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + ].join('\n'), + ); + } else { + beforeAllOrEachFixer = (fixer: RuleFixer) => + fixer.replaceText( + beforeAllArgument, + [ + `async () => {`, + STATEMENT_AGENT_CREATION, + STATEMENT_AGENT_REGISTER, + STATEMENT_AGENT_ENABLE, + STATEMENT_FIXTURE_RESET_AWAITED, + `}`, + ].join('\n'), + ); + } + agentDeclarationFixer = (fixer: RuleFixer) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`); } - agentDeclarationFixer = (fixer: RuleFixer) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - fixer.insertTextBefore(beforeAll!, `${STATEMENT_AGENT_DECLARATION}\n`); } // inject agent disposal to `afterAll` block @@ -182,7 +207,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter(lastStatement, STATEMENT_AGENT_DISPOSE); } } else { - const nextToken = sourceCode.getTokenAfter(beforeAll); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const nextToken = sourceCode.getTokenAfter(beforeAll ?? beforeEach!); afterAllFixer = (fixer: RuleFixer) => fixer.insertTextAfter( nextToken !== null && nextToken.type === AST_TOKEN_TYPES.Punctuator @@ -198,12 +224,13 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create agentImportFixer !== undefined || fixturePluginImportFixer !== undefined || agentDeclarationFixer !== undefined || - beforeAllFixer !== undefined || + beforeAllOrEachFixer !== undefined || afterAllFixer !== undefined ) { context.report({ messageId: 'updateTestWiring', - node: beforeAll, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node: beforeAll ?? beforeEach!, *fix(fixer) { if (jestImportFixer !== undefined) { yield jestImportFixer(fixer); @@ -217,8 +244,8 @@ const rule: ESLintUtils.RuleModule<'updateTestWiring' | 'unknownError'> = create if (agentDeclarationFixer !== undefined) { yield agentDeclarationFixer(fixer); } - if (beforeAllFixer !== undefined) { - yield beforeAllFixer(fixer); + if (beforeAllOrEachFixer !== undefined) { + yield beforeAllOrEachFixer(fixer); } if (afterAllFixer !== undefined) { yield afterAllFixer(fixer); diff --git a/src/agent/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts index 1bc5f22..4abc306 100644 --- a/src/agent/fetch-then.spec.ts +++ b/src/agent/fetch-then.spec.ts @@ -6,34 +6,31 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import { describe } from '@jest/globals'; - import createTester from '../tester.test'; import rule, { ruleId } from './fetch-then'; -describe(ruleId, () => { - createTester().run(ruleId, rule, { - valid: [ - { - name: 'skip regular fixture calls which will be handled in "no-fixture" rule', - code: ` +createTester().run(ruleId, rule, { + valid: [ + { + name: 'skip regular fixture calls which will be handled in "no-fixture" rule', + code: ` const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); const body = pingResponse.body; const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - }, - ], - invalid: [ - { - name: 'with assertions', - code: ` + }, + ], + invalid: [ + { + name: 'with assertions', + code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), ]); `, - output: ` + output: ` const responses = await Promise.all([ // eslint-disable-next-line @checkdigit/no-promise-instance-method fetch(\`\${BASE_PATH}/key\`, { @@ -53,11 +50,11 @@ describe(ruleId, () => { }), ]); `, - errors: 2, - }, - { - name: 'adjust header access correctly', - code: ` + errors: 2, + }, + { + name: 'adjust header access correctly', + code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), @@ -67,7 +64,7 @@ describe(ruleId, () => { assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); `, - output: ` + output: ` const responses = await Promise.all([ // eslint-disable-next-line @checkdigit/no-promise-instance-method fetch(\`\${BASE_PATH}/key\`, { @@ -91,11 +88,11 @@ describe(ruleId, () => { assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); `, - errors: 12, - }, - { - name: 'in non-async arrow function with concurrent promises', - code: ` + errors: 12, + }, + { + name: 'in non-async arrow function with concurrent promises', + code: ` await Promise.all( Object.keys(zoneKeyPartImportRequest).map((propertyName) => { const requestWithPropertyMissing = omit( @@ -111,7 +108,7 @@ describe(ruleId, () => { }), ); `, - output: ` + output: ` await Promise.all( Object.keys(zoneKeyPartImportRequest).map((propertyName) => { const requestWithPropertyMissing = omit( @@ -131,8 +128,7 @@ describe(ruleId, () => { }), ); `, - errors: 1, - }, - ], - }); + errors: 1, + }, + ], }); diff --git a/src/agent/fetch-then.ts b/src/agent/fetch-then.ts index cf87b75..bbf5cc1 100644 --- a/src/agent/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -103,10 +103,12 @@ function createResponseAssertions( responseVariableName, ); } - nonStatusAssertions.push(`assert.ok(${functionBody})`); + nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); } else if (assertionArgument.type === 'Identifier') { // callback assertion using function reference - nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`); + nonStatusAssertions.push( + `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, + ); } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { // body deep equal assertion nonStatusAssertions.push( diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index 101343a..9a5a109 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -8,6 +8,10 @@ export function getResponseBodyRetrievalText(responseVariableName: string) { return `await ${responseVariableName}.json()`; } +export function getResponseHeadersRetrievalText(responseVariableName: string) { + return `${responseVariableName}.headers`; +} + export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean { const responseHeaderAccessParent = getParent(responseHeadersAccess); if (responseHeaderAccessParent?.type === 'VariableDeclarator') { diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 59a7942..5dd9ecf 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -222,8 +222,8 @@ createTester().run(ruleId, rule, { const response = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.ok(validate(response)); - assert.ok(console.log(response)); + assert.doesNotThrow(()=>validate(response)); + assert.doesNotThrow(()=>console.log(response)); `, errors: 1, }, @@ -595,7 +595,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.NO_CONTENT); assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.ok(verifyTemporalHeaders(response, createdOn)); + assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); `, errors: 1, }, @@ -638,5 +638,19 @@ createTester().run(ruleId, rule, { `, errors: 2, }, + { + name: 'nested header destructuring', + code: ` + const { headers: { etag } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const { etag } = response.headers; + `, + errors: 1, + }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index a2e3efd..cbbbce8 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -33,7 +33,7 @@ import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; import { analyzeResponseReferences } from './response-reference'; -import { getResponseBodyRetrievalText, hasAssertions } from './fetch'; +import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, hasAssertions } from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; @@ -48,6 +48,7 @@ interface FixtureCallInformation { assertions?: Expression[][]; inlineStatementNode?: Node; inlineBodyReference?: MemberExpression; + inlineHeadersReference?: MemberExpression; } // recursively analyze the fixture/supertest call chain to collect information of request/response @@ -80,6 +81,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') { results.inlineBodyReference = awaitParent; } + if ( + awaitParent.property.type === 'Identifier' && + (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') + ) { + results.inlineHeadersReference = awaitParent; + } } else if (enclosingStatement.type === 'VariableDeclaration') { results.variableDeclaration = enclosingStatement; results.rootNode = enclosingStatement; @@ -156,10 +163,12 @@ function createResponseAssertions( responseVariableName, ); } - nonStatusAssertions.push(`assert.ok(${functionBody})`); + nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); } else if (assertionArgument.type === 'Identifier') { // callback assertion using function reference - nonStatusAssertions.push(`assert.ok(${sourceCode.getText(assertionArgument)}(${responseVariableName}))`); + nonStatusAssertions.push( + `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, + ); } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { // body deep equal assertion nonStatusAssertions.push( @@ -338,11 +347,19 @@ const rule: Rule.RuleModule = { (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; + const isResponseHeadersVariableRedefinitionNeeded = + (destructuringResponseHeadersVariable !== undefined && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') || + fixtureCallInformation.inlineHeadersReference !== undefined; + const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; + const isResponseVariableRedefinitionNeeded = (fixtureCallInformation.variableAssignment === undefined && responseVariable === undefined && fixtureCallInformation.assertions !== undefined) || - isResponseBodyVariableRedefinitionNeeded; + isResponseBodyVariableRedefinitionNeeded || + isResponseHeadersVariableRedefinitionNeeded; const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded ? [ @@ -357,9 +374,17 @@ const rule: Rule.RuleModule = { `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, ] : []), + // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable - ? [`const ${destructuringResponseHeadersVariable.name} = ${responseVariableNameToUse}.headers`] - : []), + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseHeadersVariable as ObjectPattern) : (destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseHeadersVariableRedefinitionNeeded + ? [ + `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : []), ] : []; @@ -367,7 +392,7 @@ const rule: Rule.RuleModule = { fixtureCallInformation, sourceCode, responseVariableNameToUse, - destructuringResponseHeadersVariable, + destructuringResponseHeadersVariable as Scope.Variable | undefined, ); // add variable declaration if needed diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index edeba73..7c0cd3c 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -29,7 +29,7 @@ export function analyzeResponseReferences( headersReferences: MemberExpression[]; statusReferences: MemberExpression[]; destructuringBodyVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersVariable?: Scope.Variable; + destructuringHeadersVariable?: Scope.Variable | ObjectPattern; destructuringHeadersReferences?: MemberExpression[] | undefined; } { const results: { @@ -38,7 +38,7 @@ export function analyzeResponseReferences( headersReferences: MemberExpression[]; statusReferences: MemberExpression[]; destructuringBodyVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersVariable?: Scope.Variable; + destructuringHeadersVariable?: Scope.Variable | ObjectPattern; destructuringHeadersReferences?: MemberExpression[] | undefined; } = { bodyReferences: [], @@ -105,13 +105,20 @@ export function analyzeResponseReferences( getParent(parent)?.type !== 'CallExpression', ); } else if (identifierParent.type === 'Property') { - // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..." const parent = getParent(identifierParent); if (parent?.type === 'ObjectPattern') { + // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..." const parent2 = getParent(parent); if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') { results.destructuringBodyVariable = parent; } + if ( + parent2?.type === 'Property' && + parent2.key.type === 'Identifier' && + (parent2.key.name === 'header' || parent2.key.name === 'headers') + ) { + results.destructuringHeadersVariable = parent; + } } } else { log('+++++++ can not handle identifierParent', identifierParent); diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index c0d9975..0c7db02 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -141,7 +141,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu const enclosingScopeNode = getEnclosingScopeNode(serviceCall); assert.ok(enclosingScopeNode, 'enclosingScopeNode is undefined'); - const scope = scopeManager?.acquire(enclosingScopeNode); /*?*/ + const scope = scopeManager?.acquire(enclosingScopeNode); assert.ok(scope, 'scope is undefined'); const urlArgument = serviceCall.arguments[0]; if (!isUrlArgumentValid(urlArgument, scope)) { From 9e8318b4a566f84da19f74736475c62d072fcd69 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 18 Nov 2024 09:30:02 -0500 Subject: [PATCH 094/115] deps update --- package-lock.json | 207 ++++++++++++++++++++++------------------------ package.json | 18 ++-- 2 files changed, 109 insertions(+), 116 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d45037..621d93f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "7.3.0", "license": "MIT", "dependencies": { - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/type-utils": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -18,24 +18,24 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", "@checkdigit/typescript-config": "^8.0.0", - "@eslint/js": "^9.14.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.13.0", - "@typescript-eslint/rule-tester": "8.13.0", - "eslint": "^9.14.0", + "@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.1", + "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.0.2", + "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.13.0" + "typescript-eslint": "^8.14.0" }, "engines": { "node": ">=20.17" @@ -1122,9 +1122,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.4", @@ -1158,18 +1158,18 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1212,9 +1212,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1230,9 +1230,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "license": "Apache-2.0", "dependencies": { "levn": "^0.4.1" @@ -2175,17 +2175,17 @@ "peer": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz", + "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/type-utils": "8.14.0", + "@typescript-eslint/utils": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2209,16 +2209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz", + "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4" }, "engines": { @@ -2238,14 +2238,14 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.13.0.tgz", - "integrity": "sha512-VBS9EJ/W3x3XPhqZPbfnVCBL0SXaToLvZzTnfo5JhGLEFNVmY8AMT9m/A7R/VM+TL+ecuDRIPPjkC3asFrPFAQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-8.14.0.tgz", + "integrity": "sha512-q5Gi0CMFLojXZMvdWTIjOcBtV3qUg2xtUJocBxkd6PZ5YfHw9bd/Q+P5vtBu1Mrjs8OhId5WpOaMqjbUXLLBYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", @@ -2263,13 +2263,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", - "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", + "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0" + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2280,13 +2280,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz", + "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2304,9 +2304,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", - "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", + "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2317,13 +2317,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", - "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", + "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2345,15 +2345,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", - "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", + "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0" + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2367,12 +2367,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", - "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", + "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/types": "8.14.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3107,9 +3107,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3586,26 +3586,26 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.5", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -3624,8 +3624,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -3765,9 +3764,9 @@ } }, "node_modules/eslint-plugin-eslint-plugin": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-6.3.1.tgz", - "integrity": "sha512-5OUvS+kzpfbX3Pyt7ULYLJBGdjM/tGPdjePGFE50Lqdqcn/dB0f9ifbRCrCGWBt10Ljk7O6ajj3BPOZ8vmD50g==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-6.3.2.tgz", + "integrity": "sha512-s1uCANS+Keha58j25OvirQZsxlu3yrmWzcGuotOjUmqIeEFrDDar0cobAuYjLRLzwfjMKICKoPTCbK1pDleqiw==", "dev": true, "license": "MIT", "dependencies": { @@ -3870,9 +3869,9 @@ } }, "node_modules/eslint-plugin-no-secrets": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-secrets/-/eslint-plugin-no-secrets-1.0.2.tgz", - "integrity": "sha512-lXjGcPS6ZMxAouYWsuX5NGsLlOWQ5c+YFHHZFECzRCZIssYQgWVPINgZqAU7caquB32MoEAL+dXRQNDBX0fgwQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-secrets/-/eslint-plugin-no-secrets-1.1.2.tgz", + "integrity": "sha512-FjgyBaEkQK6hrdKf0V1TnKbY3dxXmw8S7tjfHs/BMIgFGNYhzFccxbZSJtDCPHTQTSiBtdLwRlOmSF81toII4w==", "dev": true, "license": "MIT", "engines": { @@ -4006,9 +4005,9 @@ } }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", - "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -7711,12 +7710,6 @@ "node": "*" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7967,15 +7960,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", - "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.14.0.tgz", + "integrity": "sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", - "@typescript-eslint/utils": "8.13.0" + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", + "@typescript-eslint/utils": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index b578a62..3b1fab1 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "preset": "@checkdigit/jest-config" }, "dependencies": { - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/type-utils": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -70,24 +70,24 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", "@checkdigit/typescript-config": "^8.0.0", - "@eslint/js": "^9.14.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.13.0", - "@typescript-eslint/rule-tester": "^8.13.0", - "eslint": "^9.14.0", + "@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.1", + "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.0.2", + "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.13.0" + "typescript-eslint": "^8.14.0" }, "peerDependencies": { "eslint": ">=9 <10" From 622954107cf60b1df81c5011986ffadd2868030f Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 18 Nov 2024 10:40:27 -0500 Subject: [PATCH 095/115] update eslint and related dependencies; improve response header retrieval in tests --- package-lock.json | 55 +++++++++++++++++++++++------------- package.json | 4 +-- src/agent/no-fixture.spec.ts | 17 ++++++++++- src/agent/no-fixture.ts | 15 +++++++--- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 621d93f..b57a680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,13 +18,13 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", "@checkdigit/typescript-config": "^8.0.0", - "@eslint/js": "^9.15.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.14.0", "@typescript-eslint/rule-tester": "^8.14.0", - "eslint": "^9.15.0", + "eslint": "9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-eslint-plugin": "^6.3.2", @@ -1122,9 +1122,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.4", @@ -1158,9 +1158,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1215,6 +1215,7 @@ "version": "9.15.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3586,26 +3587,26 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.14.0", + "@eslint/plugin-kit": "^0.2.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -3624,7 +3625,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -4004,6 +4006,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", @@ -7710,6 +7721,12 @@ "node": "*" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 3b1fab1..4749d64 100644 --- a/package.json +++ b/package.json @@ -70,13 +70,13 @@ "@checkdigit/jest-config": "^6.0.2", "@checkdigit/prettier-config": "^5.5.1", "@checkdigit/typescript-config": "^8.0.0", - "@eslint/js": "^9.15.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.14.0", "@typescript-eslint/rule-tester": "^8.14.0", - "eslint": "^9.15.0", + "eslint": "9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-eslint-plugin": "^6.3.2", diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 5dd9ecf..aa301d1 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -648,7 +648,22 @@ createTester().run(ruleId, rule, { method: 'GET', }); assert.equal(response.status, StatusCodes.OK); - const { etag } = response.headers; + const etag = response.headers.get('etag'); + `, + errors: 1, + }, + { + name: 'nested header destructuring - string literal key with renaming', + code: ` + const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + `, + output: ` + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const createdOn = response.headers.get('created-on'); + const updatedOn = response.headers.get('updated-on'); `, errors: 1, }, diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index cbbbce8..c68551e 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -376,10 +376,17 @@ const rule: Rule.RuleModule = { : []), // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable - ? [ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseHeadersVariable as ObjectPattern) : (destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, - ] + ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' + ? (destructuringResponseHeadersVariable as ObjectPattern).properties.map((property) => { + assert.ok(property.type === 'Property'); + assert.equal(property.value.type, 'Identifier'); + // eslint-disable-next-line sonarjs/no-nested-template-literals + return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === 'Literal' ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; + }) + : [ + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] : isResponseHeadersVariableRedefinitionNeeded ? [ `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, From e2e07d9ad8636ea184b910c848947f5d757d959a Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 19 Nov 2024 00:26:42 -0500 Subject: [PATCH 096/115] support object literal for setting request headers in fixture calls; enhance header handling logic --- src/agent/no-fixture.spec.ts | 19 +++++++++++++++++++ src/agent/no-fixture.ts | 35 ++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index aa301d1..14d74ef 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -667,5 +667,24 @@ createTester().run(ruleId, rule, { `, errors: 1, }, + { + name: 'support setting headers using object literal', + code: `function doSomething() { + return fixture.api + .get(\`\${BASE_PATH}/ping\`) + .set({ + ...(options?.createdOn ? { [CREATED_ON_HEADER]: options.createdOn } : {}), + }); + }`, + output: `function doSomething() { + return fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + headers: { + ...(options?.createdOn ? { [CREATED_ON_HEADER]: options.createdOn } : {}), + }, + }); + }`, + errors: 1, + }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index c68551e..757f03d 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -15,6 +15,7 @@ import type { ExpressionStatement, MemberExpression, Node, + ObjectExpression, ObjectPattern, ReturnStatement, SimpleCallExpression, @@ -45,6 +46,7 @@ interface FixtureCallInformation { variableAssignment?: ExpressionStatement; requestBody?: Expression; requestHeaders?: { name: Expression; value: Expression }[]; + requestHeadersObjectLiteral?: ObjectExpression; assertions?: Expression[][]; inlineStatementNode?: Node; inlineBodyReference?: MemberExpression; @@ -116,8 +118,12 @@ function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInfo // request headers const setRequestHeaderCall = getParent(parent); assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); - const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression]; - results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }]; + const [arg1, arg2] = setRequestHeaderCall.arguments as [Expression, Expression]; + if (arg1.type === 'ObjectExpression') { + results.requestHeadersObjectLiteral = arg1; + } else { + results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }]; + } nextCall = setRequestHeaderCall; } } else { @@ -321,17 +327,20 @@ const rule: Rule.RuleModule = { ...(fixtureCallInformation.requestBody ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] : []), - ...(fixtureCallInformation.requestHeaders - ? [ - ` headers: {`, - ...fixtureCallInformation.requestHeaders.map( - ({ name, value }) => - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals - ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, - ), - ` },`, - ] - : []), + // eslint-disable-next-line no-nested-ternary + ...(fixtureCallInformation.requestHeadersObjectLiteral + ? [` headers: ${sourceCode.getText(fixtureCallInformation.requestHeadersObjectLiteral)},`] + : fixtureCallInformation.requestHeaders + ? [ + ` headers: {`, + ...fixtureCallInformation.requestHeaders.map( + ({ name, value }) => + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals + ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ), + ` },`, + ] + : []), '}', ].join(`\n${indentation}`); From 1ec3e9a2d0fd8e2cada666644b2690aa6b882306 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Tue, 19 Nov 2024 10:28:27 -0500 Subject: [PATCH 097/115] add fetch-response-status rule to enforce status code naming conventions --- src/agent/fetch-response-status.spec.ts | 55 ++++++++++++++++ src/agent/fetch-response-status.ts | 87 +++++++++++++++++++++++++ src/index.ts | 7 ++ 3 files changed, 149 insertions(+) create mode 100644 src/agent/fetch-response-status.spec.ts create mode 100644 src/agent/fetch-response-status.ts diff --git a/src/agent/fetch-response-status.spec.ts b/src/agent/fetch-response-status.spec.ts new file mode 100644 index 0000000..2ecb1eb --- /dev/null +++ b/src/agent/fetch-response-status.spec.ts @@ -0,0 +1,55 @@ +// agent/fetch-response-status.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './fetch-response-status'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'destructuring status code is fine', + code: `async function foo() { + const { status } = await fetch(url); + assert.equal(status, StatusCode.Ok); + }`, + }, + { + name: 'destructuring status code is fine - with renaming', + code: `async function foo() { + const { status: statusCode } = await fetch(url); + assert.equal(status, StatusCode.Ok); + }`, + }, + ], + invalid: [ + { + name: 'change statusCode to status - shorthand (no renaming)', + code: `async function foo() { + const { statusCode } = await fetch(url); + assert.equal(statusCode, StatusCode.Ok); + }`, + output: `async function foo() { + const { status: statusCode } = await fetch(url); + assert.equal(statusCode, StatusCode.Ok); + }`, + errors: [{ messageId: 'renameStatusCodeProperty' }], + }, + { + name: 'change statusCode to status - leave renamed identifier along', + code: `async function foo() { + const { statusCode: someStatusCode } = await fetch(url); + assert.equal(someStatusCode, StatusCode.Ok); + }`, + output: `async function foo() { + const { status: someStatusCode } = await fetch(url); + assert.equal(someStatusCode, StatusCode.Ok); + }`, + errors: [{ messageId: 'renameStatusCodeProperty' }], + }, + ], +}); diff --git a/src/agent/fetch-response-status.ts b/src/agent/fetch-response-status.ts new file mode 100644 index 0000000..3141fd8 --- /dev/null +++ b/src/agent/fetch-response-status.ts @@ -0,0 +1,87 @@ +// agent/fetch-response-status.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; + +import getDocumentationUrl from '../get-documentation-url'; + +export const ruleId = 'fetch-response-status'; + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'unknownError' | 'renameStatusCodeProperty'> = createRule({ + name: ruleId, + meta: { + type: 'problem', + docs: { + description: 'Replace "response.body" with "await response.json()".', + }, + messages: { + renameStatusCodeProperty: 'Rename "statusCode" with "status".', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + VariableDeclaration: (variableDeclaration: TSESTree.VariableDeclaration) => { + const variableInit = variableDeclaration.declarations[0]?.init; + if ( + !variableInit || + variableInit.type !== AST_NODE_TYPES.AwaitExpression || + variableInit.argument.type !== AST_NODE_TYPES.CallExpression || + variableInit.argument.callee.type !== AST_NODE_TYPES.Identifier || + variableInit.argument.callee.name !== 'fetch' + ) { + return; + } + + const variableId = variableDeclaration.declarations[0]?.id; + if (variableId.type !== AST_NODE_TYPES.ObjectPattern) { + return; + } + const statusCodeProperty = variableId.properties.find( + (property): property is TSESTree.Property => + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier && + property.key.name === 'statusCode', + ); + if (!statusCodeProperty) { + return; + } + + try { + context.report({ + node: statusCodeProperty, + messageId: 'renameStatusCodeProperty', + fix(fixer) { + return statusCodeProperty.shorthand + ? fixer.replaceText(statusCodeProperty, 'status: statusCode') + : fixer.replaceText(statusCodeProperty.key, 'status'); + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: statusCodeProperty, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index 2d43357..1f0d023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,9 @@ import fetchResponseBodyJson, { ruleId as fetchResponseBodyJsonRuleId } from './ import fetchResponseHeaderGetter, { ruleId as fetchResponseHeaderGetterRuleId, } from './agent/fetch-response-header-getter'; +import fetchResponseStatus, { + ruleId as fetchResponseStatusRuleId, +} from './agent/fetch-response-status'; import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; import fixFunctionCallArguments, { ruleId as fixFunctionCallArgumentsRuleId, @@ -72,6 +75,7 @@ const rules: Record = { [noStatusCodeRuleId]: noStatusCode, [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, [fetchResponseHeaderGetterRuleId]: fetchResponseHeaderGetter, + [fetchResponseStatusRuleId]: fetchResponseStatus, [addUrlDomainRuleId]: addUrlDomain, [noLegacyServiceTypingRuleId]: noLegacyServiceTyping, [noMappedResponseRuleId]: noMappedResponse, @@ -127,6 +131,7 @@ const configs: Record Date: Wed, 20 Nov 2024 05:24:08 -0500 Subject: [PATCH 098/115] update dependencies to use caret (^) versioning for better compatibility --- package-lock.json | 6 +++--- package.json | 6 +++--- src/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7503157..a5d64ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "7.5.0", "license": "MIT", "dependencies": { - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/type-utils": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -24,7 +24,7 @@ "@types/eslint-config-prettier": "^6.11.3", "@typescript-eslint/parser": "^8.15.0", "@typescript-eslint/rule-tester": "^8.15.0", - "eslint": "9.15.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", diff --git a/package.json b/package.json index 988e235..c58dc05 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "preset": "@checkdigit/jest-config" }, "dependencies": { - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/type-utils": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0", "debug": "^4.3.7", "ts-api-utils": "^1.4.0" }, @@ -76,7 +76,7 @@ "@types/eslint-config-prettier": "^6.11.3", "@typescript-eslint/parser": "^8.15.0", "@typescript-eslint/rule-tester": "^8.15.0", - "eslint": "9.15.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", diff --git a/src/index.ts b/src/index.ts index 46826a6..826c262 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,7 +100,7 @@ const plugin: TSESLint.FlatConfig.Plugin = { rules, }; -const configs: Record = { +const configs: Record = { all: [ { files: ['**/*.ts'], From d519eda11f63ce66c28dec6804abc09be4ba51b3 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 13:53:05 -0500 Subject: [PATCH 099/115] apply no-fixture to *.test.ts as well --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 826c262..a872132 100644 --- a/src/index.ts +++ b/src/index.ts @@ -221,6 +221,7 @@ const configs: Record = { [`@checkdigit/${addBasePathConstRuleId}`]: 'error', [`@checkdigit/${addBasePathImportRuleId}`]: 'error', [`@checkdigit/${addAssertImportRuleId}`]: 'error', + [`@checkdigit/${noFixtureRuleId}`]: 'error', }, }, { @@ -231,7 +232,6 @@ const configs: Record = { }, rules: { [`@checkdigit/${agentTestWiringRuleId}`]: 'error', - [`@checkdigit/${noFixtureRuleId}`]: 'error', }, }, ], From 1c9029ae1ea9fb0f2f92d2f0fb96d7e7c0a3c9ed Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 14:17:43 -0500 Subject: [PATCH 100/115] merge with main --- package-lock.json | 134 +++++++++++++++++++++++----------------------- src/index.ts | 2 +- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/package-lock.json b/package-lock.json index 559b80f..2bf95f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,9 +60,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "peer": true, @@ -76,9 +76,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz", - "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", "dev": true, "license": "MIT", "peer": true, @@ -130,14 +130,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", - "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/parser": "^7.26.0", + "@babel/parser": "^7.26.2", "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", @@ -270,9 +270,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "dev": true, "license": "MIT", "peer": true, @@ -1289,6 +1289,19 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2137,9 +2150,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", - "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", "dev": true, "license": "MIT", "peer": true, @@ -2951,9 +2964,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001673", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz", - "integrity": "sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -3348,9 +3361,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.47", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz", - "integrity": "sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ==", + "version": "1.5.63", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.63.tgz", + "integrity": "sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==", "dev": true, "license": "ISC", "peer": true @@ -3402,9 +3415,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", + "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3423,7 +3436,7 @@ "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", + "globalthis": "^1.0.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", @@ -3439,10 +3452,10 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", + "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.2", "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", @@ -4024,19 +4037,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4072,9 +4072,9 @@ } }, "node_modules/esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz", + "integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==", "dev": true, "license": "MIT", "peer": true @@ -4350,9 +4350,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "license": "ISC" }, "node_modules/for-each": { @@ -5158,14 +5158,14 @@ } }, "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@types/estree": "*" + "@types/estree": "^1.0.6" } }, "node_modules/is-regex": { @@ -6167,9 +6167,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.13", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.13.tgz", + "integrity": "sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==", "dev": true, "license": "MIT", "peer": true, @@ -6343,9 +6343,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", "engines": { @@ -6615,9 +6615,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "dev": true, "license": "ISC", "engines": { @@ -7635,9 +7635,9 @@ } }, "node_modules/svelte": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.3.tgz", - "integrity": "sha512-Sl8UFHlBvF54aK8MElFvyvaUfPE2REOz6LnhR2pBClCL11MU4qpn4V+KgAggaXxDyrP2iQixvHbtpHqL/zXlSQ==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.5.tgz", + "integrity": "sha512-D33RkKYF4AFIgM+HrItxFudmWrXOLaua8vW3Mq7bObn7UwRn6zJPZ58bEIlj8wEYfi08n8VVvTk8dCLVHNnikQ==", "dev": true, "license": "MIT", "peer": true, @@ -7651,7 +7651,7 @@ "axobject-query": "^4.1.0", "esm-env": "^1.0.0", "esrap": "^1.2.2", - "is-reference": "^3.0.2", + "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" @@ -7844,9 +7844,9 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD", "peer": true diff --git a/src/index.ts b/src/index.ts index a872132..8654bd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -264,7 +264,7 @@ const configs: Record = { }; const defaultToExport: Exclude & { - configs: Record; + configs: Record; } = { ...plugin, configs, From 58565d6e5b4651f21e5c90fdf0786832654054fa Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 15:20:43 -0500 Subject: [PATCH 101/115] refactor: update regex in getApiFolder to improve path matching --- src/agent/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/file.ts b/src/agent/file.ts index 1db9113..90f3286 100644 --- a/src/agent/file.ts +++ b/src/agent/file.ts @@ -24,7 +24,7 @@ export function loadPackageJson(projectRoot: string): string { } export function getApiFolder(folder: string): string | undefined { - if (/^(?.*\/)*src\/api\/v\d+$/u.test(folder)) { + if (/^(?(?:[^\/]+\/)*)src\/api\/v\d+$/u.test(folder)) { return folder; } const upperFolder = folder.substring(0, folder.lastIndexOf('/')); From 412109c272c4a708473cb176f6ef94f4e99f772c Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 15:24:07 -0500 Subject: [PATCH 102/115] fix: update regex in getApiFolder to allow optional leading slash --- src/agent/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent/file.ts b/src/agent/file.ts index 90f3286..7f33d67 100644 --- a/src/agent/file.ts +++ b/src/agent/file.ts @@ -24,7 +24,7 @@ export function loadPackageJson(projectRoot: string): string { } export function getApiFolder(folder: string): string | undefined { - if (/^(?(?:[^\/]+\/)*)src\/api\/v\d+$/u.test(folder)) { + if (/^\/?(?(?:[^/]+\/)*)src\/api\/v\d+$/u.test(folder)) { return folder; } const upperFolder = folder.substring(0, folder.lastIndexOf('/')); From 033ad7cebdb588a7d63de489178dc010f971f140 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 15:53:52 -0500 Subject: [PATCH 103/115] regenerate package-lock.json --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2bf95f4..18744b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7635,9 +7635,9 @@ } }, "node_modules/svelte": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.5.tgz", - "integrity": "sha512-D33RkKYF4AFIgM+HrItxFudmWrXOLaua8vW3Mq7bObn7UwRn6zJPZ58bEIlj8wEYfi08n8VVvTk8dCLVHNnikQ==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.2.6.tgz", + "integrity": "sha512-mQBm268315W4lu6LxJK0Nt3Ou/m2uFJTKzLcFWhTTiA7cbCvVUeqwQSEBkGpbxQw84VCSO6my7DUlWsSy1/tQA==", "dev": true, "license": "MIT", "peer": true, From 02b6366ecdeb126e5a463f3584db83888a9c87ca Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 16:02:14 -0500 Subject: [PATCH 104/115] chore: add allow-dependencies-licenses for flatted package in publish-beta workflow --- .github/workflows/publish-beta.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml index 22eab1a..86a1c6a 100644 --- a/.github/workflows/publish-beta.yml +++ b/.github/workflows/publish-beta.yml @@ -18,6 +18,7 @@ jobs: uses: actions/dependency-review-action@v4 with: allow-licenses: ${{ vars.ALLOW_LICENSES }} + allow-dependencies-licenses: pkg:npm/flatted - name: Setup Node.js uses: actions/setup-node@v4 with: From 3251f22133d86e06afeabb7b8fd871d17fe274d8 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 16:05:25 -0500 Subject: [PATCH 105/115] chore: bump version to 7.6.0 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c58dc05..2934ed1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "7.5.0", + "version": "7.6.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", From e7dba74b5032f3acf538f69439d5f7869fe0bb8d Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 20 Nov 2024 16:09:38 -0500 Subject: [PATCH 106/115] chore: bump version to 7.6.0 in package-lock.json --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18744b0..c057fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@checkdigit/eslint-plugin", - "version": "7.5.0", + "version": "7.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "7.5.0", + "version": "7.6.0", "license": "MIT", "dependencies": { "@typescript-eslint/type-utils": "^8.15.0", From 7eaaeaa5115a843a4e38f2291bcc4c3caa4acbcf Mon Sep 17 00:00:00 2001 From: Le Cong Date: Thu, 21 Nov 2024 11:56:45 -0500 Subject: [PATCH 107/115] new rule no-supertest --- src/agent/no-supertest.spec.ts | 369 ++++++++++++++++++++++++ src/agent/no-supertest.ts | 497 +++++++++++++++++++++++++++++++++ src/index.ts | 5 + 3 files changed, 871 insertions(+) create mode 100644 src/agent/no-supertest.spec.ts create mode 100644 src/agent/no-supertest.ts diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts new file mode 100644 index 0000000..b50ce38 --- /dev/null +++ b/src/agent/no-supertest.spec.ts @@ -0,0 +1,369 @@ +// agent/no-supertest.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../tester.test'; +import rule, { ruleId } from './no-supertest'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', + code: `async function test() { + const { headers } = await ping(); + assert.ok(headers.get(ETAG)); + }`, + }, + // { + // name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', + // code: `async function test() { + // const responses = await Promise.all([ + // ping().expect(StatusCodes.OK), + // ping().expect(StatusCodes.OK), + // ]); + // }`, + // }, + ], + invalid: [ + { + name: 'assertion without variable declaration', + code: `async function test() { + await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + }`, + errors: 1, + }, + { + name: 'assertion with variable declaration', + code: `async function test() { + const pingResponse = await ping().expect(StatusCodes.OK); + assert(pingResponse.body); + }`, + output: `async function test() { + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const pingResponseBody = await pingResponse.json(); + assert(pingResponseBody); + }`, + errors: 1, + }, + { + name: 'response headers assertion should be externalized with new variable declared if necessary', + code: `async function test() { + await ping() + .expect(StatusCodes.OK) + .expect('etag', '123') + .expect('content-type', 'application/json') + .expect(ETAG, correctVersion) + .expect(ETAG, /1.*/u); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get('etag'), '123'); + assert.equal(response.headers.get('content-type'), 'application/json'); + assert.equal(response.headers.get(ETAG), correctVersion); + assert.ok(response.headers.get(ETAG).match(/1.*/u)); + }`, + errors: 1, + }, + { + name: 'response body assertion', + code: `async function test() { + await ping().expect({message:'pong'}); + }`, + output: `async function test() { + const response = await ping(); + assert.deepEqual(await response.json(), {message:'pong'}); + }`, + errors: 1, + }, + { + name: 'response callback assertion', + code: `async function test() { + await ping() + .expect(validate) + .expect((response)=>console.log(response)); + }`, + output: `async function test() { + const response = await ping(); + assert.doesNotThrow(()=>validate(response)); + assert.doesNotThrow(()=>console.log(response)); + }`, + errors: 1, + }, + { + name: 'multiple fixture calls in the same test', + code: `async function test() { + await ping().expect(StatusCodes.OK); + const pingResponse = await ping().expect(StatusCodes.OK); + await ping().expect(StatusCodes.OK).expect({message:'pong'}); + await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const response2 = await ping(); + assert.equal(response2.status, StatusCodes.OK); + assert.deepEqual(await response2.json(), {message:'pong'}); + const response3 = await ping(); + assert.equal(response3.status, StatusCodes.OK); + }`, + errors: 4, + }, + { + name: 'directly return (no await) fixture call with assertion', + code: `async function test() { + return ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + return response; + }`, + errors: 1, + }, + { + name: 'replace header access through response.get() with response.headers.get()', + code: `async function test() { + const response = await ping().expect(StatusCodes.OK); + assert.equal(response.get(ETAG), correctVersion); + assert.equal(response.get('etag'), correctVersion); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + assert.equal(response.headers.get(ETAG), correctVersion); + assert.equal(response.headers.get('etag'), correctVersion); + }`, + errors: 1, + }, + { + name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', + code: `async function test() { + await ping().expect(200); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, 200); + }`, + errors: 1, + }, + { + name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', + code: `async function test() { + const createdOn = Date.now().toUTCString(); + await ping().expect(200).expect(validateBody(createdOn)); + }`, + output: `async function test() { + const createdOn = Date.now().toUTCString(); + const response = await ping(); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), validateBody(createdOn)); + }`, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for body', + code: `async function test() { + const { body: responseBody } = await ping().expect(StatusCodes.OK); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const responseBody = await response.json(); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }`, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for body - with nested destructuring', + code: `async function test() { + const { body: { pgpPublicKey: firstPgpPublicKey } } = await ping().expect(StatusCodes.OK); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const { pgpPublicKey: firstPgpPublicKey } = await response.json(); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + }`, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers when body is presented as well', + code: `async function test() { + const { body, headers: headers2 } = await ping().expect(StatusCodes.OK); + assert(body); + assert.ok(headers2.get(ETAG)); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const body = await response.json(); + const headers2 = response.headers; + assert(body); + assert.ok(headers2.get(ETAG)); + }`, + errors: 1, + }, + { + name: 'handle destructuring variable declaration for headers without body presented but with assertions used', + code: `async function test() { + const { headers } = await ping().expect(StatusCodes.OK); + assert.ok(headers.get(ETAG)); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const headers = response.headers; + assert.ok(headers.get(ETAG)); + }`, + errors: 1, + }, + { + name: 'avoid response variable name conflict with existing variables in the same scope', + code: `async () => { + const response = 'foo'; + const response1 = 'bar'; + await ping().expect(StatusCodes.OK); + await ping().expect(StatusCodes.OK); + }`, + output: `async () => { + const response = 'foo'; + const response1 = 'bar'; + const response2 = await ping(); + assert.equal(response2.status, StatusCodes.OK); + const response3 = await ping(); + assert.equal(response3.status, StatusCodes.OK); + }`, + errors: 2, + }, + { + name: 'response variable names in different scope do not conflict with each other', + code: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + await ping().expect(StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + await ping().expect(StatusCodes.OK); + }); + `, + output: ` + it('#1', async () => { + const response = 'foo'; + }); + it('#2', async () => { + const response = 'foo'; + const response2 = await ping(); + assert.equal(response2.status, StatusCodes.OK); + }); + it('#3', async () => { + const response3 = 'foo'; + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + }); + `, + errors: 2, + }, + { + name: 'inline access to response body should be extracted to a variable', + code: `export async function validatePin( + fixture, + ) { + const paymentSecurityServicePublicKey = (await ping().expect(StatusCodes.OK)).body.publicKey; + }`, + output: `export async function validatePin( + fixture, + ) { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const responseBody = await response.json(); + const paymentSecurityServicePublicKey = responseBody.publicKey; + }`, + errors: 1, + }, + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: `async function test() { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + await ping() + .expect(StatusCodes.NO_CONTENT) + .expect(ETAG_HEADER, '1') + .expect((res) => verifyTemporalHeaders(res, createdOn)); + }`, + output: `async function test() { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + const response = await ping(); + assert.equal(response.status, StatusCodes.NO_CONTENT); + assert.equal(response.headers.get(ETAG_HEADER), '1'); + assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); + }`, + errors: 1, + }, + { + name: 'assignment statement instead of variable declaration used for subsequent fixture calls', + code: `async function test() { + let response = await ping().expect(StatusCodes.OK); + response = await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + let response = await ping(); + assert.equal(response.status, StatusCodes.OK); + response = await ping(); + assert.equal(response.status, StatusCodes.OK); + }`, + errors: 2, + }, + { + name: 'nested header destructuring', + code: `async function test() { + const { headers: { etag } } = await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const etag = response.headers.get('etag'); + }`, + errors: 1, + }, + { + name: 'nested header destructuring - string literal key with renaming', + code: `async function test() { + const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const createdOn = response.headers.get('created-on'); + const updatedOn = response.headers.get('updated-on'); + }`, + errors: 1, + }, + ], +}); diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts new file mode 100644 index 0000000..90f05fd --- /dev/null +++ b/src/agent/no-supertest.ts @@ -0,0 +1,497 @@ +// agent/no-supertest.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +import type { + AwaitExpression, + CallExpression, + Expression, + ExpressionStatement, + MemberExpression, + Node, + ObjectExpression, + ObjectPattern, + ReturnStatement, + SimpleCallExpression, + VariableDeclaration, +} from 'estree'; +import { type Rule, type Scope, SourceCode } from 'eslint'; + +import { + getEnclosingFunction, + getEnclosingScopeNode, + getEnclosingStatement, + getParent, + isUsedInArrayOrAsArgument, +} from '../library/tree'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../library/format'; +import { analyzeResponseReferences } from './response-reference'; +import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText } from './fetch'; + +export const ruleId = 'no-supertest'; + +interface FixtureCallInformation { + rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression | ExpressionStatement; + fixtureNode: AwaitExpression | SimpleCallExpression; + variableDeclaration?: VariableDeclaration; + variableAssignment?: ExpressionStatement; + requestBody?: Expression; + requestHeaders?: { name: Expression; value: Expression }[]; + requestHeadersObjectLiteral?: ObjectExpression; + assertions?: Expression[][]; + inlineStatementNode?: Node; + inlineBodyReference?: MemberExpression; + inlineHeadersReference?: MemberExpression; +} + +// recursively analyze the fixture/supertest call chain to collect information of request/response +// eslint-disable-next-line sonarjs/cognitive-complexity +function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { + const parent = getParent(call); + assert.ok(parent, 'parent should exist for fixture/supertest call node'); + + let nextCall; + if (parent.type === 'ReturnStatement') { + // direct return, no variable declaration or await + results.fixtureNode = call; + results.rootNode = parent; + } else if ( + parent.type === 'ArrayExpression' || + parent.type === 'CallExpression' || + parent.type === 'ArrowFunctionExpression' + ) { + // direct return, no variable declaration or await + results.fixtureNode = call; + results.rootNode = call; + } else if (parent.type === 'AwaitExpression') { + results.fixtureNode = call; + const enclosingStatement = getEnclosingStatement(parent); + assert.ok(enclosingStatement); + const awaitParent = getParent(parent); + if (awaitParent?.type === 'MemberExpression') { + results.rootNode = parent; + results.inlineStatementNode = enclosingStatement; + if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') { + results.inlineBodyReference = awaitParent; + } + if ( + awaitParent.property.type === 'Identifier' && + (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') + ) { + results.inlineHeadersReference = awaitParent; + } + } else if (enclosingStatement.type === 'VariableDeclaration') { + results.variableDeclaration = enclosingStatement; + results.rootNode = enclosingStatement; + } else if ( + enclosingStatement.type === 'ExpressionStatement' && + enclosingStatement.expression.type === 'AssignmentExpression' + ) { + results.variableAssignment = enclosingStatement; + results.rootNode = enclosingStatement; + } else { + results.rootNode = parent; + } + } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + if (parent.property.name === 'expect') { + // supertest assertions + const assertionCall = getParent(parent); + assert.ok(assertionCall && assertionCall.type === 'CallExpression'); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + nextCall = assertionCall; + } else if (parent.property.name === 'send') { + // request body + const sendRequestBodyCall = getParent(parent); + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); + results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + nextCall = sendRequestBodyCall; + } else if (parent.property.name === 'set') { + // request headers + const setRequestHeaderCall = getParent(parent); + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); + const [arg1, arg2] = setRequestHeaderCall.arguments as [Expression, Expression]; + if (arg1.type === 'ObjectExpression') { + results.requestHeadersObjectLiteral = arg1; + } else { + results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }]; + } + nextCall = setRequestHeaderCall; + } + } else { + throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); + } + if (nextCall) { + analyzeFixtureCall(nextCall, results, sourceCode); + } +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function createResponseAssertions( + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + responseVariableName: string, + destructuringResponseHeadersVariable: Scope.Variable | undefined, +) { + let statusAssertion: string | undefined; + const nonStatusAssertions: string[] = []; + for (const expectArguments of fixtureCallInformation.assertions ?? []) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + if ( + (assertionArgument.type === 'MemberExpression' && + assertionArgument.object.type === 'Identifier' && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === 'Literal' || + sourceCode.getText(assertionArgument).includes('StatusCodes.') + ) { + // status code assertion + statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; + } else if (assertionArgument.type === 'ArrowFunctionExpression') { + // callback assertion using arrow function + let functionBody = sourceCode.getText(assertionArgument.body); + + const [originalResponseArgument] = assertionArgument.params; + assert.ok(originalResponseArgument?.type === 'Identifier'); + const originalResponseArgumentName = originalResponseArgument.name; + if (originalResponseArgumentName !== responseVariableName) { + functionBody = functionBody.replace( + new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), + responseVariableName, + ); + } + nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); + } else if (assertionArgument.type === 'Identifier') { + // callback assertion using function reference + nonStatusAssertions.push( + `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, + ); + } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + // body deep equal assertion + nonStatusAssertions.push( + `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, + ); + } else { + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); + } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + const headersReference = + destructuringResponseHeadersVariable !== undefined + ? destructuringResponseHeadersVariable.name + : `${responseVariableName}.headers`; + if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + nonStatusAssertions.push( + `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + nonStatusAssertions.push( + `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } + } + } + return { + statusAssertion, + nonStatusAssertions, + }; +} + +function getResponseVariableNameToUse( + scopeManager: Scope.ScopeManager, + fixtureCallInformation: FixtureCallInformation, + scopeVariablesMap: Map, +) { + if (fixtureCallInformation.variableAssignment) { + assert.ok( + fixtureCallInformation.variableAssignment.expression.type === 'AssignmentExpression' && + fixtureCallInformation.variableAssignment.expression.left.type === 'Identifier', + ); + return fixtureCallInformation.variableAssignment.expression.left.name; + } + + if (fixtureCallInformation.variableDeclaration) { + const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; + if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { + return firstDeclaration.id.name; + } + } + + const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode); + scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); + assert.ok(enclosingScopeNode); + const scope = scopeManager.acquire(enclosingScopeNode); + assert.ok(scope !== null); + let scopeVariables = scopeVariablesMap.get(scope); + if (!scopeVariables) { + scopeVariables = [...scope.set.keys()]; + scopeVariablesMap.set(scope, scopeVariables); + } + + let responseVariableCounter = 0; + let responseVariableNameToUse; + while (responseVariableNameToUse === undefined) { + responseVariableCounter++; + responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`; + if (scopeVariables.includes(responseVariableNameToUse)) { + responseVariableNameToUse = undefined; + } + } + scopeVariables.push(responseVariableNameToUse); + return responseVariableNameToUse; +} + +function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean { + const parent = getParent(responseBodyReference); + return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier'; +} + +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Transform supertest assersions to regular node assertions.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Transform supertest assersions to regular node assertions.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + // eslint-disable-next-line max-lines-per-function + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + const scopeVariablesMap = new Map(); + + return { + // eslint-disable-next-line max-lines-per-function + 'CallExpression[callee.property.name="expect"]': ( + supertestCall: CallExpression, + // eslint-disable-next-line sonarjs/cognitive-complexity + ) => { + assert.ok(supertestCall.callee.type === 'MemberExpression'); + if ( + supertestCall.callee.object.type === 'CallExpression' && + supertestCall.callee.object.callee.type === 'MemberExpression' && + supertestCall.callee.object.callee.property.type === 'Identifier' && + supertestCall.callee.object.callee.property.name === 'expect' + ) { + // skip nested expect calls, only focus on the top level + return; + } + try { + if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) { + // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here + return; + } + + assert.ok(supertestCall.type === 'CallExpression'); + const fixtureFunction = supertestCall.callee.object; + if (fixtureFunction.type !== 'CallExpression') { + return; + } + + const indentation = getIndentation(supertestCall, sourceCode); + + const fixtureCallInformation = {} as FixtureCallInformation; + analyzeFixtureCall(fixtureFunction, fixtureCallInformation, sourceCode); + sourceCode.getText(fixtureCallInformation.fixtureNode); + sourceCode.getText(fixtureCallInformation.rootNode); + fixtureCallInformation.assertions?.flat().map((ass) => sourceCode.getText(ass)); + + const { + variable: responseVariable, + bodyReferences: responseBodyReferences, + headersReferences: responseHeadersReferences, + statusReferences: responseStatusReferences, + destructuringBodyVariable: destructuringResponseBodyVariable, + destructuringHeadersVariable: destructuringResponseHeadersVariable, + } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); + + const responseVariableNameToUse = getResponseVariableNameToUse( + scopeManager, + fixtureCallInformation, + scopeVariablesMap, + ); + + const isResponseBodyVariableRedefinitionNeeded = + destructuringResponseBodyVariable !== undefined || + fixtureCallInformation.inlineBodyReference !== undefined || + (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); + const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; + + const isResponseHeadersVariableRedefinitionNeeded = + (destructuringResponseHeadersVariable !== undefined && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') || + fixtureCallInformation.inlineHeadersReference !== undefined; + const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; + + const isResponseVariableRedefinitionNeeded = + (fixtureCallInformation.variableAssignment === undefined && + responseVariable === undefined && + fixtureCallInformation.assertions !== undefined) || + isResponseBodyVariableRedefinitionNeeded || + isResponseHeadersVariableRedefinitionNeeded; + + const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded + ? [ + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseBodyVariable + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseBodyVariableRedefinitionNeeded + ? [ + `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : []), + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseHeadersVariable + ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' + ? (destructuringResponseHeadersVariable as ObjectPattern).properties.map((property) => { + assert.ok(property.type === 'Property'); + assert.equal(property.value.type, 'Identifier'); + // eslint-disable-next-line sonarjs/no-nested-template-literals + return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === 'Literal' ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; + }) + : [ + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseHeadersVariableRedefinitionNeeded + ? [ + `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : []), + ] + : []; + + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + destructuringResponseHeadersVariable as Scope.Variable | undefined, + ); + + // add variable declaration if needed + const fetchCallText = sourceCode.getText(fixtureFunction); + const fetchStatementText = !isResponseVariableRedefinitionNeeded + ? fetchCallText + : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`; + + const nodeToReplace = isResponseVariableRedefinitionNeeded + ? fixtureCallInformation.rootNode + : fixtureCallInformation.fixtureNode; + const appendingAssignmentAndAssertionText = [ + '', + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...responseBodyHeadersVariableRedefineLines, + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + + context.report({ + node: supertestCall, + messageId: 'preferNativeFetch', + + *fix(fixer) { + if (fixtureCallInformation.inlineStatementNode) { + const preInlineDeclaration = [ + fetchStatementText, + `${appendingAssignmentAndAssertionText};\n${indentation}`, + ].join(``); + yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration); + } else { + yield fixer.replaceText(nodeToReplace, fetchStatementText); + + const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); + yield fixer.insertTextAfter( + nodeToReplace, + needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, + ); + } + + // handle response body references + for (const responseBodyReference of responseBodyReferences) { + yield fixer.replaceText( + responseBodyReference, + isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference) + ? redefineResponseBodyVariableName + : getResponseBodyRetrievalText(responseVariableNameToUse), + ); + } + if (fixtureCallInformation.inlineBodyReference) { + yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); + } + + // handle response headers references + for (const responseHeadersReference of responseHeadersReferences) { + const parent = getParent(responseHeadersReference); + assert.ok(parent); + let headerName; + if (parent.type === 'MemberExpression') { + const headerNameNode = parent.property; + headerName = parent.computed + ? sourceCode.getText(headerNameNode) + : `'${sourceCode.getText(headerNameNode)}'`; + } else if (parent.type === 'CallExpression') { + const headerNameNode = parent.arguments[0]; + headerName = sourceCode.getText(headerNameNode); + } + assert.ok(headerName !== undefined); + yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); + } + + // convert response.statusCode to response.status + for (const responseStatusReference of responseStatusReferences) { + if ( + responseStatusReference.property.type === 'Identifier' && + responseStatusReference.property.name === 'statusCode' + ) { + yield fixer.replaceText(responseStatusReference.property, `status`); + } + } + + // handle direct return statement without await, e.g. "return fixture.api.get(...);" + if ( + fixtureCallInformation.rootNode.type === 'ReturnStatement' && + fixtureCallInformation.assertions !== undefined + ) { + yield fixer.insertTextAfter( + fixtureCallInformation.rootNode, + `\n${indentation}return ${responseVariableNameToUse};`, + ); + } + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: supertestCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/src/index.ts b/src/index.ts index 8654bd1..b6c1ef7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import fixFunctionCallArguments, { import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; +import noSupertest, { ruleId as noSupertestRuleId } from './agent/no-supertest'; import noLegacyServiceTyping, { ruleId as noLegacyServiceTypingRuleId } from './no-legacy-service-typing'; import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; @@ -72,6 +73,7 @@ const rules: Record = { [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, + [noSupertestRuleId]: noSupertest, [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, @@ -131,6 +133,7 @@ const configs: Record = { [`@checkdigit/${noMappedResponseRuleId}`]: 'off', [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noSupertestRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', @@ -179,6 +182,7 @@ const configs: Record = { [`@checkdigit/${noMappedResponseRuleId}`]: 'off', [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', + [`@checkdigit/${noSupertestRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', @@ -222,6 +226,7 @@ const configs: Record = { [`@checkdigit/${addBasePathImportRuleId}`]: 'error', [`@checkdigit/${addAssertImportRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', + [`@checkdigit/${noSupertestRuleId}`]: 'error', }, }, { From f4c8514a945b51f992e44e17b0f12c51da8e6426 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 22 Nov 2024 14:15:36 -0500 Subject: [PATCH 108/115] ts-ify rules --- src/agent/fetch-then.spec.ts | 21 +++- src/agent/fetch-then.ts | 96 +++++++++--------- src/agent/fetch.ts | 26 ++--- src/agent/no-fixture.spec.ts | 96 +++++++++++------- src/agent/no-fixture.ts | 171 +++++++++++++++---------------- src/agent/no-supertest.spec.ts | 49 +++++---- src/agent/no-supertest.ts | 173 ++++++++++++++++---------------- src/agent/response-reference.ts | 89 ++++++++-------- 8 files changed, 391 insertions(+), 330 deletions(-) diff --git a/src/agent/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts index 4abc306..15a7afa 100644 --- a/src/agent/fetch-then.spec.ts +++ b/src/agent/fetch-then.spec.ts @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import createTester from '../tester.test'; +import createTester from '../ts-tester.test'; import rule, { ruleId } from './fetch-then'; createTester().run(ruleId, rule, { @@ -50,7 +50,7 @@ createTester().run(ruleId, rule, { }), ]); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'adjust header access correctly', @@ -88,7 +88,20 @@ createTester().run(ruleId, rule, { assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); `, - errors: 12, + errors: [ + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + { messageId: 'shouldUseHeaderGetter' }, + ], }, { name: 'in non-async arrow function with concurrent promises', @@ -128,7 +141,7 @@ createTester().run(ruleId, rule, { }), ); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, ], }); diff --git a/src/agent/fetch-then.ts b/src/agent/fetch-then.ts index bbf5cc1..96edca6 100644 --- a/src/agent/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -8,10 +8,11 @@ import { strict as assert } from 'node:assert'; -import type { CallExpression, Expression, MemberExpression, SimpleCallExpression } from 'estree'; -import { type Rule, type Scope, SourceCode } from 'eslint'; +import { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; -import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/tree'; +import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; @@ -21,48 +22,48 @@ import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'fetch-then'; interface FixtureCallInformation { - fixtureNode: SimpleCallExpression; - requestBody?: Expression; - requestHeaders?: { name: Expression; value: Expression }[]; - assertions?: Expression[][]; + fixtureNode: TSESTree.CallExpression; + requestBody?: TSESTree.Expression; + requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; + assertions?: TSESTree.Expression[][]; } // recursively analyze the fixture/supertest call chain to collect information of request/response -function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { +function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { const parent = getParent(call); if (!parent) { return; } let nextCall; - if (parent.type !== 'MemberExpression') { + if (parent.type !== AST_NODE_TYPES.MemberExpression) { results.fixtureNode = call; return; } - if (parent.property.type === 'Identifier') { + if (parent.property.type === AST_NODE_TYPES.Identifier) { if (parent.property.name === 'expect') { // supertest assertions const assertionCall = getParent(parent); - assert.ok(assertionCall && assertionCall.type === 'CallExpression'); - results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; nextCall = assertionCall; } else if (parent.property.name === 'send') { // request body const sendRequestBodyCall = getParent(parent); - assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); - results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression); + results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression; nextCall = sendRequestBodyCall; } else if (parent.property.name === 'set') { // request headers const setRequestHeaderCall = getParent(parent); - assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); - const [name, value] = setRequestHeaderCall.arguments as [Expression, Expression]; + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression); + const [name, value] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression]; results.requestHeaders = [...(results.requestHeaders ?? []), { name, value }]; nextCall = setRequestHeaderCall; } } else { - throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); + throw new Error(`Unexpected TSESTree.Expression in fixture/supertest call ${sourceCode.getText(parent)}.`); } if (nextCall) { analyzeFixtureCall(nextCall, results, sourceCode); @@ -82,20 +83,20 @@ function createResponseAssertions( const [assertionArgument] = expectArguments; assert.ok(assertionArgument); if ( - (assertionArgument.type === 'MemberExpression' && - assertionArgument.object.type === 'Identifier' && + (assertionArgument.type === AST_NODE_TYPES.MemberExpression && + assertionArgument.object.type === AST_NODE_TYPES.Identifier && assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === 'Literal' || + assertionArgument.type === AST_NODE_TYPES.Literal || sourceCode.getText(assertionArgument).includes('StatusCodes.') ) { // status code assertion statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; - } else if (assertionArgument.type === 'ArrowFunctionExpression') { + } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { // callback assertion using arrow function let functionBody = sourceCode.getText(assertionArgument.body); const [originalResponseArgument] = assertionArgument.params; - assert.ok(originalResponseArgument?.type === 'Identifier'); + assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); const originalResponseArgumentName = originalResponseArgument.name; if (originalResponseArgumentName !== responseVariableName) { functionBody = functionBody.replace( @@ -104,12 +105,15 @@ function createResponseAssertions( ); } nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); - } else if (assertionArgument.type === 'Identifier') { + } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { // callback assertion using function reference nonStatusAssertions.push( `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, ); - } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + } else if ( + assertionArgument.type === AST_NODE_TYPES.ObjectExpression || + assertionArgument.type === AST_NODE_TYPES.CallExpression + ) { // body deep equal assertion nonStatusAssertions.push( `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, @@ -122,7 +126,7 @@ function createResponseAssertions( const [headerName, headerValue] = expectArguments; assert.ok(headerName && headerValue); const headersReference = `${responseVariableName}.headers`; - if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { nonStatusAssertions.push( `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, ); @@ -139,16 +143,12 @@ function createResponseAssertions( }; } -function getResponseHeadersAccesses( - responseVariables: Scope.Variable[], - scopeManager: Scope.ScopeManager, - sourceCode: SourceCode, -) { - const responseHeadersAccesses: MemberExpression[] = []; +function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) { + const responseHeadersAccesses: TSESTree.MemberExpression[] = []; for (const responseVariable of responseVariables) { for (const responseReference of responseVariable.references) { const responseAccess = getParent(responseReference.identifier); - if (!responseAccess || responseAccess.type !== 'MemberExpression') { + if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) { continue; } @@ -158,8 +158,8 @@ function getResponseHeadersAccesses( } if ( - responseAccessParent.type === 'CallExpression' && - responseAccessParent.arguments[0]?.type === 'ArrowFunctionExpression' + responseAccessParent.type === AST_NODE_TYPES.CallExpression && + responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression ) { // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) responseHeadersAccesses.push( @@ -174,8 +174,8 @@ function getResponseHeadersAccesses( if ( responseAccess.computed && - responseAccess.property.type === 'Literal' && - responseAccessParent.type === 'MemberExpression' + responseAccess.property.type === AST_NODE_TYPES.Literal && + responseAccessParent.type === AST_NODE_TYPES.MemberExpression ) { // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. responseHeadersAccesses.push(responseAccessParent); @@ -187,7 +187,9 @@ function getResponseHeadersAccesses( return responseHeadersAccesses; } -const rule: Rule.RuleModule = { +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shouldUseHeaderGetter'> = createRule({ + name: ruleId, meta: { type: 'suggestion', docs: { @@ -202,14 +204,15 @@ const rule: Rule.RuleModule = { fixable: 'code', schema: [], }, - + defaultOptions: [], create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; + assert.ok(scopeManager); return { 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( - fixtureCall: CallExpression, + fixtureCall: TSESTree.CallExpression, // eslint-disable-next-line sonarjs/cognitive-complexity ) => { try { @@ -222,9 +225,8 @@ const rule: Rule.RuleModule = { return; } - assert.ok(fixtureCall.type === 'CallExpression'); const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get - assert.ok(fixtureFunction.type === 'MemberExpression'); + assert.ok(fixtureFunction.type === AST_NODE_TYPES.MemberExpression); const indentation = getIndentation(fixtureCall, sourceCode); const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` @@ -239,7 +241,7 @@ const rule: Rule.RuleModule = { // fetch request argument const methodNode = fixtureFunction.property; // get/put/etc. - assert.ok(methodNode.type === 'Identifier'); + assert.ok(methodNode.type === AST_NODE_TYPES.Identifier); const fetchRequestArgumentLines = [ '{', ` method: '${methodNode.name.toUpperCase()}',`, @@ -252,7 +254,7 @@ const rule: Rule.RuleModule = { ...fixtureCallInformation.requestHeaders.map( ({ name, value }) => // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals - ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ` ${name.type === AST_NODE_TYPES.Literal ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, ), ` },`, ] @@ -306,7 +308,7 @@ const rule: Rule.RuleModule = { for (const responseHeadersAccess of responseHeadersAccesses) { if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { const headerAccess = getParent(responseHeadersAccess); - if (headerAccess?.type === 'MemberExpression') { + if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) { const headerNameNode = headerAccess.property; const headerName = headerAccess.computed ? sourceCode.getText(headerNameNode) @@ -321,8 +323,8 @@ const rule: Rule.RuleModule = { }, }); } else if ( - headerAccess?.type === 'CallExpression' && - responseHeadersAccess.property.type === 'Identifier' && + headerAccess?.type === AST_NODE_TYPES.CallExpression && + responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && responseHeadersAccess.property.name === 'get' ) { const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; @@ -352,6 +354,6 @@ const rule: Rule.RuleModule = { }, }; }, -}; +}); export default rule; diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index 9a5a109..1b1c59e 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -1,8 +1,8 @@ // agent/fetch.ts -import type { Node } from 'estree'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; -import { getParent, isBlockStatement } from '../library/tree'; +import { getParent, isBlockStatement } from '../library/ts-tree'; export function getResponseBodyRetrievalText(responseVariableName: string) { return `await ${responseVariableName}.json()`; @@ -12,29 +12,29 @@ export function getResponseHeadersRetrievalText(responseVariableName: string) { return `${responseVariableName}.headers`; } -export function isInvalidResponseHeadersAccess(responseHeadersAccess: Node): boolean { +export function isInvalidResponseHeadersAccess(responseHeadersAccess: TSESTree.Node): boolean { const responseHeaderAccessParent = getParent(responseHeadersAccess); - if (responseHeaderAccessParent?.type === 'VariableDeclarator') { + if (responseHeaderAccessParent?.type === AST_NODE_TYPES.VariableDeclarator) { return false; } if ( - responseHeaderAccessParent?.type === 'CallExpression' && - responseHeaderAccessParent.callee.type === 'MemberExpression' && - responseHeaderAccessParent.callee.property.type === 'Identifier' && + responseHeaderAccessParent?.type === AST_NODE_TYPES.CallExpression && + responseHeaderAccessParent.callee.type === AST_NODE_TYPES.MemberExpression && + responseHeaderAccessParent.callee.property.type === AST_NODE_TYPES.Identifier && responseHeaderAccessParent.callee.property.name === 'get' ) { return true; } return !( - responseHeaderAccessParent?.type === 'MemberExpression' && - responseHeaderAccessParent.property.type === 'Identifier' && + responseHeaderAccessParent?.type === AST_NODE_TYPES.MemberExpression && + responseHeaderAccessParent.property.type === AST_NODE_TYPES.Identifier && responseHeaderAccessParent.property.name === 'get' ); } -export function hasAssertions(fixtureCall: Node): boolean { +export function hasAssertions(fixtureCall: TSESTree.Node): boolean { if (isBlockStatement(fixtureCall)) { return false; } @@ -45,10 +45,10 @@ export function hasAssertions(fixtureCall: Node): boolean { } if ( - parent.type === 'MemberExpression' && - parent.property.type === 'Identifier' && + parent.type === AST_NODE_TYPES.MemberExpression && + parent.property.type === AST_NODE_TYPES.Identifier && parent.property.name === 'expect' && - getParent(parent)?.type === 'CallExpression' + getParent(parent)?.type === AST_NODE_TYPES.CallExpression ) { return true; } diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 14d74ef..57c3927 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import createTester from '../tester.test'; +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-fixture'; createTester().run(ruleId, rule, { @@ -42,7 +42,7 @@ createTester().run(ruleId, rule, { }), ]); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'assertion with variable declaration', @@ -63,7 +63,7 @@ createTester().run(ruleId, rule, { const timeDifference = Date.now() - new Date(body.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assertion without variable declaration', @@ -78,7 +78,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.OK); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assertion without variable declaration', @@ -93,7 +93,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'PUT with request body', @@ -109,7 +109,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.BAD_REQUEST); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'PUT with request header', @@ -134,7 +134,30 @@ createTester().run(ruleId, rule, { }); assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'set request header with "!" (non-null assertion operator)', + code: ` + import { BASE_PATH } from './index'; + const noFraudResponse = await fixture.api + .post(\`\${BASE_PATH}/ping\`) + .set(IF_MATCH_HEADER, originalCard.version!) + .set('x-y-z', headers[ETAG]!) + .expect(StatusCodes.NO_CONTENT); + `, + output: ` + import { BASE_PATH } from './index'; + const noFraudResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'POST', + headers: { + [IF_MATCH_HEADER]: originalCard.version!, + 'x-y-z': headers[ETAG]!, + }, + }); + assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); + `, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'POST without request header/body', @@ -151,7 +174,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.NO_CONTENT); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'replace del with DELETE', @@ -168,7 +191,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.NO_CONTENT); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response headers assertion should be externalized with new variable declared if necessary', @@ -192,7 +215,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG), correctVersion); assert.ok(response.headers.get(ETAG).match(/1.*/u)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response body assertion', @@ -207,7 +230,7 @@ createTester().run(ruleId, rule, { }); assert.deepEqual(await response.json(), {message:'pong'}); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response callback assertion', @@ -225,7 +248,7 @@ createTester().run(ruleId, rule, { assert.doesNotThrow(()=>validate(response)); assert.doesNotThrow(()=>console.log(response)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'multiple fixture calls in the same test', @@ -256,7 +279,12 @@ createTester().run(ruleId, rule, { }); assert.equal(response3.status, StatusCodes.OK); `, - errors: 4, + errors: [ + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + ], }, { name: 'directly return (no await) fixture call', @@ -272,7 +300,7 @@ createTester().run(ruleId, rule, { method: 'GET', }); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'directly return (no await) fixture call with assertion', @@ -290,7 +318,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); return response; }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'directly return (no await) fixture call with body/headers', @@ -312,7 +340,7 @@ createTester().run(ruleId, rule, { }, }); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'replace statusCode with status', @@ -336,7 +364,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response2.status, StatusCodes.OK); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'replace header access through response.get() with response.headers.get()', @@ -355,7 +383,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', @@ -370,7 +398,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, 200); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', @@ -388,7 +416,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, 200); assert.deepEqual(await response.json(), validateBody(createdOn)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for body', @@ -406,7 +434,7 @@ createTester().run(ruleId, rule, { const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for body - with nested destructuring', @@ -422,7 +450,7 @@ createTester().run(ruleId, rule, { const { pgpPublicKey: firstPgpPublicKey } = await response.json(); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for headers when body is presented as well', @@ -441,7 +469,7 @@ createTester().run(ruleId, rule, { assert(body); assert.ok(headers2.get(ETAG)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for headers without body presented but with assertions used', @@ -457,7 +485,7 @@ createTester().run(ruleId, rule, { const headers = response.headers; assert.ok(headers.get(ETAG)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', @@ -471,7 +499,7 @@ createTester().run(ruleId, rule, { }); assert.ok(headers.get(ETAG)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'avoid response variable name conflict with existing variables in the same scope', @@ -497,7 +525,7 @@ createTester().run(ruleId, rule, { assert.equal(response3.status, StatusCodes.OK); } `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'response variable names in different scope do not conflict with each other', @@ -533,7 +561,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); }); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'inline access to response body should be extracted to a variable', @@ -556,7 +584,7 @@ createTester().run(ruleId, rule, { const paymentSecurityServicePublicKey = responseBody.publicKey; } `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', @@ -597,7 +625,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG_HEADER), '1'); assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'in arrow function without concurrent promises', @@ -618,7 +646,7 @@ createTester().run(ruleId, rule, { }, 600); }); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assignment statement instead of variable declaration used for subsequent fixture calls', @@ -636,7 +664,7 @@ createTester().run(ruleId, rule, { }); assert.equal(response.status, StatusCodes.OK); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'nested header destructuring', @@ -650,7 +678,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); const etag = response.headers.get('etag'); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'nested header destructuring - string literal key with renaming', @@ -665,7 +693,7 @@ createTester().run(ruleId, rule, { const createdOn = response.headers.get('created-on'); const updatedOn = response.headers.get('updated-on'); `, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'support setting headers using object literal', @@ -684,7 +712,7 @@ createTester().run(ruleId, rule, { }, }); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 757f03d..0698a3f 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -8,20 +8,9 @@ import { strict as assert } from 'node:assert'; -import type { - AwaitExpression, - CallExpression, - Expression, - ExpressionStatement, - MemberExpression, - Node, - ObjectExpression, - ObjectPattern, - ReturnStatement, - SimpleCallExpression, - VariableDeclaration, -} from 'estree'; -import { type Rule, type Scope, SourceCode } from 'eslint'; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; import { getEnclosingFunction, @@ -29,7 +18,7 @@ import { getEnclosingStatement, getParent, isUsedInArrayOrAsArgument, -} from '../library/tree'; +} from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; @@ -40,86 +29,91 @@ import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; interface FixtureCallInformation { - rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression | ExpressionStatement; - fixtureNode: AwaitExpression | SimpleCallExpression; - variableDeclaration?: VariableDeclaration; - variableAssignment?: ExpressionStatement; - requestBody?: Expression; - requestHeaders?: { name: Expression; value: Expression }[]; - requestHeadersObjectLiteral?: ObjectExpression; - assertions?: Expression[][]; - inlineStatementNode?: Node; - inlineBodyReference?: MemberExpression; - inlineHeadersReference?: MemberExpression; + rootNode: + | TSESTree.AwaitExpression + | TSESTree.ReturnStatement + | TSESTree.VariableDeclaration + | TSESTree.CallExpression + | TSESTree.ExpressionStatement; + fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; + variableDeclaration?: TSESTree.VariableDeclaration; + variableAssignment?: TSESTree.ExpressionStatement; + requestBody?: TSESTree.Expression; + requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; + requestHeadersObjectLiteral?: TSESTree.ObjectExpression; + assertions?: TSESTree.Expression[][]; + inlineStatementNode?: TSESTree.Node; + inlineBodyReference?: TSESTree.MemberExpression; + inlineHeadersReference?: TSESTree.MemberExpression; } // recursively analyze the fixture/supertest call chain to collect information of request/response // eslint-disable-next-line sonarjs/cognitive-complexity -function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { +function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); let nextCall; - if (parent.type === 'ReturnStatement') { + if (parent.type === AST_NODE_TYPES.ReturnStatement) { // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; } else if ( - parent.type === 'ArrayExpression' || - parent.type === 'CallExpression' || - parent.type === 'ArrowFunctionExpression' + parent.type === AST_NODE_TYPES.ArrayExpression || + parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.ArrowFunctionExpression ) { // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = call; - } else if (parent.type === 'AwaitExpression') { + } else if (parent.type === AST_NODE_TYPES.AwaitExpression) { results.fixtureNode = call; const enclosingStatement = getEnclosingStatement(parent); assert.ok(enclosingStatement); const awaitParent = getParent(parent); - if (awaitParent?.type === 'MemberExpression') { + if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) { results.rootNode = parent; results.inlineStatementNode = enclosingStatement; - if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') { + if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { results.inlineBodyReference = awaitParent; } if ( - awaitParent.property.type === 'Identifier' && + awaitParent.property.type === AST_NODE_TYPES.Identifier && (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') ) { results.inlineHeadersReference = awaitParent; } - } else if (enclosingStatement.type === 'VariableDeclaration') { + } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) { results.variableDeclaration = enclosingStatement; results.rootNode = enclosingStatement; } else if ( - enclosingStatement.type === 'ExpressionStatement' && - enclosingStatement.expression.type === 'AssignmentExpression' + enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement && + enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression ) { results.variableAssignment = enclosingStatement; results.rootNode = enclosingStatement; } else { results.rootNode = parent; } - } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { if (parent.property.name === 'expect') { // supertest assertions const assertionCall = getParent(parent); - assert.ok(assertionCall && assertionCall.type === 'CallExpression'); - results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; nextCall = assertionCall; } else if (parent.property.name === 'send') { // request body const sendRequestBodyCall = getParent(parent); - assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); - results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression); + results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression; nextCall = sendRequestBodyCall; } else if (parent.property.name === 'set') { // request headers const setRequestHeaderCall = getParent(parent); - assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); - const [arg1, arg2] = setRequestHeaderCall.arguments as [Expression, Expression]; - if (arg1.type === 'ObjectExpression') { + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression); + const [arg1, arg2] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression]; + if (arg1.type === AST_NODE_TYPES.ObjectExpression) { results.requestHeadersObjectLiteral = arg1; } else { results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }]; @@ -139,7 +133,7 @@ function createResponseAssertions( fixtureCallInformation: FixtureCallInformation, sourceCode: SourceCode, responseVariableName: string, - destructuringResponseHeadersVariable: Scope.Variable | undefined, + destructuringResponseHeadersVariable: Variable | undefined, ) { let statusAssertion: string | undefined; const nonStatusAssertions: string[] = []; @@ -148,20 +142,20 @@ function createResponseAssertions( const [assertionArgument] = expectArguments; assert.ok(assertionArgument); if ( - (assertionArgument.type === 'MemberExpression' && - assertionArgument.object.type === 'Identifier' && + (assertionArgument.type === AST_NODE_TYPES.MemberExpression && + assertionArgument.object.type === AST_NODE_TYPES.Identifier && assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === 'Literal' || - sourceCode.getText(assertionArgument).includes('StatusCodes.') + assertionArgument.type === AST_NODE_TYPES.Literal || + sourceCode.getText(assertionArgument as TSESTree.Node).includes('StatusCodes.') ) { // status code assertion statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; - } else if (assertionArgument.type === 'ArrowFunctionExpression') { + } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { // callback assertion using arrow function let functionBody = sourceCode.getText(assertionArgument.body); const [originalResponseArgument] = assertionArgument.params; - assert.ok(originalResponseArgument?.type === 'Identifier'); + assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); const originalResponseArgumentName = originalResponseArgument.name; if (originalResponseArgumentName !== responseVariableName) { functionBody = functionBody.replace( @@ -170,12 +164,15 @@ function createResponseAssertions( ); } nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); - } else if (assertionArgument.type === 'Identifier') { + } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { // callback assertion using function reference nonStatusAssertions.push( `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, ); - } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + } else if ( + assertionArgument.type === AST_NODE_TYPES.ObjectExpression || + assertionArgument.type === AST_NODE_TYPES.CallExpression + ) { // body deep equal assertion nonStatusAssertions.push( `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, @@ -191,7 +188,7 @@ function createResponseAssertions( destructuringResponseHeadersVariable !== undefined ? destructuringResponseHeadersVariable.name : `${responseVariableName}.headers`; - if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { nonStatusAssertions.push( `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, ); @@ -209,21 +206,22 @@ function createResponseAssertions( } function getResponseVariableNameToUse( - scopeManager: Scope.ScopeManager, + scopeManager: ScopeManager, fixtureCallInformation: FixtureCallInformation, - scopeVariablesMap: Map, + scopeVariablesMap: Map, ) { if (fixtureCallInformation.variableAssignment) { assert.ok( - fixtureCallInformation.variableAssignment.expression.type === 'AssignmentExpression' && - fixtureCallInformation.variableAssignment.expression.left.type === 'Identifier', + fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression && + fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier, ); return fixtureCallInformation.variableAssignment.expression.left.name; } if (fixtureCallInformation.variableDeclaration) { const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; - if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) { return firstDeclaration.id.name; } } @@ -252,12 +250,15 @@ function getResponseVariableNameToUse( return responseVariableNameToUse; } -function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean { +function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean { const parent = getParent(responseBodyReference); - return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier'; + return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier; } -const rule: Rule.RuleModule = { +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({ + name: ruleId, meta: { type: 'suggestion', docs: { @@ -271,16 +272,18 @@ const rule: Rule.RuleModule = { fixable: 'code', schema: [], }, + defaultOptions: [], // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - const scopeVariablesMap = new Map(); + assert.ok(scopeManager !== null); + const scopeVariablesMap = new Map(); return { // eslint-disable-next-line max-lines-per-function 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( - fixtureCall: CallExpression, + fixtureCall: TSESTree.CallExpression, // eslint-disable-next-line sonarjs/cognitive-complexity ) => { try { @@ -292,9 +295,8 @@ const rule: Rule.RuleModule = { return; } - assert.ok(fixtureCall.type === 'CallExpression'); const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get - assert.ok(fixtureFunction.type === 'MemberExpression'); + assert.ok(fixtureFunction.type === AST_NODE_TYPES.MemberExpression); const indentation = getIndentation(fixtureCall, sourceCode); const [urlArgumentNode] = fixtureCall.arguments; // e.g. `/sample-service/v1/ping` @@ -318,7 +320,7 @@ const rule: Rule.RuleModule = { // fetch request argument const methodNode = fixtureFunction.property; // get/put/etc. - assert.ok(methodNode.type === 'Identifier'); + assert.ok(methodNode.type === AST_NODE_TYPES.Identifier); const methodName = methodNode.name.toUpperCase(); const fetchRequestArgumentLines = [ @@ -336,7 +338,7 @@ const rule: Rule.RuleModule = { ...fixtureCallInformation.requestHeaders.map( ({ name, value }) => // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, no-nested-ternary, sonarjs/no-nested-template-literals - ` ${name.type === 'Literal' ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, + ` ${name.type === AST_NODE_TYPES.Literal ? (isValidPropertyName(name.value) ? name.value : `'${name.value}'`) : `[${sourceCode.getText(name)}]`}: ${sourceCode.getText(value)},`, ), ` },`, ] @@ -359,7 +361,7 @@ const rule: Rule.RuleModule = { const isResponseHeadersVariableRedefinitionNeeded = (destructuringResponseHeadersVariable !== undefined && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') || + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern) || fixtureCallInformation.inlineHeadersReference !== undefined; const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; @@ -376,7 +378,7 @@ const rule: Rule.RuleModule = { ...(destructuringResponseBodyVariable ? [ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, ] : isResponseBodyVariableRedefinitionNeeded ? [ @@ -386,15 +388,16 @@ const rule: Rule.RuleModule = { // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' - ? (destructuringResponseHeadersVariable as ObjectPattern).properties.map((property) => { - assert.ok(property.type === 'Property'); - assert.equal(property.value.type, 'Identifier'); + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === + AST_NODE_TYPES.ObjectPattern + ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => { + assert.ok(property.type === AST_NODE_TYPES.Property); + assert.equal(property.value.type, AST_NODE_TYPES.Identifier); // eslint-disable-next-line sonarjs/no-nested-template-literals - return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === 'Literal' ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; + return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; }) : [ - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, ] : isResponseHeadersVariableRedefinitionNeeded ? [ @@ -408,7 +411,7 @@ const rule: Rule.RuleModule = { fixtureCallInformation, sourceCode, responseVariableNameToUse, - destructuringResponseHeadersVariable as Scope.Variable | undefined, + destructuringResponseHeadersVariable as Variable | undefined, ); // add variable declaration if needed @@ -466,12 +469,12 @@ const rule: Rule.RuleModule = { const parent = getParent(responseHeadersReference); assert.ok(parent); let headerName; - if (parent.type === 'MemberExpression') { + if (parent.type === AST_NODE_TYPES.MemberExpression) { const headerNameNode = parent.property; headerName = parent.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; - } else if (parent.type === 'CallExpression') { + } else if (parent.type === AST_NODE_TYPES.CallExpression) { const headerNameNode = parent.arguments[0]; headerName = sourceCode.getText(headerNameNode); } @@ -482,7 +485,7 @@ const rule: Rule.RuleModule = { // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { if ( - responseStatusReference.property.type === 'Identifier' && + responseStatusReference.property.type === AST_NODE_TYPES.Identifier && responseStatusReference.property.name === 'statusCode' ) { yield fixer.replaceText(responseStatusReference.property, `status`); @@ -491,7 +494,7 @@ const rule: Rule.RuleModule = { // handle direct return statement without await, e.g. "return fixture.api.get(...);" if ( - fixtureCallInformation.rootNode.type === 'ReturnStatement' && + fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement && fixtureCallInformation.assertions !== undefined ) { yield fixer.insertTextAfter( @@ -516,6 +519,6 @@ const rule: Rule.RuleModule = { }, }; }, -}; +}); export default rule; diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts index b50ce38..a49e994 100644 --- a/src/agent/no-supertest.spec.ts +++ b/src/agent/no-supertest.spec.ts @@ -6,7 +6,7 @@ * This code is licensed under the MIT license (see LICENSE.txt for details). */ -import createTester from '../tester.test'; +import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-supertest'; createTester().run(ruleId, rule, { @@ -38,7 +38,7 @@ createTester().run(ruleId, rule, { const response = await ping(); assert.equal(response.status, StatusCodes.OK); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assertion with variable declaration', @@ -52,7 +52,7 @@ createTester().run(ruleId, rule, { const pingResponseBody = await pingResponse.json(); assert(pingResponseBody); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response headers assertion should be externalized with new variable declared if necessary', @@ -72,7 +72,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG), correctVersion); assert.ok(response.headers.get(ETAG).match(/1.*/u)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response body assertion', @@ -83,7 +83,7 @@ createTester().run(ruleId, rule, { const response = await ping(); assert.deepEqual(await response.json(), {message:'pong'}); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'response callback assertion', @@ -97,7 +97,7 @@ createTester().run(ruleId, rule, { assert.doesNotThrow(()=>validate(response)); assert.doesNotThrow(()=>console.log(response)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'multiple fixture calls in the same test', @@ -118,7 +118,12 @@ createTester().run(ruleId, rule, { const response3 = await ping(); assert.equal(response3.status, StatusCodes.OK); }`, - errors: 4, + errors: [ + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + ], }, { name: 'directly return (no await) fixture call with assertion', @@ -130,7 +135,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); return response; }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'replace header access through response.get() with response.headers.get()', @@ -145,7 +150,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG), correctVersion); assert.equal(response.headers.get('etag'), correctVersion); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', @@ -156,7 +161,7 @@ createTester().run(ruleId, rule, { const response = await ping(); assert.equal(response.status, 200); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', @@ -170,7 +175,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, 200); assert.deepEqual(await response.json(), validateBody(createdOn)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for body', @@ -186,7 +191,7 @@ createTester().run(ruleId, rule, { const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for body - with nested destructuring', @@ -200,7 +205,7 @@ createTester().run(ruleId, rule, { const { pgpPublicKey: firstPgpPublicKey } = await response.json(); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for headers when body is presented as well', @@ -217,7 +222,7 @@ createTester().run(ruleId, rule, { assert(body); assert.ok(headers2.get(ETAG)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'handle destructuring variable declaration for headers without body presented but with assertions used', @@ -231,7 +236,7 @@ createTester().run(ruleId, rule, { const headers = response.headers; assert.ok(headers.get(ETAG)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'avoid response variable name conflict with existing variables in the same scope', @@ -249,7 +254,7 @@ createTester().run(ruleId, rule, { const response3 = await ping(); assert.equal(response3.status, StatusCodes.OK); }`, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'response variable names in different scope do not conflict with each other', @@ -281,7 +286,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); }); `, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'inline access to response body should be extracted to a variable', @@ -298,7 +303,7 @@ createTester().run(ruleId, rule, { const responseBody = await response.json(); const paymentSecurityServicePublicKey = responseBody.publicKey; }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', @@ -324,7 +329,7 @@ createTester().run(ruleId, rule, { assert.equal(response.headers.get(ETAG_HEADER), '1'); assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assignment statement instead of variable declaration used for subsequent fixture calls', @@ -338,7 +343,7 @@ createTester().run(ruleId, rule, { response = await ping(); assert.equal(response.status, StatusCodes.OK); }`, - errors: 2, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, { name: 'nested header destructuring', @@ -350,7 +355,7 @@ createTester().run(ruleId, rule, { assert.equal(response.status, StatusCodes.OK); const etag = response.headers.get('etag'); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'nested header destructuring - string literal key with renaming', @@ -363,7 +368,7 @@ createTester().run(ruleId, rule, { const createdOn = response.headers.get('created-on'); const updatedOn = response.headers.get('updated-on'); }`, - errors: 1, + errors: [{ messageId: 'preferNativeFetch' }], }, ], }); diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts index 90f05fd..a7a6400 100644 --- a/src/agent/no-supertest.ts +++ b/src/agent/no-supertest.ts @@ -8,20 +8,9 @@ import { strict as assert } from 'node:assert'; -import type { - AwaitExpression, - CallExpression, - Expression, - ExpressionStatement, - MemberExpression, - Node, - ObjectExpression, - ObjectPattern, - ReturnStatement, - SimpleCallExpression, - VariableDeclaration, -} from 'estree'; -import { type Rule, type Scope, SourceCode } from 'eslint'; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; import { getEnclosingFunction, @@ -29,7 +18,7 @@ import { getEnclosingStatement, getParent, isUsedInArrayOrAsArgument, -} from '../library/tree'; +} from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { analyzeResponseReferences } from './response-reference'; @@ -38,86 +27,91 @@ import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText } from '. export const ruleId = 'no-supertest'; interface FixtureCallInformation { - rootNode: AwaitExpression | ReturnStatement | VariableDeclaration | SimpleCallExpression | ExpressionStatement; - fixtureNode: AwaitExpression | SimpleCallExpression; - variableDeclaration?: VariableDeclaration; - variableAssignment?: ExpressionStatement; - requestBody?: Expression; - requestHeaders?: { name: Expression; value: Expression }[]; - requestHeadersObjectLiteral?: ObjectExpression; - assertions?: Expression[][]; - inlineStatementNode?: Node; - inlineBodyReference?: MemberExpression; - inlineHeadersReference?: MemberExpression; + rootNode: + | TSESTree.AwaitExpression + | TSESTree.ReturnStatement + | TSESTree.VariableDeclaration + | TSESTree.CallExpression + | TSESTree.ExpressionStatement; + fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; + variableDeclaration?: TSESTree.VariableDeclaration; + variableAssignment?: TSESTree.ExpressionStatement; + requestBody?: TSESTree.Expression; + requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; + requestHeadersObjectLiteral?: TSESTree.ObjectExpression; + assertions?: TSESTree.Expression[][]; + inlineStatementNode?: TSESTree.Node; + inlineBodyReference?: TSESTree.MemberExpression; + inlineHeadersReference?: TSESTree.MemberExpression; } // recursively analyze the fixture/supertest call chain to collect information of request/response // eslint-disable-next-line sonarjs/cognitive-complexity -function analyzeFixtureCall(call: SimpleCallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { +function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); let nextCall; - if (parent.type === 'ReturnStatement') { + if (parent.type === AST_NODE_TYPES.ReturnStatement) { // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = parent; } else if ( - parent.type === 'ArrayExpression' || - parent.type === 'CallExpression' || - parent.type === 'ArrowFunctionExpression' + parent.type === AST_NODE_TYPES.ArrayExpression || + parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.ArrowFunctionExpression ) { // direct return, no variable declaration or await results.fixtureNode = call; results.rootNode = call; - } else if (parent.type === 'AwaitExpression') { + } else if (parent.type === AST_NODE_TYPES.AwaitExpression) { results.fixtureNode = call; const enclosingStatement = getEnclosingStatement(parent); assert.ok(enclosingStatement); const awaitParent = getParent(parent); - if (awaitParent?.type === 'MemberExpression') { + if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) { results.rootNode = parent; results.inlineStatementNode = enclosingStatement; - if (awaitParent.property.type === 'Identifier' && awaitParent.property.name === 'body') { + if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { results.inlineBodyReference = awaitParent; } if ( - awaitParent.property.type === 'Identifier' && + awaitParent.property.type === AST_NODE_TYPES.Identifier && (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') ) { results.inlineHeadersReference = awaitParent; } - } else if (enclosingStatement.type === 'VariableDeclaration') { + } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) { results.variableDeclaration = enclosingStatement; results.rootNode = enclosingStatement; } else if ( - enclosingStatement.type === 'ExpressionStatement' && - enclosingStatement.expression.type === 'AssignmentExpression' + enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement && + enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression ) { results.variableAssignment = enclosingStatement; results.rootNode = enclosingStatement; } else { results.rootNode = parent; } - } else if (parent.type === 'MemberExpression' && parent.property.type === 'Identifier') { + } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { if (parent.property.name === 'expect') { // supertest assertions const assertionCall = getParent(parent); - assert.ok(assertionCall && assertionCall.type === 'CallExpression'); - results.assertions = [...(results.assertions ?? []), assertionCall.arguments as Expression[]]; + assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; nextCall = assertionCall; } else if (parent.property.name === 'send') { // request body const sendRequestBodyCall = getParent(parent); - assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === 'CallExpression'); - results.requestBody = sendRequestBodyCall.arguments[0] as Expression; + assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression); + results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression; nextCall = sendRequestBodyCall; } else if (parent.property.name === 'set') { // request headers const setRequestHeaderCall = getParent(parent); - assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === 'CallExpression'); - const [arg1, arg2] = setRequestHeaderCall.arguments as [Expression, Expression]; - if (arg1.type === 'ObjectExpression') { + assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression); + const [arg1, arg2] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression]; + if (arg1.type === AST_NODE_TYPES.ObjectExpression) { results.requestHeadersObjectLiteral = arg1; } else { results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }]; @@ -137,7 +131,7 @@ function createResponseAssertions( fixtureCallInformation: FixtureCallInformation, sourceCode: SourceCode, responseVariableName: string, - destructuringResponseHeadersVariable: Scope.Variable | undefined, + destructuringResponseHeadersVariable: Variable | undefined, ) { let statusAssertion: string | undefined; const nonStatusAssertions: string[] = []; @@ -146,20 +140,20 @@ function createResponseAssertions( const [assertionArgument] = expectArguments; assert.ok(assertionArgument); if ( - (assertionArgument.type === 'MemberExpression' && - assertionArgument.object.type === 'Identifier' && + (assertionArgument.type === AST_NODE_TYPES.MemberExpression && + assertionArgument.object.type === AST_NODE_TYPES.Identifier && assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === 'Literal' || + assertionArgument.type === AST_NODE_TYPES.Literal || sourceCode.getText(assertionArgument).includes('StatusCodes.') ) { // status code assertion statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; - } else if (assertionArgument.type === 'ArrowFunctionExpression') { + } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { // callback assertion using arrow function let functionBody = sourceCode.getText(assertionArgument.body); const [originalResponseArgument] = assertionArgument.params; - assert.ok(originalResponseArgument?.type === 'Identifier'); + assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); const originalResponseArgumentName = originalResponseArgument.name; if (originalResponseArgumentName !== responseVariableName) { functionBody = functionBody.replace( @@ -168,12 +162,15 @@ function createResponseAssertions( ); } nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); - } else if (assertionArgument.type === 'Identifier') { + } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { // callback assertion using function reference nonStatusAssertions.push( `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, ); - } else if (assertionArgument.type === 'ObjectExpression' || assertionArgument.type === 'CallExpression') { + } else if ( + assertionArgument.type === AST_NODE_TYPES.ObjectExpression || + assertionArgument.type === AST_NODE_TYPES.CallExpression + ) { // body deep equal assertion nonStatusAssertions.push( `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, @@ -189,7 +186,7 @@ function createResponseAssertions( destructuringResponseHeadersVariable !== undefined ? destructuringResponseHeadersVariable.name : `${responseVariableName}.headers`; - if (headerValue.type === 'Literal' && headerValue.value instanceof RegExp) { + if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { nonStatusAssertions.push( `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, ); @@ -207,21 +204,22 @@ function createResponseAssertions( } function getResponseVariableNameToUse( - scopeManager: Scope.ScopeManager, + scopeManager: ScopeManager, fixtureCallInformation: FixtureCallInformation, - scopeVariablesMap: Map, + scopeVariablesMap: Map, ) { if (fixtureCallInformation.variableAssignment) { assert.ok( - fixtureCallInformation.variableAssignment.expression.type === 'AssignmentExpression' && - fixtureCallInformation.variableAssignment.expression.left.type === 'Identifier', + fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression && + fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier, ); return fixtureCallInformation.variableAssignment.expression.left.name; } if (fixtureCallInformation.variableDeclaration) { const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; - if (firstDeclaration && firstDeclaration.id.type === 'Identifier') { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) { return firstDeclaration.id.name; } } @@ -250,12 +248,15 @@ function getResponseVariableNameToUse( return responseVariableNameToUse; } -function isResponseBodyRedefinition(responseBodyReference: MemberExpression): boolean { +function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean { const parent = getParent(responseBodyReference); - return parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier'; + return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier; } -const rule: Rule.RuleModule = { +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({ + name: ruleId, meta: { type: 'suggestion', docs: { @@ -269,23 +270,25 @@ const rule: Rule.RuleModule = { fixable: 'code', schema: [], }, + defaultOptions: [], // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - const scopeVariablesMap = new Map(); + assert.ok(scopeManager !== null); + const scopeVariablesMap = new Map(); return { // eslint-disable-next-line max-lines-per-function 'CallExpression[callee.property.name="expect"]': ( - supertestCall: CallExpression, + supertestCall: TSESTree.CallExpression, // eslint-disable-next-line sonarjs/cognitive-complexity ) => { - assert.ok(supertestCall.callee.type === 'MemberExpression'); + assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression); if ( - supertestCall.callee.object.type === 'CallExpression' && - supertestCall.callee.object.callee.type === 'MemberExpression' && - supertestCall.callee.object.callee.property.type === 'Identifier' && + supertestCall.callee.object.type === AST_NODE_TYPES.CallExpression && + supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && + supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && supertestCall.callee.object.callee.property.name === 'expect' ) { // skip nested expect calls, only focus on the top level @@ -297,9 +300,8 @@ const rule: Rule.RuleModule = { return; } - assert.ok(supertestCall.type === 'CallExpression'); const fixtureFunction = supertestCall.callee.object; - if (fixtureFunction.type !== 'CallExpression') { + if (fixtureFunction.type !== AST_NODE_TYPES.CallExpression) { return; } @@ -335,7 +337,7 @@ const rule: Rule.RuleModule = { const isResponseHeadersVariableRedefinitionNeeded = (destructuringResponseHeadersVariable !== undefined && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern') || + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern) || fixtureCallInformation.inlineHeadersReference !== undefined; const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; @@ -352,7 +354,7 @@ const rule: Rule.RuleModule = { ...(destructuringResponseBodyVariable ? [ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as ObjectPattern).type === 'ObjectPattern' ? sourceCode.getText(destructuringResponseBodyVariable as ObjectPattern) : (destructuringResponseBodyVariable as Scope.Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, ] : isResponseBodyVariableRedefinitionNeeded ? [ @@ -362,15 +364,16 @@ const rule: Rule.RuleModule = { // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as ObjectPattern).type === 'ObjectPattern' - ? (destructuringResponseHeadersVariable as ObjectPattern).properties.map((property) => { - assert.ok(property.type === 'Property'); - assert.equal(property.value.type, 'Identifier'); + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === + AST_NODE_TYPES.ObjectPattern + ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => { + assert.ok(property.type === AST_NODE_TYPES.Property); + assert.ok(property.value.type === AST_NODE_TYPES.Identifier); // eslint-disable-next-line sonarjs/no-nested-template-literals - return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === 'Literal' ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; + return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; }) : [ - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Scope.Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, ] : isResponseHeadersVariableRedefinitionNeeded ? [ @@ -384,7 +387,7 @@ const rule: Rule.RuleModule = { fixtureCallInformation, sourceCode, responseVariableNameToUse, - destructuringResponseHeadersVariable as Scope.Variable | undefined, + destructuringResponseHeadersVariable as Variable | undefined, ); // add variable declaration if needed @@ -442,12 +445,12 @@ const rule: Rule.RuleModule = { const parent = getParent(responseHeadersReference); assert.ok(parent); let headerName; - if (parent.type === 'MemberExpression') { + if (parent.type === AST_NODE_TYPES.MemberExpression) { const headerNameNode = parent.property; headerName = parent.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; - } else if (parent.type === 'CallExpression') { + } else if (parent.type === AST_NODE_TYPES.CallExpression) { const headerNameNode = parent.arguments[0]; headerName = sourceCode.getText(headerNameNode); } @@ -458,7 +461,7 @@ const rule: Rule.RuleModule = { // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { if ( - responseStatusReference.property.type === 'Identifier' && + responseStatusReference.property.type === AST_NODE_TYPES.Identifier && responseStatusReference.property.name === 'statusCode' ) { yield fixer.replaceText(responseStatusReference.property, `status`); @@ -467,7 +470,7 @@ const rule: Rule.RuleModule = { // handle direct return statement without await, e.g. "return fixture.api.get(...);" if ( - fixtureCallInformation.rootNode.type === 'ReturnStatement' && + fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement && fixtureCallInformation.assertions !== undefined ) { yield fixer.insertTextAfter( @@ -492,6 +495,6 @@ const rule: Rule.RuleModule = { }, }; }, -}; +}); export default rule; diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index 7c0cd3c..f8d3c45 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -7,11 +7,12 @@ */ import { strict as assert } from 'node:assert'; -import type { MemberExpression, ObjectPattern, VariableDeclaration } from 'estree'; -import { type Scope } from 'eslint'; + import debug from 'debug'; -import { getParent } from '../library/tree'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import type { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import { getParent } from '../library/ts-tree'; const log = debug('eslint-plugin:response-reference'); @@ -21,25 +22,25 @@ const log = debug('eslint-plugin:response-reference'); * @param variableDeclaration - variable declaration node */ export function analyzeResponseReferences( - variableDeclaration: VariableDeclaration | undefined, - scopeManager: Scope.ScopeManager, + variableDeclaration: TSESTree.VariableDeclaration | undefined, + scopeManager: ScopeManager, ): { - variable?: Scope.Variable; - bodyReferences: MemberExpression[]; - headersReferences: MemberExpression[]; - statusReferences: MemberExpression[]; - destructuringBodyVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersReferences?: MemberExpression[] | undefined; + variable?: Variable; + bodyReferences: TSESTree.MemberExpression[]; + headersReferences: TSESTree.MemberExpression[]; + statusReferences: TSESTree.MemberExpression[]; + destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; + destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; + destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined; } { const results: { - variable?: Scope.Variable; - bodyReferences: MemberExpression[]; - headersReferences: MemberExpression[]; - statusReferences: MemberExpression[]; - destructuringBodyVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersVariable?: Scope.Variable | ObjectPattern; - destructuringHeadersReferences?: MemberExpression[] | undefined; + variable?: Variable; + bodyReferences: TSESTree.MemberExpression[]; + headersReferences: TSESTree.MemberExpression[]; + statusReferences: TSESTree.MemberExpression[]; + destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; + destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; + destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined; } = { bodyReferences: [], headersReferences: [], @@ -55,7 +56,7 @@ export function analyzeResponseReferences( assert.ok(identifier); const identifierParent = getParent(identifier); assert.ok(identifierParent); - if (identifierParent.type === 'VariableDeclarator') { + if (identifierParent.type === AST_NODE_TYPES.VariableDeclarator) { // e.g. const response = ... results.variable = responseVariable; const responseReferences = responseVariable.references.map((responseReference) => @@ -63,34 +64,36 @@ export function analyzeResponseReferences( ); // e.g. response.body results.bodyReferences = responseReferences.filter( - (node): node is MemberExpression => - node?.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'body', + (node): node is TSESTree.MemberExpression => + node?.type === AST_NODE_TYPES.MemberExpression && + node.property.type === AST_NODE_TYPES.Identifier && + node.property.name === 'body', ); // e.g. response.headers / response.header / response.get() results.headersReferences = responseReferences.filter( - (node): node is MemberExpression => - node?.type === 'MemberExpression' && - node.property.type === 'Identifier' && + (node): node is TSESTree.MemberExpression => + node?.type === AST_NODE_TYPES.MemberExpression && + node.property.type === AST_NODE_TYPES.Identifier && (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), ); // e.g. response.status / response.statusCode results.statusReferences = responseReferences.filter( - (node): node is MemberExpression => - node?.type === 'MemberExpression' && - node.property.type === 'Identifier' && + (node): node is TSESTree.MemberExpression => + node?.type === AST_NODE_TYPES.MemberExpression && + node.property.type === AST_NODE_TYPES.Identifier && (node.property.name === 'status' || node.property.name === 'statusCode'), ); } else if ( // body reference through destruction/renaming, e.g. "const { body } = ..." - identifierParent.type === 'Property' && - identifierParent.key.type === 'Identifier' && + identifierParent.type === AST_NODE_TYPES.Property && + identifierParent.key.type === AST_NODE_TYPES.Identifier && identifierParent.key.name === 'body' ) { results.destructuringBodyVariable = responseVariable; } else if ( // header reference through destruction/renaming, e.g. "const { headers } = ..." - identifierParent.type === 'Property' && - identifierParent.key.type === 'Identifier' && + identifierParent.type === AST_NODE_TYPES.Property && + identifierParent.key.type === AST_NODE_TYPES.Identifier && identifierParent.key.name === 'headers' ) { results.destructuringHeadersVariable = responseVariable; @@ -98,23 +101,27 @@ export function analyzeResponseReferences( .map((reference) => reference.identifier) .map(getParent) .filter( - (parent): parent is MemberExpression => - parent?.type === 'MemberExpression' && - parent.property.type === 'Identifier' && + (parent): parent is TSESTree.MemberExpression => + parent?.type === AST_NODE_TYPES.MemberExpression && + parent.property.type === AST_NODE_TYPES.Identifier && parent.property.name !== 'get' && - getParent(parent)?.type !== 'CallExpression', + getParent(parent)?.type !== AST_NODE_TYPES.CallExpression, ); - } else if (identifierParent.type === 'Property') { + } else if (identifierParent.type === AST_NODE_TYPES.Property) { const parent = getParent(identifierParent); - if (parent?.type === 'ObjectPattern') { + if (parent?.type === AST_NODE_TYPES.ObjectPattern) { // body reference through nested destruction, e.g. "const { body: {bodyPropertyName: renamedBodyPropertyName}, headers: {headerPropertyName: renamedHeaderPropertyName} } = ..." const parent2 = getParent(parent); - if (parent2?.type === 'Property' && parent2.key.type === 'Identifier' && parent2.key.name === 'body') { + if ( + parent2?.type === AST_NODE_TYPES.Property && + parent2.key.type === AST_NODE_TYPES.Identifier && + parent2.key.name === 'body' + ) { results.destructuringBodyVariable = parent; } if ( - parent2?.type === 'Property' && - parent2.key.type === 'Identifier' && + parent2?.type === AST_NODE_TYPES.Property && + parent2.key.type === AST_NODE_TYPES.Identifier && (parent2.key.name === 'header' || parent2.key.name === 'headers') ) { results.destructuringHeadersVariable = parent; From 47d65b063ba5e511229f8415969efd8f38245266 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 22 Nov 2024 16:56:47 -0500 Subject: [PATCH 109/115] new rule supertest-then --- src/agent/fetch-then.spec.ts | 2 +- src/agent/no-supertest.ts | 27 ++- src/agent/supertest-then.spec.ts | 129 +++++++++++++ src/agent/supertest-then.ts | 322 +++++++++++++++++++++++++++++++ src/index.ts | 5 + 5 files changed, 468 insertions(+), 17 deletions(-) create mode 100644 src/agent/supertest-then.spec.ts create mode 100644 src/agent/supertest-then.ts diff --git a/src/agent/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts index 15a7afa..dc0726a 100644 --- a/src/agent/fetch-then.spec.ts +++ b/src/agent/fetch-then.spec.ts @@ -1,4 +1,4 @@ -// agent/concurrent-promises.spec.ts +// agent/fetch-then.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts index a7a6400..1100adf 100644 --- a/src/agent/no-supertest.ts +++ b/src/agent/no-supertest.ts @@ -284,33 +284,28 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat supertestCall: TSESTree.CallExpression, // eslint-disable-next-line sonarjs/cognitive-complexity ) => { - assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression); - if ( - supertestCall.callee.object.type === AST_NODE_TYPES.CallExpression && - supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && - supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && - supertestCall.callee.object.callee.property.name === 'expect' - ) { - // skip nested expect calls, only focus on the top level - return; - } try { - if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) { - // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here + assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression); + if ( + supertestCall.callee.object.type !== AST_NODE_TYPES.CallExpression || + (supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && + supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && + supertestCall.callee.object.callee.property.name === 'expect') + ) { + // skip nested expect calls, only focus on the top level return; } - const fixtureFunction = supertestCall.callee.object; - if (fixtureFunction.type !== AST_NODE_TYPES.CallExpression) { + if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) { + // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here return; } const indentation = getIndentation(supertestCall, sourceCode); const fixtureCallInformation = {} as FixtureCallInformation; + const fixtureFunction = supertestCall.callee.object; analyzeFixtureCall(fixtureFunction, fixtureCallInformation, sourceCode); - sourceCode.getText(fixtureCallInformation.fixtureNode); - sourceCode.getText(fixtureCallInformation.rootNode); fixtureCallInformation.assertions?.flat().map((ass) => sourceCode.getText(ass)); const { diff --git a/src/agent/supertest-then.spec.ts b/src/agent/supertest-then.spec.ts new file mode 100644 index 0000000..199416b --- /dev/null +++ b/src/agent/supertest-then.spec.ts @@ -0,0 +1,129 @@ +// agent/supertest-then.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './supertest-then'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'skip regular supertest calls which will be handled in "no-supertest" rule', + code: ` + const pingResponse = ping().expect(StatusCodes.OK); + const body = pingResponse.body; + const timeDifference = Date.now() - new Date(body.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + `, + }, + ], + invalid: [ + { + name: 'with assertions', + code: ` + const responses = await Promise.all([ + ping().expect(StatusCodes.NO_CONTENT), + ping().expect(StatusCodes.NO_CONTENT), + ]); + `, + output: ` + const responses = await Promise.all([ + // eslint-disable-next-line @checkdigit/no-promise-instance-method + ping().then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + // eslint-disable-next-line @checkdigit/no-promise-instance-method + ping().then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + ]); + `, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], + }, + // { + // name: 'adjust header access correctly', + // code: ` + // const responses = await Promise.all([ + // ping().expect(StatusCodes.NO_CONTENT), + // ping().expect(StatusCodes.NO_CONTENT), + // ]); + // assert.deepEqual(responses.map((response) => response.headers.etag).sort(), ['1', '1']); + // assert.equal(responses[0].headers[LAST_MODIFIED_HEADER], responses[1].headers[LAST_MODIFIED_HEADER]); + // assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); + // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + // `, + // output: ` + // const responses = await Promise.all([ + // // eslint-disable-next-line @checkdigit/no-promise-instance-method + // ping().then((res) => { + // assert.equal(res.status, StatusCodes.NO_CONTENT); + // return res; + // }), + // // eslint-disable-next-line @checkdigit/no-promise-instance-method + // ping().then((res) => { + // assert.equal(res.status, StatusCodes.NO_CONTENT); + // return res; + // }), + // ]); + // assert.deepEqual(responses.map((response) => response.headers.get('etag')).sort(), ['1', '1']); + // assert.equal(responses[0].headers.get(LAST_MODIFIED_HEADER), responses[1].headers.get(LAST_MODIFIED_HEADER)); + // assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); + // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + // `, + // errors: [ + // { messageId: 'preferNativeFetch' }, + // { messageId: 'preferNativeFetch' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // ], + // }, + { + name: 'in non-async arrow function with concurrent promises', + code: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + ping().expect(StatusCodes.BAD_REQUEST) + ); + }), + ); + `, + output: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + // eslint-disable-next-line @checkdigit/no-promise-instance-method + ping().then((res) => { + assert.equal(res.status, StatusCodes.BAD_REQUEST); + return res; + }) + ); + }), + ); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + ], +}); diff --git a/src/agent/supertest-then.ts b/src/agent/supertest-then.ts new file mode 100644 index 0000000..02adda9 --- /dev/null +++ b/src/agent/supertest-then.ts @@ -0,0 +1,322 @@ +// agent/supertest-then.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +// import { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; + +import { getEnclosingFunction, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../library/format'; +// import { isInvalidResponseHeadersAccess } from './fetch'; + +export const ruleId = 'supertest-then'; + +interface FixtureCallInformation { + fixtureNode: TSESTree.CallExpression; + requestBody?: TSESTree.Expression; + requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; + assertions?: TSESTree.Expression[][]; +} + +// recursively analyze the fixture/supertest call chain to collect information of request/response +function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { + const parent = getParent(call); + if (!parent) { + return; + } + + let nextCall; + if (parent.type !== AST_NODE_TYPES.MemberExpression) { + results.fixtureNode = call; + return; + } + + if (parent.property.type === AST_NODE_TYPES.Identifier) { + if (parent.property.name === 'expect') { + // supertest assertions + const assertionCall = getParent(parent); + assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; + nextCall = assertionCall; + } + } else { + throw new Error(`Unexpected TSESTree.Expression in fixture/supertest call ${sourceCode.getText(parent)}.`); + } + if (nextCall) { + analyzeFixtureCall(nextCall, results, sourceCode); + } +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function createResponseAssertions( + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + responseVariableName: string, +) { + let statusAssertion: string | undefined; + const nonStatusAssertions: string[] = []; + for (const expectArguments of fixtureCallInformation.assertions ?? []) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + if ( + (assertionArgument.type === AST_NODE_TYPES.MemberExpression && + assertionArgument.object.type === AST_NODE_TYPES.Identifier && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === AST_NODE_TYPES.Literal || + sourceCode.getText(assertionArgument).includes('StatusCodes.') + ) { + // status code assertion + statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; + } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { + // callback assertion using arrow function + let functionBody = sourceCode.getText(assertionArgument.body); + + const [originalResponseArgument] = assertionArgument.params; + assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); + const originalResponseArgumentName = originalResponseArgument.name; + if (originalResponseArgumentName !== responseVariableName) { + functionBody = functionBody.replace( + new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), + responseVariableName, + ); + } + nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); + } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { + // callback assertion using function reference + nonStatusAssertions.push( + `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, + ); + } else if ( + assertionArgument.type === AST_NODE_TYPES.ObjectExpression || + assertionArgument.type === AST_NODE_TYPES.CallExpression + ) { + // body deep equal assertion + nonStatusAssertions.push( + `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, + ); + } else { + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); + } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + const headersReference = `${responseVariableName}.headers`; + if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { + nonStatusAssertions.push( + `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + nonStatusAssertions.push( + `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } + } + } + return { + statusAssertion, + nonStatusAssertions, + }; +} + +// function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) { +// const responseHeadersAccesses: TSESTree.MemberExpression[] = []; +// for (const responseVariable of responseVariables) { +// for (const responseReference of responseVariable.references) { +// const responseAccess = getParent(responseReference.identifier); +// if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) { +// continue; +// } + +// const responseAccessParent = getParent(responseAccess); +// if (!responseAccessParent) { +// continue; +// } + +// if ( +// responseAccessParent.type === AST_NODE_TYPES.CallExpression && +// responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression +// ) { +// // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) +// responseHeadersAccesses.push( +// ...getResponseHeadersAccesses( +// scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]), +// scopeManager, +// sourceCode, +// ), +// ); +// continue; +// } + +// if ( +// responseAccess.computed && +// responseAccess.property.type === AST_NODE_TYPES.Literal && +// responseAccessParent.type === AST_NODE_TYPES.MemberExpression +// ) { +// // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. +// responseHeadersAccesses.push(responseAccessParent); +// } else { +// responseHeadersAccesses.push(responseAccess); +// } +// } +// } +// return responseHeadersAccesses; +// } + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); +const rule: ESLintUtils.RuleModule< + 'unknownError' | 'preferNativeFetch' + // | 'shouldUseHeaderGetter' +> = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer native fetch API over customized fixture API.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Prefer native fetch API over customized fixture API.', + // shouldUseHeaderGetter: 'Getter should be used to access response headers.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + const scopeManager = sourceCode.scopeManager; + assert.ok(scopeManager); + + return { + 'CallExpression[callee.property.name="expect"]': (supertestCall: TSESTree.CallExpression) => { + try { + assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression); + if ( + supertestCall.callee.object.type !== AST_NODE_TYPES.CallExpression || + (supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && + supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && + supertestCall.callee.object.callee.property.name === 'expect') + ) { + // skip nested expect calls, only focus on the top level + return; + } + + if (!(isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false)) { + return; + } + + const fixtureFunction = supertestCall.callee.object; + const indentation = getIndentation(supertestCall, sourceCode); + + const [urlArgumentNode] = supertestCall.arguments; // e.g. `/sample-service/v1/ping` + assert.ok(urlArgumentNode !== undefined); + + const fixtureCallInformation = {} as FixtureCallInformation; + analyzeFixtureCall(fixtureFunction, fixtureCallInformation, sourceCode); + + const responseVariableNameToUse = 'res'; + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + ); + + // add variable declaration if needed + const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method'; + const fetchCallText = sourceCode.getText(fixtureFunction); + const appendingAssignmentAndAssertionText = [ + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + const replacementText = fixtureCallInformation.assertions + ? [ + disableLintComment, + `${fetchCallText}.then((${responseVariableNameToUse}) => {`, + appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, + ` return ${responseVariableNameToUse};`, + `})`, + ].join(`\n${indentation}`) + : fetchCallText; + + context.report({ + node: supertestCall, + messageId: 'preferNativeFetch', + fix(fixer) { + return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); + }, + }); + + // const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); + // if (!responsesVariable) { + // return; + // } + + // const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); + // const responseHeadersAccesses = getResponseHeadersAccesses( + // responseVariableReferences, + // scopeManager, + // sourceCode, + // ); + // for (const responseHeadersAccess of responseHeadersAccesses) { + // if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { + // const headerAccess = getParent(responseHeadersAccess); + // if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) { + // const headerNameNode = headerAccess.property; + // const headerName = headerAccess.computed + // ? sourceCode.getText(headerNameNode) + // : `'${sourceCode.getText(headerNameNode)}'`; + // const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; + + // context.report({ + // node: headerAccess, + // messageId: 'shouldUseHeaderGetter', + // fix(fixer) { + // return fixer.replaceText(headerAccess, headerAccessReplacementText); + // }, + // }); + // } else if ( + // headerAccess?.type === AST_NODE_TYPES.CallExpression && + // responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && + // responseHeadersAccess.property.name === 'get' + // ) { + // const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; + + // context.report({ + // node: headerAccess, + // messageId: 'shouldUseHeaderGetter', + // fix(fixer) { + // return fixer.replaceText(headerAccess, headerAccessReplacementText); + // }, + // }); + // } + // } + // } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: supertestCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/index.ts b/src/index.ts index b6c1ef7..5465071 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './in import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; import noSupertest, { ruleId as noSupertestRuleId } from './agent/no-supertest'; +import supertestThen, { ruleId as supertestThenRuleId } from './agent/supertest-then'; import noLegacyServiceTyping, { ruleId as noLegacyServiceTypingRuleId } from './no-legacy-service-typing'; import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; @@ -74,6 +75,7 @@ const rules: Record = { [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, [noSupertestRuleId]: noSupertest, + [supertestThenRuleId]: supertestThen, [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, @@ -134,6 +136,7 @@ const configs: Record = { [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', [`@checkdigit/${noSupertestRuleId}`]: 'off', + [`@checkdigit/${supertestThenRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', @@ -183,6 +186,7 @@ const configs: Record = { [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', [`@checkdigit/${noSupertestRuleId}`]: 'off', + [`@checkdigit/${supertestThenRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', @@ -227,6 +231,7 @@ const configs: Record = { [`@checkdigit/${addAssertImportRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', [`@checkdigit/${noSupertestRuleId}`]: 'error', + [`@checkdigit/${supertestThenRuleId}`]: 'error', }, }, { From 81ab40d7f4d6615ba9de325d1a91bc14237cb853 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 22 Nov 2024 16:59:43 -0500 Subject: [PATCH 110/115] chore: comment out Dependency Review step in publish-beta workflow --- .github/workflows/publish-beta.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-beta.yml b/.github/workflows/publish-beta.yml index 86a1c6a..a69ed85 100644 --- a/.github/workflows/publish-beta.yml +++ b/.github/workflows/publish-beta.yml @@ -14,11 +14,11 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - - name: Dependency Review - uses: actions/dependency-review-action@v4 - with: - allow-licenses: ${{ vars.ALLOW_LICENSES }} - allow-dependencies-licenses: pkg:npm/flatted + # - name: Dependency Review + # uses: actions/dependency-review-action@v4 + # with: + # allow-licenses: ${{ vars.ALLOW_LICENSES }} + # allow-dependencies-licenses: pkg:npm/flatted - name: Setup Node.js uses: actions/setup-node@v4 with: From d2e05c049907d1033a0ed970905b43d6749387fc Mon Sep 17 00:00:00 2001 From: Le Cong Date: Fri, 22 Nov 2024 23:53:07 -0500 Subject: [PATCH 111/115] feat: add response status retrieval function and update tests for statusCode destructuring --- src/agent/fetch.ts | 4 ++ src/agent/no-fixture.spec.ts | 28 ++++++++++ src/agent/no-fixture.ts | 32 ++++++++++- src/agent/no-supertest.spec.ts | 42 +++++++++++---- src/agent/no-supertest.ts | 27 +++++++++- src/agent/response-reference.ts | 17 ++++++ src/agent/supertest-then.spec.ts | 45 ---------------- src/agent/supertest-then.ts | 92 -------------------------------- 8 files changed, 139 insertions(+), 148 deletions(-) diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index 1b1c59e..a59f90d 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -8,6 +8,10 @@ export function getResponseBodyRetrievalText(responseVariableName: string) { return `await ${responseVariableName}.json()`; } +export function getResponseStatusRetrievalText(responseVariableName: string) { + return `${responseVariableName}.status`; +} + export function getResponseHeadersRetrievalText(responseVariableName: string) { return `${responseVariableName}.headers`; } diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 57c3927..432ee4c 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -714,5 +714,33 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'statusCode destructuring should be renamed', + code: `async function test() { + const { statusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const statusCode = response.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'statusCode destructuring should be renamed', + code: `async function test() { + const { statusCode: pingStatusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(response.status, StatusCodes.OK); + const pingStatusCode = response.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 0698a3f..aba644a 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -23,7 +23,12 @@ import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; import { analyzeResponseReferences } from './response-reference'; -import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, hasAssertions } from './fetch'; +import { + getResponseBodyRetrievalText, + getResponseHeadersRetrievalText, + getResponseStatusRetrievalText, + hasAssertions, +} from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; @@ -44,6 +49,7 @@ interface FixtureCallInformation { assertions?: TSESTree.Expression[][]; inlineStatementNode?: TSESTree.Node; inlineBodyReference?: TSESTree.MemberExpression; + inlineStatusReference?: TSESTree.MemberExpression; inlineHeadersReference?: TSESTree.MemberExpression; } @@ -77,6 +83,12 @@ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallI if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { results.inlineBodyReference = awaitParent; } + if ( + awaitParent.property.type === AST_NODE_TYPES.Identifier && + (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode') + ) { + results.inlineStatusReference = awaitParent; + } if ( awaitParent.property.type === AST_NODE_TYPES.Identifier && (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') @@ -311,6 +323,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat headersReferences: responseHeadersReferences, statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, + destructuringStatusVariable: destructuringResponseStatusVariable, destructuringHeadersVariable: destructuringResponseHeadersVariable, } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); @@ -358,6 +371,11 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; + const isResponseStatusVariableRedefinitionNeeded = + destructuringResponseStatusVariable !== undefined || + fixtureCallInformation.inlineStatusReference !== undefined; + const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`; + const isResponseHeadersVariableRedefinitionNeeded = (destructuringResponseHeadersVariable !== undefined && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -370,6 +388,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat responseVariable === undefined && fixtureCallInformation.assertions !== undefined) || isResponseBodyVariableRedefinitionNeeded || + isResponseStatusVariableRedefinitionNeeded || isResponseHeadersVariableRedefinitionNeeded; const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded @@ -386,6 +405,17 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat ] : []), // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseStatusVariable + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseStatusVariableRedefinitionNeeded + ? [ + `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : []), + // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts index a49e994..2fc94a2 100644 --- a/src/agent/no-supertest.spec.ts +++ b/src/agent/no-supertest.spec.ts @@ -18,15 +18,15 @@ createTester().run(ruleId, rule, { assert.ok(headers.get(ETAG)); }`, }, - // { - // name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', - // code: `async function test() { - // const responses = await Promise.all([ - // ping().expect(StatusCodes.OK), - // ping().expect(StatusCodes.OK), - // ]); - // }`, - // }, + { + name: 'skip concurrent supertest calls which will be handled in "supertest-then" rule', + code: `async function test() { + const responses = await Promise.all([ + ping().expect(StatusCodes.OK), + ping().expect(StatusCodes.OK), + ]); + }`, + }, ], invalid: [ { @@ -370,5 +370,29 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'preferNativeFetch' }], }, + { + name: 'statusCode destructuring should be renamed', + code: `async function test() { + const { statusCode } = await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const statusCode = response.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'statusCode destructuring should be renamed', + code: `async function test() { + const { statusCode: pingStatusCode } = await ping().expect(StatusCodes.OK); + }`, + output: `async function test() { + const response = await ping(); + assert.equal(response.status, StatusCodes.OK); + const pingStatusCode = response.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, ], }); diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts index 1100adf..fa91725 100644 --- a/src/agent/no-supertest.ts +++ b/src/agent/no-supertest.ts @@ -22,7 +22,7 @@ import { import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { analyzeResponseReferences } from './response-reference'; -import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText } from './fetch'; +import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, getResponseStatusRetrievalText } from './fetch'; export const ruleId = 'no-supertest'; @@ -42,6 +42,7 @@ interface FixtureCallInformation { assertions?: TSESTree.Expression[][]; inlineStatementNode?: TSESTree.Node; inlineBodyReference?: TSESTree.MemberExpression; + inlineStatusReference?: TSESTree.MemberExpression; inlineHeadersReference?: TSESTree.MemberExpression; } @@ -75,6 +76,12 @@ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallI if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { results.inlineBodyReference = awaitParent; } + if ( + awaitParent.property.type === AST_NODE_TYPES.Identifier && + (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode') + ) { + results.inlineStatusReference = awaitParent; + } if ( awaitParent.property.type === AST_NODE_TYPES.Identifier && (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') @@ -315,6 +322,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, destructuringHeadersVariable: destructuringResponseHeadersVariable, + destructuringStatusVariable: destructuringResponseStatusVariable, } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); const responseVariableNameToUse = getResponseVariableNameToUse( @@ -329,6 +337,11 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; + const isResponseStatusVariableRedefinitionNeeded = + destructuringResponseStatusVariable !== undefined || + fixtureCallInformation.inlineStatusReference !== undefined; + const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`; + const isResponseHeadersVariableRedefinitionNeeded = (destructuringResponseHeadersVariable !== undefined && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -341,6 +354,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat responseVariable === undefined && fixtureCallInformation.assertions !== undefined) || isResponseBodyVariableRedefinitionNeeded || + isResponseStatusVariableRedefinitionNeeded || isResponseHeadersVariableRedefinitionNeeded; const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded @@ -357,6 +371,17 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat ] : []), // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseStatusVariable + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseStatusVariableRedefinitionNeeded + ? [ + `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : []), + // eslint-disable-next-line no-nested-ternary ...(destructuringResponseHeadersVariable ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index f8d3c45..db47280 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -21,6 +21,7 @@ const log = debug('eslint-plugin:response-reference'); * the implementation is for fixture API, but it can be used for fetch API as well since the tree structure is similar * @param variableDeclaration - variable declaration node */ +// eslint-disable-next-line sonarjs/cognitive-complexity export function analyzeResponseReferences( variableDeclaration: TSESTree.VariableDeclaration | undefined, scopeManager: ScopeManager, @@ -31,6 +32,7 @@ export function analyzeResponseReferences( statusReferences: TSESTree.MemberExpression[]; destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; + destructuringStatusVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined; } { const results: { @@ -40,6 +42,7 @@ export function analyzeResponseReferences( statusReferences: TSESTree.MemberExpression[]; destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; + destructuringStatusVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined; } = { bodyReferences: [], @@ -90,6 +93,13 @@ export function analyzeResponseReferences( identifierParent.key.name === 'body' ) { results.destructuringBodyVariable = responseVariable; + } else if ( + // body reference through destruction/renaming, e.g. "const { body } = ..." + identifierParent.type === AST_NODE_TYPES.Property && + identifierParent.key.type === AST_NODE_TYPES.Identifier && + (identifierParent.key.name === 'status' || identifierParent.key.name === 'statusCode') + ) { + results.destructuringStatusVariable = responseVariable; } else if ( // header reference through destruction/renaming, e.g. "const { headers } = ..." identifierParent.type === AST_NODE_TYPES.Property && @@ -119,6 +129,13 @@ export function analyzeResponseReferences( ) { results.destructuringBodyVariable = parent; } + if ( + parent2?.type === AST_NODE_TYPES.Property && + parent2.key.type === AST_NODE_TYPES.Identifier && + (parent2.key.name === 'status' || parent2.key.name === 'statusCode') + ) { + results.destructuringStatusVariable = parent; + } if ( parent2?.type === AST_NODE_TYPES.Property && parent2.key.type === AST_NODE_TYPES.Identifier && diff --git a/src/agent/supertest-then.spec.ts b/src/agent/supertest-then.spec.ts index 199416b..680d8fa 100644 --- a/src/agent/supertest-then.spec.ts +++ b/src/agent/supertest-then.spec.ts @@ -46,51 +46,6 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, - // { - // name: 'adjust header access correctly', - // code: ` - // const responses = await Promise.all([ - // ping().expect(StatusCodes.NO_CONTENT), - // ping().expect(StatusCodes.NO_CONTENT), - // ]); - // assert.deepEqual(responses.map((response) => response.headers.etag).sort(), ['1', '1']); - // assert.equal(responses[0].headers[LAST_MODIFIED_HEADER], responses[1].headers[LAST_MODIFIED_HEADER]); - // assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); - // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); - // `, - // output: ` - // const responses = await Promise.all([ - // // eslint-disable-next-line @checkdigit/no-promise-instance-method - // ping().then((res) => { - // assert.equal(res.status, StatusCodes.NO_CONTENT); - // return res; - // }), - // // eslint-disable-next-line @checkdigit/no-promise-instance-method - // ping().then((res) => { - // assert.equal(res.status, StatusCodes.NO_CONTENT); - // return res; - // }), - // ]); - // assert.deepEqual(responses.map((response) => response.headers.get('etag')).sort(), ['1', '1']); - // assert.equal(responses[0].headers.get(LAST_MODIFIED_HEADER), responses[1].headers.get(LAST_MODIFIED_HEADER)); - // assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); - // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); - // `, - // errors: [ - // { messageId: 'preferNativeFetch' }, - // { messageId: 'preferNativeFetch' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // { messageId: 'shouldUseHeaderGetter' }, - // ], - // }, { name: 'in non-async arrow function with concurrent promises', code: ` diff --git a/src/agent/supertest-then.ts b/src/agent/supertest-then.ts index 02adda9..709f3e0 100644 --- a/src/agent/supertest-then.ts +++ b/src/agent/supertest-then.ts @@ -8,14 +8,12 @@ import { strict as assert } from 'node:assert'; -// import { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; import { getEnclosingFunction, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; -// import { isInvalidResponseHeadersAccess } from './fetch'; export const ruleId = 'supertest-then'; @@ -128,50 +126,6 @@ function createResponseAssertions( }; } -// function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) { -// const responseHeadersAccesses: TSESTree.MemberExpression[] = []; -// for (const responseVariable of responseVariables) { -// for (const responseReference of responseVariable.references) { -// const responseAccess = getParent(responseReference.identifier); -// if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) { -// continue; -// } - -// const responseAccessParent = getParent(responseAccess); -// if (!responseAccessParent) { -// continue; -// } - -// if ( -// responseAccessParent.type === AST_NODE_TYPES.CallExpression && -// responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression -// ) { -// // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) -// responseHeadersAccesses.push( -// ...getResponseHeadersAccesses( -// scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]), -// scopeManager, -// sourceCode, -// ), -// ); -// continue; -// } - -// if ( -// responseAccess.computed && -// responseAccess.property.type === AST_NODE_TYPES.Literal && -// responseAccessParent.type === AST_NODE_TYPES.MemberExpression -// ) { -// // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. -// responseHeadersAccesses.push(responseAccessParent); -// } else { -// responseHeadersAccesses.push(responseAccess); -// } -// } -// } -// return responseHeadersAccesses; -// } - const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); const rule: ESLintUtils.RuleModule< 'unknownError' | 'preferNativeFetch' @@ -256,52 +210,6 @@ const rule: ESLintUtils.RuleModule< return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); }, }); - - // const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); - // if (!responsesVariable) { - // return; - // } - - // const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); - // const responseHeadersAccesses = getResponseHeadersAccesses( - // responseVariableReferences, - // scopeManager, - // sourceCode, - // ); - // for (const responseHeadersAccess of responseHeadersAccesses) { - // if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { - // const headerAccess = getParent(responseHeadersAccess); - // if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) { - // const headerNameNode = headerAccess.property; - // const headerName = headerAccess.computed - // ? sourceCode.getText(headerNameNode) - // : `'${sourceCode.getText(headerNameNode)}'`; - // const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; - - // context.report({ - // node: headerAccess, - // messageId: 'shouldUseHeaderGetter', - // fix(fixer) { - // return fixer.replaceText(headerAccess, headerAccessReplacementText); - // }, - // }); - // } else if ( - // headerAccess?.type === AST_NODE_TYPES.CallExpression && - // responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && - // responseHeadersAccess.property.name === 'get' - // ) { - // const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; - - // context.report({ - // node: headerAccess, - // messageId: 'shouldUseHeaderGetter', - // fix(fixer) { - // return fixer.replaceText(headerAccess, headerAccessReplacementText); - // }, - // }); - // } - // } - // } } catch (error) { // eslint-disable-next-line no-console console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); From 9112c9ae7acbce0f6acbd7cbe6a308a6004ea9a1 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sat, 23 Nov 2024 00:56:45 -0500 Subject: [PATCH 112/115] feat: implement isFetchResponse utility and update rules to utilize it for response type checks --- src/agent/fetch-response-body-json.ts | 7 ++----- src/agent/fetch-response-status.spec.ts | 18 ++++++++++++++++++ src/agent/fetch-response-status.ts | 19 ++++++++++++++++--- src/agent/fetch.ts | 8 ++++++++ src/agent/no-status-code.ts | 7 ++----- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/agent/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts index 831751e..d263a99 100644 --- a/src/agent/fetch-response-body-json.ts +++ b/src/agent/fetch-response-body-json.ts @@ -12,6 +12,7 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils' import getDocumentationUrl from '../get-documentation-url'; import { getAncestor } from '../library/ts-tree'; +import { isFetchResponse } from './fetch'; export const ruleId = 'fetch-response-body-json'; @@ -56,11 +57,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'ref const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseBodyNode.object); const responseType = typeChecker.getTypeAtLocation(responseNode); - const shouldReplace = - responseType.getProperties().some((symbol) => symbol.name === 'body') && - responseType.getProperties().some((symbol) => symbol.name === 'json'); - - if (shouldReplace) { + if (isFetchResponse(responseType)) { if (responseBodyNode.object.type !== AST_NODE_TYPES.Identifier) { context.report({ node: responseBodyNode, diff --git a/src/agent/fetch-response-status.spec.ts b/src/agent/fetch-response-status.spec.ts index 2ecb1eb..9ec9882 100644 --- a/src/agent/fetch-response-status.spec.ts +++ b/src/agent/fetch-response-status.spec.ts @@ -51,5 +51,23 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'renameStatusCodeProperty' }], }, + { + name: 'not directly destructuring fetch', + code: `function ping() { + return fetch(url); + } + async function foo() { + const { statusCode } = await ping(); + assert.equal(statusCode, StatusCode.Ok); + }`, + output: `function ping() { + return fetch(url); + } + async function foo() { + const { status: statusCode } = await ping(); + assert.equal(statusCode, StatusCode.Ok); + }`, + errors: [{ messageId: 'renameStatusCodeProperty' }], + }, ], }); diff --git a/src/agent/fetch-response-status.ts b/src/agent/fetch-response-status.ts index 3141fd8..c225fed 100644 --- a/src/agent/fetch-response-status.ts +++ b/src/agent/fetch-response-status.ts @@ -9,6 +9,7 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import getDocumentationUrl from '../get-documentation-url'; +import { isFetchResponse } from './fetch'; export const ruleId = 'fetch-response-status'; @@ -30,15 +31,16 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'renameStatusCodeProperty'> }, defaultOptions: [], create(context) { + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + return { VariableDeclaration: (variableDeclaration: TSESTree.VariableDeclaration) => { const variableInit = variableDeclaration.declarations[0]?.init; if ( !variableInit || variableInit.type !== AST_NODE_TYPES.AwaitExpression || - variableInit.argument.type !== AST_NODE_TYPES.CallExpression || - variableInit.argument.callee.type !== AST_NODE_TYPES.Identifier || - variableInit.argument.callee.name !== 'fetch' + variableInit.argument.type !== AST_NODE_TYPES.CallExpression ) { return; } @@ -57,6 +59,17 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'renameStatusCodeProperty'> return; } + if ( + variableInit.argument.callee.type !== AST_NODE_TYPES.Identifier || + variableInit.argument.callee.name !== 'fetch' + ) { + const variableNode = parserServices.esTreeNodeToTSNodeMap.get(variableId); + const variableType = typeChecker.getTypeAtLocation(variableNode); + if (!isFetchResponse(variableType)) { + return; + } + } + try { context.report({ node: statusCodeProperty, diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index a59f90d..4f3afd7 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -1,6 +1,7 @@ // agent/fetch.ts import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import ts from 'typescript'; import { getParent, isBlockStatement } from '../library/ts-tree'; @@ -59,3 +60,10 @@ export function hasAssertions(fixtureCall: TSESTree.Node): boolean { return hasAssertions(parent); } + +export function isFetchResponse(type: ts.Type): boolean { + return ( + type.getProperties().some((symbol) => symbol.name === 'body') && + type.getProperties().some((symbol) => symbol.name === 'json') + ); +} diff --git a/src/agent/no-status-code.ts b/src/agent/no-status-code.ts index 9c1bea0..71d68c8 100644 --- a/src/agent/no-status-code.ts +++ b/src/agent/no-status-code.ts @@ -9,6 +9,7 @@ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import getDocumentationUrl from '../get-documentation-url'; +import { isFetchResponse } from './fetch'; export const ruleId = 'no-status-code'; @@ -39,11 +40,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceStatusCode'> = creat const responseNode = parserServices.esTreeNodeToTSNodeMap.get(responseStatusCode.object); const responseType = typeChecker.getTypeAtLocation(responseNode); - const shouldReplace = - responseType.getProperties().some((symbol) => symbol.name === 'status') && - !responseType.getProperties().some((symbol) => symbol.name === 'statusCode'); - - if (shouldReplace) { + if (isFetchResponse(responseType)) { context.report({ messageId: 'replaceStatusCode', node: responseStatusCode.property, From cf052cd0a5329c774e38de314b184ecb6f8e355f Mon Sep 17 00:00:00 2001 From: Le Cong Date: Sun, 24 Nov 2024 06:15:23 -0500 Subject: [PATCH 113/115] refactor: standardize parser service variable naming and enhance response variable naming logic --- src/agent/no-fixture.spec.ts | 212 ++++++++++++++------------- src/agent/no-fixture.ts | 33 ++++- src/agent/no-service-wrapper.ts | 6 +- src/agent/no-supertest.spec.ts | 168 +++++++++++---------- src/agent/no-supertest.ts | 41 +++--- src/require-resolve-full-response.ts | 8 +- 6 files changed, 251 insertions(+), 217 deletions(-) diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 432ee4c..29d23e0 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -73,25 +73,25 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); + assert.equal(pingGetResponse.status, StatusCodes.OK); `, errors: [{ messageId: 'preferNativeFetch' }], }, { - name: 'assertion without variable declaration', + name: 'assertion without variable declaration - complex status assertion argument', code: ` import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, options.expectedStatusCode ?? StatusCodes.CREATED); + assert.equal(pingGetResponse.status, options.expectedStatusCode ?? StatusCodes.CREATED); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -103,11 +103,11 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + const cardPutResponse = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { method: 'PUT', body: JSON.stringify(cardCreationData), }); - assert.equal(response.status, StatusCodes.BAD_REQUEST); + assert.equal(cardPutResponse.status, StatusCodes.BAD_REQUEST); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -169,10 +169,10 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + const cardBlockPostResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'POST', }); - assert.equal(response.status, StatusCodes.NO_CONTENT); + assert.equal(cardBlockPostResponse.status, StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -186,10 +186,10 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + const cardBlockDeleteResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'DELETE', }); - assert.equal(response.status, StatusCodes.NO_CONTENT); + assert.equal(cardBlockDeleteResponse.status, StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -206,14 +206,14 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get('etag'), '123'); - assert.equal(response.headers.get('content-type'), 'application/json'); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.ok(response.headers.get(ETAG).match(/1.*/u)); + assert.equal(pingGetResponse.status, StatusCodes.OK); + assert.equal(pingGetResponse.headers.get('etag'), '123'); + assert.equal(pingGetResponse.headers.get('content-type'), 'application/json'); + assert.equal(pingGetResponse.headers.get(ETAG), correctVersion); + assert.ok(pingGetResponse.headers.get(ETAG).match(/1.*/u)); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -225,10 +225,10 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.deepEqual(await response.json(), {message:'pong'}); + assert.deepEqual(await pingGetResponse.json(), {message:'pong'}); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -242,11 +242,11 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.doesNotThrow(()=>validate(response)); - assert.doesNotThrow(()=>console.log(response)); + assert.doesNotThrow(()=>validate(pingGetResponse)); + assert.doesNotThrow(()=>console.log(pingGetResponse)); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -254,30 +254,34 @@ createTester().run(ruleId, rule, { name: 'multiple fixture calls in the same test', code: ` import { BASE_PATH } from './index'; - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); - await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + async function test() { + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + const pingGetResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + await fixture.api.get(\`/sample-service/v1/ping?param=xxx\`).expect(StatusCodes.OK).expect({message:'pong'}); + await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); + } `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingResponse.status, StatusCodes.OK); - const response2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - assert.deepEqual(await response2.json(), {message:'pong'}); - const response3 = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response3.status, StatusCodes.OK); + async function test() { + const pingGetResponse1 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(pingGetResponse1.status, StatusCodes.OK); + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const pingGetResponse2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { + method: 'GET', + }); + assert.equal(pingGetResponse2.status, StatusCodes.OK); + assert.deepEqual(await pingGetResponse2.json(), {message:'pong'}); + const pingGetResponse3 = await fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }); + assert.equal(pingGetResponse3.status, StatusCodes.OK); + } `, errors: [ { messageId: 'preferNativeFetch' }, @@ -312,11 +316,11 @@ createTester().run(ruleId, rule, { output: ` import { BASE_PATH } from './index'; async () => { - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - return response; + assert.equal(pingGetResponse.status, StatusCodes.OK); + return pingGetResponse; }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -393,10 +397,10 @@ createTester().run(ruleId, rule, { `, output: ` import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, 200); + assert.equal(pingGetResponse.status, 200); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -410,11 +414,11 @@ createTester().run(ruleId, rule, { output: ` import { BASE_PATH } from './index'; const createdOn = Date.now().toUTCString(); - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), validateBody(createdOn)); + assert.equal(pingGetResponse.status, 200); + assert.deepEqual(await pingGetResponse.json(), validateBody(createdOn)); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -426,11 +430,11 @@ createTester().run(ruleId, rule, { assert.ok(timeDifference >= 0 && timeDifference < 200); `, output: ` - const response = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const responseBody = await pingGetResponse.json(); const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); `, @@ -443,11 +447,11 @@ createTester().run(ruleId, rule, { assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); `, output: ` - const response = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const { pgpPublicKey: firstPgpPublicKey } = await response.json(); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const { pgpPublicKey: firstPgpPublicKey } = await pingGetResponse.json(); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); `, errors: [{ messageId: 'preferNativeFetch' }], @@ -460,12 +464,12 @@ createTester().run(ruleId, rule, { assert.ok(headers2.get(ETAG)); `, output: ` - const response = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const body = await response.json(); - const headers2 = response.headers; + assert.equal(pingGetResponse.status, StatusCodes.OK); + const body = await pingGetResponse.json(); + const headers2 = pingGetResponse.headers; assert(body); assert.ok(headers2.get(ETAG)); `, @@ -478,11 +482,11 @@ createTester().run(ruleId, rule, { assert.ok(headers.get(ETAG)); `, output: ` - const response = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const headers = response.headers; + assert.equal(pingGetResponse.status, StatusCodes.OK); + const headers = pingGetResponse.headers; assert.ok(headers.get(ETAG)); `, errors: [{ messageId: 'preferNativeFetch' }], @@ -505,24 +509,24 @@ createTester().run(ruleId, rule, { name: 'avoid response variable name conflict with existing variables in the same scope', code: ` async () => { - const response = 'foo'; - const response1 = 'bar'; + const pingGetResponse = 'foo'; + const pingGetResponse1 = 'bar'; await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); } `, output: ` async () => { - const response = 'foo'; - const response1 = 'bar'; - const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = 'foo'; + const pingGetResponse1 = 'bar'; + const pingGetResponse2 = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response2.status, StatusCodes.OK); - const response3 = await fetch(\`$\{BASE_PATH}/ping\`, { + assert.equal(pingGetResponse2.status, StatusCodes.OK); + const pingGetResponse3 = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response3.status, StatusCodes.OK); + assert.equal(pingGetResponse3.status, StatusCodes.OK); } `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], @@ -531,34 +535,34 @@ createTester().run(ruleId, rule, { name: 'response variable names in different scope do not conflict with each other', code: ` it('#1', async () => { - const response = 'foo'; + const pingGetResponse = 'foo'; }); it('#2', async () => { - const response = 'foo'; + const pingGetResponse = 'foo'; await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); }); it('#3', async () => { - const response3 = 'foo'; + const pingGetResponse3 = 'foo'; await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); }); `, output: ` it('#1', async () => { - const response = 'foo'; + const pingGetResponse = 'foo'; }); it('#2', async () => { - const response = 'foo'; - const response2 = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse = 'foo'; + const pingGetResponse1 = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response2.status, StatusCodes.OK); + assert.equal(pingGetResponse1.status, StatusCodes.OK); }); it('#3', async () => { - const response3 = 'foo'; - const response = await fetch(\`$\{BASE_PATH}/ping\`, { + const pingGetResponse3 = 'foo'; + const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); + assert.equal(pingGetResponse.status, StatusCodes.OK); }); `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], @@ -576,12 +580,12 @@ createTester().run(ruleId, rule, { export async function validatePin( fixture, ) { - const response = await fetch(\`\${BASE_PATH}/public-key\`, { + const publicKeyGetResponse = await fetch(\`\${BASE_PATH}/public-key\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); - const paymentSecurityServicePublicKey = responseBody.publicKey; + assert.equal(publicKeyGetResponse.status, StatusCodes.OK); + const publicKeyGetResponseBody = await publicKeyGetResponse.json(); + const paymentSecurityServicePublicKey = publicKeyGetResponseBody.publicKey; } `, errors: [{ messageId: 'preferNativeFetch' }], @@ -611,7 +615,7 @@ createTester().run(ruleId, rule, { // Import Key const keyId = uuid(); - const response = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { + const keyPutResponse = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { method: 'PUT', body: JSON.stringify({ key: '71CA52F757D7C0B45A16C6C04EAFD704', @@ -621,9 +625,9 @@ createTester().run(ruleId, rule, { [CREATED_ON_HEADER]: createdOn, }, }); - assert.equal(response.status, StatusCodes.NO_CONTENT); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); + assert.equal(keyPutResponse.status, StatusCodes.NO_CONTENT); + assert.equal(keyPutResponse.headers.get(ETAG_HEADER), '1'); + assert.doesNotThrow(()=>verifyTemporalHeaders(keyPutResponse, createdOn)); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -672,11 +676,11 @@ createTester().run(ruleId, rule, { const { headers: { etag } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); `, output: ` - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const etag = response.headers.get('etag'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const etag = pingGetResponse.headers.get('etag'); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -686,12 +690,12 @@ createTester().run(ruleId, rule, { const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); `, output: ` - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const createdOn = response.headers.get('created-on'); - const updatedOn = response.headers.get('updated-on'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const createdOn = pingGetResponse.headers.get('created-on'); + const updatedOn = pingGetResponse.headers.get('updated-on'); `, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -720,25 +724,25 @@ createTester().run(ruleId, rule, { const { statusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); }`, output: `async function test() { - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const statusCode = response.status; + assert.equal(pingGetResponse.status, StatusCodes.OK); + const statusCode = pingGetResponse.status; }`, errors: [{ messageId: 'preferNativeFetch' }], }, { - name: 'statusCode destructuring should be renamed', + name: 'statusCode destructuring should be renamed - with renaming', code: `async function test() { const { statusCode: pingStatusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); }`, output: `async function test() { - const response = await fetch(\`\${BASE_PATH}/ping\`, { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', }); - assert.equal(response.status, StatusCodes.OK); - const pingStatusCode = response.status; + assert.equal(pingGetResponse.status, StatusCodes.OK); + const pingStatusCode = pingGetResponse.status; }`, errors: [{ messageId: 'preferNativeFetch' }], }, diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index aba644a..68aa00f 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -218,6 +218,9 @@ function createResponseAssertions( } function getResponseVariableNameToUse( + methodName: string, + urlArgumentNode: TSESTree.CallExpressionArgument, + originalUrlArgumentText: string, scopeManager: ScopeManager, fixtureCallInformation: FixtureCallInformation, scopeVariablesMap: Map, @@ -249,14 +252,34 @@ function getResponseVariableNameToUse( scopeVariablesMap.set(scope, scopeVariables); } + let responseVariableNameBase = 'response'; + if (urlArgumentNode.type === AST_NODE_TYPES.Literal || urlArgumentNode.type === AST_NODE_TYPES.TemplateLiteral) { + const urlWithoutQuotes = originalUrlArgumentText.replace(/['"`]/gu, ''); + const urlWithoutQuery = urlWithoutQuotes.includes('?') + ? urlWithoutQuotes.slice(0, urlWithoutQuotes.indexOf('?')) + : urlWithoutQuotes; + const parts = urlWithoutQuery.startsWith('${') + ? urlWithoutQuery.split('/').slice(1) + : // eslint-disable-next-line no-magic-numbers + urlWithoutQuery.split('/').slice(3); + + responseVariableNameBase = [...parts, methodName.toLocaleLowerCase()] + .map((part) => part.split(/[-=]/u)) + .flat() + .filter((part) => part.trim() !== '' && !/\$\{.*\}/u.test(part)) // keep only non-empty parts that are not path parameters + .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) + .join(''); + responseVariableNameBase = `${responseVariableNameBase[0]?.toLowerCase() ?? ''}${responseVariableNameBase.slice(1)}Response`; + } + let responseVariableCounter = 0; let responseVariableNameToUse; while (responseVariableNameToUse === undefined) { - responseVariableCounter++; - responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`; + responseVariableNameToUse = `${responseVariableNameBase}${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; if (scopeVariables.includes(responseVariableNameToUse)) { responseVariableNameToUse = undefined; } + responseVariableCounter++; } scopeVariables.push(responseVariableNameToUse); return responseVariableNameToUse; @@ -335,10 +358,11 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat const methodNode = fixtureFunction.property; // get/put/etc. assert.ok(methodNode.type === AST_NODE_TYPES.Identifier); const methodName = methodNode.name.toUpperCase(); + const methodNameToUse = methodName === 'DEL' ? 'DELETE' : methodName; const fetchRequestArgumentLines = [ '{', - ` method: '${methodName === 'DEL' ? 'DELETE' : methodName}',`, + ` method: '${methodNameToUse}',`, ...(fixtureCallInformation.requestBody ? [` body: JSON.stringify(${sourceCode.getText(fixtureCallInformation.requestBody)}),`] : []), @@ -360,6 +384,9 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat ].join(`\n${indentation}`); const responseVariableNameToUse = getResponseVariableNameToUse( + methodNameToUse, + urlArgumentNode, + originalUrlArgumentText, scopeManager, fixtureCallInformation, scopeVariablesMap, diff --git a/src/agent/no-service-wrapper.ts b/src/agent/no-service-wrapper.ts index 81f5aac..b2c731a 100644 --- a/src/agent/no-service-wrapper.ts +++ b/src/agent/no-service-wrapper.ts @@ -41,8 +41,8 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'inval create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - const parserService = ESLintUtils.getParserServices(context); - const typeChecker = parserService.program.getTypeChecker(); + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) { if ( @@ -70,7 +70,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'inval } function getType(identifier: TSESTree.Identifier) { - const variable = parserService.esTreeNodeToTSNodeMap.get(identifier); + const variable = parserServices.esTreeNodeToTSNodeMap.get(identifier); const variableType = typeChecker.getTypeAtLocation(variable); return typeChecker.typeToString(variableType); } diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts index 2fc94a2..c8fd708 100644 --- a/src/agent/no-supertest.spec.ts +++ b/src/agent/no-supertest.spec.ts @@ -27,6 +27,12 @@ createTester().run(ruleId, rule, { ]); }`, }, + { + name: 'leave fixture.api.xxx() calls as is, which will be handled by no-fixture rule', + code: `async function test() { + await fixture.api.get(url).expect(StatusCodes.OK); + }`, + }, ], invalid: [ { @@ -35,8 +41,8 @@ createTester().run(ruleId, rule, { await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -65,12 +71,12 @@ createTester().run(ruleId, rule, { .expect(ETAG, /1.*/u); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get('etag'), '123'); - assert.equal(response.headers.get('content-type'), 'application/json'); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.ok(response.headers.get(ETAG).match(/1.*/u)); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + assert.equal(pingResponse.headers.get('etag'), '123'); + assert.equal(pingResponse.headers.get('content-type'), 'application/json'); + assert.equal(pingResponse.headers.get(ETAG), correctVersion); + assert.ok(pingResponse.headers.get(ETAG).match(/1.*/u)); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -80,8 +86,8 @@ createTester().run(ruleId, rule, { await ping().expect({message:'pong'}); }`, output: `async function test() { - const response = await ping(); - assert.deepEqual(await response.json(), {message:'pong'}); + const pingResponse = await ping(); + assert.deepEqual(await pingResponse.json(), {message:'pong'}); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -93,9 +99,9 @@ createTester().run(ruleId, rule, { .expect((response)=>console.log(response)); }`, output: `async function test() { - const response = await ping(); - assert.doesNotThrow(()=>validate(response)); - assert.doesNotThrow(()=>console.log(response)); + const pingResponse = await ping(); + assert.doesNotThrow(()=>validate(pingResponse)); + assert.doesNotThrow(()=>console.log(pingResponse)); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -108,15 +114,15 @@ createTester().run(ruleId, rule, { await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); + const pingResponse1 = await ping(); + assert.equal(pingResponse1.status, StatusCodes.OK); const pingResponse = await ping(); assert.equal(pingResponse.status, StatusCodes.OK); - const response2 = await ping(); - assert.equal(response2.status, StatusCodes.OK); - assert.deepEqual(await response2.json(), {message:'pong'}); - const response3 = await ping(); - assert.equal(response3.status, StatusCodes.OK); + const pingResponse2 = await ping(); + assert.equal(pingResponse2.status, StatusCodes.OK); + assert.deepEqual(await pingResponse2.json(), {message:'pong'}); + const pingResponse3 = await ping(); + assert.equal(pingResponse3.status, StatusCodes.OK); }`, errors: [ { messageId: 'preferNativeFetch' }, @@ -131,9 +137,9 @@ createTester().run(ruleId, rule, { return ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - return response; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + return pingResponse; }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -155,11 +161,11 @@ createTester().run(ruleId, rule, { { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', code: `async function test() { - await ping().expect(200); + await util.ping().expect(200); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, 200); + const pingResponse = await util.ping(); + assert.equal(pingResponse.status, 200); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -171,9 +177,9 @@ createTester().run(ruleId, rule, { }`, output: `async function test() { const createdOn = Date.now().toUTCString(); - const response = await ping(); - assert.equal(response.status, 200); - assert.deepEqual(await response.json(), validateBody(createdOn)); + const pingResponse = await ping(); + assert.equal(pingResponse.status, 200); + assert.deepEqual(await pingResponse.json(), validateBody(createdOn)); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -185,9 +191,9 @@ createTester().run(ruleId, rule, { assert.ok(timeDifference >= 0 && timeDifference < 200); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const responseBody = await pingResponse.json(); const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); assert.ok(timeDifference >= 0 && timeDifference < 200); }`, @@ -200,9 +206,9 @@ createTester().run(ruleId, rule, { assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const { pgpPublicKey: firstPgpPublicKey } = await response.json(); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const { pgpPublicKey: firstPgpPublicKey } = await pingResponse.json(); assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); }`, errors: [{ messageId: 'preferNativeFetch' }], @@ -215,10 +221,10 @@ createTester().run(ruleId, rule, { assert.ok(headers2.get(ETAG)); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const body = await response.json(); - const headers2 = response.headers; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const body = await pingResponse.json(); + const headers2 = pingResponse.headers; assert(body); assert.ok(headers2.get(ETAG)); }`, @@ -231,9 +237,9 @@ createTester().run(ruleId, rule, { assert.ok(headers.get(ETAG)); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const headers = response.headers; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const headers = pingResponse.headers; assert.ok(headers.get(ETAG)); }`, errors: [{ messageId: 'preferNativeFetch' }], @@ -241,18 +247,18 @@ createTester().run(ruleId, rule, { { name: 'avoid response variable name conflict with existing variables in the same scope', code: `async () => { - const response = 'foo'; - const response1 = 'bar'; + const pingResponse = 'foo'; + const pingResponse1 = 'bar'; await ping().expect(StatusCodes.OK); await ping().expect(StatusCodes.OK); }`, output: `async () => { - const response = 'foo'; - const response1 = 'bar'; - const response2 = await ping(); - assert.equal(response2.status, StatusCodes.OK); - const response3 = await ping(); - assert.equal(response3.status, StatusCodes.OK); + const pingResponse = 'foo'; + const pingResponse1 = 'bar'; + const pingResponse2 = await ping(); + assert.equal(pingResponse2.status, StatusCodes.OK); + const pingResponse3 = await ping(); + assert.equal(pingResponse3.status, StatusCodes.OK); }`, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, @@ -260,30 +266,30 @@ createTester().run(ruleId, rule, { name: 'response variable names in different scope do not conflict with each other', code: ` it('#1', async () => { - const response = 'foo'; + const pingResponse = 'foo'; }); it('#2', async () => { - const response = 'foo'; + const pingResponse = 'foo'; await ping().expect(StatusCodes.OK); }); it('#3', async () => { - const response3 = 'foo'; + const pingResponse3 = 'foo'; await ping().expect(StatusCodes.OK); }); `, output: ` it('#1', async () => { - const response = 'foo'; + const pingResponse = 'foo'; }); it('#2', async () => { - const response = 'foo'; - const response2 = await ping(); - assert.equal(response2.status, StatusCodes.OK); + const pingResponse = 'foo'; + const pingResponse1 = await ping(); + assert.equal(pingResponse1.status, StatusCodes.OK); }); it('#3', async () => { - const response3 = 'foo'; - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); + const pingResponse3 = 'foo'; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); }); `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], @@ -298,10 +304,10 @@ createTester().run(ruleId, rule, { output: `export async function validatePin( fixture, ) { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const responseBody = await response.json(); - const paymentSecurityServicePublicKey = responseBody.publicKey; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const pingResponseBody = await pingResponse.json(); + const paymentSecurityServicePublicKey = pingResponseBody.publicKey; }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -324,10 +330,10 @@ createTester().run(ruleId, rule, { // Import Key const keyId = uuid(); - const response = await ping(); - assert.equal(response.status, StatusCodes.NO_CONTENT); - assert.equal(response.headers.get(ETAG_HEADER), '1'); - assert.doesNotThrow(()=>verifyTemporalHeaders(response, createdOn)); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.NO_CONTENT); + assert.equal(pingResponse.headers.get(ETAG_HEADER), '1'); + assert.doesNotThrow(()=>verifyTemporalHeaders(pingResponse, createdOn)); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -351,9 +357,9 @@ createTester().run(ruleId, rule, { const { headers: { etag } } = await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const etag = response.headers.get('etag'); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const etag = pingResponse.headers.get('etag'); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -363,10 +369,10 @@ createTester().run(ruleId, rule, { const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const createdOn = response.headers.get('created-on'); - const updatedOn = response.headers.get('updated-on'); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const createdOn = pingResponse.headers.get('created-on'); + const updatedOn = pingResponse.headers.get('updated-on'); }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -376,9 +382,9 @@ createTester().run(ruleId, rule, { const { statusCode } = await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const statusCode = response.status; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const statusCode = pingResponse.status; }`, errors: [{ messageId: 'preferNativeFetch' }], }, @@ -388,9 +394,9 @@ createTester().run(ruleId, rule, { const { statusCode: pingStatusCode } = await ping().expect(StatusCodes.OK); }`, output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - const pingStatusCode = response.status; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const pingStatusCode = pingResponse.status; }`, errors: [{ messageId: 'preferNativeFetch' }], }, diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts index fa91725..bf99547 100644 --- a/src/agent/no-supertest.ts +++ b/src/agent/no-supertest.ts @@ -36,9 +36,6 @@ interface FixtureCallInformation { fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; variableDeclaration?: TSESTree.VariableDeclaration; variableAssignment?: TSESTree.ExpressionStatement; - requestBody?: TSESTree.Expression; - requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; - requestHeadersObjectLiteral?: TSESTree.ObjectExpression; assertions?: TSESTree.Expression[][]; inlineStatementNode?: TSESTree.Node; inlineBodyReference?: TSESTree.MemberExpression; @@ -107,23 +104,6 @@ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallI assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; nextCall = assertionCall; - } else if (parent.property.name === 'send') { - // request body - const sendRequestBodyCall = getParent(parent); - assert.ok(sendRequestBodyCall && sendRequestBodyCall.type === AST_NODE_TYPES.CallExpression); - results.requestBody = sendRequestBodyCall.arguments[0] as TSESTree.Expression; - nextCall = sendRequestBodyCall; - } else if (parent.property.name === 'set') { - // request headers - const setRequestHeaderCall = getParent(parent); - assert.ok(setRequestHeaderCall && setRequestHeaderCall.type === AST_NODE_TYPES.CallExpression); - const [arg1, arg2] = setRequestHeaderCall.arguments as [TSESTree.Expression, TSESTree.Expression]; - if (arg1.type === AST_NODE_TYPES.ObjectExpression) { - results.requestHeadersObjectLiteral = arg1; - } else { - results.requestHeaders = [...(results.requestHeaders ?? []), { name: arg1, value: arg2 }]; - } - nextCall = setRequestHeaderCall; } } else { throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); @@ -211,6 +191,7 @@ function createResponseAssertions( } function getResponseVariableNameToUse( + supertestFunctionName: string, scopeManager: ScopeManager, fixtureCallInformation: FixtureCallInformation, scopeVariablesMap: Map, @@ -245,11 +226,11 @@ function getResponseVariableNameToUse( let responseVariableCounter = 0; let responseVariableNameToUse; while (responseVariableNameToUse === undefined) { - responseVariableCounter++; - responseVariableNameToUse = `response${responseVariableCounter === 1 ? '' : responseVariableCounter.toString()}`; + responseVariableNameToUse = `${supertestFunctionName}Response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; if (scopeVariables.includes(responseVariableNameToUse)) { responseVariableNameToUse = undefined; } + responseVariableCounter++; } scopeVariables.push(responseVariableNameToUse); return responseVariableNameToUse; @@ -298,11 +279,26 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat (supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && supertestCall.callee.object.callee.property.name === 'expect') + ) { + // skip nested expect call chain, only focus on the first expect call + return; + } + if ( + supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && + supertestCall.callee.object.callee.object.type === AST_NODE_TYPES.MemberExpression && + supertestCall.callee.object.callee.object.object.type === AST_NODE_TYPES.Identifier && + supertestCall.callee.object.callee.object.object.name === 'fixture' && + supertestCall.callee.object.callee.object.property.type === AST_NODE_TYPES.Identifier && + supertestCall.callee.object.callee.object.property.name === 'api' ) { // skip nested expect calls, only focus on the top level return; } + const fullSupertestFunctionName = sourceCode.getText(supertestCall.callee.object.callee); + const supertestFunctionName = fullSupertestFunctionName.split('.').pop(); + assert.ok(supertestFunctionName !== undefined); + if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) { // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here return; @@ -326,6 +322,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); const responseVariableNameToUse = getResponseVariableNameToUse( + supertestFunctionName, scopeManager, fixtureCallInformation, scopeVariablesMap, diff --git a/src/require-resolve-full-response.ts b/src/require-resolve-full-response.ts index e9cf7a8..762256b 100644 --- a/src/require-resolve-full-response.ts +++ b/src/require-resolve-full-response.ts @@ -38,8 +38,8 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; - const parserService = ESLintUtils.getParserServices(context); - const typeChecker = parserService.program.getTypeChecker(); + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); function isUrlArgumentValid(urlArgument: TSESTree.Node | undefined, scope: Scope) { if ( @@ -67,7 +67,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu } function getType(identifier: TSESTree.Identifier) { - const variable = parserService.esTreeNodeToTSNodeMap.get(identifier); + const variable = parserServices.esTreeNodeToTSNodeMap.get(identifier); const variableType = typeChecker.getTypeAtLocation(variable); return typeChecker.typeToString(variableType); } @@ -171,7 +171,7 @@ const rule: ESLintUtils.RuleModule<'invalidOptions' | 'unknownError'> = createRu if (optionsTypeString === 'FullResponseOptions') { return; } - const variable = parserService.esTreeNodeToTSNodeMap.get(optionsArgument); + const variable = parserServices.esTreeNodeToTSNodeMap.get(optionsArgument); const optionType = typeChecker.getTypeAtLocation(variable); const resolveWithFullResponseProperty = optionType.getProperty('resolveWithFullResponse'); if (resolveWithFullResponseProperty?.declarations?.[0]?.getText() === 'resolveWithFullResponse: true') { From 7e61255f05712af8f2856434cd23f59e3a096fb0 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Mon, 25 Nov 2024 08:41:44 -0500 Subject: [PATCH 114/115] refactor: update response header access methods and clean up related tests --- .../fetch-response-header-getter.spec.ts | 14 +- src/agent/fetch-then.spec.ts | 102 +++++----- src/agent/fetch-then.ts | 175 +++++++++--------- src/agent/no-fixture.spec.ts | 19 -- src/agent/no-fixture.ts | 40 ++-- src/agent/no-supertest.spec.ts | 15 -- src/agent/no-supertest.ts | 36 ++-- src/agent/response-reference.ts | 20 +- 8 files changed, 194 insertions(+), 227 deletions(-) diff --git a/src/agent/fetch-response-header-getter.spec.ts b/src/agent/fetch-response-header-getter.spec.ts index ac06541..eded3f9 100644 --- a/src/agent/fetch-response-header-getter.spec.ts +++ b/src/agent/fetch-response-header-getter.spec.ts @@ -78,14 +78,16 @@ createTester().run(ruleId, rule, { invalid: [ { name: 'use get() method to get header value from the headers object if the typing allows.', - code: ` + code: `async function doSomething() { + const ETAG = 'etag'; const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); - response.headers[ETAG]; - `, - output: ` + assert.equal(response.headers[ETAG], '123'); + }`, + output: `async function doSomething() { + const ETAG = 'etag'; const response = await fetch('https://openapi-cli.checkdigit/sample/v1/ping'); - response.headers.get(ETAG); - `, + assert.equal(response.headers.get(ETAG), '123'); + }`, errors: [{ messageId: 'useGetter' }], }, { diff --git a/src/agent/fetch-then.spec.ts b/src/agent/fetch-then.spec.ts index dc0726a..d3d4e7d 100644 --- a/src/agent/fetch-then.spec.ts +++ b/src/agent/fetch-then.spec.ts @@ -52,57 +52,57 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, - { - name: 'adjust header access correctly', - code: ` - const responses = await Promise.all([ - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), - ]); - assert.deepEqual(responses.map((response) => response.headers.etag).sort(), ['1', '1']); - assert.equal(responses[0].headers[LAST_MODIFIED_HEADER], responses[1].headers[LAST_MODIFIED_HEADER]); - assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); - assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); - `, - output: ` - const responses = await Promise.all([ - // eslint-disable-next-line @checkdigit/no-promise-instance-method - fetch(\`\${BASE_PATH}/key\`, { - method: 'PUT', - body: JSON.stringify(keyData), - }).then((res) => { - assert.equal(res.status, StatusCodes.NO_CONTENT); - return res; - }), - // eslint-disable-next-line @checkdigit/no-promise-instance-method - fetch(\`\${BASE_PATH}/key\`, { - method: 'PUT', - body: JSON.stringify(keyData), - }).then((res) => { - assert.equal(res.status, StatusCodes.NO_CONTENT); - return res; - }), - ]); - assert.deepEqual(responses.map((response) => response.headers.get('etag')).sort(), ['1', '1']); - assert.equal(responses[0].headers.get(LAST_MODIFIED_HEADER), responses[1].headers.get(LAST_MODIFIED_HEADER)); - assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); - assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); - `, - errors: [ - { messageId: 'preferNativeFetch' }, - { messageId: 'preferNativeFetch' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - { messageId: 'shouldUseHeaderGetter' }, - ], - }, + // { + // name: 'adjust header access correctly', + // code: ` + // const responses = await Promise.all([ + // fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + // fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), + // ]); + // assert.deepEqual(responses.map((response) => response.headers.etag).sort(), ['1', '1']); + // assert.equal(responses[0].headers[LAST_MODIFIED_HEADER], responses[1].headers[LAST_MODIFIED_HEADER]); + // assert.equal(responses[0].get(CREATED_ON_HEADER), responses[1].get(CREATED_ON_HEADER)); + // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + // `, + // output: ` + // const responses = await Promise.all([ + // // eslint-disable-next-line @checkdigit/no-promise-instance-method + // fetch(\`\${BASE_PATH}/key\`, { + // method: 'PUT', + // body: JSON.stringify(keyData), + // }).then((res) => { + // assert.equal(res.status, StatusCodes.NO_CONTENT); + // return res; + // }), + // // eslint-disable-next-line @checkdigit/no-promise-instance-method + // fetch(\`\${BASE_PATH}/key\`, { + // method: 'PUT', + // body: JSON.stringify(keyData), + // }).then((res) => { + // assert.equal(res.status, StatusCodes.NO_CONTENT); + // return res; + // }), + // ]); + // assert.deepEqual(responses.map((response) => response.headers.get('etag')).sort(), ['1', '1']); + // assert.equal(responses[0].headers.get(LAST_MODIFIED_HEADER), responses[1].headers.get(LAST_MODIFIED_HEADER)); + // assert.equal(responses[0].headers.get(CREATED_ON_HEADER), responses[1].headers.get(CREATED_ON_HEADER)); + // assert.equal(responses[0].headers.get(UPDATED_ON_HEADER), responses[1].headers.get(UPDATED_ON_HEADER)); + // `, + // errors: [ + // { messageId: 'preferNativeFetch' }, + // { messageId: 'preferNativeFetch' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // { messageId: 'shouldUseHeaderGetter' }, + // ], + // }, { name: 'in non-async arrow function with concurrent promises', code: ` diff --git a/src/agent/fetch-then.ts b/src/agent/fetch-then.ts index 96edca6..48d7540 100644 --- a/src/agent/fetch-then.ts +++ b/src/agent/fetch-then.ts @@ -8,15 +8,15 @@ import { strict as assert } from 'node:assert'; -import { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +// import { ScopeManager, Variable } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; -import { getEnclosingFunction, getEnclosingStatement, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree'; +import { getEnclosingFunction, getParent, isUsedInArrayOrAsArgument } from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; -import { hasAssertions, isInvalidResponseHeadersAccess } from './fetch'; +import { hasAssertions } from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'fetch-then'; @@ -143,52 +143,52 @@ function createResponseAssertions( }; } -function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) { - const responseHeadersAccesses: TSESTree.MemberExpression[] = []; - for (const responseVariable of responseVariables) { - for (const responseReference of responseVariable.references) { - const responseAccess = getParent(responseReference.identifier); - if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) { - continue; - } +// function getResponseHeadersAccesses(responseVariables: Variable[], scopeManager: ScopeManager, sourceCode: SourceCode) { +// const responseHeadersAccesses: TSESTree.MemberExpression[] = []; +// for (const responseVariable of responseVariables) { +// for (const responseReference of responseVariable.references) { +// const responseAccess = getParent(responseReference.identifier); +// if (!responseAccess || responseAccess.type !== AST_NODE_TYPES.MemberExpression) { +// continue; +// } - const responseAccessParent = getParent(responseAccess); - if (!responseAccessParent) { - continue; - } +// const responseAccessParent = getParent(responseAccess); +// if (!responseAccessParent) { +// continue; +// } - if ( - responseAccessParent.type === AST_NODE_TYPES.CallExpression && - responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression - ) { - // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) - responseHeadersAccesses.push( - ...getResponseHeadersAccesses( - scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]), - scopeManager, - sourceCode, - ), - ); - continue; - } +// if ( +// responseAccessParent.type === AST_NODE_TYPES.CallExpression && +// responseAccessParent.arguments[0]?.type === AST_NODE_TYPES.ArrowFunctionExpression +// ) { +// // map-like operation against responses, e.g. responses.map((response) => response.headers.etag) +// responseHeadersAccesses.push( +// ...getResponseHeadersAccesses( +// scopeManager.getDeclaredVariables(responseAccessParent.arguments[0]), +// scopeManager, +// sourceCode, +// ), +// ); +// continue; +// } - if ( - responseAccess.computed && - responseAccess.property.type === AST_NODE_TYPES.Literal && - responseAccessParent.type === AST_NODE_TYPES.MemberExpression - ) { - // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. - responseHeadersAccesses.push(responseAccessParent); - } else { - responseHeadersAccesses.push(responseAccess); - } - } - } - return responseHeadersAccesses; -} +// if ( +// responseAccess.computed && +// responseAccess.property.type === AST_NODE_TYPES.Literal && +// responseAccessParent.type === AST_NODE_TYPES.MemberExpression +// ) { +// // header access through indexed responses array, e.g. responses[0].headers, responses[1].get(...), etc. +// responseHeadersAccesses.push(responseAccessParent); +// } else { +// responseHeadersAccesses.push(responseAccess); +// } +// } +// } +// return responseHeadersAccesses; +// } const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); -const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shouldUseHeaderGetter'> = createRule({ +const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({ name: ruleId, meta: { type: 'suggestion', @@ -198,7 +198,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shoul }, messages: { preferNativeFetch: 'Prefer native fetch API over customized fixture API.', - shouldUseHeaderGetter: 'Getter should be used to access response headers.', + // shouldUseHeaderGetter: 'Getter should be used to access response headers.', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, fixable: 'code', @@ -213,7 +213,6 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shoul return { 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( fixtureCall: TSESTree.CallExpression, - // eslint-disable-next-line sonarjs/cognitive-complexity ) => { try { if (!hasAssertions(fixtureCall)) { @@ -294,51 +293,51 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch' | 'shoul }, }); - const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); - if (!responsesVariable) { - return; - } + // const responsesVariable = getEnclosingStatement(fixtureCallInformation.fixtureNode); + // if (!responsesVariable) { + // return; + // } - const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); - const responseHeadersAccesses = getResponseHeadersAccesses( - responseVariableReferences, - scopeManager, - sourceCode, - ); - for (const responseHeadersAccess of responseHeadersAccesses) { - if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { - const headerAccess = getParent(responseHeadersAccess); - if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) { - const headerNameNode = headerAccess.property; - const headerName = headerAccess.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; + // const responseVariableReferences = scopeManager.getDeclaredVariables(responsesVariable); + // const responseHeadersAccesses = getResponseHeadersAccesses( + // responseVariableReferences, + // scopeManager, + // sourceCode, + // ); + // for (const responseHeadersAccess of responseHeadersAccesses) { + // if (isInvalidResponseHeadersAccess(responseHeadersAccess)) { + // const headerAccess = getParent(responseHeadersAccess); + // if (headerAccess?.type === AST_NODE_TYPES.MemberExpression) { + // const headerNameNode = headerAccess.property; + // const headerName = headerAccess.computed + // ? sourceCode.getText(headerNameNode) + // : `'${sourceCode.getText(headerNameNode)}'`; + // const headerAccessReplacementText = `${sourceCode.getText(headerAccess.object)}.get(${headerName})`; - context.report({ - node: headerAccess, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(headerAccess, headerAccessReplacementText); - }, - }); - } else if ( - headerAccess?.type === AST_NODE_TYPES.CallExpression && - responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && - responseHeadersAccess.property.name === 'get' - ) { - const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; + // context.report({ + // node: headerAccess, + // messageId: 'shouldUseHeaderGetter', + // fix(fixer) { + // return fixer.replaceText(headerAccess, headerAccessReplacementText); + // }, + // }); + // } else if ( + // headerAccess?.type === AST_NODE_TYPES.CallExpression && + // responseHeadersAccess.property.type === AST_NODE_TYPES.Identifier && + // responseHeadersAccess.property.name === 'get' + // ) { + // const headerAccessReplacementText = `${sourceCode.getText(responseHeadersAccess.object)}.headers.get(${sourceCode.getText(headerAccess.arguments[0])})`; - context.report({ - node: headerAccess, - messageId: 'shouldUseHeaderGetter', - fix(fixer) { - return fixer.replaceText(headerAccess, headerAccessReplacementText); - }, - }); - } - } - } + // context.report({ + // node: headerAccess, + // messageId: 'shouldUseHeaderGetter', + // fix(fixer) { + // return fixer.replaceText(headerAccess, headerAccessReplacementText); + // }, + // }); + // } + // } + // } } catch (error) { // eslint-disable-next-line no-console console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index 29d23e0..c505a90 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -370,25 +370,6 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, - { - name: 'replace header access through response.get() with response.headers.get()', - code: ` - import { BASE_PATH } from './index'; - const response = await fixture.api.get(\`/sample-service/v2/ping\`).expect(StatusCodes.OK); - assert.equal(response.get(ETAG), correctVersion); - assert.equal(response.get('etag'), correctVersion); - `, - output: ` - import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.equal(response.headers.get('etag'), correctVersion); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', code: ` diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index 68aa00f..aa3b553 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -263,8 +263,8 @@ function getResponseVariableNameToUse( : // eslint-disable-next-line no-magic-numbers urlWithoutQuery.split('/').slice(3); - responseVariableNameBase = [...parts, methodName.toLocaleLowerCase()] - .map((part) => part.split(/[-=]/u)) + responseVariableNameBase = [...parts.filter((part) => part !== 'tenant'), methodName.toLocaleLowerCase()] + .map((part) => part.split(/[-]/u)) .flat() .filter((part) => part.trim() !== '' && !/\$\{.*\}/u.test(part)) // keep only non-empty parts that are not path parameters .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) @@ -343,7 +343,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat const { variable: responseVariable, bodyReferences: responseBodyReferences, - headersReferences: responseHeadersReferences, + // headersReferences: responseHeadersReferences, statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, destructuringStatusVariable: destructuringResponseStatusVariable, @@ -521,23 +521,23 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); } - // handle response headers references - for (const responseHeadersReference of responseHeadersReferences) { - const parent = getParent(responseHeadersReference); - assert.ok(parent); - let headerName; - if (parent.type === AST_NODE_TYPES.MemberExpression) { - const headerNameNode = parent.property; - headerName = parent.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - } else if (parent.type === AST_NODE_TYPES.CallExpression) { - const headerNameNode = parent.arguments[0]; - headerName = sourceCode.getText(headerNameNode); - } - assert.ok(headerName !== undefined); - yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); - } + // // handle response headers references + // for (const responseHeadersReference of responseHeadersReferences) { + // const parent = getParent(responseHeadersReference); + // assert.ok(parent); + // let headerName; + // if (parent.type === AST_NODE_TYPES.MemberExpression) { + // const headerNameNode = parent.property; + // headerName = parent.computed + // ? sourceCode.getText(headerNameNode) + // : `'${sourceCode.getText(headerNameNode)}'`; + // } else if (parent.type === AST_NODE_TYPES.CallExpression) { + // const headerNameNode = parent.arguments[0]; + // headerName = sourceCode.getText(headerNameNode); + // } + // assert.ok(headerName !== undefined); + // yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); + // } // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts index c8fd708..cbd5806 100644 --- a/src/agent/no-supertest.spec.ts +++ b/src/agent/no-supertest.spec.ts @@ -143,21 +143,6 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'replace header access through response.get() with response.headers.get()', - code: `async function test() { - const response = await ping().expect(StatusCodes.OK); - assert.equal(response.get(ETAG), correctVersion); - assert.equal(response.get('etag'), correctVersion); - }`, - output: `async function test() { - const response = await ping(); - assert.equal(response.status, StatusCodes.OK); - assert.equal(response.headers.get(ETAG), correctVersion); - assert.equal(response.headers.get('etag'), correctVersion); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', code: `async function test() { diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts index bf99547..6a1b8b4 100644 --- a/src/agent/no-supertest.ts +++ b/src/agent/no-supertest.ts @@ -314,7 +314,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat const { variable: responseVariable, bodyReferences: responseBodyReferences, - headersReferences: responseHeadersReferences, + // headersReferences: responseHeadersReferences, statusReferences: responseStatusReferences, destructuringBodyVariable: destructuringResponseBodyVariable, destructuringHeadersVariable: destructuringResponseHeadersVariable, @@ -457,23 +457,23 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); } - // handle response headers references - for (const responseHeadersReference of responseHeadersReferences) { - const parent = getParent(responseHeadersReference); - assert.ok(parent); - let headerName; - if (parent.type === AST_NODE_TYPES.MemberExpression) { - const headerNameNode = parent.property; - headerName = parent.computed - ? sourceCode.getText(headerNameNode) - : `'${sourceCode.getText(headerNameNode)}'`; - } else if (parent.type === AST_NODE_TYPES.CallExpression) { - const headerNameNode = parent.arguments[0]; - headerName = sourceCode.getText(headerNameNode); - } - assert.ok(headerName !== undefined); - yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); - } + // // handle response headers references + // for (const responseHeadersReference of responseHeadersReferences) { + // const parent = getParent(responseHeadersReference); + // assert.ok(parent); + // let headerName; + // if (parent.type === AST_NODE_TYPES.MemberExpression) { + // const headerNameNode = parent.property; + // headerName = parent.computed + // ? sourceCode.getText(headerNameNode) + // : `'${sourceCode.getText(headerNameNode)}'`; + // } else if (parent.type === AST_NODE_TYPES.CallExpression) { + // const headerNameNode = parent.arguments[0]; + // headerName = sourceCode.getText(headerNameNode); + // } + // assert.ok(headerName !== undefined); + // yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); + // } // convert response.statusCode to response.status for (const responseStatusReference of responseStatusReferences) { diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index db47280..bb1fc52 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -28,7 +28,7 @@ export function analyzeResponseReferences( ): { variable?: Variable; bodyReferences: TSESTree.MemberExpression[]; - headersReferences: TSESTree.MemberExpression[]; + // headersReferences: TSESTree.MemberExpression[]; statusReferences: TSESTree.MemberExpression[]; destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; @@ -38,7 +38,7 @@ export function analyzeResponseReferences( const results: { variable?: Variable; bodyReferences: TSESTree.MemberExpression[]; - headersReferences: TSESTree.MemberExpression[]; + // headersReferences: TSESTree.MemberExpression[]; statusReferences: TSESTree.MemberExpression[]; destructuringBodyVariable?: Variable | TSESTree.ObjectPattern; destructuringHeadersVariable?: Variable | TSESTree.ObjectPattern; @@ -46,7 +46,7 @@ export function analyzeResponseReferences( destructuringHeadersReferences?: TSESTree.MemberExpression[] | undefined; } = { bodyReferences: [], - headersReferences: [], + // headersReferences: [], statusReferences: [], }; if (!variableDeclaration) { @@ -72,13 +72,13 @@ export function analyzeResponseReferences( node.property.type === AST_NODE_TYPES.Identifier && node.property.name === 'body', ); - // e.g. response.headers / response.header / response.get() - results.headersReferences = responseReferences.filter( - (node): node is TSESTree.MemberExpression => - node?.type === AST_NODE_TYPES.MemberExpression && - node.property.type === AST_NODE_TYPES.Identifier && - (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), - ); + // // e.g. response.headers / response.header / response.get() + // results.headersReferences = responseReferences.filter( + // (node): node is TSESTree.MemberExpression => + // node?.type === AST_NODE_TYPES.MemberExpression && + // node.property.type === AST_NODE_TYPES.Identifier && + // (node.property.name === 'header' || node.property.name === 'headers' || node.property.name === 'get'), + // ); // e.g. response.status / response.statusCode results.statusReferences = responseReferences.filter( (node): node is TSESTree.MemberExpression => From fb24e0ddd57b240a47e9f803f9a83a216fbc6fd0 Mon Sep 17 00:00:00 2001 From: Le Cong Date: Wed, 27 Nov 2024 21:45:57 -0500 Subject: [PATCH 115/115] major refactor: separate fixture call and expect assertion handling --- src/agent/fetch-response-body-json.ts | 2 +- src/agent/fetch.ts | 6 +- src/agent/no-expect-assertion.spec.ts | 503 +++++++++++++++++++++++ src/agent/no-expect-assertion.ts | 570 ++++++++++++++++++++++++++ src/agent/no-fixture.spec.ts | 507 +++-------------------- src/agent/no-fixture.ts | 470 +++------------------ src/agent/no-supertest.spec.ts | 389 ------------------ src/agent/no-supertest.ts | 517 ----------------------- src/agent/response-reference.ts | 2 +- src/agent/supertest-then.spec.ts | 2 +- src/index.ts | 32 +- 11 files changed, 1209 insertions(+), 1791 deletions(-) create mode 100644 src/agent/no-expect-assertion.spec.ts create mode 100644 src/agent/no-expect-assertion.ts delete mode 100644 src/agent/no-supertest.spec.ts delete mode 100644 src/agent/no-supertest.ts diff --git a/src/agent/fetch-response-body-json.ts b/src/agent/fetch-response-body-json.ts index d263a99..3899756 100644 --- a/src/agent/fetch-response-body-json.ts +++ b/src/agent/fetch-response-body-json.ts @@ -38,7 +38,7 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'replaceBodyWithJson' | 'ref }, messages: { refactorNeeded: - 'Please extract the fetch call and check its reponse status code before accessing its response body.', + 'Please extract the fetch call and check its response status code before accessing its response body.', replaceBodyWithJson: 'Replace "response.body" with "await response.json()".', unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', }, diff --git a/src/agent/fetch.ts b/src/agent/fetch.ts index 4f3afd7..0977bbf 100644 --- a/src/agent/fetch.ts +++ b/src/agent/fetch.ts @@ -63,7 +63,9 @@ export function hasAssertions(fixtureCall: TSESTree.Node): boolean { export function isFetchResponse(type: ts.Type): boolean { return ( - type.getProperties().some((symbol) => symbol.name === 'body') && - type.getProperties().some((symbol) => symbol.name === 'json') + type.getProperties().some((symbol) => symbol.name === 'json') && + type.getProperties().some((symbol) => symbol.name === 'status') && + type.getProperties().some((symbol) => symbol.name === 'headers') && + type.getProperties().some((symbol) => symbol.name === 'body') ); } diff --git a/src/agent/no-expect-assertion.spec.ts b/src/agent/no-expect-assertion.spec.ts new file mode 100644 index 0000000..8733a2d --- /dev/null +++ b/src/agent/no-expect-assertion.spec.ts @@ -0,0 +1,503 @@ +// agent/no-expect-assertion.spec.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import createTester from '../ts-tester.test'; +import rule, { ruleId } from './no-expect-assertion'; + +createTester().run(ruleId, rule, { + valid: [ + { + name: 'leave non-API calls as is', + code: ` + function foo() { + return 'bar'; + } + async function test() { + foo().expect(StatusCodes.OK); + }`, + }, + { + name: 'leave fixture.api.xxx() calls as is, which will wait to be converted to fetch calls first', + code: ` + async function test() { + await fixture.api.get('/ping/v1/ping').expect(StatusCodes.OK); + }`, + }, + ], + invalid: [ + { + name: 'assertion without variable declaration', + code: `async function test() { + await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion without variable declaration - with status code as number instead of StatusCodes enum value', + code: `async function test() { + await fetch('/ping/v1/ping').expect(200); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, 200); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion without variable declaration - with url as template literal', + code: `async function test() { + await fetch(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`); + assert.equal(pingGetResponse.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion without variable declaration - with RequestInit argument', + code: `async function test() { + await fetch('/ping/v1/ping', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }).expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingPutResponse = await fetch('/ping/v1/ping', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }); + assert.equal(pingPutResponse.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion without variable declaration - using utility function instead of fetch', + code: `function ping() { + return fetch('/ping/v1/ping'); + } + async function test() { + await ping().expect(StatusCodes.OK); + }`, + output: `function ping() { + return fetch('/ping/v1/ping'); + } + async function test() { + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion without variable declaration - using utility function with nested reference', + code: `function ping() { + return fetch('/ping/v1/ping'); + } + const util = { ping }; + async function test() { + await util.ping().expect(StatusCodes.OK); + }`, + output: `function ping() { + return fetch('/ping/v1/ping'); + } + const util = { ping }; + async function test() { + const pingResponse = await util.ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assertion with variable declaration', + code: `async function test() { + const pingResponse = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + assert(pingResponse); + }`, + output: `async function test() { + const pingResponse = await fetch('/ping/v1/ping'); + assert.equal(pingResponse.status, StatusCodes.OK); + assert(pingResponse); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'response headers assertion', + code: `async function test() { + await fetch('/ping/v1/ping') + .expect(StatusCodes.OK) + .expect('etag', '123') + .expect('content-type', 'application/json') + .expect(ETAG, correctVersion) + .expect(ETAG, /1.*/u); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + assert.equal(pingGetResponse.headers.get('etag'), '123'); + assert.equal(pingGetResponse.headers.get('content-type'), 'application/json'); + assert.equal(pingGetResponse.headers.get(ETAG), correctVersion); + assert.ok(pingGetResponse.headers.get(ETAG).match(/1.*/u)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'response body assertion', + code: `async function test() { + await fetch('/ping/v1/ping').expect({message:'pong'}); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.deepEqual(await pingGetResponse.json(), {message:'pong'}); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'response callback assertion', + code: `async function test() { + await fetch('/ping/v1/ping') + .expect(validate) + .expect((response)=>console.log(response)); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.doesNotThrow(()=>validate(pingGetResponse)); + assert.doesNotThrow(()=>console.log(pingGetResponse)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'multiple fetch calls in the same test', + code: `function ping() { + return fetch('/ping/v1/ping'); + } + async function test() { + await ping().expect(StatusCodes.OK); + const pingResponse = await ping().expect(StatusCodes.OK); + await ping().expect(StatusCodes.OK).expect({message:'pong'}); + await ping().expect(StatusCodes.OK); + }`, + output: `function ping() { + return fetch('/ping/v1/ping'); + } + async function test() { + const pingResponse1 = await ping(); + assert.equal(pingResponse1.status, StatusCodes.OK); + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + const pingResponse2 = await ping(); + assert.equal(pingResponse2.status, StatusCodes.OK); + assert.deepEqual(await pingResponse2.json(), {message:'pong'}); + const pingResponse3 = await ping(); + assert.equal(pingResponse3.status, StatusCodes.OK); + }`, + errors: [ + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + { messageId: 'preferNativeFetch' }, + ], + }, + { + name: 'response variable names in different scope do not conflict with each other', + code: `function ping() { + return fetch('/ping/v1/ping'); + } + it('#1', async () => { + const pingResponse = 'foo'; + }); + it('#2', async () => { + const pingResponse = 'foo'; + await ping().expect(StatusCodes.OK); + }); + it('#3', async () => { + const pingResponse3 = 'foo'; + await ping().expect(StatusCodes.OK); + }); + `, + output: `function ping() { + return fetch('/ping/v1/ping'); + } + it('#1', async () => { + const pingResponse = 'foo'; + }); + it('#2', async () => { + const pingResponse = 'foo'; + const pingResponse1 = await ping(); + assert.equal(pingResponse1.status, StatusCodes.OK); + }); + it('#3', async () => { + const pingResponse3 = 'foo'; + const pingResponse = await ping(); + assert.equal(pingResponse.status, StatusCodes.OK); + }); + `, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], + }, + { + name: 'directly return (no await) fetch call with assertion', + code: `async function test() { + return fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + return pingGetResponse; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assert response body against function call return value', + code: `async function test() { + const createdOn = Date.now().toUTCString(); + await fetch('/ping/v1/ping').expect(200).expect(validateBody(createdOn)); + }`, + output: `async function test() { + const createdOn = Date.now().toUTCString(); + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, 200); + assert.deepEqual(await pingGetResponse.json(), validateBody(createdOn)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle destructuring variable declaration for body', + code: `async function test() { + const { body: responseBody } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const responseBody = await pingGetResponse.json(); + const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); + assert.ok(timeDifference >= 0 && timeDifference < 200); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle destructuring variable declaration for body - with nested destructuring', + code: `async function test() { + const { body: { pgpPublicKey: firstPgpPublicKey } } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const { pgpPublicKey: firstPgpPublicKey } = await pingGetResponse.json(); + assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle destructuring variable declaration for headers when body is presented as well', + code: `async function test() { + const { body, headers: headers2 } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + assert(body); + assert.ok(headers2.get(ETAG)); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const body = await pingGetResponse.json(); + const headers2 = pingGetResponse.headers; + assert(body); + assert.ok(headers2.get(ETAG)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'handle destructuring variable declaration for headers without body presented but with assertions used', + code: `async function test() { + const { header } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + assert.ok(header.get(ETAG)); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const header = pingGetResponse.headers; + assert.ok(header.get(ETAG)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'nested header destructuring', + code: `async function test() { + const { headers: { etag } } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const etag = pingGetResponse.headers.get('etag'); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'nested header destructuring - string literal key with renaming', + code: `async function test() { + const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const createdOn = pingGetResponse.headers.get('created-on'); + const updatedOn = pingGetResponse.headers.get('updated-on'); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'inline access to response body should be extracted to a variable', + code: `async function test() { + const paymentSecurityServicePublicKey = (await fetch('/ping/v1/ping').expect(StatusCodes.OK)).body.publicKey; + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const pingGetResponseBody = await pingGetResponse.json(); + const paymentSecurityServicePublicKey = pingGetResponseBody.publicKey; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', + code: `async function test() { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + const pingResponse = await fetch('/ping/v1/ping') + .expect(StatusCodes.NO_CONTENT) + .expect(ETAG_HEADER, '1') + .expect((res) => verifyTemporalHeaders(res, createdOn)); + }`, + output: `async function test() { + const createdOn = new Date().toISOString(); + const zoneKeyId = uuid(); + + // Import Key + const keyId = uuid(); + const pingResponse = await fetch('/ping/v1/ping'); + assert.equal(pingResponse.status, StatusCodes.NO_CONTENT); + assert.equal(pingResponse.headers.get(ETAG_HEADER), '1'); + assert.doesNotThrow(()=>verifyTemporalHeaders(pingResponse, createdOn)); + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'assignment statement instead of variable declaration used for subsequent fixture calls', + code: `async function test() { + let response = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + response = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + let response = await fetch('/ping/v1/ping'); + assert.equal(response.status, StatusCodes.OK); + response = await fetch('/ping/v1/ping'); + assert.equal(response.status, StatusCodes.OK); + }`, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], + }, + { + name: 'statusCode destructuring should be renamed', + code: `async function test() { + const { statusCode } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const statusCode = pingGetResponse.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'statusCode destructuring should be renamed - with renaming in ObjectPattern', + code: `async function test() { + const { statusCode: pingStatusCode } = await fetch('/ping/v1/ping').expect(StatusCodes.OK); + }`, + output: `async function test() { + const pingGetResponse = await fetch('/ping/v1/ping'); + assert.equal(pingGetResponse.status, StatusCodes.OK); + const pingStatusCode = pingGetResponse.status; + }`, + errors: [{ messageId: 'preferNativeFetch' }], + }, + { + name: 'inside Promise.all', + code: ` + const responses = await Promise.all([ + fetch('/ping/v1/ping').expect(StatusCodes.NO_CONTENT), + fetch('/ping/v1/ping').expect(StatusCodes.NO_CONTENT), + ]); + `, + output: ` + const responses = await Promise.all([ + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch('/ping/v1/ping').then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch('/ping/v1/ping').then((res) => { + assert.equal(res.status, StatusCodes.NO_CONTENT); + return res; + }), + ]); + `, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], + }, + { + name: 'in non-async arrow function with concurrent promises', + code: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + fetch(\`\${BASE_PATH}/zone-key/\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify(requestWithPropertyMissing), + }).expect(StatusCodes.BAD_REQUEST) + ); + }), + ); + `, + output: ` + await Promise.all( + Object.keys(zoneKeyPartImportRequest).map((propertyName) => { + const requestWithPropertyMissing = omit( + zoneKeyPartImportRequest, + propertyName, + ); + return ( + // eslint-disable-next-line @checkdigit/no-promise-instance-method + fetch(\`\${BASE_PATH}/zone-key/\${zoneKeyId}\`, { + method: 'PUT', + body: JSON.stringify(requestWithPropertyMissing), + }).then((res) => { + assert.equal(res.status, StatusCodes.BAD_REQUEST); + return res; + }) + ); + }), + ); + `, + errors: [{ messageId: 'preferNativeFetch' }], + }, + ], +}); diff --git a/src/agent/no-expect-assertion.ts b/src/agent/no-expect-assertion.ts new file mode 100644 index 0000000..20f0149 --- /dev/null +++ b/src/agent/no-expect-assertion.ts @@ -0,0 +1,570 @@ +// agent/no-expect-assertion.ts + +/* + * Copyright (c) 2021-2024 Check Digit, LLC + * + * This code is licensed under the MIT license (see LICENSE.txt for details). + */ + +import { strict as assert } from 'node:assert'; + +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; +import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; +import ts from 'typescript'; + +import { + getEnclosingFunction, + getEnclosingScopeNode, + getEnclosingStatement, + getParent, + isUsedInArrayOrAsArgument, +} from '../library/ts-tree'; +import getDocumentationUrl from '../get-documentation-url'; +import { getIndentation } from '../library/format'; +import { analyzeResponseReferences } from './response-reference'; +import { + getResponseBodyRetrievalText, + getResponseHeadersRetrievalText, + getResponseStatusRetrievalText, + isFetchResponse, +} from './fetch'; + +export const ruleId = 'no-expect-assertion'; + +interface FixtureCallInformation { + rootNode: + | TSESTree.AwaitExpression + | TSESTree.ReturnStatement + | TSESTree.VariableDeclaration + | TSESTree.CallExpression + | TSESTree.ExpressionStatement; + fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; + variableDeclaration?: TSESTree.VariableDeclaration; + variableAssignment?: TSESTree.ExpressionStatement; + assertions?: TSESTree.Expression[][]; + inlineStatementNode?: TSESTree.Node; + inlineBodyReference?: TSESTree.MemberExpression; + inlineStatusReference?: TSESTree.MemberExpression; + inlineHeadersReference?: TSESTree.MemberExpression; +} + +// recursively analyze the fixture/supertest call chain to collect information of request/response +// eslint-disable-next-line sonarjs/cognitive-complexity +function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { + const parent = getParent(call); + assert.ok(parent, 'parent should exist for fixture/supertest call node'); + + let nextCall; + if (parent.type === AST_NODE_TYPES.ReturnStatement) { + // direct return, no variable declaration or await + results.fixtureNode = call; + results.rootNode = parent; + } else if ( + parent.type === AST_NODE_TYPES.ArrayExpression || + parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.ArrowFunctionExpression + ) { + // direct return, no variable declaration or await + results.fixtureNode = call; + results.rootNode = call; + } else if (parent.type === AST_NODE_TYPES.AwaitExpression) { + results.fixtureNode = call; + const enclosingStatement = getEnclosingStatement(parent); + assert.ok(enclosingStatement); + const awaitParent = getParent(parent); + if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) { + results.rootNode = parent; + results.inlineStatementNode = enclosingStatement; + if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { + results.inlineBodyReference = awaitParent; + } + if ( + awaitParent.property.type === AST_NODE_TYPES.Identifier && + (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode') + ) { + results.inlineStatusReference = awaitParent; + } + if ( + awaitParent.property.type === AST_NODE_TYPES.Identifier && + (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') + ) { + results.inlineHeadersReference = awaitParent; + } + } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) { + results.variableDeclaration = enclosingStatement; + results.rootNode = enclosingStatement; + } else if ( + enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement && + enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression + ) { + results.variableAssignment = enclosingStatement; + results.rootNode = enclosingStatement; + } else { + results.rootNode = parent; + } + } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { + if (parent.property.name === 'expect') { + // supertest assertions + const assertionCall = getParent(parent); + assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); + results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; + nextCall = assertionCall; + } + } else { + throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); + } + if (nextCall) { + analyzeFixtureCall(nextCall, results, sourceCode); + } +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function createResponseAssertions( + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + responseVariableName: string, + destructuringResponseHeadersVariable: Variable | undefined, +) { + let statusAssertion: string | undefined; + const nonStatusAssertions: string[] = []; + for (const expectArguments of fixtureCallInformation.assertions ?? []) { + if (expectArguments.length === 1) { + const [assertionArgument] = expectArguments; + assert.ok(assertionArgument); + if ( + (assertionArgument.type === AST_NODE_TYPES.MemberExpression && + assertionArgument.object.type === AST_NODE_TYPES.Identifier && + assertionArgument.object.name === 'StatusCodes') || + assertionArgument.type === AST_NODE_TYPES.Literal || + sourceCode.getText(assertionArgument).includes('StatusCodes.') + ) { + // status code assertion + statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; + } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { + // callback assertion using arrow function + let functionBody = sourceCode.getText(assertionArgument.body); + + const [originalResponseArgument] = assertionArgument.params; + assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); + const originalResponseArgumentName = originalResponseArgument.name; + if (originalResponseArgumentName !== responseVariableName) { + functionBody = functionBody.replace( + new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), + responseVariableName, + ); + } + nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); + } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { + // callback assertion using function reference + nonStatusAssertions.push( + `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, + ); + } else if ( + assertionArgument.type === AST_NODE_TYPES.ObjectExpression || + assertionArgument.type === AST_NODE_TYPES.CallExpression + ) { + // body deep equal assertion + nonStatusAssertions.push( + `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, + ); + } else { + throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); + } + } else if (expectArguments.length === 2) { + // header assertion + const [headerName, headerValue] = expectArguments; + assert.ok(headerName && headerValue); + const headersReference = + destructuringResponseHeadersVariable !== undefined + ? destructuringResponseHeadersVariable.name + : `${responseVariableName}.headers`; + if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { + nonStatusAssertions.push( + `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, + ); + } else { + nonStatusAssertions.push( + `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, + ); + } + } + } + return { + statusAssertion, + nonStatusAssertions, + }; +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +function getResponseVariableNameToUse( + fetchFunction: TSESTree.CallExpression, + fixtureCallInformation: FixtureCallInformation, + sourceCode: SourceCode, + scopeManager: ScopeManager, + scopeVariablesMap: Map, +) { + // use existing variable assignment if it's already defined + if (fixtureCallInformation.variableAssignment) { + assert.ok( + fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression && + fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier, + ); + return fixtureCallInformation.variableAssignment.expression.left.name; + } + + // use existing variable declaration if it's already defined + if (fixtureCallInformation.variableDeclaration) { + const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) { + return firstDeclaration.id.name; + } + } + + // prepare scope variables for checking if the variable name is already used + const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode); + assert.ok(enclosingScopeNode); + const scope = scopeManager.acquire(enclosingScopeNode); + assert.ok(scope); + let scopeVariables = scopeVariablesMap.get(scope); + if (!scopeVariables) { + scopeVariables = [...scope.set.keys()]; + scopeVariablesMap.set(scope, scopeVariables); + } + + let responseVariableNameBase: string | undefined; + if (fetchFunction.callee.type === AST_NODE_TYPES.Identifier && fetchFunction.callee.name === 'fetch') { + const [urlArg, initArg] = fetchFunction.arguments; + if (urlArg?.type === AST_NODE_TYPES.Literal || urlArg?.type === AST_NODE_TYPES.TemplateLiteral) { + const urlValue = urlArg.type === AST_NODE_TYPES.Literal ? String(urlArg.value) : sourceCode.getText(urlArg); + + const urlWithoutQuotes = urlValue.replace(/['"`]/gu, ''); + const urlWithoutQuery = urlWithoutQuotes.includes('?') + ? urlWithoutQuotes.slice(0, urlWithoutQuotes.indexOf('?')) + : urlWithoutQuotes; + const parts = urlWithoutQuery.startsWith('${') + ? urlWithoutQuery.split('/').slice(1) + : // eslint-disable-next-line no-magic-numbers + urlWithoutQuery.split('/').slice(3); + + let methodName; + if (initArg?.type === AST_NODE_TYPES.ObjectExpression) { + methodName = /method:\s*['"`](?\w+)['"`]/u.exec(sourceCode.getText(initArg))?.groups?.['method']; + } + methodName ??= 'GET'; + responseVariableNameBase = [...parts.filter((part) => part !== 'tenant'), methodName.toLowerCase()] + .map((part) => part.split(/[-]/u)) + .flat() + .filter((part) => part.trim() !== '' && !/\$\{.*\}/u.test(part)) // remove path parameter placeholders + .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) + .join(''); + responseVariableNameBase = `${responseVariableNameBase[0]?.toLowerCase() ?? ''}${responseVariableNameBase.slice(1)}`; + } + } else { + // this should be the case that a reference to utility function is used + const fullUtilityFunctionReference = sourceCode.getText(fetchFunction.callee); + responseVariableNameBase = fullUtilityFunctionReference.split('.').pop(); + } + responseVariableNameBase = + responseVariableNameBase === undefined ? 'response' : `${responseVariableNameBase}Response`; + + let responseVariableCounter = 0; + let responseVariableNameToUse = responseVariableNameBase; + while (scopeVariables.includes(responseVariableNameToUse)) { + responseVariableCounter++; + responseVariableNameToUse = `${responseVariableNameBase}${String(responseVariableCounter)}`; + } + scopeVariables.push(responseVariableNameToUse); + return responseVariableNameToUse; +} + +function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean { + const parent = getParent(responseBodyReference); + return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier; +} + +const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); + +const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({ + name: ruleId, + meta: { + type: 'suggestion', + docs: { + description: 'Transform supertest assersions to regular node assertions.', + url: getDocumentationUrl(ruleId), + }, + messages: { + preferNativeFetch: 'Transform supertest assersions to regular node assertions.', + unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', + }, + fixable: 'code', + schema: [], + }, + defaultOptions: [], + // eslint-disable-next-line max-lines-per-function + create(context) { + const sourceCode = context.sourceCode; + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + const scopeManager = sourceCode.scopeManager; + assert.ok(scopeManager !== null); + const scopeVariablesMap = new Map(); + + return { + // eslint-disable-next-line max-lines-per-function + 'CallExpression[callee.property.name="expect"]': ( + expectCall: TSESTree.CallExpression, + // eslint-disable-next-line sonarjs/cognitive-complexity + ) => { + try { + if ( + expectCall.callee.type !== AST_NODE_TYPES.MemberExpression || + expectCall.callee.object.type !== AST_NODE_TYPES.CallExpression + ) { + return; + } + + // Check if it's a Promise like object + const calleeObject = expectCall.callee.object; + const calleeObjectTsNode = parserServices.esTreeNodeToTSNodeMap.get(calleeObject); + const calleeObjectType = typeChecker.getTypeAtLocation(calleeObjectTsNode); + const calleeObjectTypeSymbol = calleeObjectType.getSymbol(); + if (!calleeObjectTypeSymbol || calleeObjectTypeSymbol.name !== 'Promise') { + return; + } + const [calleeObjectPromiseType] = typeChecker.getTypeArguments(calleeObjectType as ts.TypeReference); + if (calleeObjectPromiseType === undefined || !isFetchResponse(calleeObjectPromiseType)) { + return; + } + + const indentation = getIndentation(expectCall, sourceCode); + + const fixtureCallInformation = {} as FixtureCallInformation; + const fetchFunction = expectCall.callee.object; + analyzeFixtureCall(fetchFunction, fixtureCallInformation, sourceCode); + + const { + variable: responseVariable, + bodyReferences: responseBodyReferences, + // headersReferences: responseHeadersReferences, + statusReferences: responseStatusReferences, + destructuringBodyVariable: destructuringResponseBodyVariable, + destructuringHeadersVariable: destructuringResponseHeadersVariable, + destructuringStatusVariable: destructuringResponseStatusVariable, + } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); + + const shouldUsePromiseThen = + isUsedInArrayOrAsArgument(expectCall) || getEnclosingFunction(expectCall)?.async === false; + if (shouldUsePromiseThen) { + const responseVariableNameToUse = 'res'; + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + destructuringResponseHeadersVariable as Variable | undefined, + ); + const fetchCallText = sourceCode.getText(fetchFunction); + const disableLintComment = '// eslint-disable-next-line @checkdigit/no-promise-instance-method'; + const appendingAssignmentAndAssertionText = [ + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + const replacementText = fixtureCallInformation.assertions + ? [ + disableLintComment, + `${fetchCallText}.then((${responseVariableNameToUse}) => {`, + appendingAssignmentAndAssertionText === '' ? '' : ` ${appendingAssignmentAndAssertionText};`, + ` return ${responseVariableNameToUse};`, + `})`, + ].join(`\n${indentation}`) + : fetchCallText; + context.report({ + node: fixtureCallInformation.rootNode, + messageId: 'preferNativeFetch', + fix(fixer) { + return fixer.replaceText(fixtureCallInformation.fixtureNode, replacementText); + }, + }); + } else { + const responseVariableNameToUse = getResponseVariableNameToUse( + fetchFunction, + fixtureCallInformation, + sourceCode, + scopeManager, + scopeVariablesMap, + ); + + const isResponseBodyVariableRedefinitionNeeded = + destructuringResponseBodyVariable !== undefined || + fixtureCallInformation.inlineBodyReference !== undefined || + (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); + const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; + + const isResponseStatusVariableRedefinitionNeeded = + destructuringResponseStatusVariable !== undefined || + fixtureCallInformation.inlineStatusReference !== undefined; + const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`; + + const isResponseHeadersVariableRedefinitionNeeded = + (destructuringResponseHeadersVariable !== undefined && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === + AST_NODE_TYPES.ObjectPattern) || + fixtureCallInformation.inlineHeadersReference !== undefined; + const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; + + const isResponseVariableRedefinitionNeeded = + (fixtureCallInformation.variableAssignment === undefined && + responseVariable === undefined && + fixtureCallInformation.assertions !== undefined) || + isResponseBodyVariableRedefinitionNeeded || + isResponseStatusVariableRedefinitionNeeded || + isResponseHeadersVariableRedefinitionNeeded; + + const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded + ? [ + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseBodyVariable + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseBodyVariableRedefinitionNeeded + ? [ + `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, + ] + : []), + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseStatusVariable + ? [ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseStatusVariableRedefinitionNeeded + ? [ + `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, + ] + : []), + // eslint-disable-next-line no-nested-ternary + ...(destructuringResponseHeadersVariable + ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === + AST_NODE_TYPES.ObjectPattern + ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => { + assert.ok(property.type === AST_NODE_TYPES.Property); + assert.ok(property.value.type === AST_NODE_TYPES.Identifier); + // eslint-disable-next-line sonarjs/no-nested-template-literals + return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; + }) + : [ + `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : isResponseHeadersVariableRedefinitionNeeded + ? [ + `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, + ] + : []), + ] + : []; + + const { statusAssertion, nonStatusAssertions } = createResponseAssertions( + fixtureCallInformation, + sourceCode, + responseVariableNameToUse, + destructuringResponseHeadersVariable as Variable | undefined, + ); + + // add variable declaration if needed + const fetchCallText = sourceCode.getText(fetchFunction); + const fetchStatementText = !isResponseVariableRedefinitionNeeded + ? fetchCallText + : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`; + + const nodeToReplace = isResponseVariableRedefinitionNeeded + ? fixtureCallInformation.rootNode + : fixtureCallInformation.fixtureNode; + const appendingAssignmentAndAssertionText = [ + '', + ...(statusAssertion !== undefined ? [statusAssertion] : []), + ...responseBodyHeadersVariableRedefineLines, + ...nonStatusAssertions, + ].join(`;\n${indentation}`); + + context.report({ + node: expectCall, + messageId: 'preferNativeFetch', + + *fix(fixer) { + if (fixtureCallInformation.inlineStatementNode) { + const preInlineDeclaration = [ + fetchStatementText, + `${appendingAssignmentAndAssertionText};\n${indentation}`, + ].join(``); + yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration); + } else { + yield fixer.replaceText(nodeToReplace, fetchStatementText); + + const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); + yield fixer.insertTextAfter( + nodeToReplace, + needEndingSemiColon + ? `${appendingAssignmentAndAssertionText};` + : appendingAssignmentAndAssertionText, + ); + } + + // handle response body references + for (const responseBodyReference of responseBodyReferences) { + yield fixer.replaceText( + responseBodyReference, + isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference) + ? redefineResponseBodyVariableName + : getResponseBodyRetrievalText(responseVariableNameToUse), + ); + } + if (fixtureCallInformation.inlineBodyReference) { + yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); + } + + // convert response.statusCode to response.status + for (const responseStatusReference of responseStatusReferences) { + if ( + responseStatusReference.property.type === AST_NODE_TYPES.Identifier && + responseStatusReference.property.name === 'statusCode' + ) { + yield fixer.replaceText(responseStatusReference.property, `status`); + } + } + + // handle direct return statement without await, e.g. "return fixture.api.get(...);" + if ( + fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement && + fixtureCallInformation.assertions !== undefined + ) { + yield fixer.insertTextAfter( + fixtureCallInformation.rootNode, + `\n${indentation}return ${responseVariableNameToUse};`, + ); + } + }, + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); + context.report({ + node: expectCall, + messageId: 'unknownError', + data: { + fileName: context.filename, + error: error instanceof Error ? error.toString() : JSON.stringify(error), + }, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/src/agent/no-fixture.spec.ts b/src/agent/no-fixture.spec.ts index c505a90..5737522 100644 --- a/src/agent/no-fixture.spec.ts +++ b/src/agent/no-fixture.spec.ts @@ -10,20 +10,10 @@ import createTester from '../ts-tester.test'; import rule, { ruleId } from './no-fixture'; createTester().run(ruleId, rule, { - valid: [ - { - name: 'skip concurrent fixture calls which will be handled in concurrent-promises rule', - code: ` - const responses = await Promise.all([ - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), - fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData).expect(StatusCodes.NO_CONTENT), - ]); - `, - }, - ], + valid: [], invalid: [ { - name: 'without assertions', + name: 'concurrent fixture calls inside Promise.all() - without assertions', code: ` const responses = await Promise.all([ fixture.api.put(\`\${BASE_PATH}/key\`).send(keyData), @@ -44,77 +34,80 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], }, + { + name: 'concurrent fixture calls inside Promise.all() - with assertions', + code: `const responses = await Promise.all([ + fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK), + fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK), + ]);`, + output: `const responses = await Promise.all([ + fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }) + .expect(StatusCodes.OK), + fetch(\`\${BASE_PATH}/ping\`, { + method: 'GET', + }) + .expect(StatusCodes.OK), + ]);`, + errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], + }, { name: 'assertion with variable declaration', code: ` - import { BASE_PATH } from './index'; const pingResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); - const body = pingResponse.body; - const timeDifference = Date.now() - new Date(body.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); `, output: ` - import { BASE_PATH } from './index'; const pingResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingResponse.status, StatusCodes.OK); - const body = await pingResponse.json(); - const timeDifference = Date.now() - new Date(body.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); + }) + .expect(StatusCodes.OK); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assertion without variable declaration', code: ` - import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); `, output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); + }) + .expect(StatusCodes.OK); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assertion without variable declaration - complex status assertion argument', code: ` - import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v1/ping\`).expect(options.expectedStatusCode ?? StatusCodes.CREATED); `, output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, options.expectedStatusCode ?? StatusCodes.CREATED); + }) + .expect(options.expectedStatusCode ?? StatusCodes.CREATED); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'PUT with request body', code: ` - import { BASE_PATH } from './index'; await fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`).send(cardCreationData).expect(StatusCodes.BAD_REQUEST); `, output: ` - import { BASE_PATH } from './index'; - const cardPutResponse = await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { + await fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { method: 'PUT', body: JSON.stringify(cardCreationData), - }); - assert.equal(cardPutResponse.status, StatusCodes.BAD_REQUEST); + }) + .expect(StatusCodes.BAD_REQUEST); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'PUT with request header', code: ` - import { BASE_PATH } from './index'; const noFraudResponse = await fixture.api .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .set(IF_MATCH_HEADER, originalCard.version) @@ -123,7 +116,6 @@ createTester().run(ruleId, rule, { .expect(StatusCodes.NO_CONTENT); `, output: ` - import { BASE_PATH } from './index'; const noFraudResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'POST', headers: { @@ -131,15 +123,14 @@ createTester().run(ruleId, rule, { abc: originalCard.name, 'x-y-z': '123', }, - }); - assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); + }) + .expect(StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'set request header with "!" (non-null assertion operator)', code: ` - import { BASE_PATH } from './index'; const noFraudResponse = await fixture.api .post(\`\${BASE_PATH}/ping\`) .set(IF_MATCH_HEADER, originalCard.version!) @@ -147,113 +138,50 @@ createTester().run(ruleId, rule, { .expect(StatusCodes.NO_CONTENT); `, output: ` - import { BASE_PATH } from './index'; const noFraudResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'POST', headers: { [IF_MATCH_HEADER]: originalCard.version!, 'x-y-z': headers[ETAG]!, }, - }); - assert.equal(noFraudResponse.status, StatusCodes.NO_CONTENT); + }) + .expect(StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'POST without request header/body', code: ` - import { BASE_PATH } from './index'; await fixture.api .post(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .expect(StatusCodes.NO_CONTENT); `, output: ` - import { BASE_PATH } from './index'; - const cardBlockPostResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'POST', - }); - assert.equal(cardBlockPostResponse.status, StatusCodes.NO_CONTENT); + }) + .expect(StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'replace del with DELETE', code: ` - import { BASE_PATH } from './index'; await fixture.api .del(\`/sample-service/v2/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`) .expect(StatusCodes.NO_CONTENT); `, output: ` - import { BASE_PATH } from './index'; - const cardBlockDeleteResponse = await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { + await fetch(\`\${BASE_PATH}/card/\${originalCard.card.cardId}/block/\${encodeURIComponent('BLOCKED NO FRAUD')}\`, { method: 'DELETE', - }); - assert.equal(cardBlockDeleteResponse.status, StatusCodes.NO_CONTENT); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response headers assertion should be externalized with new variable declared if necessary', - code: ` - import { BASE_PATH } from './index'; - await fixture.api.get(\`/sample-service/v2/ping\`) - .expect(StatusCodes.OK) - .expect('etag', '123') - .expect('content-type', 'application/json') - .expect(ETAG, correctVersion) - .expect(ETAG, /1.*/u); - `, - output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - assert.equal(pingGetResponse.headers.get('etag'), '123'); - assert.equal(pingGetResponse.headers.get('content-type'), 'application/json'); - assert.equal(pingGetResponse.headers.get(ETAG), correctVersion); - assert.ok(pingGetResponse.headers.get(ETAG).match(/1.*/u)); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response body assertion', - code: ` - import { BASE_PATH } from './index'; - await fixture.api.get(\`/sample-service/v2/ping\`).expect({message:'pong'}); - `, - output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.deepEqual(await pingGetResponse.json(), {message:'pong'}); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response callback assertion', - code: ` - import { BASE_PATH } from './index'; - await fixture.api.get(\`/sample-service/v2/ping\`) - .expect(validate) - .expect((response)=>console.log(response)); - `, - output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.doesNotThrow(()=>validate(pingGetResponse)); - assert.doesNotThrow(()=>console.log(pingGetResponse)); + }) + .expect(StatusCodes.NO_CONTENT); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'multiple fixture calls in the same test', code: ` - import { BASE_PATH } from './index'; async function test() { await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); const pingGetResponse = await fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); @@ -262,25 +190,24 @@ createTester().run(ruleId, rule, { } `, output: ` - import { BASE_PATH } from './index'; async function test() { - const pingGetResponse1 = await fetch(\`\${BASE_PATH}/ping\`, { + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse1.status, StatusCodes.OK); + }) + .expect(StatusCodes.OK); const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const pingGetResponse2 = await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { + }) + .expect(StatusCodes.OK); + await fetch(\`\${BASE_PATH}/ping?param=xxx\`, { method: 'GET', - }); - assert.equal(pingGetResponse2.status, StatusCodes.OK); - assert.deepEqual(await pingGetResponse2.json(), {message:'pong'}); - const pingGetResponse3 = await fetch(\`\${BASE_PATH}/ping\`, { + }) + .expect(StatusCodes.OK) + .expect({message:'pong'}); + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse3.status, StatusCodes.OK); + }) + .expect(StatusCodes.OK); } `, errors: [ @@ -293,12 +220,10 @@ createTester().run(ruleId, rule, { { name: 'directly return (no await) fixture call', code: ` - import { BASE_PATH } from './index'; () => { return fixture.api.get(\`/sample-service/v1/ping\`); }`, output: ` - import { BASE_PATH } from './index'; () => { return fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', @@ -309,32 +234,27 @@ createTester().run(ruleId, rule, { { name: 'directly return (no await) fixture call with assertion', code: ` - import { BASE_PATH } from './index'; async () => { return fixture.api.get(\`/sample-service/v1/ping\`).expect(StatusCodes.OK); }`, output: ` - import { BASE_PATH } from './index'; async () => { - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + return fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - return pingGetResponse; + }) + .expect(StatusCodes.OK); }`, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'directly return (no await) fixture call with body/headers', code: ` - import { BASE_PATH } from './index'; () => { return fixture.api.put(\`/sample-service/v2/card/\${uuid()}\`) .set(IF_MATCH_HEADER, originalCard.version) .send({}); }`, output: ` - import { BASE_PATH } from './index'; () => { return fetch(\`\${BASE_PATH}/card/\${uuid()}\`, { method: 'PUT', @@ -346,272 +266,54 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'replace statusCode with status', - code: ` - import { BASE_PATH } from './index'; - const response = await fixture.api.get(\`/sample-service/v2/ping\`); - assert.equal(response.statusCode, StatusCodes.OK); - console.log('status:', response.statusCode); - const response2 = await fixture.api.get(\`/sample-service/v2/ping\`); - assert.equal(response2.status, StatusCodes.OK); - `, - output: ` - import { BASE_PATH } from './index'; - const response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - console.log('status:', response.status); - const response2 = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response2.status, StatusCodes.OK); - `, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, { name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', code: ` - import { BASE_PATH } from './index'; await fixture.api.get(\`/sample-service/v2/ping\`).expect(200); `, output: ` - import { BASE_PATH } from './index'; - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, 200); + }) + .expect(200); `, errors: [{ messageId: 'preferNativeFetch' }], }, { name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', code: ` - import { BASE_PATH } from './index'; - const createdOn = Date.now().toUTCString(); await fixture.api.get(\`/sample-service/v2/ping\`).expect(200).expect(validateBody(createdOn)); `, output: ` - import { BASE_PATH } from './index'; - const createdOn = Date.now().toUTCString(); - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { + await fetch(\`\${BASE_PATH}/ping\`, { method: 'GET', - }); - assert.equal(pingGetResponse.status, 200); - assert.deepEqual(await pingGetResponse.json(), validateBody(createdOn)); + }) + .expect(200) + .expect(validateBody(createdOn)); `, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'handle destructuring variable declaration for body', - code: ` - const { body: responseBody } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - `, - output: ` - const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const responseBody = await pingGetResponse.json(); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for body - with nested destructuring', - code: ` - const { body: { pgpPublicKey: firstPgpPublicKey } } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); - `, - output: ` - const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const { pgpPublicKey: firstPgpPublicKey } = await pingGetResponse.json(); - assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for headers when body is presented as well', - code: ` - const { body, headers: headers2 } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - assert(body); - assert.ok(headers2.get(ETAG)); - `, - output: ` - const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const body = await pingGetResponse.json(); - const headers2 = pingGetResponse.headers; - assert(body); - assert.ok(headers2.get(ETAG)); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for headers without body presented but with assertions used', - code: ` - const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - assert.ok(headers.get(ETAG)); - `, - output: ` - const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const headers = pingGetResponse.headers; - assert.ok(headers.get(ETAG)); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', - code: ` - const { headers } = await fixture.api.get(\`$\{BASE_PATH}/ping\`); - assert.ok(headers.get(ETAG)); - `, - output: ` - const { headers } = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.ok(headers.get(ETAG)); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'avoid response variable name conflict with existing variables in the same scope', - code: ` - async () => { - const pingGetResponse = 'foo'; - const pingGetResponse1 = 'bar'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - } - `, - output: ` - async () => { - const pingGetResponse = 'foo'; - const pingGetResponse1 = 'bar'; - const pingGetResponse2 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse2.status, StatusCodes.OK); - const pingGetResponse3 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse3.status, StatusCodes.OK); - } - `, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'response variable names in different scope do not conflict with each other', - code: ` - it('#1', async () => { - const pingGetResponse = 'foo'; - }); - it('#2', async () => { - const pingGetResponse = 'foo'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - }); - it('#3', async () => { - const pingGetResponse3 = 'foo'; - await fixture.api.get(\`$\{BASE_PATH}/ping\`).expect(StatusCodes.OK); - }); - `, - output: ` - it('#1', async () => { - const pingGetResponse = 'foo'; - }); - it('#2', async () => { - const pingGetResponse = 'foo'; - const pingGetResponse1 = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse1.status, StatusCodes.OK); - }); - it('#3', async () => { - const pingGetResponse3 = 'foo'; - const pingGetResponse = await fetch(\`$\{BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - }); - `, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, { name: 'inline access to response body should be extracted to a variable', code: ` export async function validatePin( fixture, ) { - const paymentSecurityServicePublicKey = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; + const publicKeyGetResponse = (await fixture.api.get(\`\${BASE_PATH}/public-key\`).expect(StatusCodes.OK)).body.publicKey; } `, output: ` export async function validatePin( fixture, ) { - const publicKeyGetResponse = await fetch(\`\${BASE_PATH}/public-key\`, { + const publicKeyGetResponse = (await fetch(\`\${BASE_PATH}/public-key\`, { method: 'GET', - }); - assert.equal(publicKeyGetResponse.status, StatusCodes.OK); - const publicKeyGetResponseBody = await publicKeyGetResponse.json(); - const paymentSecurityServicePublicKey = publicKeyGetResponseBody.publicKey; + }) + .expect(StatusCodes.OK)).body.publicKey; } `, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - code: ` - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - await fixture.api - .put(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`) - .set(CREATED_ON_HEADER, createdOn) - .send({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }) - .expect(StatusCodes.NO_CONTENT) - .expect(ETAG_HEADER, '1') - .expect((res) => verifyTemporalHeaders(res, createdOn)); - `, - output: ` - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - const keyPutResponse = await fetch(\`\${BASE_PATH}/key/\${keyId}?zoneKeyId=\${zoneKeyId}\`, { - method: 'PUT', - body: JSON.stringify({ - key: '71CA52F757D7C0B45A16C6C04EAFD704', - checkValue: '4F35C4', - }), - headers: { - [CREATED_ON_HEADER]: createdOn, - }, - }); - assert.equal(keyPutResponse.status, StatusCodes.NO_CONTENT); - assert.equal(keyPutResponse.headers.get(ETAG_HEADER), '1'); - assert.doesNotThrow(()=>verifyTemporalHeaders(keyPutResponse, createdOn)); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, { name: 'in arrow function without concurrent promises', code: ` @@ -633,53 +335,6 @@ createTester().run(ruleId, rule, { `, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'assignment statement instead of variable declaration used for subsequent fixture calls', - code: ` - let response = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); - response = await fixture.api.get(\`\${BASE_PATH}/ping2\`).expect(StatusCodes.OK); - `, - output: ` - let response = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - response = await fetch(\`\${BASE_PATH}/ping2\`, { - method: 'GET', - }); - assert.equal(response.status, StatusCodes.OK); - `, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'nested header destructuring', - code: ` - const { headers: { etag } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); - `, - output: ` - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const etag = pingGetResponse.headers.get('etag'); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'nested header destructuring - string literal key with renaming', - code: ` - const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); - `, - output: ` - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const createdOn = pingGetResponse.headers.get('created-on'); - const updatedOn = pingGetResponse.headers.get('updated-on'); - `, - errors: [{ messageId: 'preferNativeFetch' }], - }, { name: 'support setting headers using object literal', code: `function doSomething() { @@ -699,33 +354,5 @@ createTester().run(ruleId, rule, { }`, errors: [{ messageId: 'preferNativeFetch' }], }, - { - name: 'statusCode destructuring should be renamed', - code: `async function test() { - const { statusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const statusCode = pingGetResponse.status; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'statusCode destructuring should be renamed - with renaming', - code: `async function test() { - const { statusCode: pingStatusCode } = await fixture.api.get(\`\${BASE_PATH}/ping\`).expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingGetResponse = await fetch(\`\${BASE_PATH}/ping\`, { - method: 'GET', - }); - assert.equal(pingGetResponse.status, StatusCodes.OK); - const pingStatusCode = pingGetResponse.status; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, ], }); diff --git a/src/agent/no-fixture.ts b/src/agent/no-fixture.ts index aa3b553..6565f87 100644 --- a/src/agent/no-fixture.ts +++ b/src/agent/no-fixture.ts @@ -9,110 +9,60 @@ import { strict as assert } from 'node:assert'; import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager'; import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; -import { - getEnclosingFunction, - getEnclosingScopeNode, - getEnclosingStatement, - getParent, - isUsedInArrayOrAsArgument, -} from '../library/ts-tree'; +import { getParent } from '../library/ts-tree'; import getDocumentationUrl from '../get-documentation-url'; import { getIndentation } from '../library/format'; import { isValidPropertyName } from '../library/variable'; -import { analyzeResponseReferences } from './response-reference'; -import { - getResponseBodyRetrievalText, - getResponseHeadersRetrievalText, - getResponseStatusRetrievalText, - hasAssertions, -} from './fetch'; import { replaceEndpointUrlPrefixWithBasePath } from './url'; export const ruleId = 'no-fixture'; interface FixtureCallInformation { - rootNode: - | TSESTree.AwaitExpression - | TSESTree.ReturnStatement - | TSESTree.VariableDeclaration - | TSESTree.CallExpression - | TSESTree.ExpressionStatement; - fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; - variableDeclaration?: TSESTree.VariableDeclaration; - variableAssignment?: TSESTree.ExpressionStatement; + fixtureNode: TSESTree.CallExpression; requestBody?: TSESTree.Expression; requestHeaders?: { name: TSESTree.Expression; value: TSESTree.Expression }[]; requestHeadersObjectLiteral?: TSESTree.ObjectExpression; - assertions?: TSESTree.Expression[][]; - inlineStatementNode?: TSESTree.Node; - inlineBodyReference?: TSESTree.MemberExpression; - inlineStatusReference?: TSESTree.MemberExpression; - inlineHeadersReference?: TSESTree.MemberExpression; + statusAssertion?: TSESTree.CallExpressionArgument[]; + nonStatusAssertions?: TSESTree.CallExpressionArgument[][]; +} + +function isStatusAssertion(expectArguments: TSESTree.CallExpressionArgument[]) { + if (expectArguments.length === 1) { + const [maybeStatusAssertion] = expectArguments; + assert.ok(maybeStatusAssertion); + if ( + (maybeStatusAssertion.type === AST_NODE_TYPES.MemberExpression && + maybeStatusAssertion.object.type === AST_NODE_TYPES.Identifier && + maybeStatusAssertion.object.name === 'StatusCodes') || + (maybeStatusAssertion.type === AST_NODE_TYPES.Literal && typeof maybeStatusAssertion.value === 'number') + ) { + return true; + } + } + return false; } // recursively analyze the fixture/supertest call chain to collect information of request/response -// eslint-disable-next-line sonarjs/cognitive-complexity function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { + sourceCode.getText(call); + results.fixtureNode = call; + + let nextCall; const parent = getParent(call); assert.ok(parent, 'parent should exist for fixture/supertest call node'); - let nextCall; - if (parent.type === AST_NODE_TYPES.ReturnStatement) { - // direct return, no variable declaration or await - results.fixtureNode = call; - results.rootNode = parent; - } else if ( - parent.type === AST_NODE_TYPES.ArrayExpression || - parent.type === AST_NODE_TYPES.CallExpression || - parent.type === AST_NODE_TYPES.ArrowFunctionExpression - ) { - // direct return, no variable declaration or await - results.fixtureNode = call; - results.rootNode = call; - } else if (parent.type === AST_NODE_TYPES.AwaitExpression) { - results.fixtureNode = call; - const enclosingStatement = getEnclosingStatement(parent); - assert.ok(enclosingStatement); - const awaitParent = getParent(parent); - if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) { - results.rootNode = parent; - results.inlineStatementNode = enclosingStatement; - if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { - results.inlineBodyReference = awaitParent; - } - if ( - awaitParent.property.type === AST_NODE_TYPES.Identifier && - (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode') - ) { - results.inlineStatusReference = awaitParent; - } - if ( - awaitParent.property.type === AST_NODE_TYPES.Identifier && - (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') - ) { - results.inlineHeadersReference = awaitParent; - } - } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) { - results.variableDeclaration = enclosingStatement; - results.rootNode = enclosingStatement; - } else if ( - enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement && - enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression - ) { - results.variableAssignment = enclosingStatement; - results.rootNode = enclosingStatement; - } else { - results.rootNode = parent; - } - } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { + if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { if (parent.property.name === 'expect') { // supertest assertions const assertionCall = getParent(parent); assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); - results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; + if (isStatusAssertion(assertionCall.arguments)) { + results.statusAssertion = assertionCall.arguments; + } else { + results.nonStatusAssertions = [...(results.nonStatusAssertions ?? []), assertionCall.arguments]; + } nextCall = assertionCall; } else if (parent.property.name === 'send') { // request body @@ -132,162 +82,14 @@ function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallI } nextCall = setRequestHeaderCall; } - } else { - throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); } if (nextCall) { analyzeFixtureCall(nextCall, results, sourceCode); } } -// eslint-disable-next-line sonarjs/cognitive-complexity -function createResponseAssertions( - fixtureCallInformation: FixtureCallInformation, - sourceCode: SourceCode, - responseVariableName: string, - destructuringResponseHeadersVariable: Variable | undefined, -) { - let statusAssertion: string | undefined; - const nonStatusAssertions: string[] = []; - for (const expectArguments of fixtureCallInformation.assertions ?? []) { - if (expectArguments.length === 1) { - const [assertionArgument] = expectArguments; - assert.ok(assertionArgument); - if ( - (assertionArgument.type === AST_NODE_TYPES.MemberExpression && - assertionArgument.object.type === AST_NODE_TYPES.Identifier && - assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === AST_NODE_TYPES.Literal || - sourceCode.getText(assertionArgument as TSESTree.Node).includes('StatusCodes.') - ) { - // status code assertion - statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; - } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { - // callback assertion using arrow function - let functionBody = sourceCode.getText(assertionArgument.body); - - const [originalResponseArgument] = assertionArgument.params; - assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); - const originalResponseArgumentName = originalResponseArgument.name; - if (originalResponseArgumentName !== responseVariableName) { - functionBody = functionBody.replace( - new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), - responseVariableName, - ); - } - nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); - } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { - // callback assertion using function reference - nonStatusAssertions.push( - `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, - ); - } else if ( - assertionArgument.type === AST_NODE_TYPES.ObjectExpression || - assertionArgument.type === AST_NODE_TYPES.CallExpression - ) { - // body deep equal assertion - nonStatusAssertions.push( - `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, - ); - } else { - throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); - } - } else if (expectArguments.length === 2) { - // header assertion - const [headerName, headerValue] = expectArguments; - assert.ok(headerName && headerValue); - const headersReference = - destructuringResponseHeadersVariable !== undefined - ? destructuringResponseHeadersVariable.name - : `${responseVariableName}.headers`; - if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { - nonStatusAssertions.push( - `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, - ); - } else { - nonStatusAssertions.push( - `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, - ); - } - } - } - return { - statusAssertion, - nonStatusAssertions, - }; -} - -function getResponseVariableNameToUse( - methodName: string, - urlArgumentNode: TSESTree.CallExpressionArgument, - originalUrlArgumentText: string, - scopeManager: ScopeManager, - fixtureCallInformation: FixtureCallInformation, - scopeVariablesMap: Map, -) { - if (fixtureCallInformation.variableAssignment) { - assert.ok( - fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression && - fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier, - ); - return fixtureCallInformation.variableAssignment.expression.left.name; - } - - if (fixtureCallInformation.variableDeclaration) { - const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) { - return firstDeclaration.id.name; - } - } - - const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode); - scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); - assert.ok(enclosingScopeNode); - const scope = scopeManager.acquire(enclosingScopeNode); - assert.ok(scope !== null); - let scopeVariables = scopeVariablesMap.get(scope); - if (!scopeVariables) { - scopeVariables = [...scope.set.keys()]; - scopeVariablesMap.set(scope, scopeVariables); - } - - let responseVariableNameBase = 'response'; - if (urlArgumentNode.type === AST_NODE_TYPES.Literal || urlArgumentNode.type === AST_NODE_TYPES.TemplateLiteral) { - const urlWithoutQuotes = originalUrlArgumentText.replace(/['"`]/gu, ''); - const urlWithoutQuery = urlWithoutQuotes.includes('?') - ? urlWithoutQuotes.slice(0, urlWithoutQuotes.indexOf('?')) - : urlWithoutQuotes; - const parts = urlWithoutQuery.startsWith('${') - ? urlWithoutQuery.split('/').slice(1) - : // eslint-disable-next-line no-magic-numbers - urlWithoutQuery.split('/').slice(3); - - responseVariableNameBase = [...parts.filter((part) => part !== 'tenant'), methodName.toLocaleLowerCase()] - .map((part) => part.split(/[-]/u)) - .flat() - .filter((part) => part.trim() !== '' && !/\$\{.*\}/u.test(part)) // keep only non-empty parts that are not path parameters - .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) - .join(''); - responseVariableNameBase = `${responseVariableNameBase[0]?.toLowerCase() ?? ''}${responseVariableNameBase.slice(1)}Response`; - } - - let responseVariableCounter = 0; - let responseVariableNameToUse; - while (responseVariableNameToUse === undefined) { - responseVariableNameToUse = `${responseVariableNameBase}${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; - if (scopeVariables.includes(responseVariableNameToUse)) { - responseVariableNameToUse = undefined; - } - responseVariableCounter++; - } - scopeVariables.push(responseVariableNameToUse); - return responseVariableNameToUse; -} - -function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean { - const parent = getParent(responseBodyReference); - return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier; +function getExpectAssertion(expectArguments: TSESTree.CallExpressionArgument[], sourceCode: SourceCode) { + return `expect(${expectArguments.map((arg) => sourceCode.getText(arg)).join(', ')})`; } const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); @@ -308,28 +110,16 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat schema: [], }, defaultOptions: [], - // eslint-disable-next-line max-lines-per-function create(context) { const sourceCode = context.sourceCode; const scopeManager = sourceCode.scopeManager; assert.ok(scopeManager !== null); - const scopeVariablesMap = new Map(); return { - // eslint-disable-next-line max-lines-per-function 'CallExpression[callee.object.object.name="fixture"][callee.object.property.name="api"]': ( fixtureCall: TSESTree.CallExpression, - // eslint-disable-next-line sonarjs/cognitive-complexity ) => { try { - if ( - hasAssertions(fixtureCall) && - (isUsedInArrayOrAsArgument(fixtureCall) || getEnclosingFunction(fixtureCall)?.async === false) - ) { - // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here - return; - } - const fixtureFunction = fixtureCall.callee; // e.g. fixture.api.get assert.ok(fixtureFunction.type === AST_NODE_TYPES.MemberExpression); const indentation = getIndentation(fixtureCall, sourceCode); @@ -340,16 +130,6 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat const fixtureCallInformation = {} as FixtureCallInformation; analyzeFixtureCall(fixtureCall, fixtureCallInformation, sourceCode); - const { - variable: responseVariable, - bodyReferences: responseBodyReferences, - // headersReferences: responseHeadersReferences, - statusReferences: responseStatusReferences, - destructuringBodyVariable: destructuringResponseBodyVariable, - destructuringStatusVariable: destructuringResponseStatusVariable, - destructuringHeadersVariable: destructuringResponseHeadersVariable, - } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); - // convert url from `/sample-service/v1/ping` to `${BASE_PATH}/ping` const originalUrlArgumentText = sourceCode.getText(urlArgumentNode); const fetchUrlArgumentText = replaceEndpointUrlPrefixWithBasePath(originalUrlArgumentText); @@ -383,182 +163,24 @@ const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = creat '}', ].join(`\n${indentation}`); - const responseVariableNameToUse = getResponseVariableNameToUse( - methodNameToUse, - urlArgumentNode, - originalUrlArgumentText, - scopeManager, - fixtureCallInformation, - scopeVariablesMap, - ); - - const isResponseBodyVariableRedefinitionNeeded = - destructuringResponseBodyVariable !== undefined || - fixtureCallInformation.inlineBodyReference !== undefined || - (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); - const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; - - const isResponseStatusVariableRedefinitionNeeded = - destructuringResponseStatusVariable !== undefined || - fixtureCallInformation.inlineStatusReference !== undefined; - const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`; - - const isResponseHeadersVariableRedefinitionNeeded = - (destructuringResponseHeadersVariable !== undefined && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern) || - fixtureCallInformation.inlineHeadersReference !== undefined; - const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; - - const isResponseVariableRedefinitionNeeded = - (fixtureCallInformation.variableAssignment === undefined && - responseVariable === undefined && - fixtureCallInformation.assertions !== undefined) || - isResponseBodyVariableRedefinitionNeeded || - isResponseStatusVariableRedefinitionNeeded || - isResponseHeadersVariableRedefinitionNeeded; - - const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded - ? [ - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseBodyVariable - ? [ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseBodyVariableRedefinitionNeeded - ? [ - `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, - ] - : []), - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseStatusVariable - ? [ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseStatusVariableRedefinitionNeeded - ? [ - `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, - ] - : []), - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseHeadersVariable - ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === - AST_NODE_TYPES.ObjectPattern - ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => { - assert.ok(property.type === AST_NODE_TYPES.Property); - assert.equal(property.value.type, AST_NODE_TYPES.Identifier); - // eslint-disable-next-line sonarjs/no-nested-template-literals - return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; - }) - : [ - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseHeadersVariableRedefinitionNeeded - ? [ - `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, - ] - : []), - ] - : []; - - const { statusAssertion, nonStatusAssertions } = createResponseAssertions( - fixtureCallInformation, - sourceCode, - responseVariableNameToUse, - destructuringResponseHeadersVariable as Variable | undefined, - ); - - // add variable declaration if needed const fetchCallText = `fetch(${fetchUrlArgumentText}, ${fetchRequestArgumentLines})`; - const fetchStatementText = !isResponseVariableRedefinitionNeeded - ? fetchCallText - : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`; - - const nodeToReplace = isResponseVariableRedefinitionNeeded - ? fixtureCallInformation.rootNode - : fixtureCallInformation.fixtureNode; - const appendingAssignmentAndAssertionText = [ - '', - ...(statusAssertion !== undefined ? [statusAssertion] : []), - ...responseBodyHeadersVariableRedefineLines, - ...nonStatusAssertions, - ].join(`;\n${indentation}`); + const fetchStatementText = [ + fetchCallText, + ...(fixtureCallInformation.statusAssertion === undefined + ? [] + : [getExpectAssertion(fixtureCallInformation.statusAssertion, sourceCode)]), + ...(fixtureCallInformation.nonStatusAssertions === undefined + ? [] + : fixtureCallInformation.nonStatusAssertions.map((assertion) => + getExpectAssertion(assertion, sourceCode), + )), + ].join(`\n${indentation}.`); context.report({ - node: fixtureCall, + node: fixtureCallInformation.fixtureNode, messageId: 'preferNativeFetch', - - *fix(fixer) { - if (fixtureCallInformation.inlineStatementNode) { - const preInlineDeclaration = [ - fetchStatementText, - `${appendingAssignmentAndAssertionText};\n${indentation}`, - ].join(``); - yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration); - } else { - yield fixer.replaceText(nodeToReplace, fetchStatementText); - - const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); - yield fixer.insertTextAfter( - nodeToReplace, - needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, - ); - } - - // handle response body references - for (const responseBodyReference of responseBodyReferences) { - yield fixer.replaceText( - responseBodyReference, - isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference) - ? redefineResponseBodyVariableName - : getResponseBodyRetrievalText(responseVariableNameToUse), - ); - } - if (fixtureCallInformation.inlineBodyReference) { - yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); - } - - // // handle response headers references - // for (const responseHeadersReference of responseHeadersReferences) { - // const parent = getParent(responseHeadersReference); - // assert.ok(parent); - // let headerName; - // if (parent.type === AST_NODE_TYPES.MemberExpression) { - // const headerNameNode = parent.property; - // headerName = parent.computed - // ? sourceCode.getText(headerNameNode) - // : `'${sourceCode.getText(headerNameNode)}'`; - // } else if (parent.type === AST_NODE_TYPES.CallExpression) { - // const headerNameNode = parent.arguments[0]; - // headerName = sourceCode.getText(headerNameNode); - // } - // assert.ok(headerName !== undefined); - // yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); - // } - - // convert response.statusCode to response.status - for (const responseStatusReference of responseStatusReferences) { - if ( - responseStatusReference.property.type === AST_NODE_TYPES.Identifier && - responseStatusReference.property.name === 'statusCode' - ) { - yield fixer.replaceText(responseStatusReference.property, `status`); - } - } - - // handle direct return statement without await, e.g. "return fixture.api.get(...);" - if ( - fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement && - fixtureCallInformation.assertions !== undefined - ) { - yield fixer.insertTextAfter( - fixtureCallInformation.rootNode, - `\n${indentation}return ${responseVariableNameToUse};`, - ); - } + fix(fixer) { + return fixer.replaceText(fixtureCallInformation.fixtureNode, fetchStatementText); }, }); } catch (error) { diff --git a/src/agent/no-supertest.spec.ts b/src/agent/no-supertest.spec.ts deleted file mode 100644 index cbd5806..0000000 --- a/src/agent/no-supertest.spec.ts +++ /dev/null @@ -1,389 +0,0 @@ -// agent/no-supertest.spec.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import createTester from '../ts-tester.test'; -import rule, { ruleId } from './no-supertest'; - -createTester().run(ruleId, rule, { - valid: [ - { - name: 'handle destructuring variable declaration for headers without body/assertion presented does not need to change', - code: `async function test() { - const { headers } = await ping(); - assert.ok(headers.get(ETAG)); - }`, - }, - { - name: 'skip concurrent supertest calls which will be handled in "supertest-then" rule', - code: `async function test() { - const responses = await Promise.all([ - ping().expect(StatusCodes.OK), - ping().expect(StatusCodes.OK), - ]); - }`, - }, - { - name: 'leave fixture.api.xxx() calls as is, which will be handled by no-fixture rule', - code: `async function test() { - await fixture.api.get(url).expect(StatusCodes.OK); - }`, - }, - ], - invalid: [ - { - name: 'assertion without variable declaration', - code: `async function test() { - await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'assertion with variable declaration', - code: `async function test() { - const pingResponse = await ping().expect(StatusCodes.OK); - assert(pingResponse.body); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const pingResponseBody = await pingResponse.json(); - assert(pingResponseBody); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response headers assertion should be externalized with new variable declared if necessary', - code: `async function test() { - await ping() - .expect(StatusCodes.OK) - .expect('etag', '123') - .expect('content-type', 'application/json') - .expect(ETAG, correctVersion) - .expect(ETAG, /1.*/u); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - assert.equal(pingResponse.headers.get('etag'), '123'); - assert.equal(pingResponse.headers.get('content-type'), 'application/json'); - assert.equal(pingResponse.headers.get(ETAG), correctVersion); - assert.ok(pingResponse.headers.get(ETAG).match(/1.*/u)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response body assertion', - code: `async function test() { - await ping().expect({message:'pong'}); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.deepEqual(await pingResponse.json(), {message:'pong'}); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'response callback assertion', - code: `async function test() { - await ping() - .expect(validate) - .expect((response)=>console.log(response)); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.doesNotThrow(()=>validate(pingResponse)); - assert.doesNotThrow(()=>console.log(pingResponse)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'multiple fixture calls in the same test', - code: `async function test() { - await ping().expect(StatusCodes.OK); - const pingResponse = await ping().expect(StatusCodes.OK); - await ping().expect(StatusCodes.OK).expect({message:'pong'}); - await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse1 = await ping(); - assert.equal(pingResponse1.status, StatusCodes.OK); - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const pingResponse2 = await ping(); - assert.equal(pingResponse2.status, StatusCodes.OK); - assert.deepEqual(await pingResponse2.json(), {message:'pong'}); - const pingResponse3 = await ping(); - assert.equal(pingResponse3.status, StatusCodes.OK); - }`, - errors: [ - { messageId: 'preferNativeFetch' }, - { messageId: 'preferNativeFetch' }, - { messageId: 'preferNativeFetch' }, - { messageId: 'preferNativeFetch' }, - ], - }, - { - name: 'directly return (no await) fixture call with assertion', - code: `async function test() { - return ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - return pingResponse; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'work with response status literal (e.g. 200 instead of StatusCoodes.OK) as well', - code: `async function test() { - await util.ping().expect(200); - }`, - output: `async function test() { - const pingResponse = await util.ping(); - assert.equal(pingResponse.status, 200); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'assert response body against function call\'s return value ".expect(validateBody(response))"', - code: `async function test() { - const createdOn = Date.now().toUTCString(); - await ping().expect(200).expect(validateBody(createdOn)); - }`, - output: `async function test() { - const createdOn = Date.now().toUTCString(); - const pingResponse = await ping(); - assert.equal(pingResponse.status, 200); - assert.deepEqual(await pingResponse.json(), validateBody(createdOn)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for body', - code: `async function test() { - const { body: responseBody } = await ping().expect(StatusCodes.OK); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const responseBody = await pingResponse.json(); - const timeDifference = Date.now() - new Date(responseBody.serverTime).getTime(); - assert.ok(timeDifference >= 0 && timeDifference < 200); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for body - with nested destructuring', - code: `async function test() { - const { body: { pgpPublicKey: firstPgpPublicKey } } = await ping().expect(StatusCodes.OK); - assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const { pgpPublicKey: firstPgpPublicKey } = await pingResponse.json(); - assert.ok(firstPgpPublicKey.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----')); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for headers when body is presented as well', - code: `async function test() { - const { body, headers: headers2 } = await ping().expect(StatusCodes.OK); - assert(body); - assert.ok(headers2.get(ETAG)); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const body = await pingResponse.json(); - const headers2 = pingResponse.headers; - assert(body); - assert.ok(headers2.get(ETAG)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'handle destructuring variable declaration for headers without body presented but with assertions used', - code: `async function test() { - const { headers } = await ping().expect(StatusCodes.OK); - assert.ok(headers.get(ETAG)); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const headers = pingResponse.headers; - assert.ok(headers.get(ETAG)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'avoid response variable name conflict with existing variables in the same scope', - code: `async () => { - const pingResponse = 'foo'; - const pingResponse1 = 'bar'; - await ping().expect(StatusCodes.OK); - await ping().expect(StatusCodes.OK); - }`, - output: `async () => { - const pingResponse = 'foo'; - const pingResponse1 = 'bar'; - const pingResponse2 = await ping(); - assert.equal(pingResponse2.status, StatusCodes.OK); - const pingResponse3 = await ping(); - assert.equal(pingResponse3.status, StatusCodes.OK); - }`, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'response variable names in different scope do not conflict with each other', - code: ` - it('#1', async () => { - const pingResponse = 'foo'; - }); - it('#2', async () => { - const pingResponse = 'foo'; - await ping().expect(StatusCodes.OK); - }); - it('#3', async () => { - const pingResponse3 = 'foo'; - await ping().expect(StatusCodes.OK); - }); - `, - output: ` - it('#1', async () => { - const pingResponse = 'foo'; - }); - it('#2', async () => { - const pingResponse = 'foo'; - const pingResponse1 = await ping(); - assert.equal(pingResponse1.status, StatusCodes.OK); - }); - it('#3', async () => { - const pingResponse3 = 'foo'; - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - }); - `, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'inline access to response body should be extracted to a variable', - code: `export async function validatePin( - fixture, - ) { - const paymentSecurityServicePublicKey = (await ping().expect(StatusCodes.OK)).body.publicKey; - }`, - output: `export async function validatePin( - fixture, - ) { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const pingResponseBody = await pingResponse.json(); - const paymentSecurityServicePublicKey = pingResponseBody.publicKey; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'callback assertion using arrow function that accesses to response might conflict with the new/redefined response variable', - code: `async function test() { - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - await ping() - .expect(StatusCodes.NO_CONTENT) - .expect(ETAG_HEADER, '1') - .expect((res) => verifyTemporalHeaders(res, createdOn)); - }`, - output: `async function test() { - const createdOn = new Date().toISOString(); - const zoneKeyId = uuid(); - - // Import Key - const keyId = uuid(); - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.NO_CONTENT); - assert.equal(pingResponse.headers.get(ETAG_HEADER), '1'); - assert.doesNotThrow(()=>verifyTemporalHeaders(pingResponse, createdOn)); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'assignment statement instead of variable declaration used for subsequent fixture calls', - code: `async function test() { - let response = await ping().expect(StatusCodes.OK); - response = await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - let response = await ping(); - assert.equal(response.status, StatusCodes.OK); - response = await ping(); - assert.equal(response.status, StatusCodes.OK); - }`, - errors: [{ messageId: 'preferNativeFetch' }, { messageId: 'preferNativeFetch' }], - }, - { - name: 'nested header destructuring', - code: `async function test() { - const { headers: { etag } } = await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const etag = pingResponse.headers.get('etag'); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'nested header destructuring - string literal key with renaming', - code: `async function test() { - const { headers: { 'created-on': createdOn, 'updated-on': updatedOn } } = await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const createdOn = pingResponse.headers.get('created-on'); - const updatedOn = pingResponse.headers.get('updated-on'); - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'statusCode destructuring should be renamed', - code: `async function test() { - const { statusCode } = await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const statusCode = pingResponse.status; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - { - name: 'statusCode destructuring should be renamed', - code: `async function test() { - const { statusCode: pingStatusCode } = await ping().expect(StatusCodes.OK); - }`, - output: `async function test() { - const pingResponse = await ping(); - assert.equal(pingResponse.status, StatusCodes.OK); - const pingStatusCode = pingResponse.status; - }`, - errors: [{ messageId: 'preferNativeFetch' }], - }, - ], -}); diff --git a/src/agent/no-supertest.ts b/src/agent/no-supertest.ts deleted file mode 100644 index 6a1b8b4..0000000 --- a/src/agent/no-supertest.ts +++ /dev/null @@ -1,517 +0,0 @@ -// agent/no-supertest.ts - -/* - * Copyright (c) 2021-2024 Check Digit, LLC - * - * This code is licensed under the MIT license (see LICENSE.txt for details). - */ - -import { strict as assert } from 'node:assert'; - -import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'; -import type { Scope, ScopeManager, Variable } from '@typescript-eslint/scope-manager'; -import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; - -import { - getEnclosingFunction, - getEnclosingScopeNode, - getEnclosingStatement, - getParent, - isUsedInArrayOrAsArgument, -} from '../library/ts-tree'; -import getDocumentationUrl from '../get-documentation-url'; -import { getIndentation } from '../library/format'; -import { analyzeResponseReferences } from './response-reference'; -import { getResponseBodyRetrievalText, getResponseHeadersRetrievalText, getResponseStatusRetrievalText } from './fetch'; - -export const ruleId = 'no-supertest'; - -interface FixtureCallInformation { - rootNode: - | TSESTree.AwaitExpression - | TSESTree.ReturnStatement - | TSESTree.VariableDeclaration - | TSESTree.CallExpression - | TSESTree.ExpressionStatement; - fixtureNode: TSESTree.AwaitExpression | TSESTree.CallExpression; - variableDeclaration?: TSESTree.VariableDeclaration; - variableAssignment?: TSESTree.ExpressionStatement; - assertions?: TSESTree.Expression[][]; - inlineStatementNode?: TSESTree.Node; - inlineBodyReference?: TSESTree.MemberExpression; - inlineStatusReference?: TSESTree.MemberExpression; - inlineHeadersReference?: TSESTree.MemberExpression; -} - -// recursively analyze the fixture/supertest call chain to collect information of request/response -// eslint-disable-next-line sonarjs/cognitive-complexity -function analyzeFixtureCall(call: TSESTree.CallExpression, results: FixtureCallInformation, sourceCode: SourceCode) { - const parent = getParent(call); - assert.ok(parent, 'parent should exist for fixture/supertest call node'); - - let nextCall; - if (parent.type === AST_NODE_TYPES.ReturnStatement) { - // direct return, no variable declaration or await - results.fixtureNode = call; - results.rootNode = parent; - } else if ( - parent.type === AST_NODE_TYPES.ArrayExpression || - parent.type === AST_NODE_TYPES.CallExpression || - parent.type === AST_NODE_TYPES.ArrowFunctionExpression - ) { - // direct return, no variable declaration or await - results.fixtureNode = call; - results.rootNode = call; - } else if (parent.type === AST_NODE_TYPES.AwaitExpression) { - results.fixtureNode = call; - const enclosingStatement = getEnclosingStatement(parent); - assert.ok(enclosingStatement); - const awaitParent = getParent(parent); - if (awaitParent?.type === AST_NODE_TYPES.MemberExpression) { - results.rootNode = parent; - results.inlineStatementNode = enclosingStatement; - if (awaitParent.property.type === AST_NODE_TYPES.Identifier && awaitParent.property.name === 'body') { - results.inlineBodyReference = awaitParent; - } - if ( - awaitParent.property.type === AST_NODE_TYPES.Identifier && - (awaitParent.property.name === 'status' || awaitParent.property.name === 'statusCode') - ) { - results.inlineStatusReference = awaitParent; - } - if ( - awaitParent.property.type === AST_NODE_TYPES.Identifier && - (awaitParent.property.name === 'header' || awaitParent.property.name === 'headers') - ) { - results.inlineHeadersReference = awaitParent; - } - } else if (enclosingStatement.type === AST_NODE_TYPES.VariableDeclaration) { - results.variableDeclaration = enclosingStatement; - results.rootNode = enclosingStatement; - } else if ( - enclosingStatement.type === AST_NODE_TYPES.ExpressionStatement && - enclosingStatement.expression.type === AST_NODE_TYPES.AssignmentExpression - ) { - results.variableAssignment = enclosingStatement; - results.rootNode = enclosingStatement; - } else { - results.rootNode = parent; - } - } else if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property.type === AST_NODE_TYPES.Identifier) { - if (parent.property.name === 'expect') { - // supertest assertions - const assertionCall = getParent(parent); - assert.ok(assertionCall && assertionCall.type === AST_NODE_TYPES.CallExpression); - results.assertions = [...(results.assertions ?? []), assertionCall.arguments as TSESTree.Expression[]]; - nextCall = assertionCall; - } - } else { - throw new Error(`Unexpected expression in fixture/supertest call ${sourceCode.getText(parent)}.`); - } - if (nextCall) { - analyzeFixtureCall(nextCall, results, sourceCode); - } -} - -// eslint-disable-next-line sonarjs/cognitive-complexity -function createResponseAssertions( - fixtureCallInformation: FixtureCallInformation, - sourceCode: SourceCode, - responseVariableName: string, - destructuringResponseHeadersVariable: Variable | undefined, -) { - let statusAssertion: string | undefined; - const nonStatusAssertions: string[] = []; - for (const expectArguments of fixtureCallInformation.assertions ?? []) { - if (expectArguments.length === 1) { - const [assertionArgument] = expectArguments; - assert.ok(assertionArgument); - if ( - (assertionArgument.type === AST_NODE_TYPES.MemberExpression && - assertionArgument.object.type === AST_NODE_TYPES.Identifier && - assertionArgument.object.name === 'StatusCodes') || - assertionArgument.type === AST_NODE_TYPES.Literal || - sourceCode.getText(assertionArgument).includes('StatusCodes.') - ) { - // status code assertion - statusAssertion = `assert.equal(${responseVariableName}.status, ${sourceCode.getText(assertionArgument)})`; - } else if (assertionArgument.type === AST_NODE_TYPES.ArrowFunctionExpression) { - // callback assertion using arrow function - let functionBody = sourceCode.getText(assertionArgument.body); - - const [originalResponseArgument] = assertionArgument.params; - assert.ok(originalResponseArgument?.type === AST_NODE_TYPES.Identifier); - const originalResponseArgumentName = originalResponseArgument.name; - if (originalResponseArgumentName !== responseVariableName) { - functionBody = functionBody.replace( - new RegExp(`\\b${originalResponseArgumentName}\\b`, 'ug'), - responseVariableName, - ); - } - nonStatusAssertions.push(`assert.doesNotThrow(()=>${functionBody})`); - } else if (assertionArgument.type === AST_NODE_TYPES.Identifier) { - // callback assertion using function reference - nonStatusAssertions.push( - `assert.doesNotThrow(()=>${sourceCode.getText(assertionArgument)}(${responseVariableName}))`, - ); - } else if ( - assertionArgument.type === AST_NODE_TYPES.ObjectExpression || - assertionArgument.type === AST_NODE_TYPES.CallExpression - ) { - // body deep equal assertion - nonStatusAssertions.push( - `assert.deepEqual(await ${responseVariableName}.json(), ${sourceCode.getText(assertionArgument)})`, - ); - } else { - throw new Error(`Unexpected Supertest assertion argument: ".expect(${sourceCode.getText(assertionArgument)})`); - } - } else if (expectArguments.length === 2) { - // header assertion - const [headerName, headerValue] = expectArguments; - assert.ok(headerName && headerValue); - const headersReference = - destructuringResponseHeadersVariable !== undefined - ? destructuringResponseHeadersVariable.name - : `${responseVariableName}.headers`; - if (headerValue.type === AST_NODE_TYPES.Literal && headerValue.value instanceof RegExp) { - nonStatusAssertions.push( - `assert.ok(${headersReference}.get(${sourceCode.getText(headerName)}).match(${sourceCode.getText(headerValue)}))`, - ); - } else { - nonStatusAssertions.push( - `assert.equal(${headersReference}.get(${sourceCode.getText(headerName)}), ${sourceCode.getText(headerValue)})`, - ); - } - } - } - return { - statusAssertion, - nonStatusAssertions, - }; -} - -function getResponseVariableNameToUse( - supertestFunctionName: string, - scopeManager: ScopeManager, - fixtureCallInformation: FixtureCallInformation, - scopeVariablesMap: Map, -) { - if (fixtureCallInformation.variableAssignment) { - assert.ok( - fixtureCallInformation.variableAssignment.expression.type === AST_NODE_TYPES.AssignmentExpression && - fixtureCallInformation.variableAssignment.expression.left.type === AST_NODE_TYPES.Identifier, - ); - return fixtureCallInformation.variableAssignment.expression.left.name; - } - - if (fixtureCallInformation.variableDeclaration) { - const firstDeclaration = fixtureCallInformation.variableDeclaration.declarations[0]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (firstDeclaration !== undefined && firstDeclaration.id.type === AST_NODE_TYPES.Identifier) { - return firstDeclaration.id.name; - } - } - - const enclosingScopeNode = getEnclosingScopeNode(fixtureCallInformation.rootNode); - scopeManager.getDeclaredVariables(fixtureCallInformation.rootNode); - assert.ok(enclosingScopeNode); - const scope = scopeManager.acquire(enclosingScopeNode); - assert.ok(scope !== null); - let scopeVariables = scopeVariablesMap.get(scope); - if (!scopeVariables) { - scopeVariables = [...scope.set.keys()]; - scopeVariablesMap.set(scope, scopeVariables); - } - - let responseVariableCounter = 0; - let responseVariableNameToUse; - while (responseVariableNameToUse === undefined) { - responseVariableNameToUse = `${supertestFunctionName}Response${responseVariableCounter === 0 ? '' : responseVariableCounter.toString()}`; - if (scopeVariables.includes(responseVariableNameToUse)) { - responseVariableNameToUse = undefined; - } - responseVariableCounter++; - } - scopeVariables.push(responseVariableNameToUse); - return responseVariableNameToUse; -} - -function isResponseBodyRedefinition(responseBodyReference: TSESTree.MemberExpression): boolean { - const parent = getParent(responseBodyReference); - return parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier; -} - -const createRule = ESLintUtils.RuleCreator((name) => getDocumentationUrl(name)); - -const rule: ESLintUtils.RuleModule<'unknownError' | 'preferNativeFetch'> = createRule({ - name: ruleId, - meta: { - type: 'suggestion', - docs: { - description: 'Transform supertest assersions to regular node assertions.', - url: getDocumentationUrl(ruleId), - }, - messages: { - preferNativeFetch: 'Transform supertest assersions to regular node assertions.', - unknownError: 'Unknown error occurred in file "{{fileName}}": {{ error }}.', - }, - fixable: 'code', - schema: [], - }, - defaultOptions: [], - // eslint-disable-next-line max-lines-per-function - create(context) { - const sourceCode = context.sourceCode; - const scopeManager = sourceCode.scopeManager; - assert.ok(scopeManager !== null); - const scopeVariablesMap = new Map(); - - return { - // eslint-disable-next-line max-lines-per-function - 'CallExpression[callee.property.name="expect"]': ( - supertestCall: TSESTree.CallExpression, - // eslint-disable-next-line sonarjs/cognitive-complexity - ) => { - try { - assert.ok(supertestCall.callee.type === AST_NODE_TYPES.MemberExpression); - if ( - supertestCall.callee.object.type !== AST_NODE_TYPES.CallExpression || - (supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && - supertestCall.callee.object.callee.property.type === AST_NODE_TYPES.Identifier && - supertestCall.callee.object.callee.property.name === 'expect') - ) { - // skip nested expect call chain, only focus on the first expect call - return; - } - if ( - supertestCall.callee.object.callee.type === AST_NODE_TYPES.MemberExpression && - supertestCall.callee.object.callee.object.type === AST_NODE_TYPES.MemberExpression && - supertestCall.callee.object.callee.object.object.type === AST_NODE_TYPES.Identifier && - supertestCall.callee.object.callee.object.object.name === 'fixture' && - supertestCall.callee.object.callee.object.property.type === AST_NODE_TYPES.Identifier && - supertestCall.callee.object.callee.object.property.name === 'api' - ) { - // skip nested expect calls, only focus on the top level - return; - } - - const fullSupertestFunctionName = sourceCode.getText(supertestCall.callee.object.callee); - const supertestFunctionName = fullSupertestFunctionName.split('.').pop(); - assert.ok(supertestFunctionName !== undefined); - - if (isUsedInArrayOrAsArgument(supertestCall) || getEnclosingFunction(supertestCall)?.async === false) { - // skip and leave it to "fetch-then" rule to handle it because no "await" can be used here - return; - } - - const indentation = getIndentation(supertestCall, sourceCode); - - const fixtureCallInformation = {} as FixtureCallInformation; - const fixtureFunction = supertestCall.callee.object; - analyzeFixtureCall(fixtureFunction, fixtureCallInformation, sourceCode); - fixtureCallInformation.assertions?.flat().map((ass) => sourceCode.getText(ass)); - - const { - variable: responseVariable, - bodyReferences: responseBodyReferences, - // headersReferences: responseHeadersReferences, - statusReferences: responseStatusReferences, - destructuringBodyVariable: destructuringResponseBodyVariable, - destructuringHeadersVariable: destructuringResponseHeadersVariable, - destructuringStatusVariable: destructuringResponseStatusVariable, - } = analyzeResponseReferences(fixtureCallInformation.variableDeclaration, scopeManager); - - const responseVariableNameToUse = getResponseVariableNameToUse( - supertestFunctionName, - scopeManager, - fixtureCallInformation, - scopeVariablesMap, - ); - - const isResponseBodyVariableRedefinitionNeeded = - destructuringResponseBodyVariable !== undefined || - fixtureCallInformation.inlineBodyReference !== undefined || - (responseBodyReferences.length > 0 && !responseBodyReferences.some(isResponseBodyRedefinition)); - const redefineResponseBodyVariableName = `${responseVariableNameToUse}Body`; - - const isResponseStatusVariableRedefinitionNeeded = - destructuringResponseStatusVariable !== undefined || - fixtureCallInformation.inlineStatusReference !== undefined; - const redefineResponseStatusVariableName = `${responseVariableNameToUse}Status`; - - const isResponseHeadersVariableRedefinitionNeeded = - (destructuringResponseHeadersVariable !== undefined && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern) || - fixtureCallInformation.inlineHeadersReference !== undefined; - const redefineResponseHeadersVariableName = `${responseVariableNameToUse}Headers`; - - const isResponseVariableRedefinitionNeeded = - (fixtureCallInformation.variableAssignment === undefined && - responseVariable === undefined && - fixtureCallInformation.assertions !== undefined) || - isResponseBodyVariableRedefinitionNeeded || - isResponseStatusVariableRedefinitionNeeded || - isResponseHeadersVariableRedefinitionNeeded; - - const responseBodyHeadersVariableRedefineLines = isResponseVariableRedefinitionNeeded - ? [ - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseBodyVariable - ? [ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseBodyVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseBodyVariable as TSESTree.ObjectPattern) : (destructuringResponseBodyVariable as Variable).name} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseBodyVariableRedefinitionNeeded - ? [ - `const ${redefineResponseBodyVariableName} = ${getResponseBodyRetrievalText(responseVariableNameToUse)}`, - ] - : []), - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseStatusVariable - ? [ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseStatusVariable as TSESTree.ObjectPattern).type === AST_NODE_TYPES.ObjectPattern ? sourceCode.getText(destructuringResponseStatusVariable as TSESTree.ObjectPattern) : (destructuringResponseStatusVariable as Variable).name} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseStatusVariableRedefinitionNeeded - ? [ - `const ${redefineResponseStatusVariableName} = ${getResponseStatusRetrievalText(responseVariableNameToUse)}`, - ] - : []), - // eslint-disable-next-line no-nested-ternary - ...(destructuringResponseHeadersVariable - ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).type === - AST_NODE_TYPES.ObjectPattern - ? (destructuringResponseHeadersVariable as TSESTree.ObjectPattern).properties.map((property) => { - assert.ok(property.type === AST_NODE_TYPES.Property); - assert.ok(property.value.type === AST_NODE_TYPES.Identifier); - // eslint-disable-next-line sonarjs/no-nested-template-literals - return `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${property.value.name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}.get(${property.key.type === AST_NODE_TYPES.Literal ? sourceCode.getText(property.key) : `'${sourceCode.getText(property.key)}'`})`; - }) - : [ - `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${(destructuringResponseHeadersVariable as Variable).name} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, - ] - : isResponseHeadersVariableRedefinitionNeeded - ? [ - `const ${redefineResponseHeadersVariableName} = ${getResponseHeadersRetrievalText(responseVariableNameToUse)}`, - ] - : []), - ] - : []; - - const { statusAssertion, nonStatusAssertions } = createResponseAssertions( - fixtureCallInformation, - sourceCode, - responseVariableNameToUse, - destructuringResponseHeadersVariable as Variable | undefined, - ); - - // add variable declaration if needed - const fetchCallText = sourceCode.getText(fixtureFunction); - const fetchStatementText = !isResponseVariableRedefinitionNeeded - ? fetchCallText - : `${fixtureCallInformation.variableDeclaration?.kind ?? 'const'} ${responseVariableNameToUse} = await ${fetchCallText}`; - - const nodeToReplace = isResponseVariableRedefinitionNeeded - ? fixtureCallInformation.rootNode - : fixtureCallInformation.fixtureNode; - const appendingAssignmentAndAssertionText = [ - '', - ...(statusAssertion !== undefined ? [statusAssertion] : []), - ...responseBodyHeadersVariableRedefineLines, - ...nonStatusAssertions, - ].join(`;\n${indentation}`); - - context.report({ - node: supertestCall, - messageId: 'preferNativeFetch', - - *fix(fixer) { - if (fixtureCallInformation.inlineStatementNode) { - const preInlineDeclaration = [ - fetchStatementText, - `${appendingAssignmentAndAssertionText};\n${indentation}`, - ].join(``); - yield fixer.insertTextBefore(fixtureCallInformation.inlineStatementNode, preInlineDeclaration); - } else { - yield fixer.replaceText(nodeToReplace, fetchStatementText); - - const needEndingSemiColon = sourceCode.getText(nodeToReplace).endsWith(';'); - yield fixer.insertTextAfter( - nodeToReplace, - needEndingSemiColon ? `${appendingAssignmentAndAssertionText};` : appendingAssignmentAndAssertionText, - ); - } - - // handle response body references - for (const responseBodyReference of responseBodyReferences) { - yield fixer.replaceText( - responseBodyReference, - isResponseBodyVariableRedefinitionNeeded || !isResponseBodyRedefinition(responseBodyReference) - ? redefineResponseBodyVariableName - : getResponseBodyRetrievalText(responseVariableNameToUse), - ); - } - if (fixtureCallInformation.inlineBodyReference) { - yield fixer.replaceText(fixtureCallInformation.inlineBodyReference, redefineResponseBodyVariableName); - } - - // // handle response headers references - // for (const responseHeadersReference of responseHeadersReferences) { - // const parent = getParent(responseHeadersReference); - // assert.ok(parent); - // let headerName; - // if (parent.type === AST_NODE_TYPES.MemberExpression) { - // const headerNameNode = parent.property; - // headerName = parent.computed - // ? sourceCode.getText(headerNameNode) - // : `'${sourceCode.getText(headerNameNode)}'`; - // } else if (parent.type === AST_NODE_TYPES.CallExpression) { - // const headerNameNode = parent.arguments[0]; - // headerName = sourceCode.getText(headerNameNode); - // } - // assert.ok(headerName !== undefined); - // yield fixer.replaceText(parent, `${responseVariableNameToUse}.headers.get(${headerName})`); - // } - - // convert response.statusCode to response.status - for (const responseStatusReference of responseStatusReferences) { - if ( - responseStatusReference.property.type === AST_NODE_TYPES.Identifier && - responseStatusReference.property.name === 'statusCode' - ) { - yield fixer.replaceText(responseStatusReference.property, `status`); - } - } - - // handle direct return statement without await, e.g. "return fixture.api.get(...);" - if ( - fixtureCallInformation.rootNode.type === AST_NODE_TYPES.ReturnStatement && - fixtureCallInformation.assertions !== undefined - ) { - yield fixer.insertTextAfter( - fixtureCallInformation.rootNode, - `\n${indentation}return ${responseVariableNameToUse};`, - ); - } - }, - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error); - context.report({ - node: supertestCall, - messageId: 'unknownError', - data: { - fileName: context.filename, - error: error instanceof Error ? error.toString() : JSON.stringify(error), - }, - }); - } - }, - }; - }, -}); - -export default rule; diff --git a/src/agent/response-reference.ts b/src/agent/response-reference.ts index bb1fc52..7108656 100644 --- a/src/agent/response-reference.ts +++ b/src/agent/response-reference.ts @@ -104,7 +104,7 @@ export function analyzeResponseReferences( // header reference through destruction/renaming, e.g. "const { headers } = ..." identifierParent.type === AST_NODE_TYPES.Property && identifierParent.key.type === AST_NODE_TYPES.Identifier && - identifierParent.key.name === 'headers' + (identifierParent.key.name === 'headers' || identifierParent.key.name === 'header') ) { results.destructuringHeadersVariable = responseVariable; results.destructuringHeadersReferences = responseVariable.references diff --git a/src/agent/supertest-then.spec.ts b/src/agent/supertest-then.spec.ts index 680d8fa..96dd6c7 100644 --- a/src/agent/supertest-then.spec.ts +++ b/src/agent/supertest-then.spec.ts @@ -12,7 +12,7 @@ import rule, { ruleId } from './supertest-then'; createTester().run(ruleId, rule, { valid: [ { - name: 'skip regular supertest calls which will be handled in "no-supertest" rule', + name: 'skip regular supertest calls which will be handled in "no-expect-assertion" rule', code: ` const pingResponse = ping().expect(StatusCodes.OK); const body = pingResponse.body; diff --git a/src/index.ts b/src/index.ts index 5465071..151d731 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,15 +15,15 @@ import fetchResponseHeaderGetter, { ruleId as fetchResponseHeaderGetterRuleId, } from './agent/fetch-response-header-getter'; import fetchResponseStatus, { ruleId as fetchResponseStatusRuleId } from './agent/fetch-response-status'; -import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; +// import fetchThen, { ruleId as fetchThenRuleId } from './agent/fetch-then'; import fixFunctionCallArguments, { ruleId as fixFunctionCallArgumentsRuleId, } from './agent/fix-function-call-arguments'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; import noFixture, { ruleId as noFixtureRuleId } from './agent/no-fixture'; -import noSupertest, { ruleId as noSupertestRuleId } from './agent/no-supertest'; -import supertestThen, { ruleId as supertestThenRuleId } from './agent/supertest-then'; +import noExpectAssertion, { ruleId as noExpectAssertionRuleId } from './agent/no-expect-assertion'; +// import supertestThen, { ruleId as supertestThenRuleId } from './agent/supertest-then'; import noLegacyServiceTyping, { ruleId as noLegacyServiceTypingRuleId } from './no-legacy-service-typing'; import noMappedResponse, { ruleId as noMappedResponseRuleId } from './agent/no-mapped-response'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; @@ -74,9 +74,9 @@ const rules: Record = { [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, [noFixtureRuleId]: noFixture, - [noSupertestRuleId]: noSupertest, - [supertestThenRuleId]: supertestThen, - [fetchThenRuleId]: fetchThen, + [noExpectAssertionRuleId]: noExpectAssertion, + // [supertestThenRuleId]: supertestThen, + // [fetchThenRuleId]: fetchThen, [noServiceWrapperRuleId]: noServiceWrapper, [noStatusCodeRuleId]: noStatusCode, [fetchResponseBodyJsonRuleId]: fetchResponseBodyJson, @@ -135,14 +135,14 @@ const configs: Record = { [`@checkdigit/${noMappedResponseRuleId}`]: 'off', [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noSupertestRuleId}`]: 'off', - [`@checkdigit/${supertestThenRuleId}`]: 'off', + [`@checkdigit/${noExpectAssertionRuleId}`]: 'off', + // [`@checkdigit/${supertestThenRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', [`@checkdigit/${fetchResponseStatusRuleId}`]: 'off', - [`@checkdigit/${fetchThenRuleId}`]: 'off', + // [`@checkdigit/${fetchThenRuleId}`]: 'off', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', @@ -185,14 +185,14 @@ const configs: Record = { [`@checkdigit/${noMappedResponseRuleId}`]: 'off', [`@checkdigit/${addUrlDomainRuleId}`]: 'off', [`@checkdigit/${noFixtureRuleId}`]: 'off', - [`@checkdigit/${noSupertestRuleId}`]: 'off', - [`@checkdigit/${supertestThenRuleId}`]: 'off', + [`@checkdigit/${noExpectAssertionRuleId}`]: 'off', + // [`@checkdigit/${supertestThenRuleId}`]: 'off', [`@checkdigit/${noServiceWrapperRuleId}`]: 'off', [`@checkdigit/${noStatusCodeRuleId}`]: 'off', [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'off', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'off', [`@checkdigit/${fetchResponseStatusRuleId}`]: 'off', - [`@checkdigit/${fetchThenRuleId}`]: 'off', + // [`@checkdigit/${fetchThenRuleId}`]: 'off', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'off', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'off', [`@checkdigit/${noUnusedImportsRuleId}`]: 'off', @@ -221,7 +221,7 @@ const configs: Record = { [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchResponseStatusRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', + // [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', [`@checkdigit/${noUnusedImportsRuleId}`]: 'error', @@ -230,8 +230,8 @@ const configs: Record = { [`@checkdigit/${addBasePathImportRuleId}`]: 'error', [`@checkdigit/${addAssertImportRuleId}`]: 'error', [`@checkdigit/${noFixtureRuleId}`]: 'error', - [`@checkdigit/${noSupertestRuleId}`]: 'error', - [`@checkdigit/${supertestThenRuleId}`]: 'error', + [`@checkdigit/${noExpectAssertionRuleId}`]: 'error', + // [`@checkdigit/${supertestThenRuleId}`]: 'error', }, }, { @@ -260,7 +260,7 @@ const configs: Record = { [`@checkdigit/${fetchResponseBodyJsonRuleId}`]: 'error', [`@checkdigit/${fetchResponseHeaderGetterRuleId}`]: 'error', [`@checkdigit/${fetchResponseStatusRuleId}`]: 'error', - [`@checkdigit/${fetchThenRuleId}`]: 'error', + // [`@checkdigit/${fetchThenRuleId}`]: 'error', [`@checkdigit/${noUnusedFunctionArgumentsRuleId}`]: 'error', [`@checkdigit/${noUnusedServiceVariablesRuleId}`]: 'error', [`@checkdigit/${noUnusedImportsRuleId}`]: 'error',