diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index 3ccca4f74a04..d3f14037d537 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -4,15 +4,6 @@ import * as 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 @@ -43,65 +34,52 @@ export default util.createRule<[], MessageIds>({ create(context) { const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); + const sourceCode = context.getSourceCode(); function checkTSArgsAndParameters( esParameters: TSESTree.TSTypeParameterInstantiation, - { typeArguments, typeParameters }: ArgsAndParams, + typeParameters: readonly ts.TypeParameterDeclaration[], ): 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 i = esParameters.params.length - 1; + const arg = esParameters.params[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() + !param.default || + param.default.getText() !== sourceCode.getText(arg) ) { return; } context.report({ + node: arg, + messageId: 'unnecessaryTypeParameter', fix: fixer => fixer.removeRange( i === 0 - ? [typeArguments.pos - 1, typeArguments.end + 1] - : [typeArguments[i - 1].end, arg.end], + ? esParameters.range + : [esParameters.params[i - 1].range[1], arg.range[1]], ), - messageId: 'unnecessaryTypeParameter', - node: esParameters.params[i], }); } return { TSTypeParameterInstantiation(node): void { - const parentDeclaration = parserServices.esTreeNodeToTSNodeMap.get< - ExtendingClassLikeDeclaration | ParameterCapableTSNode - >(node.parent!); - - const expression = tsutils.isClassLikeDeclaration(parentDeclaration) - ? parentDeclaration.heritageClauses[0].types[0] - : parentDeclaration; + const expression = parserServices.esTreeNodeToTSNodeMap.get< + ParameterCapableTSNode + >(node); - const argsAndParams = getArgsAndParameters(expression, checker); - if (argsAndParams !== undefined) { - checkTSArgsAndParameters(node, argsAndParams); + const typeParameters = getTypeParametersFromNode(expression, checker); + if (typeParameters) { + checkTSArgsAndParameters(node, typeParameters); } }, }; }, }); -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, @@ -126,14 +104,11 @@ function getTypeParametersFromType( checker: ts.TypeChecker, ): readonly ts.TypeParameterDeclaration[] | undefined { const symAtLocation = checker.getSymbolAtLocation(type); - if (symAtLocation === undefined) { + if (!symAtLocation) { return undefined; } const sym = getAliasedSymbol(symAtLocation, checker); - if (sym === undefined || sym.declarations === undefined) { - return undefined; - } return findFirstResult(sym.declarations, decl => tsutils.isClassLikeDeclaration(decl) || @@ -149,8 +124,8 @@ function getTypeParametersFromCall( checker: ts.TypeChecker, ): readonly ts.TypeParameterDeclaration[] | undefined { const sig = checker.getResolvedSignature(node); - const sigDecl = sig === undefined ? undefined : sig.getDeclaration(); - if (sigDecl === undefined) { + const sigDecl = sig?.getDeclaration(); + if (!sigDecl) { return ts.isNewExpression(node) ? getTypeParametersFromType(node.expression, checker) : undefined; @@ -162,7 +137,7 @@ function getTypeParametersFromCall( function getAliasedSymbol( symbol: ts.Symbol, checker: ts.TypeChecker, -): ts.Symbol | undefined { +): ts.Symbol { return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol; 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 index b22d123cbcb5..df79528403df 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-type-arguments.test.ts @@ -14,6 +14,12 @@ const ruleTester = new RuleTester({ ruleTester.run('no-unnecessary-type-arguments', rule, { valid: [ + `f<>();`, + `f();`, + `class Foo extends Bar<> {}`, + `class Foo extends Bar {}`, + `class Foo implements Bar<> {}`, + `class Foo implements Bar {}`, `function f() { } f();`, `function f() { } @@ -60,6 +66,14 @@ ruleTester.run('no-unnecessary-type-arguments', rule, { `class Foo {} const foo = new Foo();`, `type Foo = import('foo').Foo;`, + `class Bar {} + class Foo extends Bar {}`, + `interface Bar {} + class Foo implements Bar {}`, + `class Bar {} + class Foo extends Bar {}`, + `interface Bar {} + class Foo implements Bar {}`, ], invalid: [ { @@ -141,5 +155,27 @@ ruleTester.run('no-unnecessary-type-arguments', rule, { output: `class Foo {} const foo = new Foo();`, }, + { + code: `interface Bar {} + class Foo implements Bar {}`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `interface Bar {} + class Foo implements Bar {}`, + }, + { + code: `class Bar {} + class Foo extends Bar {}`, + errors: [ + { + messageId: 'unnecessaryTypeParameter', + }, + ], + output: `class Bar {} + class Foo extends Bar {}`, + }, ], }); diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index f65ac54867aa..4a5d4accb408 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -300,20 +300,21 @@ export class Converter { /** * Converts a ts.Node's typeArguments to TSTypeParameterInstantiation node - * @param typeArguments ts.Node typeArguments + * @param typeArguments ts.NodeArray typeArguments + * @param node parent used to create this node * @returns TypeParameterInstantiation node */ private convertTypeArgumentsToTypeParameters( typeArguments: ts.NodeArray, + node: ts.Node, ): TSESTree.TSTypeParameterInstantiation { const greaterThanToken = findNextToken(typeArguments, this.ast, this.ast)!; - return { + return this.createNode(node, { type: AST_NODE_TYPES.TSTypeParameterInstantiation, range: [typeArguments.pos - 1, greaterThanToken.end], - loc: getLocFor(typeArguments.pos - 1, greaterThanToken.end, this.ast), params: typeArguments.map(typeArgument => this.convertType(typeArgument)), - }; + }); } /** @@ -386,7 +387,7 @@ export class Converter { if ('typeArguments' in node) { result.typeParameters = node.typeArguments && 'pos' in node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters(node.typeArguments, node) : null; } if ('typeParameters' in node) { @@ -1296,7 +1297,10 @@ export class Converter { return this.createNode(node, { type: AST_NODE_TYPES.TaggedTemplateExpression, typeParameters: node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters( + node.typeArguments, + node, + ) : undefined, tag: this.convertChild(node.tag), quasi: this.convertChild(node.template), @@ -1440,6 +1444,7 @@ export class Converter { if (superClass.types[0] && superClass.types[0].typeArguments) { result.superTypeParameters = this.convertTypeArgumentsToTypeParameters( superClass.types[0].typeArguments, + superClass.types[0], ); } } @@ -1781,6 +1786,7 @@ export class Converter { if (node.typeArguments) { result.typeParameters = this.convertTypeArgumentsToTypeParameters( node.typeArguments, + node, ); } return result; @@ -1798,6 +1804,7 @@ export class Converter { if (node.typeArguments) { result.typeParameters = this.convertTypeArgumentsToTypeParameters( node.typeArguments, + node, ); } return result; @@ -1958,7 +1965,10 @@ export class Converter { openingElement: this.createNode(node, { type: AST_NODE_TYPES.JSXOpeningElement, typeParameters: node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters( + node.typeArguments, + node, + ) : undefined, selfClosing: true, name: this.convertJSXTagName(node.tagName, node), @@ -1976,7 +1986,10 @@ export class Converter { return this.createNode(node, { type: AST_NODE_TYPES.JSXOpeningElement, typeParameters: node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters( + node.typeArguments, + node, + ) : undefined, selfClosing: false, name: this.convertJSXTagName(node.tagName, node), @@ -2081,7 +2094,10 @@ export class Converter { type: AST_NODE_TYPES.TSTypeReference, typeName: this.convertType(node.typeName), typeParameters: node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters( + node.typeArguments, + node, + ) : undefined, }); } @@ -2362,6 +2378,7 @@ export class Converter { if (node.typeArguments) { result.typeParameters = this.convertTypeArgumentsToTypeParameters( node.typeArguments, + node, ); } return result; @@ -2454,7 +2471,10 @@ export class Converter { parameter: this.convertChild(node.argument), qualifier: this.convertChild(node.qualifier), typeParameters: node.typeArguments - ? this.convertTypeArgumentsToTypeParameters(node.typeArguments) + ? this.convertTypeArgumentsToTypeParameters( + node.typeArguments, + node, + ) : null, });