Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-unnecessary-type-parameters] initial implementation #8173

Open
wants to merge 166 commits into
base: v8
Choose a base branch
from

Conversation

danvk
Copy link

@danvk danvk commented Jan 2, 2024

PR Checklist

Overview

See linked issue. I've incorporated the tests from my blog post and all the existing rules. We sometimes (but not always) agree with them.

I'm pretty happy with how this turned out! The logic winds up being pretty straightforward: count the number of times each type parameter is referenced, whether explicitly or implicitly in an inferred return type. If that's exactly one, then report an issue.

One fun discovery: I always thought that this rule had some unavoidable false positives, e.g. my both function from cartant/eslint-plugin-etc#15. But looking at that again a few years later, I think it's a true positive that can be rewritten to avoid breaking the golden rule:

❌ Incorrect

/**
 * Call two functions with the same args, e.g.
 *
 * onClick={both(action('click'), setState)}
 */
export 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);
  };
}

✅ Correct

export function both<Args extends unknown[]>(
  fn1: (...args: Args) => void,
  fn2: (...args: Args) => void,
): (...args: Args) => void {
  return function (...args: Args) {
    fn1(...args);
    fn2(...args);
  };
}

🎉


Notes from Josh (April 2024): Dan and I chatted and I'm pitching in to help get this to review state. The design is actually more intricate than we'd initially hoped:

  • We'd initially hoped to look at AST nodes primarily and use type information to augment inferred types. However, there's so much in the types that can be inferred, the types have to nearly always be checked anyway - so I ended up dropping the AST node checks altogether!
  • I had to implement some tricky, rough heuristics around whether type parameter referenced existed in general space (e.g. as types of runtime parameters) vs. in return space (e.g. return types).

The logic is still straightforward: recursively descend into types, kind of like how scope analyzer does in AST-land. But the edge cases were quite tricky.

💖

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @danvk!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

Copy link

netlify bot commented Jan 2, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 7ed4c77
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/6628139dc9cd21000870d3ae
😎 Deploy Preview https://deploy-preview-8173--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 98 (🟢 up 5 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 98 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

@danvk danvk changed the title Add no-unnecessary-type-parameters rule (🚧 WIP 🚧) feat(eslint-plugin): Add no-unnecessary-type-parameters rule (🚧 WIP 🚧) Jan 2, 2024
@danvk danvk changed the title feat(eslint-plugin): Add no-unnecessary-type-parameters rule (🚧 WIP 🚧) feat(eslint-plugin): [no-unnecessary-type-parameters] initial implementation (🚧 WIP 🚧) Jan 2, 2024
@JoshuaKGoldberg
Copy link
Member

I'd like to test this on my repo, but I'm getting a ton of errors from git pull && yarn && yarn build. Any ideas?

Hmm, interesting. Might be a caching issue with Nx? If you can get a reliable repro for it I'd love to see an issue. I've had the occasional issue myself but haven't been able to pinpoint anything I can report...

@danvk
Copy link
Author

danvk commented Apr 21, 2024

I am getting this error consistently. Here's my shell session:

(09:04:04)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) yarn cache clean
➤ YN0000: Done in 0s 181ms
(09:06:37)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) yarn
➤ YN0000: ┌ Project validation
➤ YN0057: │ website: Resolutions field will be ignored
➤ YN0000: └ Completed
➤ YN0000: ┌ Resolution step
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide @algolia/client-search (pbc410), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide algoliasearch (p8ad5b), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide search-insights (pfdb70), requested by @algolia/autocomplete-plugin-algolia-insights
➤ YN0002: │ @algolia/autocomplete-plugin-algolia-insights@npm:1.9.3 [e43f4] doesn't provide @algolia/client-search (pdf3b9), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-plugin-algolia-insights@npm:1.9.3 [e43f4] doesn't provide algoliasearch (p0f6bb), requested by @algolia/autocomplete-shared
➤ YN0002: │ @docsearch/react@npm:3.5.2 [a86ce] doesn't provide @algolia/client-search (p4bd8f), requested by @algolia/autocomplete-preset-algolia
➤ YN0002: │ @nrwl/devkit@npm:18.2.3 doesn't provide nx (pf5126), requested by @nx/devkit
➤ YN0002: │ @nx/eslint@npm:18.2.3 [43684] doesn't provide nx (p2dd81), requested by @nx/devkit
➤ YN0002: │ @nx/jest@npm:18.2.3 doesn't provide nx (p8258e), requested by @nx/devkit
➤ YN0002: │ @nx/jest@npm:18.2.3 doesn't provide typescript (p7a004), requested by @phenomnomnominal/tsquery
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide @types/node (p9bf55), requested by ts-node
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide nx (pbafad), requested by @nx/devkit
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide typescript (pdf7d5), requested by @phenomnomnominal/tsquery
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide typescript (pf7e78), requested by ts-node
➤ YN0002: │ @typescript-eslint/ast-spec@workspace:packages/ast-spec doesn't provide eslint (pc82ae), requested by @babel/eslint-parser
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide @eslint/eslintrc (p6b500), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p2a941), requested by @typescript-eslint/utils
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p04dec), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p49e61), requested by @typescript-eslint/type-utils
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin doesn't provide @eslint/eslintrc (p55ce1), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin [2529e] doesn't provide @eslint/eslintrc (p14d9d), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin [f1c8b] doesn't provide @eslint/eslintrc (pe2c69), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/repo-tools@workspace:packages/repo-tools doesn't provide nx (p8fc50), requested by @nx/devkit
➤ YN0002: │ @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types doesn't provide eslint (p57f93), requested by @typescript-eslint/utils
➤ YN0002: │ @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types doesn't provide eslint (pb69d2), requested by @typescript-eslint/type-utils
➤ YN0002: │ @typescript-eslint/scope-manager@workspace:packages/scope-manager doesn't provide jest (p03229), requested by jest-specific-snapshot
➤ YN0002: │ @typescript-eslint/typescript-eslint@workspace:. doesn't provide webpack (p14bbf), requested by raw-loader
➤ YN0000: │ Some peer dependencies are incorrectly met; run yarn explain peer-requirements <hash> for details, where <hash> is the six-letter p-prefixed code
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0013: │ yn@npm:3.1.1 can't be found in the cache and will be fetched from the remote registry
➤ YN0013: │ yocto-queue@npm:0.1.0 can't be found in the cache and will be fetched from the remote registry
➤ YN0013: │ yocto-queue@npm:1.0.0 can't be found in the cache and will be fetched from the remote registry
➤ YN0013: │ z-schema@npm:5.0.2 can't be found in the cache and will be fetched from the remote registry
➤ YN0013: │ zwitch@npm:2.0.4 can't be found in the cache and will be fetched from the remote registry
➤ YN0000: └ Completed in 13s 768ms
➤ YN0000: ┌ Link step
➤ YN0007: │ @typescript-eslint/typescript-eslint@workspace:. must be built because it never has been before or the last one failed
➤ YN0009: │ @typescript-eslint/typescript-eslint@workspace:. couldn't be built successfully (exit code 1, logs can be found here: /private/var/folders/t_/3xnk295j79v51cmlqvtnhslc0000gn/T/xfs-fdadc1c8/build.log)
➤ YN0000: └ Completed in 20s 897ms
➤ YN0000: Failed with errors in 34s 894ms
yarn  41.82s user 5.03s system 132% cpu 35.258 total
(09:07:15)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) node --version
v20.11.0
(09:13:26)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) npm --version
10.2.4
(09:13:29)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) yarn --version
3.8.1

and here's the contents of the build.log file:

# This file contains the result of Yarn building a package (@typescript-eslint/typescript-eslint@workspace:.)
# Script name: postinstall


> nx run repo-tools:postinstall-script

[09:06:57.129] yarn husky install
husky - Git hooks installed
[09:06:57.643] yarn clean
�[0m�[7m�[1m�[36m NX �[39m�[22m�[27m�[0m  �[36mRunning target �[1mclean�[22m for 12 projects:�[39m
�[2m-�[22m eslint-plugin-internal
�[2m-�[22m typescript-eslint
�[2m-�[22m typescript-estree
�[2m-�[22m eslint-plugin
�[2m-�[22m scope-manager
�[2m-�[22m visitor-keys
�[2m-�[22m rule-tester
�[2m-�[22m type-utils
�[2m-�[22m ast-spec
�[2m-�[22m parser
�[2m-�[22m types
�[2m-�[22m utils
�[2m> �[22m�[2mnx run�[22m ast-spec:clean
�[2m> �[22m�[2mnx run�[22m visitor-keys:clean
�[2m> �[22m�[2mnx run�[22m types:clean
�[2m> �[22m�[2mnx run�[22m scope-manager:clean
�[2m> �[22mtsc -b tsconfig.build.json --clean
�[2m> �[22mrimraf dist
�[2m> �[22mrimraf _ts4.3
�[2m> �[22mrimraf coverage
�[2m> �[22m�[2mnx run�[22m typescript-estree:clean
�[2m> �[22m�[2mnx run�[22m parser:clean
�[2m> �[22m�[2mnx run�[22m utils:clean
�[2m> �[22m�[2mnx run�[22m type-utils:clean
�[2m> �[22m�[2mnx run�[22m rule-tester:clean
�[2m> �[22m�[2mnx run�[22m eslint-plugin:clean
�[2m> �[22m�[2mnx run�[22m eslint-plugin-internal:clean
�[2m> �[22m�[2mnx run�[22m typescript-eslint:clean
�[0m�[7m�[1m�[32m NX �[39m�[22m�[27m�[0m  �[32mSuccessfully ran target �[1mclean�[22m for 12 projects�[39m
[09:07:03.756] yarn build
�[0m�[7m�[1m�[36m NX �[39m�[22m�[27m�[0m  �[36mRunning target �[1mbuild�[22m for 13 projects and �[1m1�[22m task they depend on:�[39m
�[2m-�[22m rule-schema-to-typescript-types
�[2m-�[22m eslint-plugin-internal
�[2m-�[22m typescript-eslint
�[2m-�[22m typescript-estree
�[2m-�[22m eslint-plugin
�[2m-�[22m scope-manager
�[2m-�[22m visitor-keys
�[2m-�[22m rule-tester
�[2m-�[22m type-utils
�[2m-�[22m ast-spec
�[2m-�[22m parser
�[2m-�[22m types
�[2m-�[22m utils
�[2m> �[22m�[2mnx run�[22m ast-spec:build  �[2m[local cache]�[22m
�[2m> �[22myarn build
�[1mapi-extractor 7.43.0 �[36m - https://api-extractor.com/�[39m
�[22m
Using configuration from ./api-extractor.json
Analysis will use the bundled TypeScript version 5.4.3
API Extractor completed successfully
�[2m> �[22m�[2mnx run�[22m types:copy-ast-spec  �[2m[existing outputs match the cache, left as is]�[22m
src/generated/ast-spec.ts 193ms
Copied ast-spec.ts
�[2m> �[22m�[2mnx run�[22m types:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m visitor-keys:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m typescript-estree:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m scope-manager:build  �[2m[local cache]�[22m
�[2m> �[22mrimraf _ts4.3
�[2m> �[22mtsc -b tsconfig.build.json
�[2m> �[22mdownlevel-dts dist _ts4.3/dist --to=4.3
�[2m> �[22m�[2mnx run�[22m parser:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m utils:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m rule-tester:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m type-utils:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m eslint-plugin-internal:build  �[2m[local cache]�[22m
�[2m> �[22m�[2mnx run�[22m rule-schema-to-typescript-types:build  �[2m[existing outputs match the cache, left as is]�[22m
�[2m> �[22m�[2mnx run�[22m eslint-plugin:build
src/rules/ban-ts-comment.ts(40,7): error TS2322: Type '{ recommended: true; strict: { minimumDescriptionLength: number; }[]; }' is not assignable to type 'RuleRecommendation | undefined'.
src/rules/block-spacing.ts(68,9): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/comma-spacing.ts(118,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/comma-spacing.ts(118,25): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/comma-spacing.ts(155,24): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/comma-spacing.ts(155,24): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/consistent-indexed-object-style.ts(64,23): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/consistent-indexed-object-style.ts(64,23): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/consistent-type-definitions.ts(36,14): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/consistent-type-imports.ts(214,30): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/consistent-type-imports.ts(214,30): error TS18048: 'context.sourceCode.getDeclaredVariables' is possibly 'undefined'.
src/rules/explicit-module-boundary-types.ts(323,21): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/explicit-module-boundary-types.ts(323,21): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/naming-convention.ts(281,26): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(293,27): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(293,27): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/naming-convention.ts(330,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(381,38): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(604,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(630,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(630,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/naming-convention.ts(652,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(652,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/naming-convention.ts(675,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(697,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/naming-convention.ts(697,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-empty-interface.ts(75,27): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-empty-interface.ts(75,27): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-implied-eval.ts(128,19): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-loop-func.ts(50,26): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-mixed-enums.ts(48,33): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-non-null-asserted-nullish-coalescing.ts(56,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-non-null-asserted-nullish-coalescing.ts(56,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-redeclare.ts(237,21): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-redeclare.ts(237,21): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-redeclare.ts(250,23): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-redeclare.ts(250,23): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-require-imports.ts(64,11): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-shadow.ts(650,29): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-shadow.ts(650,29): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-unsafe-declaration-merging.ts(52,32): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-unsafe-declaration-merging.ts(66,11): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-unused-vars.ts(195,24): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-unused-vars.ts(195,24): error TS18048: 'context.sourceCode.getDeclaredVariables' is possibly 'undefined'.
src/rules/no-unused-vars.ts(528,19): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-unused-vars.ts(528,19): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/no-use-before-define.ts(381,30): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/no-var-requires.ts(81,13): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/object-curly-spacing.ts(175,29): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/object-curly-spacing.ts(175,29): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/object-curly-spacing.ts(223,28): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/object-curly-spacing.ts(223,28): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/prefer-find.ts(35,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-find.ts(35,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/prefer-for-of.ts(151,28): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-for-of.ts(151,28): error TS18048: 'context.sourceCode.getDeclaredVariables' is possibly 'undefined'.
src/rules/prefer-includes.ts(35,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-includes.ts(35,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts(161,33): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-promise-reject-errors.ts(136,32): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-regexp-exec.ts(41,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-regexp-exec.ts(41,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/prefer-string-starts-ends-with.ts(64,25): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/prefer-string-starts-ends-with.ts(64,25): error TS18048: 'context.sourceCode.getScope' is possibly 'undefined'.
src/rules/promise-function-async.ts(198,34): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/restrict-plus-operands.ts(34,7): error TS2322: Type '{ recommended: true; strict: { allowAny: false; allowBoolean: false; allowNullish: false; allowNumberAndString: false; allowRegExp: false; }[]; }' is not assignable to type 'RuleRecommendation | undefined'.
src/rules/restrict-template-expressions.ts(65,7): error TS2322: Type '{ recommended: true; strict: { allowAny: false; allowBoolean: false; allowNullish: false; allowNumber: false; allowRegExp: false; allowNever: false; }[]; }' is not assignable to type 'RuleRecommendation | undefined'.
src/rules/space-before-blocks.ts(53,26): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-before-blocks.ts(53,26): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/space-before-function-paren.ts(158,26): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-before-function-paren.ts(158,26): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/space-infix-ops.ts(96,10): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-infix-ops.ts(96,10): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/space-infix-ops.ts(97,10): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-infix-ops.ts(97,10): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/space-infix-ops.ts(150,14): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-infix-ops.ts(150,14): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/space-infix-ops.ts(151,14): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/space-infix-ops.ts(151,14): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/type-annotation-spacing.ts(186,11): error TS2722: Cannot invoke an object which is possibly 'undefined'.
src/rules/type-annotation-spacing.ts(186,11): error TS18048: 'context.sourceCode.isSpaceBetween' is possibly 'undefined'.
src/rules/index.ts(293,20): error TS2694: Namespace 'Linter' has no exported member 'PluginRules'.
src/index.ts(42,3): error TS2353: Object literal may only specify known properties, and 'meta' does not exist in type 'Plugin'.
�[0m�[7m�[1m�[31m NX �[39m�[22m�[27m�[0m  �[31mRunning target �[1mbuild�[22m for 13 projects and �[1m1�[22m task they depend on failed�[39m
�[2mTasks not run because their dependencies failed or --nx-bail=true:�[22m
�[2m-�[22m typescript-eslint:build
�[2mFailed tasks:�[22m
�[2m-�[22m eslint-plugin:build
file:///Users/danvk/github/typescript-eslint/packages/repo-tools/node_modules/execa/lib/error.js:60
		error = new Error(message);
		        ^
Error: Command failed with exit code 1: yarn build
    at makeError �[90m(file:///Users/danvk/github/typescript-eslint/�[39mpackages/repo-tools/node_modules/�[4mexeca�[24m/lib/error.js:60:11�[90m)�[39m
    at handlePromise �[90m(file:///Users/danvk/github/typescript-eslint/�[39mpackages/repo-tools/node_modules/�[4mexeca�[24m/index.js:124:26�[90m)�[39m
�[90m    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)�[39m
    at <anonymous> �[90m(/Users/danvk/github/typescript-eslint/�[39mpackages/repo-tools/src/postinstall.mts:38:5�[90m)�[39m {
  shortMessage: �[32m'Command failed with exit code 1: yarn build'�[39m,
  command: �[32m'yarn build'�[39m,
  escapedCommand: �[32m'yarn build'�[39m,
  exitCode: �[33m1�[39m,
  signal: �[90mundefined�[39m,
  signalDescription: �[90mundefined�[39m,
  stdout: �[90mundefined�[39m,
  stderr: �[90mundefined�[39m,
  cwd: �[32m'/Users/danvk/github/typescript-eslint'�[39m,
  failed: �[33mtrue�[39m,
  timedOut: �[33mfalse�[39m,
  isCanceled: �[33mfalse�[39m,
  killed: �[33mfalse�[39m
}
Node.js v20.11.0



 NX   Running target postinstall-script for project repo-tools failed

Failed tasks:

- repo-tools:postinstall-script

Hint: run the command with --verbose for more details.

View structured, searchable error logs at https://nx.app/runs/VPTTovTOet


@danvk
Copy link
Author

danvk commented Apr 21, 2024

(A fresh clone worked, so there's definitely some sort of bad state here.)

@JamesHenry
Copy link
Member

@danvk please can you see if running

yarn nx reset

and then rerunning the install resolves the issue?

@danvk
Copy link
Author

danvk commented Apr 21, 2024

@danvk please can you see if running

yarn nx reset

and then rerunning the install resolves the issue?

@JamesHenry Running yarn nx reset and then yarn did not seem to make a difference. The build.log looks identical to the one above.

(15:41:06)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) yarn nx reset

 NX   Resetting the Nx workspace cache and stopping the Nx Daemon.

This might take a few minutes.


 NX   Daemon Server - Stopped


 NX   Successfully reset the Nx workspace.

(15:41:16)danvk@mbp:~/github/typescript-eslint(no-unnecessary-type-parameters ✔) yarn
➤ YN0000: ┌ Project validation
➤ YN0057: │ website: Resolutions field will be ignored
➤ YN0000: └ Completed
➤ YN0000: ┌ Resolution step
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide @algolia/client-search (pbc410), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide algoliasearch (p8ad5b), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-core@npm:1.9.3 doesn't provide search-insights (pfdb70), requested by @algolia/autocomplete-plugin-algolia-insights
➤ YN0002: │ @algolia/autocomplete-plugin-algolia-insights@npm:1.9.3 [e43f4] doesn't provide @algolia/client-search (pdf3b9), requested by @algolia/autocomplete-shared
➤ YN0002: │ @algolia/autocomplete-plugin-algolia-insights@npm:1.9.3 [e43f4] doesn't provide algoliasearch (p0f6bb), requested by @algolia/autocomplete-shared
➤ YN0002: │ @docsearch/react@npm:3.5.2 [a86ce] doesn't provide @algolia/client-search (p4bd8f), requested by @algolia/autocomplete-preset-algolia
➤ YN0002: │ @nrwl/devkit@npm:18.2.3 doesn't provide nx (pf5126), requested by @nx/devkit
➤ YN0002: │ @nx/eslint@npm:18.2.3 [43684] doesn't provide nx (p2dd81), requested by @nx/devkit
➤ YN0002: │ @nx/jest@npm:18.2.3 doesn't provide nx (p8258e), requested by @nx/devkit
➤ YN0002: │ @nx/jest@npm:18.2.3 doesn't provide typescript (p7a004), requested by @phenomnomnominal/tsquery
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide @types/node (p9bf55), requested by ts-node
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide nx (pbafad), requested by @nx/devkit
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide typescript (pdf7d5), requested by @phenomnomnominal/tsquery
➤ YN0002: │ @nx/js@npm:18.2.3 [3e46d] doesn't provide typescript (pf7e78), requested by ts-node
➤ YN0002: │ @typescript-eslint/ast-spec@workspace:packages/ast-spec doesn't provide eslint (pc82ae), requested by @babel/eslint-parser
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide @eslint/eslintrc (p6b500), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p2a941), requested by @typescript-eslint/utils
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p04dec), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin-internal@workspace:packages/eslint-plugin-internal doesn't provide eslint (p49e61), requested by @typescript-eslint/type-utils
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin doesn't provide @eslint/eslintrc (p55ce1), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin [2529e] doesn't provide @eslint/eslintrc (p14d9d), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/eslint-plugin@workspace:packages/eslint-plugin [f1c8b] doesn't provide @eslint/eslintrc (pe2c69), requested by @typescript-eslint/rule-tester
➤ YN0002: │ @typescript-eslint/repo-tools@workspace:packages/repo-tools doesn't provide nx (p8fc50), requested by @nx/devkit
➤ YN0002: │ @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types doesn't provide eslint (p57f93), requested by @typescript-eslint/utils
➤ YN0002: │ @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types doesn't provide eslint (pb69d2), requested by @typescript-eslint/type-utils
➤ YN0002: │ @typescript-eslint/scope-manager@workspace:packages/scope-manager doesn't provide jest (p03229), requested by jest-specific-snapshot
➤ YN0002: │ @typescript-eslint/typescript-eslint@workspace:. doesn't provide webpack (p14bbf), requested by raw-loader
➤ YN0000: │ Some peer dependencies are incorrectly met; run yarn explain peer-requirements <hash> for details, where <hash> is the six-letter p-prefixed code
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0007: │ @typescript-eslint/typescript-eslint@workspace:. must be built because it never has been before or the last one failed
➤ YN0009: │ @typescript-eslint/typescript-eslint@workspace:. couldn't be built successfully (exit code 1, logs can be found here: /private/var/folders/t_/3xnk295j79v51cmlqvtnhslc0000gn/T/xfs-8e8d73a5/build.log)
➤ YN0000: └ Completed in 38s 299ms
➤ YN0000: Failed with errors in 38s 695ms
yarn  73.06s user 5.02s system 199% cpu 39.054 total

@danvk
Copy link
Author

danvk commented Apr 21, 2024

I was able to build this and run it on some of my code! Here's the one false positive I found:

type AllValues<T extends Record<PropertyKey, PropertyKey>> = {
  [P in keyof T]: {key: P; value: T[P]};
}[keyof T];
type InvertResult<T extends Record<PropertyKey, PropertyKey>> = {} & {
  [P in AllValues<T>['value']]: Extract<AllValues<T>, {value: P}>['key'];
};

declare function invert<T extends Record<PropertyKey, PropertyKey>>(obj: T): InvertResult<T>;

This rule reports that T only appears once in that definition which does not seem right.

@JoshuaKGoldberg JoshuaKGoldberg removed the awaiting response Issues waiting for a reply from the OP or another party label Apr 23, 2024
@JoshuaKGoldberg JoshuaKGoldberg requested a review from a team April 23, 2024 19:59
@JoshuaKGoldberg JoshuaKGoldberg changed the base branch from main to v8 April 23, 2024 19:59
@JoshuaKGoldberg JoshuaKGoldberg changed the base branch from v8 to main April 23, 2024 19:59
@JoshuaKGoldberg JoshuaKGoldberg changed the base branch from main to v8 April 23, 2024 20:01
Copy link
Author

@danvk danvk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your tweet about carving out dedicated time to finish this inspired me to carve out some time to understand how it works. It's not what I expected! But I think it's fine for now. See my comments / questions. Some more clarifying comments would be helpful.

if (
reference.identifier.parent.type === AST_NODE_TYPES.TSTypeReference &&
reference.identifier.parent.parent.type ===
AST_NODE_TYPES.TSTypeParameterInstantiation &&
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little more strict than necessary. It misses cases where the type parameter is used in a type expression that's passed as the type argument, for example Promise<T | null>. The slow path will still handle this correctly, but might be a nice optimization in the future.

visitType(type, false);
}

function visitType(type: ts.Type | undefined, asRepeatedType: boolean): void {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing the discussion from #8173

I spent some time stepping through this in the debugger trying to understand why asRepeatedType was necessary. The good news is that I get it now! The interesting news is that this rule works totally differently than how I expected. I think the way it's implemented is fine, but I think some clarifying comments and name changes could make it much clearer.

My main point of confusion was around what collectTypeParameterUsageCounts does, specifically what "recursively descending through type references" meant. I thought it meant that, when this function saw Set<T>, it would instantiate the type declaration for Set and see how many times T appeared in that. I don't think it does that, though! If it hits Fn<Expr of T>, then it will descend into Expr of T but not Fn. So asRepeatedType is a shortcut to make sure that Fn<T> always counts as a repeated use of T.

This isn't really correct since you could write code like this:

type Identity<T> = T;
declare function takeIdentity<T>(x: Identity<T>): void;

This is not a good use of generics, but the lint rule will allow it because of asRepeatedType.

I think this is an OK concession to expediency. But I can imagine real world examples of this getting thrown off by general helper types, e.g.:

type NonNullish<T> = T & {};
declare function foo<T>(x: NonNullish<T>): void;

So, a few comments / suggestions:

  1. How hard would it be to recurse into the type aliases, not just the their type arguments? If this is impossible, then that's OK, but it would be good to at least note that (and add my two examples as tests!).
  2. Clarify what is "recursively descended" into in the comment. Or maybe don't mention recursion at all, it's just walking the type equivalent of a syntax tree.
  3. I think using the term "count" in this function name and its parameter (identifierCounts) is misleading since it doesn't really return a count. It kind of returns a lower bound, but not really because of the Identify / NonNullish examples above. I'm not sure what a better name would be, maybe just something generic like typeParameterWorker?
  4. For asRepeatedType, maybe the name could be assumeMultipleUses? Or maybe asRepeatedType is fine, just add a comment where you set it to true, e.g.:
// We don't know how many times the type argument is used in this type alias.
// Assume it's at least twice.
visitType(typeArgument, true);

// Generic type references like `Map<K, V>`
else if (tsutils.isTupleType(type) || tsutils.isTypeReference(type)) {
for (const typeArgument of type.typeArguments ?? []) {
visitType(typeArgument, true);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the idea behind this for type aliases (see my long comment above) but I don't understand it for tuples. In fact, I think it might be wrong! This is considered valid code by this rule but it should not be:

function takeTuple<T>(x: [T]): void;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't array methods like map also count as references to T, though?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I guess they would.

I do think it would be interesting to explore adding a list of "singular types" to this rule in the future. Array<T> does, in theory, define a complex set of relationships between methods involving T. But in practice, you probably just want to pass in a JS array. Same for Set<T>, Map<K, V> and I'm sure other types, too.

@Josh-Cena
Copy link
Member

Josh-Cena commented Jun 1, 2024

Question: what's the motivation for counting the references to T for the underlying generic type definition? For example, in the case f<T>(): G<T>, I would very much prefer it to be f(): G<unknown> instead, and then I can cast it like f() as G<number>.

@danvk
Copy link
Author

danvk commented Jun 1, 2024

Question: what's the motivation for counting the references to T for the underlying generic type definition? For example, in the case f<T>(): G<T>, I would very much prefer it to be f(): G<unknown> instead, and then I can cast it like f() as G<number>.

@Josh-Cena see discussion about return types here: #8173 (review). Depending on G, it's possible that G<T> has meaningfully different behavior than G<unknown>, even when it only appears as a return type.

@JoshuaKGoldberg JoshuaKGoldberg added the team assigned A member of the typescript-eslint team should work on this. label Jun 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement: new plugin rule New rule request for eslint-plugin team assigned A member of the typescript-eslint team should work on this.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rule proposal: warn against unnecessary type parameters