diff --git a/README.md b/README.md index b49cb52..a73935a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Copyright (c) 2021-2024 [Check Digit, LLC](https://checkdigit.com) - `@checkdigit/no-side-effects` - `@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/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/package-lock.json b/package-lock.json index 559b264..eb3427c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@checkdigit/eslint-plugin", - "version": "7.4.1", + "version": "7.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@checkdigit/eslint-plugin", - "version": "7.4.1", + "version": "7.5.0", "license": "MIT", "dependencies": { "@typescript-eslint/type-utils": "^8.15.0", @@ -85,23 +85,23 @@ } }, "node_modules/@babel/core": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", - "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@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", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -253,15 +253,15 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", - "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" }, "engines": { "node": ">=6.9.0" @@ -344,14 +344,14 @@ } }, "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==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -524,14 +524,14 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "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==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1093,16 +1093,19 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -2129,14 +2132,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", - "integrity": "sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/stack-utils": { @@ -2943,9 +2946,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001669", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", - "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "version": "1.0.30001678", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001678.tgz", + "integrity": "sha512-RR+4U/05gNtps58PEBDZcPWTgEO2MBeoPZ96aQcjmfkBWRIDfN451fW2qyDA9/+HohLLIL5GqiMwA+IB1pWarw==", "dev": true, "funding": [ { @@ -3340,9 +3343,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.41", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", - "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", + "version": "1.5.53", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.53.tgz", + "integrity": "sha512-7F6qFMWzBArEFK4PLE+c+nWzhS1kIoNkQvGnNDogofxQAym+roQ0GUIdw6C/4YdJ6JKGp19c2a/DLcfKTi4wRQ==", "dev": true, "license": "ISC", "peer": true @@ -4060,9 +4063,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 @@ -6603,9 +6606,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": { @@ -7623,9 +7626,9 @@ } }, "node_modules/svelte": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.5.tgz", - "integrity": "sha512-f4WBlP5g8W6pEoDfx741lewMlemy+LIGpEqjGPWqnHVP92wqlQXl87U5O5Bi2tkSUrO95OxOoqwU8qlqiHmFKA==", + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.12.tgz", + "integrity": "sha512-U9BwbSybb9QAKAHg4hl61hVBk97U2QjUKmZa5++QEGoi6Nml6x6cC9KmNT1XObGawToN3DdLpdCs/Z5Yl5IXjQ==", "dev": true, "license": "MIT", "peer": true, @@ -7832,9 +7835,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/package.json b/package.json index 58d63d8..5d064fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@checkdigit/eslint-plugin", - "version": "7.4.1", + "version": "7.5.0", "description": "Check Digit eslint plugins", "keywords": [ "eslint", 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 b5657d4..89cbb38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import type { TSESLint } from '@typescript-eslint/utils'; import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify'; import noDuplicatedImports, { ruleId as noDuplicatedImportsRuleId } from './no-duplicated-imports'; -import noFullResponse, { ruleId as noFullResponseRuleId } from './agent/no-full-response'; +import noLegacyServiceTyping, { ruleId as noLegacyServiceTypingRuleId } from './no-legacy-service-typing'; import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method'; import requireFixedServicesImport, { ruleId as requireFixedServicesImportRuleId, @@ -21,7 +21,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 filePathComment from './file-path-comment'; import noCardNumbers from './no-card-numbers'; import noSideEffects from './no-side-effects'; @@ -48,69 +48,83 @@ const rules: Record = { 'object-literal-response': objectLiteralResponse, [invalidJsonStringifyRuleId]: invalidJsonStringify, [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod, - [noFullResponseRuleId]: noFullResponse, + [noLegacyServiceTypingRuleId]: noLegacyServiceTyping, [requireResolveFullResponseRuleId]: requireResolveFullResponse, - [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, [noDuplicatedImportsRuleId]: noDuplicatedImports, - [requireFixedServicesImportRuleId]: requireFixedServicesImport, [noServeRuntimeRuleId]: noServeRuntime, + [requireFixedServicesImportRuleId]: requireFixedServicesImport, + [requireTypeOutOfTypeOnlyImportsRuleId]: requireTypeOutOfTypeOnlyImports, }; const plugin: TSESLint.FlatConfig.Plugin = { rules, }; -const configs: Record = { - all: { - plugins: { - '@checkdigit': plugin, - }, - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'error', - '@checkdigit/no-random-v4-uuid': 'error', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'error', - '@checkdigit/no-side-effects': ['error', { excludedIdentifiers: ['assert', 'debug', 'log', 'promisify'] }], - '@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/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', - [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', - [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', - [`@checkdigit/${noServeRuntimeRuleId}`]: 'error', - }, - }, - recommended: { - plugins: { - '@checkdigit': plugin, +const configs: Record = { + all: [ + { + files: ['**/*.ts'], + plugins: { + '@checkdigit': plugin, + }, + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'error', + '@checkdigit/no-random-v4-uuid': 'error', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'error', + '@checkdigit/no-side-effects': ['error', { excludedIdentifiers: ['assert', 'debug', 'log', 'promisify'] }], + '@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/${noLegacyServiceTypingRuleId}`]: 'error', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'error', + [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', + [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'error', + [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + [`@checkdigit/${noServeRuntimeRuleId}`]: 'error', + }, }, - rules: { - '@checkdigit/no-card-numbers': 'error', - '@checkdigit/file-path-comment': 'off', - '@checkdigit/no-random-v4-uuid': 'error', - '@checkdigit/no-uuid': 'error', - '@checkdigit/require-strict-assert': 'error', - '@checkdigit/no-wallaby-comment': 'off', - '@checkdigit/no-side-effects': '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', + ], + recommended: [ + { + files: ['**/*.ts'], + plugins: { + '@checkdigit': plugin, + }, + rules: { + '@checkdigit/no-card-numbers': 'error', + '@checkdigit/file-path-comment': 'off', + '@checkdigit/no-random-v4-uuid': 'error', + '@checkdigit/no-uuid': 'error', + '@checkdigit/require-strict-assert': 'error', + '@checkdigit/no-wallaby-comment': 'off', + '@checkdigit/no-side-effects': '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}`]: 'off', + [`@checkdigit/${noLegacyServiceTypingRuleId}`]: 'off', + [`@checkdigit/${requireResolveFullResponseRuleId}`]: 'off', + [`@checkdigit/${noDuplicatedImportsRuleId}`]: 'error', + [`@checkdigit/${requireFixedServicesImportRuleId}`]: 'off', + [`@checkdigit/${requireTypeOutOfTypeOnlyImportsRuleId}`]: 'error', + [`@checkdigit/${noServeRuntimeRuleId}`]: 'off', + }, }, - }, + ], }; -const pluginToExport: TSESLint.FlatConfig.Plugin = { +const defaultToExport: Exclude & { + configs: Record; +} = { ...plugin, configs, }; -export default pluginToExport; +export default defaultToExport; 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/src/agent/no-full-response.spec.ts b/src/no-legacy-service-typing.spec.ts similarity index 75% rename from src/agent/no-full-response.spec.ts rename to src/no-legacy-service-typing.spec.ts index 2feed5a..3b84f9c 100644 --- a/src/agent/no-full-response.spec.ts +++ b/src/no-legacy-service-typing.spec.ts @@ -1,4 +1,4 @@ -// agent/no-full-response.spec.ts +// no-legacy-service-typing.spec.ts /* * Copyright (c) 2021-2024 Check Digit, LLC @@ -6,8 +6,8 @@ * 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 createTester from './ts-tester.test'; +import rule, { ruleId } from './no-legacy-service-typing'; createTester().run(ruleId, rule, { valid: [], @@ -15,7 +15,7 @@ createTester().run(ruleId, rule, { { name: 'report type annotation from variable declaration', code: `const responses: FullResponse = 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..cb1fdd3 --- /dev/null +++ b/src/no-legacy-service-typing.ts @@ -0,0 +1,49 @@ +// no-legacy-service-typing.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..9efc925 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-serve-runtime.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'; diff --git a/src/require-resolve-full-response.spec.ts b/src/require-resolve-full-response.spec.ts index 717cff0..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: [ { @@ -186,5 +204,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..e9cf7a8 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; } } @@ -156,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', @@ -164,23 +166,36 @@ 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);