Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(eslint-plugin): added new rule use-default-type-parameter #562

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -161,6 +161,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 @@ -98,7 +98,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 @@ -614,6 +614,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
[`@typescript-eslint/prefer-readonly`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly.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 @@ -45,6 +45,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
4 changes: 3 additions & 1 deletion packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -59,6 +59,7 @@ import tripleSlashReference from './triple-slash-reference';
import typeAnnotationSpacing from './type-annotation-spacing';
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 @@ -100,6 +101,7 @@ export default {
'no-triple-slash-reference': noTripleSlashReference,
'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 @@ -116,10 +118,10 @@ export default {
'promise-function-async': promiseFunctionAsync,
'require-array-sort-compare': requireArraySortCompare,
'restrict-plus-operands': restrictPlusOperands,
semi: semi,
'strict-boolean-expressions': strictBooleanExpressions,
'triple-slash-reference': tripleSlashReference,
'type-annotation-spacing': typeAnnotationSpacing,
'unbound-method': unboundMethod,
'unified-signatures': unifiedSignatures,
semi: semi,
};
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 @@ -84,6 +84,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