Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): added new rule use-default-type-parameter (#562)
  • Loading branch information
Josh Goldberg authored and JamesHenry committed Jul 25, 2019
1 parent 55e788c commit 2b942ba
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: | | |
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin/ROADMAP.md
Expand Up @@ -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] |

<sup>[1]</sup> The ESLint rule also supports silencing with an extra set of parens (`if ((foo = bar)) {}`)<br>
Expand Down Expand Up @@ -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

Expand Down
53 changes: 53 additions & 0 deletions 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<T = number>() {}
```

It is redundant to provide an explicit type parameter equal to that default.

Examples of **incorrect** code for this rule:

```ts
function f<T = number>() {}
f<number>();

function g<T = number, U = string>() {}
g<string, string>();

class C<T = number> {}
function h(c: C<number>) {}
new C<number>();
class D extends C<number> {}

interface I<T = number> {}
class Impl implements I<number> {}
```

Examples of **correct** code for this rule:

```ts
function f<T = number>() {}
f<string>();

function g<T = number, U = string>() {}
g<number, number>();

class C<T = number> {}
new C<string>();
class D extends C<string> {}

interface I<T = number> {}
class Impl implements I<string> {}
```

## Related to

- TSLint: [use-default-type-parameter](https://palantir.github.io/tslint/rules/use-default-type-parameter)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -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",
Expand Down
Expand Up @@ -17,7 +17,7 @@ export interface TreeValue {
* can easily be swapped out.
*/
export class BinarySearchTree {
private rbTree = createTree<TreeValue, number>();
private rbTree = createTree<TreeValue>();

/**
* Inserts an entry into the tree.
Expand Down
6 changes: 4 additions & 2 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
};
159 changes: 159 additions & 0 deletions 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<ts.TypeNode>;
typeParameters: readonly ts.TypeParameterDeclaration[];
}

type ExtendingClassLikeDeclaration = ts.ClassLikeDeclaration & {
heritageClauses: ts.NodeArray<ts.HeritageClause>;
};

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;
}
14 changes: 14 additions & 0 deletions packages/eslint-plugin/src/util/misc.ts
Expand Up @@ -77,6 +77,20 @@ export function arraysAreEqual<T>(
);
}

/** Returns the first non-`undefined` result. */
export function findFirstResult<T, U>(
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.
Expand Down

0 comments on commit 2b942ba

Please sign in to comment.