From cd5e9a475c2be471e88e6b149602422d68fdfb03 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 17:38:04 +0200 Subject: [PATCH 01/45] wip 2 usages --- .../rules/no-unnecessary-type-parameters.ts | 14 +++++++-- .../eslint-plugin/tests/rules/000tt.test.ts | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/tests/rules/000tt.test.ts diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 0f78a6d14ea6..5783f9516f46 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -115,7 +115,12 @@ function isTypeParameterRepeatedInAST( ); if ( grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation && - grandparent.params.includes(reference.identifier.parent) + grandparent.params.includes(reference.identifier.parent) && + !( + grandparent.parent.type === AST_NODE_TYPES.TSTypeReference && + grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier && + ['Array', 'ReadonlyArray'].includes(grandparent.parent.typeName.name) + ) ) { return true; } @@ -249,7 +254,12 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTupleType(type) || tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - visitType(typeArgument, true); + visitType( + typeArgument, + !['Array', 'ReadonlyArray'].includes( + type.symbol.escapedName.toString(), + ), + ); } } diff --git a/packages/eslint-plugin/tests/rules/000tt.test.ts b/packages/eslint-plugin/tests/rules/000tt.test.ts new file mode 100644 index 000000000000..66c2f6353396 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/000tt.test.ts @@ -0,0 +1,30 @@ +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: [], + + invalid: [ + { + code: ` + function getLength(array: Array) { + return array.length; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], +}); From baadbe9592ff6fc8370a9b35c45192baa4edc3d6 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:10:17 +0200 Subject: [PATCH 02/45] move tests --- .../eslint-plugin/tests/rules/000tt.test.ts | 30 ------------------- .../no-unnecessary-type-parameters.test.ts | 16 ++++++++++ 2 files changed, 16 insertions(+), 30 deletions(-) delete mode 100644 packages/eslint-plugin/tests/rules/000tt.test.ts diff --git a/packages/eslint-plugin/tests/rules/000tt.test.ts b/packages/eslint-plugin/tests/rules/000tt.test.ts deleted file mode 100644 index 66c2f6353396..000000000000 --- a/packages/eslint-plugin/tests/rules/000tt.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -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: [], - - invalid: [ - { - code: ` - function getLength(array: Array) { - return array.length; - } - `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - ], -}); 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 index f1d98ea5734e..bd418c1a9a96 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -608,5 +608,21 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = () => `a${T}b`;', errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + function getLength(array: Array) { + return array.length; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + function getLength(array: ReadonlyArray) { + return array.length; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, ], }); From f655fef2c6ec40590b723161bf4b3cc75ca8acee Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:11:11 +0200 Subject: [PATCH 03/45] add tests --- .../no-unnecessary-type-parameters.test.ts | 345 +----------------- 1 file changed, 17 insertions(+), 328 deletions(-) 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 index bd418c1a9a96..7eaa4d4a5561 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -15,334 +15,7 @@ const ruleTester = new RuleTester({ }); 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; - `, - ], + valid: [], invalid: [ { @@ -624,5 +297,21 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + function getLength(array: T[]) { + return array.length; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + function getLength(array: readonly T[]) { + return array.length; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, ], }); From 3eeb08a0091548eb081fda93ac07de0e794c483f Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:12:09 +0200 Subject: [PATCH 04/45] restore tests --- .../no-unnecessary-type-parameters.test.ts | 329 +++++++++++++++++- 1 file changed, 328 insertions(+), 1 deletion(-) 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 index 7eaa4d4a5561..d4bfeb9fd1ee 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -15,7 +15,334 @@ const ruleTester = new RuleTester({ }); ruleTester.run('no-unnecessary-type-parameters', rule, { - valid: [], + 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: [ { From 021f12c50c8c30141b46b3f70bfe8eecfaf13712 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:26:19 +0200 Subject: [PATCH 05/45] point failing tests --- .../no-unnecessary-type-parameters.test.ts | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) 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 index d4bfeb9fd1ee..528421a8127d 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -16,11 +16,11 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unnecessary-type-parameters', rule, { valid: [ - ` - class ClassyArray { - arr: T[]; - } - `, + // ` + // class ClassyArray { + // arr: T[]; + // } + // `, ` class ClassyArray { value1: T; @@ -57,20 +57,20 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { } } `, - ` - class Joiner { - join(els: T[]) { - return els.map(el => '' + el).join(','); - } - } - `, - ` - class Joiner { - join(els: T[]) { - return els.map(el => '' + el).join(','); - } - } - `, + // ` + // 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; @@ -146,11 +146,11 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { return r; } `, - ` - function arrayOfPairs() { - return [] as [T, T][]; - } - `, + // ` + // function arrayOfPairs() { + // return [] as [T, T][]; + // } + // `, ` function isNonNull(v: T): v is Exclude { return v !== null; @@ -202,12 +202,12 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { getResult: (t: unknown) => U | undefined, ): U | undefined; `, - ` - function findFirstResult( - inputs: T[], - getResult: (t: T) => () => [U | undefined], - ): () => [U | undefined]; - `, + // ` + // function findFirstResult( + // inputs: T[], + // getResult: (t: T) => () => [U | undefined], + // ): () => [U | undefined]; + // `, ` function getData(url: string): Promise { return Promise.resolve(null); @@ -243,7 +243,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeSets(): [Set][];', 'declare function makeMap(): Map;', 'declare function makeMap(): [Map];', - 'declare function arrayOfPairs(): [T, T][];', + // '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];', @@ -300,48 +300,48 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { return options; } `, - ` - import * as ts from 'typescript'; + // ` + // 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 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'; + // 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'; + // 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; - `, + // 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: [ From 6caeb43a1e251b5d7288a78df68caab92d4f0a7f Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:41:41 +0200 Subject: [PATCH 06/45] fix possible undefined symbol --- .../rules/no-unnecessary-type-parameters.ts | 3 +- .../no-unnecessary-type-parameters.test.ts | 100 +++++++++--------- 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 5783f9516f46..7146c22bb20c 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -257,7 +257,8 @@ function collectTypeParameterUsageCounts( visitType( typeArgument, !['Array', 'ReadonlyArray'].includes( - type.symbol.escapedName.toString(), + (type.symbol as ts.Symbol | undefined)?.escapedName ?? + ('' as string), ), ); } 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 index 528421a8127d..ac6a146d8cbf 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -146,11 +146,11 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { return r; } `, - // ` - // function arrayOfPairs() { - // return [] as [T, T][]; - // } - // `, + ` + function arrayOfPairs() { + return [] as [T, T][]; + } + `, ` function isNonNull(v: T): v is Exclude { return v !== null; @@ -202,12 +202,12 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { getResult: (t: unknown) => U | undefined, ): U | undefined; `, - // ` - // function findFirstResult( - // inputs: T[], - // getResult: (t: T) => () => [U | undefined], - // ): () => [U | undefined]; - // `, + ` + function findFirstResult( + inputs: T[], + getResult: (t: T) => () => [U | undefined], + ): () => [U | undefined]; + `, ` function getData(url: string): Promise { return Promise.resolve(null); @@ -243,7 +243,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeSets(): [Set][];', 'declare function makeMap(): Map;', 'declare function makeMap(): [Map];', - // 'declare function arrayOfPairs(): [T, T][];', + '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];', @@ -300,48 +300,48 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { return options; } `, - // ` - // import * as ts from 'typescript'; + ` + 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 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'; + 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'; + 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; - // `, + 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: [ From bd6f08bf3d24e421ce35a5db245e97ddfd62a8cd Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:42:30 +0200 Subject: [PATCH 07/45] left a comment --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 7146c22bb20c..d2618bedaf6f 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -257,6 +257,7 @@ function collectTypeParameterUsageCounts( visitType( typeArgument, !['Array', 'ReadonlyArray'].includes( + // it seems that type.symbol is not always defined (type.symbol as ts.Symbol | undefined)?.escapedName ?? ('' as string), ), From 74b2e17efc36232449236cb1a899efc13f2e2493 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:54:20 +0200 Subject: [PATCH 08/45] ok --- .../rules/no-unnecessary-type-parameters.ts | 21 +++++---- .../no-unnecessary-type-parameters.test.ts | 47 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index d2618bedaf6f..8ecc3aa9cda7 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -172,6 +172,12 @@ function countTypeParameterUsage( return counts; } +// Usually when we encounter a generic type like `Fn`, we assume it uses T +// in multiple places because it might be something like `{a: T, b: T}`. But for +// a few special types like Arrays, we want Array (or T[]) to only count as +// a single use. +const SINGULAR_TYPES = new Set(['Array', 'ReadonlyArray']); + /** * Populates {@link foundIdentifierUsages} by the number of times each type parameter * appears in the given type by checking its uses through its type references. @@ -252,16 +258,13 @@ function collectTypeParameterUsageCounts( // Tuple types like `[K, V]` // Generic type references like `Map` - else if (tsutils.isTupleType(type) || tsutils.isTypeReference(type)) { + else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - visitType( - typeArgument, - !['Array', 'ReadonlyArray'].includes( - // it seems that type.symbol is not always defined - (type.symbol as ts.Symbol | undefined)?.escapedName ?? - ('' as string), - ), - ); + const assumeMultipleUses = + !tsutils.isTupleType(type.target) && + !SINGULAR_TYPES.has(type.symbol.getName()); + + visitType(typeArgument, assumeMultipleUses); } } 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 index ac6a146d8cbf..22e166abd160 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -16,11 +16,6 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unnecessary-type-parameters', rule, { valid: [ - // ` - // class ClassyArray { - // arr: T[]; - // } - // `, ` class ClassyArray { value1: T; @@ -57,20 +52,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { } } `, - // ` - // 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; @@ -640,5 +621,33 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + class ClassyArray { + arr: T[]; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + class Joiner { + join(els: T[]) { + return els.map(el => '' + el).join(','); + } + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + { + code: ` + class Joiner { + join(els: T[]) { + return els.map(el => '' + el).join(','); + } + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, ], }); From 18f89519a23690f78bfcb19090d6b51e0783263a Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 18:57:16 +0200 Subject: [PATCH 09/45] add test --- .../src/rules/no-unnecessary-type-parameters.ts | 14 +++++++------- .../rules/no-unnecessary-type-parameters.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 8ecc3aa9cda7..2bea009a74a0 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -82,6 +82,12 @@ export default createRule({ }, }); +// Usually when we encounter a generic type like `Fn`, we assume it uses T +// in multiple places because it might be something like `{a: T, b: T}`. But for +// a few special types like Arrays, we want Array (or T[]) to only count as +// a single use. +const SINGULAR_TYPES = new Set(['Array', 'ReadonlyArray']); + function isTypeParameterRepeatedInAST( node: TSESTree.TSTypeParameter, references: Reference[], @@ -119,7 +125,7 @@ function isTypeParameterRepeatedInAST( !( grandparent.parent.type === AST_NODE_TYPES.TSTypeReference && grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier && - ['Array', 'ReadonlyArray'].includes(grandparent.parent.typeName.name) + SINGULAR_TYPES.has(grandparent.parent.typeName.name) ) ) { return true; @@ -172,12 +178,6 @@ function countTypeParameterUsage( return counts; } -// Usually when we encounter a generic type like `Fn`, we assume it uses T -// in multiple places because it might be something like `{a: T, b: T}`. But for -// a few special types like Arrays, we want Array (or T[]) to only count as -// a single use. -const SINGULAR_TYPES = new Set(['Array', 'ReadonlyArray']); - /** * Populates {@link foundIdentifierUsages} by the number of times each type parameter * appears in the given type by checking its uses through its type references. 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 index 22e166abd160..cbf79b1ab183 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -649,5 +649,15 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + declare function triple(input: [A, B, C]): void; + `, + errors: [ + { messageId: 'sole', data: { name: 'A' } }, + { messageId: 'sole', data: { name: 'B' } }, + { messageId: 'sole', data: { name: 'C' } }, + ], + }, ], }); From 4602ca733e432a9e451930e67223c5c1c02f4257 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 20:31:14 +0200 Subject: [PATCH 10/45] update doc --- .../docs/rules/no-unnecessary-type-parameters.mdx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx index 130c40b1ce04..b0d9cb4bef3f 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -77,16 +77,22 @@ function getProperty(obj: T, key: K) { ## 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: +For example, the following `T` is used multiple times by virtue of being in an `Set`, even though its name only appears once after declaration: ```ts -declare function createStateHistory(): T[]; +declare function createStateHistory(): Set; ``` -This is because the type parameter `T` relates multiple methods in the `T[]` together, making it used more than once. +This is because the type parameter `T` relates multiple methods in the `Set` 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`. +That includes type arguments given to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray` and `Array` are special cases that are reported on: + +```ts +declare function createStateHistory(): Array; +``` + +In such case an error will be reported because `T` is used only once as type argument for the `Array` global type. ## When Not To Use It From 366158dff10b7202aabf841aa041ecc1afba33c4 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 22:13:58 +0200 Subject: [PATCH 11/45] chore: consider existing assumeMultipleUses value --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 4 ++-- .../tests/rules/no-unnecessary-type-parameters.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 2bea009a74a0..9824b3d0556e 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -260,11 +260,11 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - const assumeMultipleUses = + const thisAssumeMultipleUses = !tsutils.isTupleType(type.target) && !SINGULAR_TYPES.has(type.symbol.getName()); - visitType(typeArgument, assumeMultipleUses); + visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); } } 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 index cbf79b1ab183..6787e3ab5e8f 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -226,6 +226,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeMap(): [Map];', 'declare function arrayOfPairs(): [T, T][];', 'declare function fetchJson(url: string): Promise;', + 'declare function fetchJsonArray(url: string): Promise;', 'declare function fn(input: T): 0 extends 0 ? T : never;', 'declare function useFocus(): [React.RefObject];', ` From 6d386c57ca07fb96b0300922d0c8118e4f0704b1 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 22:18:36 +0200 Subject: [PATCH 12/45] add missing test case --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 6787e3ab5e8f..d0610cee401a 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -660,5 +660,11 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { { messageId: 'sole', data: { name: 'C' } }, ], }, + { + code: ` + declare function foo(input: [T, string]): void; + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, ], }); From 9979cf9887f05da38d64a3c8d2111a7034a63789 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 10 Jul 2024 22:19:24 +0200 Subject: [PATCH 13/45] doc --- .../eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx index b0d9cb4bef3f..7d351823dd4f 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -86,7 +86,7 @@ declare function createStateHistory(): Set; This is because the type parameter `T` relates multiple methods in the `Set` 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 `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray` and `Array` are special cases that are reported on: +That includes type arguments given to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array` and tuples are special cases that are reported on: ```ts declare function createStateHistory(): Array; From 8fea88e6cf50ce0ac485be7607ceca2df2ff1bd9 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Fri, 12 Jul 2024 18:20:36 +0200 Subject: [PATCH 14/45] no error for factory functions --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 4 +++- .../tests/rules/no-unnecessary-type-parameters.test.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 9824b3d0556e..5e9fc0b2a48d 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -210,6 +210,7 @@ function collectTypeParameterUsageCounts( function visitType( type: ts.Type | undefined, assumeMultipleUses: boolean, + isReturnType = false, ): void { // Seeing the same type > (threshold=3 ** 2) times indicates a likely // recursive type, like `type T = { [P in keyof T]: T }`. @@ -262,7 +263,7 @@ function collectTypeParameterUsageCounts( for (const typeArgument of type.typeArguments ?? []) { const thisAssumeMultipleUses = !tsutils.isTupleType(type.target) && - !SINGULAR_TYPES.has(type.symbol.getName()); + (!SINGULAR_TYPES.has(type.symbol.getName()) || isReturnType); visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); } @@ -357,6 +358,7 @@ function collectTypeParameterUsageCounts( checker.getTypePredicateOfSignature(signature)?.type ?? signature.getReturnType(), false, + true, ); } 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 index d0610cee401a..5bd06f1260c1 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -211,6 +211,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, 'declare function get(): void;', 'declare function get(param: T[]): T;', + 'declare function factory(): T[];', 'declare function box(val: T): { val: T };', 'declare function identity(param: T): T;', 'declare function compare(param1: T, param2: T): boolean;', From fe7efb154c010dbb348fe336ae5b9e0fd8192045 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Tue, 16 Jul 2024 09:44:52 +0200 Subject: [PATCH 15/45] add more tests --- .../rules/no-unnecessary-type-parameters.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 index 5bd06f1260c1..48efef3603a0 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -211,7 +211,10 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, 'declare function get(): void;', 'declare function get(param: T[]): T;', - 'declare function factory(): T[];', + 'declare function factoryArray(): Array;', + 'declare function factoryReadonlyArray(): ReadonlyArray;', + 'declare function factorySyntacticArray(): T[];', + 'declare function factorySyntacticReadonlyArray(): readonly T[];', 'declare function box(val: T): { val: T };', 'declare function identity(param: T): T;', 'declare function compare(param1: T, param2: T): boolean;', @@ -667,5 +670,11 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + declare function returnsTuple(): [T]; + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, ], }); From 2f26941a2da89ff9107a3f9adbef87ea5c8f04cc Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:14:39 +0200 Subject: [PATCH 16/45] non methods member not considered as singular --- .../rules/no-unnecessary-type-parameters.ts | 26 +++++++++++++--- .../no-unnecessary-type-parameters.test.ts | 31 +++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index d77827a48dda..0d19187989ff 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -167,10 +167,10 @@ function countTypeParameterUsage( if (ts.isClassLike(node)) { for (const typeParameter of node.typeParameters) { - collectTypeParameterUsageCounts(checker, typeParameter, counts); + collectTypeParameterUsageCounts(checker, typeParameter, counts, true); } for (const member of node.members) { - collectTypeParameterUsageCounts(checker, member, counts); + collectTypeParameterUsageCounts(checker, member, counts, true); } } else { collectTypeParameterUsageCounts(checker, node, counts); @@ -188,6 +188,7 @@ function collectTypeParameterUsageCounts( checker: ts.TypeChecker, node: ts.Node, foundIdentifierUsages: Map, + isNodeClassLike = false, ): void { const visitedSymbolLists = new Set(); const type = checker.getTypeAtLocation(node); @@ -262,9 +263,26 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { + const isntTuple = !tsutils.isTupleType(type.target); + + const isntSingularNotInReturnPosition = !( + SINGULAR_TYPES.has( + (type.symbol as ts.Symbol | undefined)?.getName() ?? '', + ) && !isReturnType + ); + + const isntClassFunctionalAttribute = + isNodeClassLike && + ts.isPropertyDeclaration(node) && + (node.initializer + ? !ts.isArrowFunction(node.initializer) + : node.type + ? !ts.isFunctionTypeNode(node.type) + : true); + const thisAssumeMultipleUses = - !tsutils.isTupleType(type.target) && - (!SINGULAR_TYPES.has(type.symbol.getName()) || isReturnType); + (isntTuple && isntSingularNotInReturnPosition) || + isntClassFunctionalAttribute; visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); } 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 index 0f178c760d5e..91a933cf391f 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -16,6 +16,12 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unnecessary-type-parameters', rule, { valid: [ + ` + class ClassyArray { + arr: T[]; + label: string; + } + `, ` class ClassyArray { value1: T; @@ -243,12 +249,10 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 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, @@ -258,37 +262,31 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 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, @@ -296,14 +294,12 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, ` 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) => ( @@ -313,7 +309,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, ` import type { AST_TOKEN_TYPES, TSESTree } from '@typescript-eslint/types'; - export const isNotTokenOfTypeWithConditions = < TokenType extends AST_TOKEN_TYPES, @@ -634,8 +629,8 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { }, { code: ` - class ClassyArray { - arr: T[]; + class Joiner { + join: (els: T[]) => string; } `, errors: [{ messageId: 'sole', data: { name: 'T' } }], @@ -650,6 +645,16 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [{ messageId: 'sole', data: { name: 'T' } }], }, + { + code: ` + class Joiner2 { + join = (els: T[]) => { + return els.map(el => '' + el).join(','); + }; + } + `, + errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, { code: ` class Joiner { From 678e6b862335d0c1dddad01daf4256f84ed07202 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:15:19 +0200 Subject: [PATCH 17/45] nvm --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 91a933cf391f..923fe8aec5b7 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -647,7 +647,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { }, { code: ` - class Joiner2 { + class Joiner { join = (els: T[]) => { return els.map(el => '' + el).join(','); }; From 3d36117121a3f23c0dc880551092854f4dfce45f Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:19:09 +0200 Subject: [PATCH 18/45] add tests --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 923fe8aec5b7..74b8431ca72c 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -19,11 +19,10 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { ` class ClassyArray { arr: T[]; - label: string; } `, ` - class ClassyArray { + class LabeledArray { value1: T; value2: T; } From 667e71517e43d25e465d9ed56fda6cef73d83487 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:19:45 +0200 Subject: [PATCH 19/45] fix tests --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 index 74b8431ca72c..9aea78d0bdad 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -19,10 +19,17 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { ` class ClassyArray { arr: T[]; + label: string; } `, ` class LabeledArray { + arr: T[]; + label: string; + } + `, + ` + class ClassyArray { value1: T; value2: T; } From 13733da0d0f3ddd45d85b5ce63651e3b5a89ea0a Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:31:38 +0200 Subject: [PATCH 20/45] add tests --- .../no-unnecessary-type-parameters.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 index 9aea78d0bdad..c52d2a5eb8c2 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -227,6 +227,27 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function factoryReadonlyArray(): ReadonlyArray;', 'declare function factorySyntacticArray(): T[];', 'declare function factorySyntacticReadonlyArray(): readonly T[];', + ` + class ArrayProducer { + produce(): T[] { + return []; + } + } + `, + ` + class ArrayProducer { + produce(): T[] { + return []; + } + } + `, + ` + class ArrayProducer { + static produce(): T[] { + return []; + } + } + `, 'declare function box(val: T): { val: T };', 'declare function identity(param: T): T;', 'declare function compare(param1: T, param2: T): boolean;', From 89812f4ec0b88d899f5dfa6414c58817372e1fd4 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:32:05 +0200 Subject: [PATCH 21/45] renaming --- .../rules/no-unnecessary-type-parameters.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index c52d2a5eb8c2..6c589c6b0ce6 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -228,22 +228,22 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function factorySyntacticArray(): T[];', 'declare function factorySyntacticReadonlyArray(): readonly T[];', ` - class ArrayProducer { - produce(): T[] { + class ArrayFactory { + factory(): T[] { return []; } } `, ` - class ArrayProducer { - produce(): T[] { + class ArrayFactory { + factory(): T[] { return []; } } `, ` - class ArrayProducer { - static produce(): T[] { + class ArrayFactory { + static factory(): T[] { return []; } } From 4232c347891458452fe110d4d2cbb900e546eee3 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 22:35:07 +0200 Subject: [PATCH 22/45] add test --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 6c589c6b0ce6..3fcb21aaefcb 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -16,6 +16,12 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unnecessary-type-parameters', rule, { valid: [ + ` + class ClassyArray { + #arr: T[]; + label: string; + } + `, ` class ClassyArray { arr: T[]; From b9578787f0cbbda4fa1f079adff57e152b5b069d Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Thu, 8 Aug 2024 23:02:53 +0200 Subject: [PATCH 23/45] add test --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 3fcb21aaefcb..7539653daa99 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -18,19 +18,19 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { valid: [ ` class ClassyArray { - #arr: T[]; + arr: T[]; label: string; } `, ` - class ClassyArray { - arr: T[]; + class ClassyTuple { + arr: [T]; label: string; } `, ` class LabeledArray { - arr: T[]; + #arr: T[]; label: string; } `, From d7cb27e38e1642d0fe87612985b21318238d6262 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Fri, 9 Aug 2024 08:37:57 +0200 Subject: [PATCH 24/45] enhance tests --- .../rules/no-unnecessary-type-parameters.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 index 7539653daa99..2beca4abac8f 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -19,19 +19,27 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { ` class ClassyArray { arr: T[]; - label: string; } `, ` class ClassyTuple { arr: [T]; - label: string; } `, ` - class LabeledArray { + class ClassyArrayPrivate { #arr: T[]; + } + `, + ` + class LabeledArray { + arr: T[] | undefined; label: string; + + constructor(label: string) { + this.arr = []; + this.label = label; + } } `, ` From 38776d1d98d1b7a8b08407f6775649ff4f047a97 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Fri, 9 Aug 2024 08:44:33 +0200 Subject: [PATCH 25/45] add comment --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 0d19187989ff..daf1e12e1a3a 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -280,6 +280,8 @@ function collectTypeParameterUsageCounts( ? !ts.isFunctionTypeNode(node.type) : true); + // if it's a tuple or a singular type in a return position, we don't want to assume multiple uses + // unless it's a class property that doesn't contain a function const thisAssumeMultipleUses = (isntTuple && isntSingularNotInReturnPosition) || isntClassFunctionalAttribute; From 8b35cab26a33dd475769a40505426dfc41038db7 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Fri, 9 Aug 2024 08:45:16 +0200 Subject: [PATCH 26/45] fix comment --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index daf1e12e1a3a..fa755fb400eb 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -280,7 +280,7 @@ function collectTypeParameterUsageCounts( ? !ts.isFunctionTypeNode(node.type) : true); - // if it's a tuple or a singular type in a return position, we don't want to assume multiple uses + // if it's a tuple or a singular type in a input position, we don't want to assume multiple uses // unless it's a class property that doesn't contain a function const thisAssumeMultipleUses = (isntTuple && isntSingularNotInReturnPosition) || From 32ca4c78985bc258703ebdc8477e1bf800a2f32e Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Fri, 9 Aug 2024 08:49:24 +0200 Subject: [PATCH 27/45] better names --- .../src/rules/no-unnecessary-type-parameters.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index fa755fb400eb..29e3d861211b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -263,15 +263,14 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - const isntTuple = !tsutils.isTupleType(type.target); + const isTuple = tsutils.isTupleType(type.target); - const isntSingularNotInReturnPosition = !( + const isSingularInInputPosition = SINGULAR_TYPES.has( (type.symbol as ts.Symbol | undefined)?.getName() ?? '', - ) && !isReturnType - ); + ) && !isReturnType; - const isntClassFunctionalAttribute = + const isNonFunctionalClassProperty = isNodeClassLike && ts.isPropertyDeclaration(node) && (node.initializer @@ -283,8 +282,8 @@ function collectTypeParameterUsageCounts( // if it's a tuple or a singular type in a input position, we don't want to assume multiple uses // unless it's a class property that doesn't contain a function const thisAssumeMultipleUses = - (isntTuple && isntSingularNotInReturnPosition) || - isntClassFunctionalAttribute; + (!isTuple && !isSingularInInputPosition) || + isNonFunctionalClassProperty; visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); } From 6cfb31b49758298d58b2f89f77ccae749d2df5eb Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 2 Dec 2024 11:34:31 -0500 Subject: [PATCH 28/45] Swap test cases to new proposal --- .../no-unnecessary-type-parameters.test.ts | 934 ++++++++++++++---- 1 file changed, 759 insertions(+), 175 deletions(-) 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 index d4d4de90a384..c32762b2b0d9 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -1,4 +1,4 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/no-unnecessary-type-parameters'; import { getFixturesRootDir } from '../RuleTester'; @@ -8,8 +8,8 @@ const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootPath, project: './tsconfig.json', + tsconfigRootDir: rootPath, }, }, }); @@ -21,27 +21,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { arr: T[]; } `, - ` - class ClassyTuple { - arr: [T]; - } - `, - ` - class ClassyArrayPrivate { - #arr: T[]; - } - `, - ` - class LabeledArray { - arr: T[] | undefined; - label: string; - - constructor(label: string) { - this.arr = []; - this.label = label; - } - } - `, ` class ClassyArray { value1: T; @@ -78,6 +57,20 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { } } `, + ` + 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; @@ -237,31 +230,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, 'declare function get(): void;', 'declare function get(param: T[]): T;', - 'declare function factoryArray(): Array;', - 'declare function factoryReadonlyArray(): ReadonlyArray;', - 'declare function factorySyntacticArray(): T[];', - 'declare function factorySyntacticReadonlyArray(): readonly T[];', - ` - class ArrayFactory { - factory(): T[] { - return []; - } - } - `, - ` - class ArrayFactory { - factory(): T[] { - return []; - } - } - `, - ` - class ArrayFactory { - static factory(): T[] { - return []; - } - } - `, 'declare function box(val: T): { val: T };', 'declare function identity(param: T): T;', 'declare function compare(param1: T, param2: T): boolean;', @@ -275,9 +243,10 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeSets(): [Set][];', 'declare function makeMap(): Map;', 'declare function makeMap(): [Map];', + 'declare function makeArray(): T[];', + 'declare function makeTupleMulti(): [T | null, T | null];', 'declare function arrayOfPairs(): [T, T][];', 'declare function fetchJson(url: string): Promise;', - 'declare function fetchJsonArray(url: string): Promise;', 'declare function fn(input: T): 0 extends 0 ? T : never;', 'declare function useFocus(): [React.RefObject];', ` @@ -290,10 +259,12 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 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, @@ -303,31 +274,37 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 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, @@ -335,12 +312,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, ` 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) => ( @@ -350,6 +329,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, ` import type { AST_TOKEN_TYPES, TSESTree } from '@typescript-eslint/types'; + export const isNotTokenOfTypeWithConditions = < TokenType extends AST_TOKEN_TYPES, @@ -415,6 +395,42 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { return [{ value: () => mappedReturnType(x) }]; } `, + ` +type Identity = T; + +type Mapped = Identity<{ [P in keyof T]: Value }>; + +declare function sillyFoo( + c: Value, +): (data: Data) => Mapped; + `, + ` +type Silly = { [P in keyof T]: T[P] }; + +type SillyFoo = Silly<{ [P in keyof T]: Value }>; + +type Foo = { [P in keyof T]: Value }; + +declare function foo(data: T, c: Constant): Foo; +declare function foo(c: Constant): (data: T) => Foo; + +declare function sillyFoo( + data: T, + c: Constant, +): SillyFoo; +declare function sillyFoo( + c: Constant, +): (data: T) => SillyFoo; + `, + ` +const f = (setValue: (v: T) => void, getValue: () => NoInfer) => {}; + `, + ` +const f = ( + setValue: (v: T) => NoInfer, + getValue: (v: NoInfer) => NoInfer, +) => {}; + `, ], invalid: [ @@ -422,17 +438,49 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'const func = (param: T) => null;', errors: [ { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'const func = (param: unknown) => null;', + }, + ], + }, + ], + }, + { + code: 'const func = (param: [T]) => null;', + errors: [ + { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions }, ], }, { - code: 'const f1 = (): T => {};', + code: 'const func = (param: T[]) => null;', errors: [ { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'const f1 = (): T => {};', + errors: [ + { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'const f1 = (): unknown => {};', + }, + ], }, ], }, @@ -444,8 +492,18 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + interface I { + (value: unknown): void; + } + `, + }, + ], }, ], }, @@ -455,7 +513,21 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { m(x: T): void; } `, - errors: [{ messageId: 'sole' }], + errors: [ + { + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + interface I { + m(x: unknown): void; + } + `, + }, + ], + }, + ], }, { code: ` @@ -467,8 +539,20 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'class', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + class Joiner { + join(el: string | number, other: string) { + return [el, other].join(','); + } + } + `, + }, + ], }, ], }, @@ -478,8 +562,16 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'class', name: 'V', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C {} + `, + }, + ], }, ], }, @@ -491,12 +583,32 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'class', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C { + method(param: unknown): U; + } + `, + }, + ], }, { - messageId: 'sole', data: { descriptor: 'class', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C { + method(param: T): unknown; + } + `, + }, + ], }, ], }, @@ -508,12 +620,32 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C { + method(param: unknown): U; + } + `, + }, + ], }, { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C { + method(param: T): unknown; + } + `, + }, + ], }, ], }, @@ -525,8 +657,18 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'P', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class C { + prop: () => unknown; + } + `, + }, + ], }, ], }, @@ -538,8 +680,18 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare class Foo { + foo(this: unknown): void; + } + `, + }, + ], }, ], }, @@ -551,12 +703,32 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'A', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function third(a: unknown, b: B, c: C): C { + return c; + } + `, + }, + ], }, { - messageId: 'sole', data: { descriptor: 'function', name: 'B', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function third(a: A, b: unknown, c: C): C { + return c; + } + `, + }, + ], }, ], }, @@ -569,8 +741,19 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function foo(_: unknown) { + const x: unknown = null!; + const y: unknown = null!; + } + `, + }, + ], }, ], }, @@ -583,22 +766,46 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function foo(_: unknown): void { + const x: unknown = null!; + const y: unknown = null!; + } + `, + }, + ], }, ], }, { code: ` - function foo(_: T): (input: T) => T { - const x: T = null!; - const y: T = null!; - } +function foo(_: T): (input: T) => T { + const x: T = null!; + const y: T = null!; + return null!; +} `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function foo(_: unknown): (input: T) => T { + const x: unknown = null!; + const y: unknown = null!; + return null!; +} + `, + }, + ], }, ], }, @@ -615,8 +822,23 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function foo(_: unknown) { + function withX(): unknown { + return null!; + } + function withY(): unknown { + return null!; + } + } + `, + }, + ], }, ], }, @@ -628,8 +850,18 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function parseYAML(input: string): unknown { + return input as any as unknown; + } + `, + }, + ], }, ], }, @@ -641,8 +873,18 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'K', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function printProperty(obj: T, key: keyof T) { + console.log(obj[key]); + } + `, + }, + ], }, ], }, @@ -657,6 +899,17 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function fn(param: string) { + let v: unknown = null!; + return v; + } + `, + }, + ], }, ], }, @@ -675,12 +928,44 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'CB1', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function both< + Args extends unknown[], + CB2 extends (...args: Args) => void, + >(fn1: (...args: Args) => void, fn2: CB2): (...args: Args) => void { + return function (...args: Args) { + fn1(...args); + fn2(...args); + }; + } + `, + }, + ], }, { - messageId: 'sole', data: { descriptor: 'function', name: 'CB2', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function both< + Args extends unknown[], + CB1 extends (...args: Args) => void, + >(fn1: CB1, fn2: (...args: Args) => void): (...args: Args) => void { + return function (...args: Args) { + fn1(...args); + fn2(...args); + }; + } + `, + }, + ], }, ], }, @@ -694,6 +979,16 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + function getLength(x: { length: number }) { + return x.length; + } + `, + }, + ], }, ], }, @@ -710,6 +1005,19 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + interface Lengthy { + length: number; + } + function getLength(x: Lengthy) { + return x.length; + } + `, + }, + ], }, ], }, @@ -717,8 +1025,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(): unknown;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function get(): unknown;', + }, + ], }, ], }, @@ -726,8 +1040,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(): T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function get(): unknown;', + }, + ], }, ], }, @@ -735,8 +1055,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(): T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function get(): object;', + }, + ], }, ], }, @@ -744,8 +1070,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function take(param: T): void;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function take(param: unknown): void;', + }, + ], }, ], }, @@ -753,8 +1085,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function take(param: T): void;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function take(param: object): void;', + }, + ], }, ], }, @@ -762,8 +1100,15 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function take(param1: T, param2: U): void;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function take(param1: T, param2: unknown): void;', + }, + ], }, ], }, @@ -771,8 +1116,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function take(param: T): U;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function take(param: T): T;', + }, + ], }, ], }, @@ -780,8 +1131,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function take(param: U): U;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function take(param: U): U;', + }, + ], }, ], }, @@ -789,8 +1146,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(param: U): U;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function get(param: U): U;', + }, + ], }, ], }, @@ -798,8 +1161,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(param: T): U;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function get(param: T): T;', + }, + ], }, ], }, @@ -807,8 +1176,15 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function compare(param1: T, param2: U): boolean;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function compare(param1: T, param2: T): boolean;', + }, + ], }, ], }, @@ -816,16 +1192,37 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(param: (param: U) => V): T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: U) => V): unknown;', + }, + ], }, { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: unknown) => V): T;', + }, + ], }, { - messageId: 'sole', data: { descriptor: 'function', name: 'V', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: U) => unknown): T;', + }, + ], }, ], }, @@ -833,16 +1230,99 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'declare function get(param: (param: T) => U): T;', errors: [ { + column: 22, + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + endColumn: 23, + line: 1, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: T) => U): unknown;', + }, + ], + }, + { + column: 33, + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + endColumn: 34, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: unknown) => U): T;', + }, + ], + }, + { + column: 36, + data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + endColumn: 37, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function get(param: (param: T) => unknown): T;', + }, + ], + }, + ], + }, + { + code: 'declare function makeReadonlyArray(): readonly T[];', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function makeReadonlyTuple(): readonly [T];', + errors: [ + { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions }, + ], + }, + { + code: 'declare function makeReadonlyTupleNullish(): readonly [T | null];', + errors: [ { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function makeReadonlyTupleMulti(): readonly [T, T];', + errors: [ + { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions }, + ], + }, + { + code: ` + declare function makeReadonlyTupleMultiNullish(): readonly [ + T | null, + T | null, + ]; + `, + errors: [ { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + // TODO: suggestions }, ], }, @@ -850,8 +1330,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = () => T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = () => unknown;', + }, + ], }, ], }, @@ -859,8 +1345,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = () => [];', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = () => [];', + }, + ], }, ], }, @@ -871,8 +1363,17 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + type Other = 0; + type Fn = () => Other; + `, + }, + ], }, ], }, @@ -883,8 +1384,17 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { `, errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + type Other = 0 | 1; + type Fn = () => Other; + `, + }, + ], }, ], }, @@ -892,8 +1402,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = (param: U) => void;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'U', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = (param: unknown) => void;', + }, + ], }, ], }, @@ -901,8 +1417,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Ctr = new () => T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Ctr = new () => unknown;', + }, + ], }, ], }, @@ -910,8 +1432,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = () => { [K in keyof T]: K };', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = () => { [K in keyof unknown]: K };', + }, + ], }, ], }, @@ -919,8 +1447,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: "type Fn = () => { [K in 'a']: T };", errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: "type Fn = () => { [K in 'a']: unknown };", + }, + ], }, ], }, @@ -928,8 +1462,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = (value: unknown) => value is T;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = (value: unknown) => value is unknown;', + }, + ], }, ], }, @@ -937,114 +1477,158 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { code: 'type Fn = () => `a${T}b`;', errors: [ { - messageId: 'sole', data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'type Fn = () => `a${string}b`;', + }, + ], }, ], }, { code: ` - function getLength(array: Array) { - return array.length; - } - `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - function getLength(array: ReadonlyArray) { - return array.length; - } - `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - function getLength(array: T[]) { - return array.length; - } + declare function mapObj( + obj: { [key in K]?: V }, + fn: (key: K) => number, + ): number[]; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - function getLength(array: readonly T[]) { - return array.length; - } + errors: [ + { + data: { descriptor: 'function', name: 'V', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare function mapObj( + obj: { [key in K]?: unknown }, + fn: (key: K) => number, + ): number[]; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], + }, + ], }, { code: ` - class Joiner { - join: (els: T[]) => string; - } +declare function setItem(T): T; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - class Joiner { - join(els: T[]) { - return els.map(el => '' + el).join(','); - } - } + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +declare function setItem(T): unknown; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], + }, + ], }, { code: ` - class Joiner { - join = (els: T[]) => { - return els.map(el => '' + el).join(','); - }; - } +interface StorageService { + setItem({ key: string, value: T }): Promise; +} `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - class Joiner { - join(els: T[]) { - return els.map(el => '' + el).join(','); - } - } + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'never used' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +interface StorageService { + setItem({ key: string, value: T }): Promise; +} `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], + }, + ], }, { - code: ` - declare function triple(input: [A, B, C]): void; + // This isn't actually an important test case. + // However, we use it as an example in the docs of code that is flagged, + // but shouldn't necessarily be. So, if you make a change to the rule logic + // that resolves this sort-of-false-positive, please update the docs + // accordingly. + // Original discussion in https://github.com/typescript-eslint/typescript-eslint/issues/9709 + code: noFormat` +type Compute = A extends Function ? A : { [K in keyof A]: Compute }; +type Equal = + (() => T1 extends Compute ? 1 : 2) extends + (() => T2 extends Compute ? 1 : 2) + ? true + : false; `, errors: [ - { messageId: 'sole', data: { name: 'A' } }, - { messageId: 'sole', data: { name: 'B' } }, - { messageId: 'sole', data: { name: 'C' } }, - ], - }, - { - code: ` - declare function foo(input: [T, string]): void; + { + data: { descriptor: 'function', name: 'T1', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +type Compute = A extends Function ? A : { [K in keyof A]: Compute }; +type Equal = + (() => unknown extends Compute ? 1 : 2) extends + (() => T2 extends Compute ? 1 : 2) + ? true + : false; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], - }, - { - code: ` - declare function returnsTuple(): [T]; + }, + ], + }, + { + data: { descriptor: 'function', name: 'T2', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +type Compute = A extends Function ? A : { [K in keyof A]: Compute }; +type Equal = + (() => T1 extends Compute ? 1 : 2) extends + (() => unknown extends Compute ? 1 : 2) + ? true + : false; `, - errors: [{ messageId: 'sole', data: { name: 'T' } }], + }, + ], + }, + ], }, { code: ` - declare function mapObj( - obj: { [key in K]?: V }, - fn: (key: K) => number, - ): number[]; +function f(x: T): void { + // @ts-expect-error + x.notAMethod(); +} `, errors: [ { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - data: { descriptor: 'function', name: 'V', uses: 'used only once' }, + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +function f(x: unknown): void { + // @ts-expect-error + x.notAMethod(); +} + `, + }, + ], }, ], }, From c8cee06d1a4422f6d7bcc159d9cdba0a6facae93 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 2 Dec 2024 11:36:15 -0500 Subject: [PATCH 29/45] Add back fetchJson case + one more --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 1 + 1 file changed, 1 insertion(+) 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 index c32762b2b0d9..8256cf714fae 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -247,6 +247,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeTupleMulti(): [T | null, T | null];', 'declare function arrayOfPairs(): [T, T][];', 'declare function fetchJson(url: string): Promise;', + 'declare function fetchJsonTuple(url: string): Promise<[T]>;', 'declare function fn(input: T): 0 extends 0 ? T : never;', 'declare function useFocus(): [React.RefObject];', ` From a9f1684a5b03b126628ece4a4b63d3c4e6ce0325 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 2 Dec 2024 11:42:04 -0500 Subject: [PATCH 30/45] Some more test cases while we're at it --- .../no-unnecessary-type-parameters.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) 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 index 8256cf714fae..73d9e4ae1c28 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -244,6 +244,7 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeMap(): Map;', 'declare function makeMap(): [Map];', 'declare function makeArray(): T[];', + 'declare function makeArrayNullish(): (T | null)[];', 'declare function makeTupleMulti(): [T | null, T | null];', 'declare function arrayOfPairs(): [T, T][];', 'declare function fetchJson(url: string): Promise;', @@ -1327,6 +1328,56 @@ function foo(_: unknown): (input: T) => T { }, ], }, + { + code: 'declare function takeArray(input: T[]): void;', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function takeArrayNullish(input: (T | null)[]): void;', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function takeTuple(input: [T]): void;', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function takeTupleMulti(input: [T, T]): void;', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions + }, + ], + }, + { + code: 'declare function takeTupleMultiNullish(input: [T | null, T | null]): void;', + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + // TODO: suggestions + }, + ], + }, { code: 'type Fn = () => T;', errors: [ From 2e1c31cbe4d8b23a5a8512b8a8d78ff2d229c38e Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Mon, 2 Dec 2024 19:48:38 +0100 Subject: [PATCH 31/45] wip --- .../rules/no-unnecessary-type-parameters.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 774132b0bbca..feac56e03b28 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -298,7 +298,12 @@ function countTypeParameterUsage( collectTypeParameterUsageCounts(checker, member, counts, true); } } else { - collectTypeParameterUsageCounts(checker, node, counts); + collectTypeParameterUsageCounts( + checker, + node, + counts, + ts.isClassElement(node), + ); } return counts; @@ -313,7 +318,7 @@ function collectTypeParameterUsageCounts( checker: ts.TypeChecker, node: ts.Node, foundIdentifierUsages: Map, - isNodeClassLike = false, + fromClass: boolean, // We are talking about the type parameters of a class or one of its methods ): void { const visitedSymbolLists = new Set(); const type = checker.getTypeAtLocation(node); @@ -402,20 +407,10 @@ function collectTypeParameterUsageCounts( (type.symbol as ts.Symbol | undefined)?.getName() ?? '', ) && !isReturnType; - const isNonFunctionalClassProperty = - isNodeClassLike && - ts.isPropertyDeclaration(node) && - (node.initializer - ? !ts.isArrowFunction(node.initializer) - : node.type - ? !ts.isFunctionTypeNode(node.type) - : true); - // if it's a tuple or a singular type in a input position, we don't want to assume multiple uses // unless it's a class property that doesn't contain a function const thisAssumeMultipleUses = - (!isTuple && !isSingularInInputPosition) || - isNonFunctionalClassProperty; + (!isTuple && !isSingularInInputPosition) || fromClass; visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); } From f185577f35f71e6e820a9298b8ecab1512daf5ef Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Mon, 2 Dec 2024 20:38:42 +0100 Subject: [PATCH 32/45] fup --- .../src/rules/no-unnecessary-type-parameters.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index feac56e03b28..3505a3a86b0e 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -245,6 +245,17 @@ function isTypeParameterRepeatedInAST( const grandparent = skipConstituentsUpward( reference.identifier.parent.parent, ); + + // tuple types and array types don't count as multiple uses + // just as a single use overall + if ( + grandparent.type === AST_NODE_TYPES.TSArrayType || + grandparent.type === AST_NODE_TYPES.TSTupleType + ) { + // defer the check + continue; + } + if ( grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation && grandparent.params.includes(reference.identifier.parent) && From fed012a33f527fe75daef1b318cb986a35fa9d7e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 3 Dec 2024 09:08:24 -0500 Subject: [PATCH 33/45] Fixed up tests per multi-element tuples --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 index 73d9e4ae1c28..656377fff7fe 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -246,6 +246,8 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { 'declare function makeArray(): T[];', 'declare function makeArrayNullish(): (T | null)[];', 'declare function makeTupleMulti(): [T | null, T | null];', + 'declare function takeTupleMulti(input: [T, T]): void;', + 'declare function takeTupleMultiNullish(input: [T | null, T | null]): void;', 'declare function arrayOfPairs(): [T, T][];', 'declare function fetchJson(url: string): Promise;', 'declare function fetchJsonTuple(url: string): Promise<[T]>;', @@ -1359,7 +1361,7 @@ function foo(_: unknown): (input: T) => T { ], }, { - code: 'declare function takeTupleMulti(input: [T, T]): void;', + code: 'declare function takeTupleMultiUnrelated(input: [T, number]): void;', errors: [ { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, @@ -1369,7 +1371,11 @@ function foo(_: unknown): (input: T) => T { ], }, { - code: 'declare function takeTupleMultiNullish(input: [T | null, T | null]): void;', + code: ` + declare function takeTupleMultiUnrelatedNullish( + input: [T | null, null], + ): void; + `, errors: [ { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, From c94a9de8526265ff5e924237f73861e2507df19f Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Tue, 3 Dec 2024 18:36:44 +0100 Subject: [PATCH 34/45] progress --- .../rules/no-unnecessary-type-parameters.ts | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 3505a3a86b0e..2cb11cf280ae 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -202,12 +202,6 @@ export default createRule({ }, }); -// Usually when we encounter a generic type like `Fn`, we assume it uses T -// in multiple places because it might be something like `{a: T, b: T}`. But for -// a few special types like Arrays, we want Array (or T[]) to only count as -// a single use. -const SINGULAR_TYPES = new Set(['Array', 'ReadonlyArray']); - function isTypeParameterRepeatedInAST( node: TSESTree.TSTypeParameter, references: Reference[], @@ -246,27 +240,14 @@ function isTypeParameterRepeatedInAST( reference.identifier.parent.parent, ); - // tuple types and array types don't count as multiple uses - // just as a single use overall + // tuple types and array types must be handled carefully, so it's better to + // defer the check to the types aware phase if ( grandparent.type === AST_NODE_TYPES.TSArrayType || grandparent.type === AST_NODE_TYPES.TSTupleType ) { - // defer the check continue; } - - if ( - grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation && - grandparent.params.includes(reference.identifier.parent) && - !( - grandparent.parent.type === AST_NODE_TYPES.TSTypeReference && - grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier && - SINGULAR_TYPES.has(grandparent.parent.typeName.name) - ) - ) { - return true; - } } total += 1; @@ -411,19 +392,28 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - const isTuple = tsutils.isTupleType(type.target); - - const isSingularInInputPosition = - SINGULAR_TYPES.has( - (type.symbol as ts.Symbol | undefined)?.getName() ?? '', - ) && !isReturnType; - - // if it's a tuple or a singular type in a input position, we don't want to assume multiple uses - // unless it's a class property that doesn't contain a function - const thisAssumeMultipleUses = - (!isTuple && !isSingularInInputPosition) || fromClass; + // at the moment, if we are in a "class context", everything is accepted + let thisAssumeMultipleUses = fromClass || assumeMultipleUses; + + if (!thisAssumeMultipleUses) { + const isTuple = tsutils.isTupleType(type.target); + const isMutableTuple = + isTuple && !(type.target as ts.TupleType).readonly; + const typeName = + (type.symbol as ts.Symbol | undefined)?.getName() ?? ''; + const isMutableArray = typeName === 'Array'; + const isReadonlyArray = typeName === 'ReadonlyArray'; + const isArray = isMutableArray || isReadonlyArray; + + if (isArray || isTuple) { + thisAssumeMultipleUses = + isReturnType && (isMutableTuple || isMutableArray); + } else { + thisAssumeMultipleUses = true; + } + } - visitType(typeArgument, assumeMultipleUses || thisAssumeMultipleUses); + visitType(typeArgument, thisAssumeMultipleUses); } } From 015c68cfb3153e3afc17c7f1fd9a6bb42adec19c Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Tue, 3 Dec 2024 18:37:18 +0100 Subject: [PATCH 35/45] comments --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 2cb11cf280ae..500583aa3dd4 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -392,7 +392,7 @@ function collectTypeParameterUsageCounts( // Generic type references like `Map` else if (tsutils.isTypeReference(type)) { for (const typeArgument of type.typeArguments ?? []) { - // at the moment, if we are in a "class context", everything is accepted + // currently, if we are in a "class context", everything is accepted let thisAssumeMultipleUses = fromClass || assumeMultipleUses; if (!thisAssumeMultipleUses) { @@ -409,6 +409,7 @@ function collectTypeParameterUsageCounts( thisAssumeMultipleUses = isReturnType && (isMutableTuple || isMutableArray); } else { + // other kind of type references always count as multiple uses thisAssumeMultipleUses = true; } } From 0eb9d8cd448d8f18dd213243d28e69fa8c321c04 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Tue, 3 Dec 2024 19:44:19 +0100 Subject: [PATCH 36/45] restore optimization --- .../src/rules/no-unnecessary-type-parameters.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 500583aa3dd4..3c8ad7f76512 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -240,13 +240,18 @@ function isTypeParameterRepeatedInAST( reference.identifier.parent.parent, ); - // tuple types and array types must be handled carefully, so it's better to - // defer the check to the types aware phase if ( - grandparent.type === AST_NODE_TYPES.TSArrayType || - grandparent.type === AST_NODE_TYPES.TSTupleType + grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation && + grandparent.params.includes(reference.identifier.parent) && + // Array and ReadonlyArray must be handled carefully + // let's defer the check to the type-aware phase + !( + grandparent.parent.type === AST_NODE_TYPES.TSTypeReference && + grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier && + ['Array', 'ReadonlyArray'].includes(grandparent.parent.typeName.name) + ) ) { - continue; + return true; } } From dd914896ed4b33f930bc770f22f2b32cef97b6d7 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Wed, 4 Dec 2024 09:13:52 +0100 Subject: [PATCH 37/45] propagate return type context --- .../eslint-plugin/src/rules/no-unnecessary-type-parameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 3c8ad7f76512..a6aed1e55a01 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -419,7 +419,7 @@ function collectTypeParameterUsageCounts( } } - visitType(typeArgument, thisAssumeMultipleUses); + visitType(typeArgument, thisAssumeMultipleUses, isReturnType); } } From 286e7ae106ae844394631bc576fe7195d01a7871 Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Sat, 14 Dec 2024 18:15:58 +0100 Subject: [PATCH 38/45] remove readonly tests --- .../no-unnecessary-type-parameters.test.ts | 25 ------------------- 1 file changed, 25 deletions(-) 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 index 656377fff7fe..41be133418ab 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -1305,31 +1305,6 @@ function foo(_: unknown): (input: T) => T { }, ], }, - { - code: 'declare function makeReadonlyTupleMulti(): readonly [T, T];', - errors: [ - { - data: { descriptor: 'function', name: 'T', uses: 'used only once' }, - messageId: 'sole', - // TODO: suggestions - }, - ], - }, - { - code: ` - declare function makeReadonlyTupleMultiNullish(): readonly [ - T | null, - T | null, - ]; - `, - errors: [ - { - data: { descriptor: 'function', name: 'T', uses: 'used only once' }, - messageId: 'sole', - // TODO: suggestions - }, - ], - }, { code: 'declare function takeArray(input: T[]): void;', errors: [ From ae4e5236cac72724ec6a86bff463689b5269b05e Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Sat, 14 Dec 2024 18:19:44 +0100 Subject: [PATCH 39/45] doc --- .../docs/rules/no-unnecessary-type-parameters.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx index 7115f6fbbf9e..08df0d35d888 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -85,13 +85,13 @@ declare function createStateHistory(): Set; This is because the type parameter `T` relates multiple methods in the `Set` 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 `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array` and tuples are special cases that are reported on: +This includes type arguments provided to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array`, and tuples are special cases that are specifically reported on when used as input types. ```ts -declare function createStateHistory(): Array; +declare function length(array: ReadonlyArray): number; ``` -In such case an error will be reported because `T` is used only once as type argument for the `Array` global type. +In such case an error will be reported because `T` is used only once as type argument for the `ReadonlyArray` global type. ## FAQ From 48cfe54f13f8a0a33fe9dc5afdc2a7157c569c1d Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Sat, 14 Dec 2024 20:16:12 +0100 Subject: [PATCH 40/45] docs again --- .../eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx index 08df0d35d888..59ea2d1e2991 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -85,7 +85,7 @@ declare function createStateHistory(): Set; This is because the type parameter `T` relates multiple methods in the `Set` together, making it used more than once. Therefore, this rule won't report on type parameters used as a type argument. -This includes type arguments provided to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array`, and tuples are special cases that are specifically reported on when used as input types. +This includes type arguments provided to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array`, and tuples are special cases that are specifically reported on when used as input types, or as `readonly` output types. ```ts declare function length(array: ReadonlyArray): number; From 0d1f8df2bf2d2311f6fa6ec89802c51c3f0215ef Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 7 Jan 2025 08:51:48 -0500 Subject: [PATCH 41/45] Filled in snapshots --- .../no-unnecessary-type-parameters.test.ts | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) 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 index 41be133418ab..461c716fe49c 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -459,7 +459,12 @@ const f = ( { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'const func = (param: [unknown]) => null;', + }, + ], }, ], }, @@ -469,7 +474,12 @@ const f = ( { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'const func = (param: unknown[]) => null;', + }, + ], }, ], }, @@ -1281,7 +1291,13 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function makeReadonlyArray(): readonly unknown[];', + }, + ], }, ], }, @@ -1291,7 +1307,13 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function makeReadonlyTuple(): readonly [unknown];', + }, + ], }, ], }, @@ -1301,7 +1323,13 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function makeReadonlyTupleNullish(): readonly [unknown | null];', + }, + ], }, ], }, @@ -1311,7 +1339,12 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function takeArray(input: unknown[]): void;', + }, + ], }, ], }, @@ -1321,7 +1354,13 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function takeArrayNullish(input: (unknown | null)[]): void;', + }, + ], }, ], }, @@ -1331,7 +1370,12 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: 'declare function takeTuple(input: [unknown]): void;', + }, + ], }, ], }, @@ -1341,7 +1385,13 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: + 'declare function takeTupleMultiUnrelated(input: [unknown, number]): void;', + }, + ], }, ], }, @@ -1355,7 +1405,16 @@ function foo(_: unknown): (input: T) => T { { data: { descriptor: 'function', name: 'T', uses: 'used only once' }, messageId: 'sole', - // TODO: suggestions + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` + declare function takeTupleMultiUnrelatedNullish( + input: [unknown | null, null], + ): void; + `, + }, + ], }, ], }, From 27f29e2b774fa93c8eac155582d22319e967440f Mon Sep 17 00:00:00 2001 From: Andrea Simone Costa Date: Tue, 28 Jan 2025 18:02:05 +0100 Subject: [PATCH 42/45] implement some suggested changes --- .../rules/no-unnecessary-type-parameters.ts | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index a6aed1e55a01..448d17ac2dca 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -400,24 +400,16 @@ function collectTypeParameterUsageCounts( // currently, if we are in a "class context", everything is accepted let thisAssumeMultipleUses = fromClass || assumeMultipleUses; - if (!thisAssumeMultipleUses) { - const isTuple = tsutils.isTupleType(type.target); - const isMutableTuple = - isTuple && !(type.target as ts.TupleType).readonly; - const typeName = - (type.symbol as ts.Symbol | undefined)?.getName() ?? ''; - const isMutableArray = typeName === 'Array'; - const isReadonlyArray = typeName === 'ReadonlyArray'; - const isArray = isMutableArray || isReadonlyArray; - - if (isArray || isTuple) { - thisAssumeMultipleUses = - isReturnType && (isMutableTuple || isMutableArray); - } else { - // other kind of type references always count as multiple uses - thisAssumeMultipleUses = true; - } - } + // special cases - readonly arrays/tuples are considered only to use the + // type parameter once. Mutable arrays/tuples are considered to use the + // type parameter multiple times if and only if they are returned. + // other kind of type references always count as multiple uses + thisAssumeMultipleUses ||= tsutils.isTupleType(type.target) + ? isReturnType && !type.target.readonly + : checker.isArrayType(type.target) + ? isReturnType && + (type.symbol as ts.Symbol | undefined)?.getName() === 'Array' + : true; visitType(typeArgument, thisAssumeMultipleUses, isReturnType); } From 21cbd341aa9340658ad803cbd2e039f44165a847 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 24 Feb 2025 09:53:30 -0500 Subject: [PATCH 43/45] Finally commit docs reverts/touchups --- .../rules/no-unnecessary-type-parameters.mdx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx index 59ea2d1e2991..06a0a9e07163 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-parameters.mdx @@ -76,22 +76,36 @@ function getProperty(obj: T, key: K) { ## 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 `Set`, even though its name only appears once after declaration: +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(): Set; +declare function createStateHistory(): T[]; ``` -This is because the type parameter `T` relates multiple methods in the `Set` together, making it used more than once. +This is because the type parameter `T` relates multiple methods in `T[]` (`Array`) together, making it used more than once. Therefore, this rule won't report on type parameters used as a type argument. -This includes type arguments provided to global types such as `Map` and `Set`, whereas `readonly T[]`, `T[]`, `ReadonlyArray`, `Array`, and tuples are special cases that are specifically reported on when used as input types, or as `readonly` output types. +This includes type arguments provided to global types such as `Array`, `Map`, and `Set` that have multiple methods and properties that can change values based on the type parameter. + +On the other hand, readonly and fixed array-likes such as `readonly T[]`, `ReadonlyArray`, and tuples such as `[T]` are special cases that are specifically reported on when used as input types, or as `readonly` output types. +The following example will be reported because `T` is used only once as type argument for the `ReadonlyArray` global type: + + + ```ts declare function length(array: ReadonlyArray): number; ``` -In such case an error will be reported because `T` is used only once as type argument for the `ReadonlyArray` global type. + + + +```ts +declare function length(array: ReadonlyArray): number; +``` + + + ## FAQ From fc006d39a2d0343b5cd7c0dd3f6ec989ec755e2d Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:39:49 -0700 Subject: [PATCH 44/45] last piece of feedback --- .../rules/no-unnecessary-type-parameters.ts | 7 +--- .../no-unnecessary-type-parameters.shot | 15 ++++++++ .../no-unnecessary-type-parameters.test.ts | 34 +++++++++++++++---- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index 448d17ac2dca..42305e0a1d9b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -295,12 +295,7 @@ function countTypeParameterUsage( collectTypeParameterUsageCounts(checker, member, counts, true); } } else { - collectTypeParameterUsageCounts( - checker, - node, - counts, - ts.isClassElement(node), - ); + collectTypeParameterUsageCounts(checker, node, counts, false); } return counts; 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 index 7e52aaf3324d..53dc464211ba 100644 --- 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 @@ -47,3 +47,18 @@ function getProperty(obj: T, key: K) { } " `; + +exports[`Validating rule docs no-unnecessary-type-parameters.mdx code examples ESLint output 3`] = ` +"Incorrect + +declare function length(array: ReadonlyArray): number; + ~ Type parameter T is used only once in the function signature. +" +`; + +exports[`Validating rule docs no-unnecessary-type-parameters.mdx code examples ESLint output 4`] = ` +"Correct + +declare function length(array: ReadonlyArray): number; +" +`; 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 index 461c716fe49c..1faa1c4f2866 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -64,13 +64,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, { } } `, - ` - class Joiner { - join(els: T[]) { - return els.map(el => '' + el).join(','); - } - } - `, ` declare class Foo { getProp(this: Record<'prop', T>): T; @@ -1724,5 +1717,32 @@ function f(x: unknown): void { }, ], }, + { + code: ` +class Joiner { + join(els: T[]) { + return els.map(el => '' + el).join(','); + } +} + `, + errors: [ + { + data: { descriptor: 'function', name: 'T', uses: 'used only once' }, + messageId: 'sole', + suggestions: [ + { + messageId: 'replaceUsagesWithConstraint', + output: ` +class Joiner { + join(els: number[]) { + return els.map(el => '' + el).join(','); + } +} + `, + }, + ], + }, + ], + }, ], }); From 8a24eca5c960bf0dd1f9e38f9a2519b7db2ae4b7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 2 Mar 2025 06:38:17 +0100 Subject: [PATCH 45/45] chore: fix formatting --- .../tests/rules/no-unnecessary-type-parameters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1faa1c4f2866..9a636116cc37 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-parameters.test.ts @@ -1724,7 +1724,7 @@ class Joiner { return els.map(el => '' + el).join(','); } } - `, + `, errors: [ { data: { descriptor: 'function', name: 'T', uses: 'used only once' },