From c2fc0ae692553d79834a7b1ba12d5246fe0889a0 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 26 Jun 2024 10:27:57 -0400 Subject: [PATCH] feat(eslint-plugin): [no-unnecessary-type-parameters] initial implementation (#8173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * new rule, pairing w/ josh * start adding tests, discover some problesm * failing testgs * very simple version that fails with inferred return types * some processing of inferred return types * try to add inferred usages to count * 11/12 passing! * 12/12 pass * weird circular JSON error * oops * add test with inferred tuple type * add test with inferred tuple type * failing test with inferred object type * descend into object types * copy over another test * failing test case on method * Run on MethodDefinition * add TODO * comment * re-enable invalid eslint-plugin-etc test * port one more * shadowing test case * port one more test * port over DefinitelyTyped-tools tests; two failing * resolve one failure * resolve second failure * enable two more tests * one failing test from eslint-plugin-etc * one more failing test * full port of eslint-plugin-etc tests * one failing test from ETS post * fix previous error, expose a new one with a test * resolve inferred return types on methods * add both() function to test cases * enable eslint rule, cleanup * cleanup, add docs * add to rules list * generate configs * update tests, docs * Update packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.md Co-authored-by: Josh Goldberg ✨ * Update packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts Co-authored-by: Josh Goldberg ✨ * fix(utils): improve error message on typed rule with invalid parser (#8146) * feat(utils): throw error on typed rule with invalid parser * Switch to more informative error message * fix tests * chore: enable no-dynamic-delete internally (#7954) * chore: rename release.ts to release.mts (#8204) * chore: rename release.ts to release.mts * Update tsconfig.eslint.json Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> --------- Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> * chore: enable eslint-plugin-jsdoc internally (#8145) * chore: enable eslint-plugin-jsdoc internally * Disable jsdoc/check-param-names, with link to issue * Sort jsdoc rule disables and add links to issues * Switch typescript-estree-import.ts to line comments * docs: document allowAutomaticSingleRunInference (#8138) * docs: document allowAutomaticSingleRunInference * so 'no' need * chore: prevent a11y-alt-text-bot workflow when author is a bot (#8212) * chore: prevent a11y-alt-text-bot workflow when author is a bot * Update .github/workflows/a11y-alt-bot.yml --------- Co-authored-by: Josh Goldberg ✨ * chore(typescript-estree): remove unnecessary old snapshots (#8198) * chore(typescript-estree): remove unnecessary old snapshots Co-authored-by: Brad Zacher * chore: remove unused jest include patterns --------- Co-authored-by: Brad Zacher * fix(eslint-plugin): [no-non-null-assertion] provide valid fix when member access is on next line (#8185) * fix(eslint-plugin): [no-non-null-assertion] provide valid fix when member access is on next line * test: add new case * fix(eslint-plugin): [no-unnecessary-condition] improve checking optional callee (#8178) * fix(eslint-plugin): [prefer-readonly] support modifiers of unions and intersections (#8169) * fix(eslint-plugin): [switch-exhaustiveness-check] fix new allowDefaultCaseForExhaustiveSwitch option (#8176) * fix(allowDefaultCaseForExhaustiveSwitch): rule * chore: format * fix: add containsNonLiteralType * chore: cleanup * refactor: use type flags instead of strings * Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts * Update packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts --------- Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency tsx to v4.7.0 (#8162) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: fix yargs call in release script (#8221) * chore: fix test formatting in prefer-readonly.test.ts (#8223) * chore(release): publish 6.18.1 * feat(eslint-plugin): [prefer-promise-reject-errors] add rule (#8011) * feat(eslint-plugin): [prefer-promise-reject-errors] new rule! * test: ~100% coverage * docs: add rule docs * test: add some cases * chore: lint --fix * chore: reformat tests * feat: add support for literal computed reject name * chore: lint --fix * refactor: get rid of one @ts-expect-error * docs: refer to the original rule description * test: add few cases * docs: remove some examples * refactor: move check if symbol is from default lib or not to new fn * refactor: assert that rejectVariable is non-nullable * chore: remove assertion in skipChainExpression * test: specify error ranges for invalid test cases * chore: format tests * chore: remove unused check if variable reference is read or not * chore: include rule to `strict-type-checked` config * refactor: simplify isSymbolFromDefaultLibrary * chore: remove ts-expect-error comment * feat: add checks for Promise child classes and unions/intersections * Update packages/eslint-plugin/docs/rules/prefer-promise-reject-errors.md Co-authored-by: Josh Goldberg ✨ * refactor: `program` -> `services.program` * refactor: split unreadable if condition * docs: simplify examples * refactor: rename `isBuiltinSymbolLike.ts` -> `builtinSymbolLikes.ts` * perf: get type of `reject` callee lazily * test: add cases with arrays,never,unknown * feat: add support for `Readonly` and similar * chore: fix lint issues --------- Co-authored-by: Josh Goldberg ✨ * docs: base Testing Rules documentation (#8033) * docs: base Testing Rules documentation * fix(eslint-plugin): add no-unsafe-unary-minus, prefer-destructuring to disable-type-checked (#8038) fix: add no-unsafe-unary-minus, prefer-destructuring to disable-type-checked * docs: testing rules review changes + build callout * docs(eslint-plugin): [require-array-sort-compare] sync rule description (#8061) * chore: resolve internal lint issues with new no-useless-template-literals rule (#8060) * docs(eslint-plugin): [require-array-sort-compare] generalize sort method names (#8062) docs: streamline * chore: update sponsors (#8069) Co-authored-by: typescript-eslint[bot] * Update docs/contributing/local-development/Local_Linking.mdx Co-authored-by: Josh Goldberg ✨ * docs: changed section name + callout type from caution to note --------- Co-authored-by: Josh Goldberg ✨ Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> Co-authored-by: James <5511220+Zamiell@users.noreply.github.com> Co-authored-by: typescript-eslint[bot] <53356952+typescript-eslint[bot]@users.noreply.github.com> Co-authored-by: typescript-eslint[bot] Co-authored-by: Gabriel Costa Moura * feat(eslint-plugin): [no-array-delete] add new rule (#8067) * feat(eslint-plugin): [no-array-delete] add new rule * small refactor * add more cases * fix docs * fix message * use suggestion instead of fix * added more test cases * remove redundant condition * keep comments * docs: force space after await in no-floating-promises snippet (#8228) force space after await * feat(eslint-plugin): [no-useless-template-literals] add fix suggestions (#8065) * feat(eslint-plugin): [no-useless-template-literals] add fix suggestions * change message * add quasis to cspell * add some test cases * use fix instead of suggestion * docs: link version in website header to GitHub release (#8236) * fix(typescript-estree): add JSDocParsingMode enum merge for typescript/lib/tsserverlibrary (#8193) * fix(eslint-plugin): [no-unnecessary-type-assertion] detect unnecessary non-null-assertion on a call expression (#8143) * feat(eslint-plugin): [no-unnecessary-type-assertion] add `Identifier` check * feat(eslint-plugin): [no-unnecessary-type-assertion] update fixer for `CallExpression` * feat(eslint-plugin): [no-unnecessary-type-assertion] add test case * feat(eslint-plugin): [no-unnecessary-type-assertion] fit more cases * feat(eslint-plugin): [no-unnecessary-type-assertion] add valid test cases * fix: fix a gap and add tests * fix: typo * fix(typescript-estree): disallow `using` as the variable keyword for `for..in` loops (#7649) Co-authored-by: Brad Zacher * fix(eslint-plugin): [no-unnecesary-type-assertion] treat unknown/any as nullable (#8089) Co-authored-by: Brad Zacher * fix(typescript-estree): fix incorrect backwards-compat augmentation in TS 5.3 (#8181) * chore: make lint job use eslint-plugin outputs as inputs (#8245) * chore(release): publish 6.19.0 * fix(type-utils): preventing isUnsafeAssignment infinite recursive calls (#8237) * fix(type-utils): preventing isUnsafeAssignment infinite recursive calls * chore: apply reviews * fix(eslint-plugin): [no-unnecessary-condition] fix false positive for type variable (#8235) * fix(eslint-plugin): [no-unnecessary-condition] fix false positive for type variable * chore: simplify test cases * chore(release): publish 6.19.1 * fix(eslint-plugin): [no-useless-template-literals] incorrect bigint autofix result (#8283) * docs: underline URLs, change contrast in syntax highlighting (#8225) * Add underlines on all links other than menu, table of contents * Fix comment in css Include next page in comment as I forgot to include it beforehand * Add comment whitespace * Remove underlines from navbar, buttons * Fix CSS in blog page causing headers, sidebar links to be underlined * Alter CSS to change only anchor elements inside .markdown class * Update styles to change color contrast in syntax highlighting * chore: update sponsors (#8303) Co-authored-by: typescript-eslint[bot] * docs: add `import/no-unresolved` to perf troubleshooting docs (#8190) * feat(eslint-plugin): [member-ordering] allow easy reuse of the default ordering (#8248) * docs: show all articles in sidebar, and a missing truncate (#8306) * chore: enable prefer-nullish-coalescing internally (#7955) * chore: enable prefer-nullish-calescing internally * A couple complaints * One last complaint * Enable ignoreConditionalTests * fix(eslint-plugin): [prefer-nullish-coalescing] treat any/unknown as non-nullable (#8262) * fix(eslint-plugin): [prefer-nullish-coalescing] treat any/unknown as non-nullable * chore: rm unrelated changes * test: add declarations of 'y' variable * fix(eslint-plugin): [no-useless-template-literals] report Infinity & NaN (#8295) * fix(eslint-plugin): [no-useless-template-literals] report Infinity & NaN Closes #8294 * remove unnecessary tests * chore(deps): update react (#8042) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(eslint-plugin): fix typos in schema definitions (#8311) chore: Fix typos in schema definitions * chore: fix broken and outdated links (#8264) * fix(eslint-plugin): [prefer-readonly] disable checking accessors (#8300) * Disable checking accessors for prefer-readonly * Granular accessor tests * Update packages/eslint-plugin/src/rules/prefer-readonly.ts * fix: formatting and || for isModifierFlagSet suggestion --------- Co-authored-by: Josh Goldberg ✨ * chore(release): publish 6.20.0 * chore(eslint-plugin): [no-unused-vars] remove unused nested TSModuleDeclaration rule listener (#8279) * fix(eslint-plugin): [no-unused-vars] don't report on types referenced in export assignment expression (#8265) * docs(eslint-plugin): remove `fixable` and `hasSuggestions` from rules that don't provide them (#8253) * feat(typescript-estree): forbid duplicated accessibility modifiers (#8257) * test(eslint-plugin): assert that `ts`/`tsx` code blocks in docs are syntactically valid (#8142) * test(eslint-plugin): assert that `ts`/`tsx` code blocks in docs are syntactically valid * revert unintended changes in space-before-blocks.md * chore: shorten examples in consistent-type-assertions.md * refactor: do not parse md file again * docs: more consistent examples for consistent-type-assertions * docs: convert `js` code blocks to `ts` * Update packages/eslint-plugin/tests/docs.test.ts Co-authored-by: Josh Goldberg ✨ * chore: use regex instead of startsWith * chore: fix few codesamples --------- Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency @swc/jest to v0.2.31 (#8313) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.3.106 (#8219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update babel to v7.23.9 (#8199) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: unpin primary node version in ci (#8167) * fix(eslint-plugin): [switch-exhaustiveness-check] better support for intersections, infinite types, non-union values (#8250) * feat(eslint-plugin): [switch-exhaustiveness-check] better support for intersections, infinite types, non-union values * chore: try to fix weird diff with main * refactor: no need to collect missing branches in function * fix: provide valid fixes for unique symbols * fix: valid fixes for unique symbols + few test cases for enums * docs: add /maintenance/team page (#8057) * docs: add /maintenance/team page * Apply Brad's requested changes * Styles and social links * Mentioned Contributor Tiers * Added Sponsor Us section * chore(deps): update dependency eslint-plugin-jest to v27.6.3 (#230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency netlify to v13.1.13 (#231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency yargs to v17.7.2 (#233) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @prettier/sync to v0.5.0 (#235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.11.9 (#236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency chai to v4.4.1 (#237) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @swc/core to v1.3.107 (#238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency esbuild to ~0.20.0 (#240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Alphabetized CSS (sorry StyleShit 😛) * Update packages/website/src/components/team/TeamBio.module.css Co-authored-by: StyleShit <32631382+StyleShit@users.noreply.github.com> * The rest of .serviceIconLink *
* Shrunk images and used my newer colors --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: StyleShit <32631382+StyleShit@users.noreply.github.com> * chore: move generate-configs to repo-tools (#8329) * chore: add `typescript-eslint` package scaffold (#8296) * fix(eslint-plugin): [consistent-type-imports] dont report on types used in export assignment expressions (#8332) * fix(eslint-plugin): [no-unnecessary-condition] handle left-hand optional with exactOptionalPropertyTypes option (#8249) * fix(eslint-plugin): [no-unnecessary-condition] handle left-hand optional with exactOptionalPropertyTypes option * typo: remove the 's' --------- Co-authored-by: Josh Goldberg * chore: remove unnecessary eslint-disable comments (#8336) * chore: update sponsors (#8340) Co-authored-by: typescript-eslint[bot] * chore: bump eslint versions (#8338) * chore: cleanup test-utils naming/locations (#8341) * chore(website): [playground] add twoslash queries (#8119) * fix(eslint-plugin): [class-literal-property-style] allow getter when same key setter exists (#8277) * feat(utils): improve eslint types (#8344) * feat: export plugin metadata (#8331) * feat: export plugin metadata * Update Config.ts * feat(eslint-plugin): add rule prefer-find (#8216) * Add rule prefer-find * address lots of stuff * remove console statement * tweaks * extract fix to function * improve behavior around nulls * add comments around array indexing checks * messages were backwards * filter syntax * formatting * add extra comma operator test * pr feedback round 2 * Fix the the Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> * fix up imports * address intersections of arrays --------- Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> * chore(deps): update dependency @prettier/sync to ^0.5.0 (#8342) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency clsx to v2.1.0 (#8343) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency netlify to v13.1.14 (#8321) chore(deps): update dependency netlify to v13.1.13 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency lint-staged to v15.2.0 (#8043) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency prettier to v3.2.4 (#8127) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency markdownlint-cli to ^0.38.0 (#8159) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency monaco-editor to ~0.45.0 (#8161) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency react-resizable-panels to ^0.0.63 (#8328) * chore(deps): update dependency react-resizable-panels to ^0.0.63 * Updated props for '*Percentage' --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg * chore(deps): update dependency lint-staged to v15.2.1 (#8350) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(ast-spec): add `JSXElement` type to the `JSXAttribute['value']` (#8285) * fix(ast-spec): add `JSXElement` type to the `JSXAttribute['value']` * test: add fixtures * chore: sync snapshots * fix(eslint-plugin): [no-unnecessary-type-assertion] provide valid fixes for assertions with extra tokens before `as` keyword (#8326) * fix(eslint-plugin): [no-unnecessary-type-assertion] provide valid fixes for assertions with extra tokens before `as` keyword * fix: provide fixes for angle bracket assertions when there're tokens inside * chore(eslint-plugin): [no-invalid-void-type] fix `Options` typing to reflect `minItems: 1` (#8312) * fix(rule-tester): fix a phantom dependency on the "semver" package (#8260) * fix(rule-tester): Fix a phantom dependency on the "semver" package in DependencyConstraint.d.ts * Update packages/rule-tester/src/types/DependencyConstraint.ts Co-authored-by: Josh Goldberg ✨ * Remove 'extends Options' --------- Co-authored-by: Josh Goldberg ✨ Co-authored-by: Brad Zacher * feat: allow `parserOptions.project: false` (#8339) * chore(deps): update dependency @swc/jest to v0.2.34 (#8363) chore(deps): update dependency @swc/jest to v0.2.33 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency ignore to v5.3.1 (#8360) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to v20.11.15 (#8356) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/react to v18.2.51 (#8361) chore(deps): update dependency @types/react to v18.2.49 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency netlify to v13.1.14 (#8353) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency webpack to v5.90.0 (#8359) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency @babel/eslint-parser to v7.23.10 (#8349) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency @types/jest to v29.5.12 (#8371) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency markdownlint-cli to ^0.39.0 (#8354) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josh Goldberg ✨ * chore(deps): update dependency prettier to v3.2.4 (#8357) * chore(deps): update dependency prettier to v3.2.4 * chore: update formatting after prettier upgrade --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: JamesHenry * code review * Apply suggestions from code review * Fixed up the tsNode.body complaint * Apply suggestions from code review * Generated configs, and touched up docs * Let's make it strict * Account for type parameters used as type arguments * Tweaked docs * Correct lint and snapshot issues * Aha, one more T * Fixed up last test cases * Reworked to be types-only * Repo is passing build, lint, and typecheck * Aha, no need for infinite type checking * Updated snapshot * Revert "Aha, no need for infinite type checking" This reverts commit 581937d86a5edcd03dcf409a27f394ea9ac9b65c. * Simplier general/return logic * Wow so much simpler * Adjusted comments * Forward along asRepeatedType * List -> Item * fetchJson, alas * Initial pass of the AST check * chore: add missing output: nulls in prefer-optional-chain.test.ts Result of merging a few PRs into main. * skipConstituentsUpward optimization * Docs and naming improvements * Allow introduced convert.test.ts complaint * Ah, yes, Prettier * Disable comment for helpers isNodeOfTypeWithConditions * Remove from strict and add an experimental notice * shakes fist at nx --------- Co-authored-by: Josh Goldberg ✨ Co-authored-by: auvred <61150013+auvred@users.noreply.github.com> Co-authored-by: Hao Cheng Co-authored-by: Brad Zacher Co-authored-by: YeonJuan Co-authored-by: James <5511220+Zamiell@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: typescript-eslint[bot] Co-authored-by: Gabriel Costa Co-authored-by: typescript-eslint[bot] <53356952+typescript-eslint[bot]@users.noreply.github.com> Co-authored-by: Gabriel Costa Moura Co-authored-by: StyleShit <32631382+StyleShit@users.noreply.github.com> Co-authored-by: kirkwaiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Co-authored-by: Flo Edelmann Co-authored-by: LJX <11309921+lvjiaxuan@users.noreply.github.com> Co-authored-by: Steven Co-authored-by: Joshua Chen Co-authored-by: James Henry Co-authored-by: Lucas Amberg <102396588+lucas-amberg@users.noreply.github.com> Co-authored-by: Alex Parloti Co-authored-by: Edwin Kofler Co-authored-by: NanderTGA <65074195+NanderTGA@users.noreply.github.com> Co-authored-by: James Browning Co-authored-by: Khairul Azhar Kasmiran Co-authored-by: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> --- eslint.config.mjs | 1 + .../rules/no-unnecessary-type-parameters.mdx | 115 ++++ packages/eslint-plugin/src/configs/all.ts | 1 + .../src/configs/disable-type-checked.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/no-unnecessary-type-parameters.ts | 387 +++++++++++ .../src/util/collectUnusedVariables.ts | 12 +- .../no-unnecessary-type-parameters.shot | 49 ++ .../no-unnecessary-type-parameters.test.ts | 612 ++++++++++++++++++ .../no-unnecessary-type-parameters.shot | 14 + .../repo-tools/src/generate-contributors.mts | 2 + .../tests/test-utils/getSpecificNode.ts | 5 +- packages/typescript-eslint/src/configs/all.ts | 1 + .../src/configs/disable-type-checked.ts | 1 + .../typescript-estree/src/parser-options.ts | 2 + .../tests/test-utils/test-utils.ts | 8 +- packages/utils/src/ast-utils/helpers.ts | 2 + packages/utils/src/eslint-utils/deepMerge.ts | 2 +- packages/website/src/globals.d.ts | 3 + 19 files changed, 1206 insertions(+), 14 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx create mode 100644 packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-parameters.shot create mode 100644 packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-parameters.shot diff --git a/eslint.config.mjs b/eslint.config.mjs index 3e71ec470c9d..4c259bee01a1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -134,6 +134,7 @@ export default tseslint.config( 'error', { allowConstantLoopConditions: true }, ], + '@typescript-eslint/no-unnecessary-type-parameters': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-literal-enum-member': [ 'error', diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx new file mode 100644 index 000000000000..130c40b1ce04 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -0,0 +1,115 @@ +--- +description: 'Disallow type parameters that only appear once.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unnecessary-type-parameters** for documentation. + +This rule forbids type parameters that only appear once in a function, method, or class definition. + +Type parameters relate two types. +If a type parameter only appears once, then it is not relating anything. +It can usually be replaced with explicit types such as `unknown`. + +At best unnecessary type parameters make code harder to read. +At worst they can be used to disguise unsafe type assertions. + +:::warning Early Stage +This rule was recently added to typescript-eslint and still considered experimental. +It might change significantly between minor versions. +Please try it out and give us feedback! +::: + +## Examples + + + + +```ts +function second(a: A, b: B): B { + return b; +} + +function parseJSON(input: string): T { + return JSON.parse(input); +} + +function printProperty(obj: T, key: K) { + console.log(obj[key]); +} +``` + + + + +```ts +function second(a: unknown, b: B): B { + return b; +} + +function parseJSON(input: string): unknown { + return JSON.parse(input); +} + +function printProperty(obj: T, key: keyof T) { + console.log(obj[key]); +} + +// T appears twice: in the type of arg and as the return type +function identity(arg: T): T { + return arg; +} + +// T appears twice: "keyof T" and in the inferred return type (T[K]). +// K appears twice: "key: K" and in the inferred return type (T[K]). +function getProperty(obj: T, key: K) { + return obj[key]; +} +``` + + + + +## Limitations + +Note that this rule allows any type parameter that is used multiple times, even if those uses are via a type argument. +For example, the following `T` is used multiple times by virtue of being in an `Array`, even though its name only appears once after declaration: + +```ts +declare function createStateHistory(): T[]; +``` + +This is because the type parameter `T` relates multiple methods in the `T[]` together, making it used more than once. + +Therefore, this rule won't report on type parameters used as a type argument. +That includes type arguments given to global types such as `Array` (including the `T[]` shorthand and in tuples), `Map`, and `Set`. + +## When Not To Use It + +This rule will report on functions that use type parameters solely to test types, for example: + +```ts +function assertType(arg: T) {} + +assertType(123); +assertType('abc'); +// ~~~~~ +// Argument of type 'string' is not assignable to parameter of type 'number'. +``` + +If you're using this pattern then you'll want to disable this rule on files that test types. + +## Further Reading + +- TypeScript handbook: [Type Parameters Should Appear Twice](https://microsoft.github.io/TypeScript-New-Handbook/everything/#type-parameters-should-appear-twice) +- Effective TypeScript: [The Golden Rule of Generics](https://effectivetypescript.com/2020/08/12/generics-golden-rule/) + +## Related To + +- eslint-plugin-etc's [`no-misused-generics`](https://github.com/cartant/eslint-plugin-etc/blob/main/docs/rules/no-misused-generics.md) +- wotan's [`no-misused-generics`](https://github.com/fimbullinter/wotan/blob/master/packages/mimir/docs/no-misused-generics.md) +- DefinitelyTyped-tools' [`no-unnecessary-generics`](https://github.com/microsoft/DefinitelyTyped-tools/blob/main/packages/eslint-plugin/docs/rules/no-unnecessary-generics.md) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index ee778e7e48cd..04ee8a56fc5c 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -97,6 +97,7 @@ export = { '@typescript-eslint/no-unnecessary-type-arguments': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-unnecessary-type-parameters': 'error', '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-call': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 5b3e65c7635e..f0ba1bc0225e 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -37,6 +37,7 @@ export = { '@typescript-eslint/no-unnecessary-template-expression': 'off', '@typescript-eslint/no-unnecessary-type-arguments': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', + '@typescript-eslint/no-unnecessary-type-parameters': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index fe0d36026bd1..877e8a32e689 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -85,6 +85,7 @@ import noUnnecessaryTemplateExpression from './no-unnecessary-template-expressio import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUnnecessaryTypeConstraint from './no-unnecessary-type-constraint'; +import noUnnecessaryTypeParameters from './no-unnecessary-type-parameters'; import noUnsafeArgument from './no-unsafe-argument'; import noUnsafeAssignment from './no-unsafe-assignment'; import noUnsafeCall from './no-unsafe-call'; @@ -231,6 +232,7 @@ export default { 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-unnecessary-type-constraint': noUnnecessaryTypeConstraint, + 'no-unnecessary-type-parameters': noUnnecessaryTypeParameters, 'no-unsafe-argument': noUnsafeArgument, 'no-unsafe-assignment': noUnsafeAssignment, 'no-unsafe-call': noUnsafeCall, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts new file mode 100644 index 000000000000..0f78a6d14ea6 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -0,0 +1,387 @@ +import type { Reference } from '@typescript-eslint/scope-manager'; +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; + +import type { MakeRequired } from '../util'; +import { createRule, getParserServices } from '../util'; + +type NodeWithTypeParameters = MakeRequired< + ts.SignatureDeclaration | ts.ClassLikeDeclaration, + 'typeParameters' +>; + +export default createRule({ + defaultOptions: [], + meta: { + docs: { + description: 'Disallow type parameters that only appear once', + requiresTypeChecking: true, + }, + messages: { + sole: 'Type parameter {{name}} is used only once.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unnecessary-type-parameters', + create(context) { + const parserServices = getParserServices(context); + return { + [[ + 'ArrowFunctionExpression[typeParameters]', + 'ClassDeclaration[typeParameters]', + 'ClassExpression[typeParameters]', + 'FunctionDeclaration[typeParameters]', + 'FunctionExpression[typeParameters]', + 'TSCallSignatureDeclaration[typeParameters]', + 'TSConstructorType[typeParameters]', + 'TSDeclareFunction[typeParameters]', + 'TSEmptyBodyFunctionExpression[typeParameters]', + 'TSFunctionType[typeParameters]', + 'TSMethodSignature[typeParameters]', + ].join(', ')](node: TSESTree.FunctionLike): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node, + ) as NodeWithTypeParameters; + + const checker = parserServices.program.getTypeChecker(); + let counts: Map | undefined; + + for (const typeParameter of tsNode.typeParameters) { + const esTypeParameter = + parserServices.tsNodeToESTreeNodeMap.get( + typeParameter, + ); + const scope = context.sourceCode.getScope(esTypeParameter); + + // Quick path: if the type parameter is used multiple times in the AST, + // we don't need to dip into types to know it's repeated. + if (isTypeParameterRepeatedInAST(esTypeParameter, scope.references)) { + continue; + } + + // For any inferred types, we have to dip into type checking. + counts ??= countTypeParameterUsage(checker, tsNode); + const identifierCounts = counts.get(typeParameter.name); + if (!identifierCounts || identifierCounts > 2) { + continue; + } + + context.report({ + data: { + name: typeParameter.name.text, + }, + node: esTypeParameter, + messageId: 'sole', + }); + } + }, + }; + }, +}); + +function isTypeParameterRepeatedInAST( + node: TSESTree.TSTypeParameter, + references: Reference[], +): boolean { + let total = 0; + + for (const reference of references) { + // References inside the type parameter's definition don't count. + if ( + reference.identifier.range[0] < node.range[1] && + reference.identifier.range[1] > node.range[0] + ) { + continue; + } + + // Neither do references that aren't to the same type parameter, + // namely value-land (non-type) identifiers of the type parameter's type, + // and references to different type parameters or values. + if ( + !reference.isTypeReference || + reference.identifier.name !== node.name.name + ) { + continue; + } + + // If the type parameter is being used as a type argument, then we + // know the type parameter is being reused and can't be reported. + if (reference.identifier.parent.type === AST_NODE_TYPES.TSTypeReference) { + const grandparent = skipConstituentsUpward( + reference.identifier.parent.parent, + ); + if ( + grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation && + grandparent.params.includes(reference.identifier.parent) + ) { + return true; + } + } + + total += 1; + + if (total > 2) { + return true; + } + } + + return false; +} + +function skipConstituentsUpward(node: TSESTree.Node): TSESTree.Node { + switch (node.type) { + case AST_NODE_TYPES.TSIntersectionType: + case AST_NODE_TYPES.TSUnionType: + return skipConstituentsUpward(node.parent); + default: + return node; + } +} + +/** + * Count uses of type parameters in inferred return types. + * We need to resolve and analyze the inferred return type of a function + * to see whether it contains additional references to the type parameters. + * For classes, we need to do this for all their methods. + */ +function countTypeParameterUsage( + checker: ts.TypeChecker, + node: NodeWithTypeParameters, +): Map { + const counts = new Map(); + + if (ts.isClassLike(node)) { + for (const typeParameter of node.typeParameters) { + collectTypeParameterUsageCounts(checker, typeParameter, counts); + } + for (const member of node.members) { + collectTypeParameterUsageCounts(checker, member, counts); + } + } else { + collectTypeParameterUsageCounts(checker, node, counts); + } + + return counts; +} + +/** + * Populates {@link foundIdentifierUsages} by the number of times each type parameter + * appears in the given type by checking its uses through its type references. + * This is essentially a limited subset of the scope manager, but for types. + */ +function collectTypeParameterUsageCounts( + checker: ts.TypeChecker, + node: ts.Node, + foundIdentifierUsages: Map, +): void { + const visitedSymbolLists = new Set(); + const type = checker.getTypeAtLocation(node); + const typeUsages = new Map(); + const visitedConstraints = new Set(); + let functionLikeType = false; + let visitedDefault = false; + + if ( + ts.isCallSignatureDeclaration(node) || + ts.isConstructorDeclaration(node) + ) { + functionLikeType = true; + visitSignature(checker.getSignatureFromDeclaration(node)); + } + + if (!functionLikeType) { + visitType(type, false); + } + + function visitType( + type: ts.Type | undefined, + assumeMultipleUses: boolean, + ): void { + // Seeing the same type > (threshold=3 ** 2) times indicates a likely + // recursive type, like `type T = { [P in keyof T]: T }`. + // If it's not recursive, then heck, we've seen it enough times that any + // referenced types have been counted enough to qualify as used. + if (!type || incrementTypeUsages(type) > 9) { + return; + } + + // https://github.com/JoshuaKGoldberg/ts-api-utils/issues/382 + if ((tsutils.isTypeParameter as (type: ts.Type) => boolean)(type)) { + const declaration = type.getSymbol()?.getDeclarations()?.[0] as + | ts.TypeParameterDeclaration + | undefined; + + if (declaration) { + incrementIdentifierCount(declaration.name, assumeMultipleUses); + + // Visiting the type of a constrained type parameter will recurse into + // the constraint. We avoid infinite loops by visiting each only once. + if ( + declaration.constraint && + !visitedConstraints.has(declaration.constraint) + ) { + visitedConstraints.add(declaration.constraint); + visitType(checker.getTypeAtLocation(declaration.constraint), false); + } + + if (declaration.default && !visitedDefault) { + visitedDefault = true; + visitType(checker.getTypeAtLocation(declaration.default), false); + } + } + } + + // Intersections and unions like `0 | 1` + else if (tsutils.isUnionOrIntersectionType(type)) { + visitTypesList(type.types, assumeMultipleUses); + } + + // Index access types like `T[K]` + else if (tsutils.isIndexedAccessType(type)) { + visitType(type.objectType, assumeMultipleUses); + visitType(type.indexType, assumeMultipleUses); + } + + // Tuple types like `[K, V]` + // Generic type references like `Map` + else if (tsutils.isTupleType(type) || tsutils.isTypeReference(type)) { + for (const typeArgument of type.typeArguments ?? []) { + visitType(typeArgument, true); + } + } + + // Template literals like `a${T}b` + else if (tsutils.isTemplateLiteralType(type)) { + for (const subType of type.types) { + visitType(subType, assumeMultipleUses); + } + } + + // Conditional types like `T extends string ? T : never` + else if (tsutils.isConditionalType(type)) { + visitType(type.checkType, assumeMultipleUses); + visitType(type.extendsType, assumeMultipleUses); + } + + // Catch-all: inferred object types like `{ K: V }`. + // These catch-alls should be _after_ more specific checks like + // `isTypeReference` to avoid descending into all the properties of a + // generic interface/class, e.g. `Map`. + else if (tsutils.isObjectType(type)) { + visitSymbolsListOnce(type.getProperties(), false); + + if (isMappedType(type)) { + visitType(type.typeParameter, false); + } + + for (const typeArgument of type.aliasTypeArguments ?? []) { + visitType(typeArgument, true); + } + + visitType(type.getNumberIndexType(), true); + visitType(type.getStringIndexType(), true); + + type.getCallSignatures().forEach(signature => { + functionLikeType = true; + visitSignature(signature); + }); + + type.getConstructSignatures().forEach(signature => { + functionLikeType = true; + visitSignature(signature); + }); + } + + // Catch-all: operator types like `keyof T` + else if (isOperatorType(type)) { + visitType(type.type, assumeMultipleUses); + } + + // Catch-all: generic type references like `Exclude` + else if (type.aliasTypeArguments) { + visitTypesList(type.aliasTypeArguments, true); + } + } + + function incrementIdentifierCount( + id: ts.Identifier, + assumeMultipleUses: boolean, + ): void { + const identifierCount = foundIdentifierUsages.get(id) ?? 0; + const value = assumeMultipleUses ? 2 : 1; + foundIdentifierUsages.set(id, identifierCount + value); + } + + function incrementTypeUsages(type: ts.Type): number { + const count = (typeUsages.get(type) ?? 0) + 1; + typeUsages.set(type, count); + return count; + } + + function visitSignature(signature: ts.Signature | undefined): void { + if (!signature) { + return; + } + + if (signature.thisParameter) { + visitType(checker.getTypeOfSymbol(signature.thisParameter), false); + } + + for (const parameter of signature.parameters) { + visitType(checker.getTypeOfSymbol(parameter), false); + } + + for (const typeParameter of signature.getTypeParameters() ?? []) { + visitType(typeParameter, false); + } + + visitType( + checker.getTypePredicateOfSignature(signature)?.type ?? + signature.getReturnType(), + false, + ); + } + + function visitSymbolsListOnce( + symbols: ts.Symbol[], + assumeMultipleUses: boolean, + ): void { + if (visitedSymbolLists.has(symbols)) { + return; + } + + visitedSymbolLists.add(symbols); + + for (const symbol of symbols) { + visitType(checker.getTypeOfSymbol(symbol), assumeMultipleUses); + } + } + + function visitTypesList( + types: readonly ts.Type[], + assumeMultipleUses: boolean, + ): void { + for (const type of types) { + visitType(type, assumeMultipleUses); + } + } +} + +interface MappedType extends ts.ObjectType { + typeParameter?: ts.Type; +} + +function isMappedType(type: ts.Type): type is MappedType { + return 'typeParameter' in type; +} + +interface OperatorType extends ts.Type { + type: ts.Type; +} + +function isOperatorType(type: ts.Type): type is OperatorType { + return 'type' in type && !!type.type; +} diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index ae70407e00a8..83d2348895b5 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -90,9 +90,7 @@ class UnusedVarsVisitor< //#region HELPERS - private getScope( - currentNode: TSESTree.Node, - ): T { + private getScope(currentNode: TSESTree.Node): TSESLint.Scope.Scope { // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. const inner = currentNode.type !== AST_NODE_TYPES.Program; @@ -102,15 +100,15 @@ class UnusedVarsVisitor< if (scope) { if (scope.type === ScopeType.functionExpressionName) { - return scope.childScopes[0] as T; + return scope.childScopes[0]; } - return scope as T; + return scope; } node = node.parent; } - return this.#scopeManager.scopes[0] as T; + return this.#scopeManager.scopes[0]; } private markVariableAsUsed( @@ -162,7 +160,7 @@ class UnusedVarsVisitor< node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, ): void { // skip a variable of class itself name in the class scope - const scope = this.getScope(node); + const scope = this.getScope(node) as TSESLint.Scope.Scopes.ClassScope; for (const variable of scope.variables) { if (variable.identifiers[0] === scope.block.id) { this.markVariableAsUsed(variable); diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-parameters.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-parameters.shot new file mode 100644 index 000000000000..ff3a00a42b6e --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-type-parameters.shot @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-unnecessary-type-parameters.mdx code examples ESLint output 1`] = ` +"Incorrect + +function second(a: A, b: B): B { + ~ Type parameter A is used only once. + return b; +} + +function parseJSON(input: string): T { + ~ Type parameter T is used only once. + return JSON.parse(input); +} + +function printProperty(obj: T, key: K) { + ~~~~~~~~~~~~~~~~~ Type parameter K is used only once. + console.log(obj[key]); +} +" +`; + +exports[`Validating rule docs no-unnecessary-type-parameters.mdx code examples ESLint output 2`] = ` +"Correct + +function second(a: unknown, b: B): B { + return b; +} + +function parseJSON(input: string): unknown { + return JSON.parse(input); +} + +function printProperty(obj: T, key: keyof T) { + console.log(obj[key]); +} + +// T appears twice: in the type of arg and as the return type +function identity(arg: T): T { + return arg; +} + +// T appears twice: "keyof T" and in the inferred return type (T[K]). +// K appears twice: "key: K" and in the inferred return type (T[K]). +function getProperty(obj: T, key: K) { + return obj[key]; +} +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts new file mode 100644 index 000000000000..f1d98ea5734e --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -0,0 +1,612 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unnecessary-type-parameters'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-unnecessary-type-parameters', rule, { + valid: [ + ` + class ClassyArray { + arr: T[]; + } + `, + ` + class ClassyArray { + value1: T; + value2: T; + } + `, + ` + class ClassyArray { + arr: T[]; + constructor(arr: T[]) { + this.arr = arr; + } + } + `, + ` + class ClassyArray { + arr: T[]; + workWith(value: T) { + this.arr.indexOf(value); + } + } + `, + ` + abstract class ClassyArray { + arr: T[]; + abstract workWith(value: T): void; + } + `, + ` + class Box { + val: T | null = null; + get() { + return this.val; + } + } + `, + ` + class Joiner { + join(els: T[]) { + return els.map(el => '' + el).join(','); + } + } + `, + ` + class Joiner { + join(els: T[]) { + return els.map(el => '' + el).join(','); + } + } + `, + ` + declare class Foo { + getProp(this: Record<'prop', T>): T; + } + `, + 'type Fn = (input: T) => T;', + 'type Fn = (input: T) => T;', + 'type Fn = (input: T) => `a${T}b`;', + 'type Fn = new (input: T) => T;', + 'type Fn = (input: T) => typeof input;', + 'type Fn = (input: T) => keyof typeof input;', + 'type Fn = (input: Partial) => typeof input;', + 'type Fn = (input: Partial) => input is T;', + 'type Fn = (input: T) => { [K in keyof T]: K };', + 'type Fn = (input: T) => { [K in keyof T as K]: string };', + 'type Fn = (input: T) => { [K in keyof T as `${K & string}`]: string };', + 'type Fn = (input: T) => Partial;', + 'type Fn = (input: { [i: number]: T }) => T;', + 'type Fn = (input: { [i: number]: T }) => Partial;', + 'type Fn = (input: { [i: string]: T }) => Partial;', + 'type Fn = (input: T) => { [i: number]: T };', + 'type Fn = (input: T) => { [i: string]: T };', + "type Fn = (input: T) => Omit;", + ` + interface I { + (value: T): T; + } + `, + ` + interface I { + new (value: T): T; + } + `, + ` + function identity(arg: T): T { + return arg; + } + `, + ` + function printProperty(obj: T, key: keyof T) { + console.log(obj[key]); + } + `, + ` + function getProperty(obj: T, key: K) { + return obj[key]; + } + `, + ` + function box(val: T) { + return { val }; + } + `, + ` + function doStuff(map: Map, key: K) { + let v = map.get(key); + v = 1; + map.set(key, v); + return v; + } + `, + ` + function makeMap() { + return new Map(); + } + `, + ` + function makeMap(ks: K[], vs: V[]) { + const r = new Map(); + ks.forEach((k, i) => { + r.set(k, vs[i]); + }); + return r; + } + `, + ` + function arrayOfPairs() { + return [] as [T, T][]; + } + `, + ` + function isNonNull(v: T): v is Exclude { + return v !== null; + } + `, + ` + function both( + fn1: (...args: Args) => void, + fn2: (...args: Args) => void, + ): (...args: Args) => void { + return function (...args: Args) { + fn1(...args); + fn2(...args); + }; + } + `, + ` + function lengthyIdentity(x: T) { + return x; + } + `, + ` + interface Lengthy { + length: number; + } + function lengthyIdentity(x: T) { + return x; + } + `, + ` + function ItemComponent(props: { item: T; onSelect: (item: T) => void }) {} + `, + ` + interface ItemProps { + item: readonly T; + onSelect: (item: T) => void; + } + function ItemComponent(props: ItemProps) {} + `, + ` + function useFocus(): [ + React.RefObject, + () => void, + ]; + `, + ` + function findFirstResult( + inputs: unknown[], + getResult: (t: unknown) => U | undefined, + ): U | undefined; + `, + ` + function findFirstResult( + inputs: T[], + getResult: (t: T) => () => [U | undefined], + ): () => [U | undefined]; + `, + ` + function getData(url: string): Promise { + return Promise.resolve(null); + } + `, + ` + function getData(url: string): Promise { + return Promise.resolve(null); + } + `, + ` + function getData(url: string): Promise<\`a\${T}b\`> { + return Promise.resolve(null); + } + `, + ` + async function getData(url: string): Promise { + return null; + } + `, + 'declare function get(): void;', + 'declare function get(param: T[]): T;', + 'declare function box(val: T): { val: T };', + 'declare function identity(param: T): T;', + 'declare function compare(param1: T, param2: T): boolean;', + 'declare function example(a: Set): T;', + 'declare function example(a: Set, b: T[]): void;', + 'declare function example(a: Map): void;', + 'declare function example(t: T, u: U): U;', + 'declare function makeSet(): Set;', + 'declare function makeSet(): [Set];', + 'declare function makeSets(): Set[];', + 'declare function makeSets(): [Set][];', + 'declare function makeMap(): Map;', + 'declare function makeMap(): [Map];', + 'declare function arrayOfPairs(): [T, T][];', + 'declare function fetchJson(url: string): Promise;', + 'declare function fn(input: T): 0 extends 0 ? T : never;', + 'declare function useFocus(): [React.RefObject];', + ` + declare function useFocus(): { + ref: React.RefObject; + }; + `, + ` + interface TwoMethods { + a(x: T): void; + b(x: T): void; + } + + declare function two(props: TwoMethods): void; + `, + ` + type Obj = { a: string }; + + declare function hasOwnProperty( + obj: Obj, + key: K, + ): obj is Obj & { [key in K]-?: Obj[key] }; + `, + ` + type AsMutable = { + -readonly [Key in keyof T]: T[Key]; + }; + + declare function makeMutable(input: T): MakeMutable; + `, + ` + type AsMutable = { + -readonly [Key in keyof T]: T[Key]; + }; + + declare function makeMutable(input: T): MakeMutable; + `, + ` + type ValueNulls = {} & { + [P in U]: null; + }; + + declare function invert(obj: T): ValueNulls; + `, + ` + interface Middle { + inner: boolean; + } + + type Conditional = {} & (T['inner'] extends true ? {} : {}); + + function withMiddle(options: T): Conditional { + return options; + } + `, + ` + import * as ts from 'typescript'; + + declare function forEachReturnStatement( + body: ts.Block, + visitor: (stmt: ts.ReturnStatement) => T, + ): T | undefined; + `, + ` + import type { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types'; + + declare const isNodeOfType: ( + nodeType: NodeType, + ) => node is Extract; + `, + ` + import type { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types'; + + const isNodeOfType = + (nodeType: NodeType) => + ( + node: TSESTree.Node | null, + ): node is Extract => + node?.type === nodeType; + `, + ` + import type { AST_TOKEN_TYPES, TSESTree } from '@typescript-eslint/types'; + + export const isNotTokenOfTypeWithConditions = + < + TokenType extends AST_TOKEN_TYPES, + ExtractedToken extends Extract, + Conditions extends Partial, + >( + tokenType: TokenType, + conditions: Conditions, + ): (( + token: TSESTree.Token | null | undefined, + ) => token is Exclude) => + (token): token is Exclude => + tokenType in conditions; + `, + ], + + invalid: [ + { + code: 'const func = (param: T) => null;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'const f1 = (): T => {};', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + interface I { + (value: T): void; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + interface I { + m(x: T): void; + } + `, + errors: [{ messageId: 'sole' }], + }, + { + code: ` + class Joiner { + join(el: T, other: string) { + return [el, other].join(','); + } + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + declare class C {} + `, + errors: [{ messageId: 'sole', data: { name: 'V' } }], + }, + { + code: ` + declare class C { + method(param: T): U; + } + `, + errors: [ + { messageId: 'sole', data: { name: 'T' } }, + { messageId: 'sole', data: { name: 'U' } }, + ], + }, + { + code: ` + declare class C { + method(param: T): U; + } + `, + errors: [ + { messageId: 'sole', data: { name: 'T' } }, + { messageId: 'sole', data: { name: 'U' } }, + ], + }, + { + code: ` + declare class C { + prop:

() => P; + } + `, + errors: [{ messageId: 'sole', data: { name: 'P' } }], + }, + { + code: ` + declare class Foo { + foo(this: T): void; + } + `, + errors: [ + { + messageId: 'sole', + data: { name: 'T' }, + }, + ], + }, + { + code: ` + function third(a: A, b: B, c: C): C { + return c; + } + `, + errors: [ + { messageId: 'sole', data: { name: 'A' } }, + { messageId: 'sole', data: { name: 'B' } }, + ], + }, + { + code: ` + function parseYAML(input: string): T { + return input as any as T; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + function printProperty(obj: T, key: K) { + console.log(obj[key]); + } + `, + errors: [{ messageId: 'sole', data: { name: 'K' } }], + }, + { + code: ` + function fn(param: string) { + let v: T = null!; + return v; + } + `, + errors: [ + { + data: { name: 'T' }, + messageId: 'sole', + }, + ], + }, + { + code: ` + function both< + Args extends unknown[], + CB1 extends (...args: Args) => void, + CB2 extends (...args: Args) => void, + >(fn1: CB1, fn2: CB2): (...args: Args) => void { + return function (...args: Args) { + fn1(...args); + fn2(...args); + }; + } + `, + errors: [ + { messageId: 'sole', data: { name: 'CB1' } }, + { messageId: 'sole', data: { name: 'CB2' } }, + ], + }, + { + code: ` + function getLength(x: T) { + return x.length; + } + `, + errors: [{ messageId: 'sole' }], + }, + { + code: ` + interface Lengthy { + length: number; + } + function getLength(x: T) { + return x.length; + } + `, + errors: [{ messageId: 'sole' }], + }, + { + code: 'declare function get(): T;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function get(): T;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function take(param: T): void;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function take(param: T): void;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function take(param1: T, param2: U): void;', + errors: [{ messageId: 'sole', data: { name: 'U' } }], + }, + { + code: 'declare function take(param: T): U;', + errors: [{ messageId: 'sole', data: { name: 'U' } }], + }, + { + code: 'declare function take(param: U): U;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function get(param: U): U;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'declare function get(param: T): U;', + errors: [{ messageId: 'sole', data: { name: 'U' } }], + }, + { + code: 'declare function compare(param1: T, param2: U): boolean;', + errors: [{ messageId: 'sole', data: { name: 'U' } }], + }, + { + code: 'declare function get(param: (param: U) => V): T;', + errors: [ + { messageId: 'sole', data: { name: 'T' } }, + { messageId: 'sole', data: { name: 'U' } }, + { messageId: 'sole', data: { name: 'V' } }, + ], + }, + { + code: 'declare function get(param: (param: T) => U): T;', + errors: [ + { messageId: 'sole', data: { name: 'T' } }, + { messageId: 'sole', data: { name: 'T' } }, + { messageId: 'sole', data: { name: 'U' } }, + ], + }, + { + code: 'type Fn = () => T;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'type Fn = () => [];', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + type Other = 0; + type Fn = () => Other; + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + type Other = 0 | 1; + type Fn = () => Other; + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'type Fn = (param: U) => void;', + errors: [{ messageId: 'sole', data: { name: 'U' } }], + }, + { + code: 'type Ctr = new () => T;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'type Fn = () => { [K in keyof T]: K };', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: "type Fn = () => { [K in 'a']: T };", + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'type Fn = (value: unknown) => value is T;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: 'type Fn = () => `a${T}b`;', + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-parameters.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-parameters.shot new file mode 100644 index 000000000000..a1e7d6b72117 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unnecessary-type-parameters.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-unnecessary-type-parameters 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/repo-tools/src/generate-contributors.mts b/packages/repo-tools/src/generate-contributors.mts index 220750350dd5..7e27ce3345fc 100644 --- a/packages/repo-tools/src/generate-contributors.mts +++ b/packages/repo-tools/src/generate-contributors.mts @@ -35,6 +35,8 @@ interface User { html_url: string; } +// This is an internal script, we're ok with the unsafe assertion. 🤫 +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters async function getData(url: string | undefined): Promise { if (url == null) { return null; diff --git a/packages/scope-manager/tests/test-utils/getSpecificNode.ts b/packages/scope-manager/tests/test-utils/getSpecificNode.ts index d8189a9cdfed..aa075fb059fe 100644 --- a/packages/scope-manager/tests/test-utils/getSpecificNode.ts +++ b/packages/scope-manager/tests/test-utils/getSpecificNode.ts @@ -11,12 +11,13 @@ function getSpecificNode< ): Node; function getSpecificNode< Selector extends AST_NODE_TYPES, - Node extends Extract, ReturnType extends TSESTree.Node, >( ast: TSESTree.Node, selector: Selector, - cb: (node: Node) => ReturnType | null | undefined, + cb: ( + node: Extract, + ) => ReturnType | null | undefined, ): ReturnType; function getSpecificNode( diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index c92e80a1f275..ebe463063f53 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -106,6 +106,7 @@ export default ( '@typescript-eslint/no-unnecessary-type-arguments': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-unnecessary-type-parameters': 'error', '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-call': 'error', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 734f5cbc71e8..531593aed938 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -36,6 +36,7 @@ export default ( '@typescript-eslint/no-unnecessary-template-expression': 'off', '@typescript-eslint/no-unnecessary-type-arguments': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', + '@typescript-eslint/no-unnecessary-type-parameters': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 8211f40f5446..678716305141 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -251,6 +251,8 @@ export type TSESTreeOptions = ParseAndGenerateServicesOptions; // This lets us use generics to type the return value, and removes the need to // handle the undefined type in the get method export interface ParserWeakMap { + // This is unsafe internally, so it should only be exposed via safe wrappers. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters get(key: Key): Value; has(key: unknown): boolean; } diff --git a/packages/typescript-estree/tests/test-utils/test-utils.ts b/packages/typescript-estree/tests/test-utils/test-utils.ts index bb8fbf290136..6acf026f5d0a 100644 --- a/packages/typescript-estree/tests/test-utils/test-utils.ts +++ b/packages/typescript-estree/tests/test-utils/test-utils.ts @@ -77,7 +77,7 @@ export function isJSXFileType(fileType: string): boolean { * @param ast the AST object * @returns copy of the AST object */ -export function deeplyCopy(ast: T): T { +export function deeplyCopy>(ast: T): T { return omitDeep(ast) as T; } @@ -96,8 +96,8 @@ function isObjectLike(value: unknown): value is UnknownObject { * @param selectors advance ast modifications * @returns formatted object */ -export function omitDeep( - root: T, +export function omitDeep( + root: UnknownObject, keysToOmit: { key: string; predicate: (value: unknown) => boolean }[] = [], selectors: Record< string, @@ -152,5 +152,5 @@ export function omitDeep( return node; } - return visit(root as UnknownObject, null); + return visit(root, null); } diff --git a/packages/utils/src/ast-utils/helpers.ts b/packages/utils/src/ast-utils/helpers.ts index afe8a6fede89..c76d24f88246 100644 --- a/packages/utils/src/ast-utils/helpers.ts +++ b/packages/utils/src/ast-utils/helpers.ts @@ -40,6 +40,8 @@ export const isNodeOfTypeWithConditions = < export const isTokenOfTypeWithConditions = < TokenType extends AST_TOKEN_TYPES, + // This is technically unsafe, but we find it useful to extract out the type + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters ExtractedToken extends Extract, Conditions extends Partial, >( diff --git a/packages/utils/src/eslint-utils/deepMerge.ts b/packages/utils/src/eslint-utils/deepMerge.ts index f6513944c1bd..a4ace614df5d 100644 --- a/packages/utils/src/eslint-utils/deepMerge.ts +++ b/packages/utils/src/eslint-utils/deepMerge.ts @@ -4,7 +4,7 @@ type ObjectLike = Record; * Check if the variable contains an object strictly rejecting arrays * @returns `true` if obj is an object */ -function isObjectNotArray(obj: unknown): obj is T { +function isObjectNotArray(obj: unknown): obj is ObjectLike { return typeof obj === 'object' && obj != null && !Array.isArray(obj); } diff --git a/packages/website/src/globals.d.ts b/packages/website/src/globals.d.ts index c7bf5f2aa0f6..966cee026906 100644 --- a/packages/website/src/globals.d.ts +++ b/packages/website/src/globals.d.ts @@ -3,6 +3,9 @@ import type * as ts from 'typescript'; declare global { interface WindowRequire { + // We know it's an unsafe assertion. It's for window.require usage, so we + // don't have to use verbose type assertions on every call. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters ( files: string[], success?: (...arg: T) => void,