diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index f33823200e7..05916fa1cbd 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -160,6 +160,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | | | | | [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Warns if an explicitly specified type argument is the default for that type parameter | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index cdad2d46f1b..58580935012 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -99,7 +99,7 @@ | [`triple-equals`] | 🌟 | [`eqeqeq`][eqeqeq] | | [`typeof-compare`] | 🌟 | [`valid-typeof`][valid-typeof] | | [`unnecessary-constructor`] | 🌟 | [`no-useless-constructor`][no-useless-constructor] | -| [`use-default-type-parameter`] | 🛑 | N/A | +| [`use-default-type-parameter`] | ✅ | [`@typescript-eslint/no-unnecessary-type-arguments`] | | [`use-isnan`] | 🌟 | [`use-isnan`][use-isnan] | [1] The ESLint rule also supports silencing with an extra set of parens (`if ((foo = bar)) {}`)
@@ -617,6 +617,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/require-await`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/require-await.md [`@typescript-eslint/no-for-in-array`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-for-in-array.md [`@typescript-eslint/no-unnecessary-qualifier`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md +[`@typescript-eslint/no-unnecessary-type-arguments`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-arguments.md [`@typescript-eslint/semi`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md [`@typescript-eslint/no-floating-promises`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-floating-promises.md diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-type-arguments.md b/packages/eslint-plugin/docs/rules/no-unnecessary-type-arguments.md new file mode 100644 index 00000000000..35fb14c5468 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-type-arguments.md @@ -0,0 +1,53 @@ +# Enforces that types will not to be used (no-unnecessary-type-arguments) + +Warns if an explicitly specified type argument is the default for that type parameter. + +## Rule Details + +Type parameters in TypeScript may specify a default value. +For example: + +```ts +function f() {} +``` + +It is redundant to provide an explicit type parameter equal to that default. + +Examples of **incorrect** code for this rule: + +```ts +function f() {} +f(); + +function g() {} +g(); + +class C {} +function h(c: C) {} +new C(); +class D extends C {} + +interface I {} +class Impl implements I {} +``` + +Examples of **correct** code for this rule: + +```ts +function f() {} +f(); + +function g() {} +g(); + +class C {} +new C(); +class D extends C {} + +interface I {} +class Impl implements I {} +``` + +## Related to + +- TSLint: [use-default-type-parameter](https://palantir.github.io/tslint/rules/use-default-type-parameter) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 00bdf366600..0c17ccb0426 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -44,6 +44,7 @@ "@typescript-eslint/no-this-alias": "error", "@typescript-eslint/no-type-alias": "error", "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", diff --git a/packages/eslint-plugin/src/rules/indent-new-do-not-use/BinarySearchTree.ts b/packages/eslint-plugin/src/rules/indent-new-do-not-use/BinarySearchTree.ts index e530efb4099..0010750ebfd 100644 --- a/packages/eslint-plugin/src/rules/indent-new-do-not-use/BinarySearchTree.ts +++ b/packages/eslint-plugin/src/rules/indent-new-do-not-use/BinarySearchTree.ts @@ -17,7 +17,7 @@ export interface TreeValue { * can easily be swapped out. */ export class BinarySearchTree { - private rbTree = createTree(); + private rbTree = createTree(); /** * Inserts an entry into the tree. diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 3f10d73426e..db155e691bf 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -58,6 +58,7 @@ import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; +import useDefaultTypeParameter from './no-unnecessary-type-arguments'; export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, @@ -97,6 +98,7 @@ export default { 'no-this-alias': noThisAlias, 'no-type-alias': noTypeAlias, 'no-unnecessary-qualifier': noUnnecessaryQualifier, + 'no-unnecessary-type-arguments': useDefaultTypeParameter, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-unused-vars': noUnusedVars, 'no-use-before-define': noUseBeforeDefine, @@ -113,11 +115,11 @@ export default { 'require-array-sort-compare': requireArraySortCompare, 'require-await': requireAwait, 'restrict-plus-operands': restrictPlusOperands, + semi: semi, 'strict-boolean-expressions': strictBooleanExpressions, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, + typedef: typedef, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, - semi: semi, - typedef: typedef, }; diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts new file mode 100644 index 00000000000..39f506cd9ee --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -0,0 +1,159 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as tsutils from 'tsutils'; +import ts from 'typescript'; +import * as util from '../util'; +import { findFirstResult } from '../util'; + +interface ArgsAndParams { + typeArguments: ts.NodeArray; + typeParameters: readonly ts.TypeParameterDeclaration[]; +} + +type ExtendingClassLikeDeclaration = ts.ClassLikeDeclaration & { + heritageClauses: ts.NodeArray; +}; + +type ParameterCapableTSNode = + | ts.CallExpression + | ts.NewExpression + | ts.TypeReferenceNode + | ts.ExpressionWithTypeArguments; + +type MessageIds = 'unnecessaryTypeParameter'; + +export default util.createRule<[], MessageIds>({ + name: 'no-unnecessary-type-arguments', + meta: { + docs: { + description: + 'Warns if an explicitly specified type argument is the default for that type parameter', + category: 'Best Practices', + recommended: false, + }, + fixable: 'code', + messages: { + unnecessaryTypeParameter: + 'This is the default value for this type parameter, so it can be omitted.', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + function checkTSArgsAndParameters( + esParameters: TSESTree.TSTypeParameterInstantiation, + { typeArguments, typeParameters }: ArgsAndParams, + ): void { + // Just check the last one. Must specify previous type parameters if the last one is specified. + const i = typeArguments.length - 1; + const arg = typeArguments[i]; + const param = typeParameters[i]; + + // TODO: would like checker.areTypesEquivalent. https://github.com/Microsoft/TypeScript/issues/13502 + if ( + param.default === undefined || + param.default.getText() !== arg.getText() + ) { + return; + } + + context.report({ + fix: fixer => + fixer.removeRange( + i === 0 + ? [typeArguments.pos - 1, typeArguments.end + 1] + : [typeArguments[i - 1].end, arg.end], + ), + messageId: 'unnecessaryTypeParameter', + node: esParameters!.params[i], + }); + } + + return { + TSTypeParameterInstantiation(node) { + const parentDeclaration = parserServices.esTreeNodeToTSNodeMap.get( + node.parent!, + ) as ExtendingClassLikeDeclaration | ParameterCapableTSNode; + + const expression = tsutils.isClassLikeDeclaration(parentDeclaration) + ? parentDeclaration.heritageClauses[0].types[0] + : parentDeclaration; + + const argsAndParams = getArgsAndParameters(expression, checker); + if (argsAndParams !== undefined) { + checkTSArgsAndParameters(node, argsAndParams); + } + }, + }; + }, +}); + +function getArgsAndParameters( + node: ParameterCapableTSNode, + checker: ts.TypeChecker, +): ArgsAndParams | undefined { + const typeParameters = getTypeParametersFromNode(node, checker); + return typeParameters === undefined + ? undefined + : { typeArguments: node.typeArguments!, typeParameters }; +} + +function getTypeParametersFromNode( + node: ParameterCapableTSNode, + checker: ts.TypeChecker, +) { + if (ts.isExpressionWithTypeArguments(node)) { + return getTypeParametersFromType(node.expression, checker); + } + + if (ts.isTypeReferenceNode(node)) { + return getTypeParametersFromType(node.typeName, checker); + } + + return getTypeParametersFromCall(node, checker); +} + +function getTypeParametersFromType( + type: ts.EntityName | ts.Expression | ts.ClassDeclaration, + checker: ts.TypeChecker, +): readonly ts.TypeParameterDeclaration[] | undefined { + const sym = getAliasedSymbol(checker.getSymbolAtLocation(type)!, checker); + if (sym === undefined || sym.declarations === undefined) { + return undefined; + } + + return findFirstResult(sym.declarations, decl => + tsutils.isClassLikeDeclaration(decl) || + ts.isTypeAliasDeclaration(decl) || + ts.isInterfaceDeclaration(decl) + ? decl.typeParameters + : undefined, + ); +} + +function getTypeParametersFromCall( + node: ts.CallExpression | ts.NewExpression, + checker: ts.TypeChecker, +): readonly ts.TypeParameterDeclaration[] | undefined { + const sig = checker.getResolvedSignature(node); + const sigDecl = sig === undefined ? undefined : sig.getDeclaration(); + if (sigDecl === undefined) { + return ts.isNewExpression(node) + ? getTypeParametersFromType(node.expression, checker) + : undefined; + } + + return sigDecl.typeParameters; +} + +function getAliasedSymbol( + symbol: ts.Symbol, + checker: ts.TypeChecker, +): ts.Symbol | undefined { + return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) + ? checker.getAliasedSymbol(symbol) + : symbol; +} diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 3c88d766fe9..050438d3843 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -77,6 +77,20 @@ export function arraysAreEqual( ); } +/** Returns the first non-`undefined` result. */ +export function findFirstResult( + inputs: T[], + getResult: (t: T) => U | undefined, +): U | undefined { + for (const element of inputs) { + const result = getResult(element); + if (result !== undefined) { + return result; + } + } + return undefined; +} + /** * Gets a string name representation of the name of the given MethodDefinition * or ClassProperty node, with handling for computed property names. diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts new file mode 100644 index 00000000000..d6ea679f823 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts @@ -0,0 +1,126 @@ +import path from 'path'; +import rule from '../../src/rules/no-unnecessary-type-arguments'; +import { RuleTester } from '../RuleTester'; + +const rootDir = path.join(process.cwd(), 'tests/fixtures'); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-unnecessary-type-arguments', rule, { + valid: [ + `function f() { } + f();`, + `function f() { } + f();`, + `declare const f: any; + f();`, + `declare const f: any; + f();`, + `declare const f: unknown; + f();`, + `declare const f: unknown; + f();`, + `function g() { } + g();`, + `declare const g: any; + g();`, + `declare const g: unknown; + g();`, + `class C { } + new C();`, + `declare const C: any; + new C();`, + `declare const C: unknown; + new C();`, + `class C { } + class D extends C { }`, + `declare const C: any; + class D extends C { }`, + `declare const C: unknown; + class D extends C { }`, + `interface I { } + class Impl implements I { }`, + `class C { } + class D extends C { }`, + `declare const C: any; + class D extends C { }`, + `declare const C: unknown; + class D extends C { }`, + ], + invalid: [ + { + code: `function f() { } + f();`, + errors: [ + { + column: 11, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `function f() { } + f();`, + }, + { + code: `function g() { } + g();`, + errors: [ + { + column: 19, + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `function g() { } + g();`, + }, + { + code: `class C { } + function h(c: C) { }`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `class C { } + function h(c: C) { }`, + }, + { + code: `class C { } + new C();`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `class C { } + new C();`, + }, + { + code: `class C { } + class D extends C { }`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `class C { } + class D extends C { }`, + }, + { + code: `interface I { } + class Impl implements I { }`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `interface I { } + class Impl implements I { }`, + }, + ], +});