8000 Merge branch 'master' into migrate-plugin-to-ts · macklinu/typescript-eslint@4158deb · GitHub 10000
[go: up one dir, main page]

Skip to content

Commit 4158deb

Browse files
committed
Merge branch 'master' into migrate-plugin-to-ts
# Conflicts: # packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js
2 parents 5f6c1ed + 5e3e285 commit 4158deb

File tree

11 files changed

+1027
-20
lines changed

11 files changed

+1027
-20
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
144144
| [`@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: | |
145145
| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | |
146146
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements (`no-var-requires` from TSLint) | :heavy_check_mark: | |
147+
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures (`callable-types` from TSLint) | | :wrench: |
147148
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | :heavy_check_mark: | :wrench: |
148149
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | :heavy_check_mark: | :wrench: |
149150
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | |

packages/eslint-plugin/ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
| [`arrow-parens`] | 🌟 | [`arrow-parens`][arrow-parens] |
133133
| [`arrow-return-shorthand`] | 🌟 | [`arrow-body-style`][arrow-body-style] |
134134
| [`binary-expression-operand-order`] | 🌟 | [`yoda`][yoda] |
135-
| [`callable-types`] | 🛑 | N/A |
135+
| [`callable-types`] | | [`@typescript-eslint/prefer-function-type`] |
136136
| [`class-name`] || [`@typescript-eslint/class-name-casing`] |
137137
| [`comment-format`] | 🌟 | [`capitalized-comments`][capitalized-comments] & [`spaced-comment`][spaced-comment] |
138138
| [`completed-docs`] | 🔌 | [`eslint-plugin-jsdoc`][plugin:jsdoc] |
@@ -587,6 +587,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
587587
[`@typescript-eslint/member-delimiter-style`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
588588
[`@typescript-eslint/prefer-interface`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-interface.md
589589
[`@typescript-eslint/no-array-constructor`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md
590+
[`@typescript-eslint/prefer-function-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-function-type.md
590591
[`@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
591592

592593
<!-- eslint-plugin-import -->
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Use function types instead of interfaces with call signatures (prefer-function-type)
2+
3+
## Rule Details
4+
5+
This rule suggests using a function type instead of an interface or object type literal with a single call signature.
6+
7+
Examples of **incorrect** code for this rule:
8+
9+
```ts
10+
interface Foo {
11+
(): string;
12+
}
13+
```
14+
15+
```ts
16+
function foo(bar: { (): number }): number {
17+
return bar();
18+
}
19+
```
20+
21+
```ts
22+
interface Foo extends Function {
23+
(): void;
24+
}
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```ts
30+
interface Foo {
31+
(): void;
32+
bar: number;
33+
}
34+
```
35+
36+
```ts
37+
function foo(bar: { (): string; baz: number }): string {
38+
return bar();
39+
}
40+
```
41+
42+
```ts
43+
interface Foo {
44+
bar: string;
45+
}
46+
interface Bar extends Foo {
47+
(): void;
48+
}
49+
```
50+
51+
## When Not To Use It
52+
53+
If you specifically want to use an interface or type literal with a single call signature for stylistic reasons, you can disable this rule.
54+
55+
## Further Reading
56+
57+
- TSLint: [`callable-types`](https://palantir.github.io/tslint/rules/callable-types/)

packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,12 @@ export default util.createRule<Options, MessageIds>({
4747
},
4848
defaultOptions: [{}],
4949
create(context, [options]) {
50+
const sourceCode = context.getSourceCode();
5051
const parserServices = util.getParserServices(context);
5152

5253
/**
5354
* Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type.
5455
* So, in addition, check if there are integer properties 0..n and no other numeric keys
55-
* @param type type
56-
* @returns true if type could be a tuple type
5756
*/
5857
function couldBeTupleType(type: ts.ObjectType): boolean {
5958
const properties = type.getProperties();
@@ -82,17 +81,10 @@ export default util.createRule<Options, MessageIds>({
8281
return true;
8382
}
8483

85-
/**
86-
* @param node node being linted
87-
* @param checker TypeScript typechecker
88-
*/
8984
function checkNonNullAssertion(
9085
node: TSESTree.Node,
9186
checker: ts.TypeChecker
9287
): void {
93-
/**
94-
* Corresponding TSNode is guaranteed to be in map
95-
*/
9688
const originalNode = parserServices.esTreeNodeToTSNodeMap.get<
9789
ts.NonNullExpression
9890
>(node);
@@ -112,20 +104,23 @@ export default util.createRule<Options, MessageIds>({
112104
}
113105
}
114106

115-
/**
116-
* @param node node being linted
117-
* @param checker TypeScript typechecker
118-
*/
119-
function verifyCast(node: TSESTree.Node, checker: ts.TypeChecker): void {
120-
const originalNode = parserServices.esTreeNodeToTSNodeMap.get<
121-
ts.AssertionExpression
122-
>(node);
107+
function verifyCast(
108+
node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression,
109+
checker: ts.TypeChecker
110+
): void {
123111
if (
112+
options &&
124113
options.typesToIgnore &&
125-
options.typesToIgnore.indexOf(originalNode.type.getText()) !== -1
114+
options.typesToIgnore.indexOf(
115+
sourceCode.getText(node.typeAnnotation)
116+
) !== -1
126117
) {
127118
return;
128119
}
120+
121+
const originalNode = parserServices.esTreeNodeToTSNodeMap.get<
122+
ts.AssertionExpression
123+
>(node);
129124
const castType = checker.getTypeAtLocation(originalNode);
130125

131126
if (
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* @fileoverview Use function types instead of interfaces with call signatures
3+
* @author Benjamin Lichtman
4+
*/
5+
6+
import {
7+
AST_NODE_TYPES,
8+
TSESTree,
9+
AST_TOKEN_TYPES
10+
} from '@typescript-eslint/typescript-estree';
11+
import * as util from '../util';
12+
13+
export default util.createRule({
14+
name: 'prefer-function-type',
15+
meta: {
16+
docs: {
17+
description:
18+
'Use function types instead of interfaces with call signatures',
19+
category: 'Best Practices',
20+
recommended: false,
21+
tslintName: 'callable-types'
22+
},
23+
fixable: 'code',
24+
messages: {
25+
functionTypeOverCallableType:
26+
"{{ type }} has only a call signature - use '{{ sigSuggestion }}' instead."
27+
},
28+
schema: [],
29+
type: 'suggestion'
30+
},
31+
defaultOptions: [],
32+
create(context) {
33+
const sourceCode = context.getSourceCode();
34+
35+
/**
36+
* Checks if there the interface has exactly one supertype that isn't named 'Function'
37+
* @param node The node being checked
38+
*/
39+
function hasOneSupertype(node: TSESTree.TSInterfaceDeclaration): boolean {
40+
if (!node.extends || node.extends.length === 0) {
41+
return false;
42+
}
43+
if (node.extends.length !== 1) {
44+
return true;
45+
}
46+
const expr = node.extends[0].expression;
47+
48+
return (
49+
expr.type !== AST_NODE_TYPES.Identifier || expr.name !== 'Function'
50+
);
51+
}
52+
53+
/**
54+
* @param parent The parent of the call signature causing the diagnostic
55+
*/
56+
function shouldWrapSuggestion(parent: TSESTree.Node | undefined): boolean {
57+
if (!parent) {
58+
return false;
59+
}
60+
61+
switch (parent.type) {
62+
case AST_NODE_TYPES.TSUnionType:
63+
case AST_NODE_TYPES.TSIntersectionType:
64+
case AST_NODE_TYPES.TSArrayType:
65+
return true;
66+
default:
67+
return false;
68+
}
69+
}
70+
71+
/**
72+
* @param call The call signature causing the diagnostic
73+
* @param parent The parent of the call
74+
* @returns The suggestion to report
75+
*/
76+
function renderSuggestion(
77+
call:
78+
| TSESTree.TSCallSignatureDeclaration
79+
| TSESTree.TSConstructSignatureDeclaration,
80+
parent: TSESTree.Node
81+
) {
82+
const start = call.range[0];
83+
const colonPos = call.returnType!.range[0] - start;
84+
const text = sourceCode.getText().slice(start, call.range[1]);
85+
86+
let suggestion = `${text.slice(0, colonPos)} =>${text.slice(
87+
colonPos + 1
88+
)}`;
89+
90+
if (shouldWrapSuggestion(parent.parent)) {
91+
suggestion = `(${suggestion})`;
92+
}
93+
if (parent.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
94+
if (typeof parent.typeParameters !== 'undefined') {
95+
return `type ${sourceCode
96+
.getText()
97+
.slice(
98+
parent.id.range[0],
99+
parent.typeParameters.range[1]
100+
)} = ${suggestion}`;
101+
}
102+
return `type ${parent.id.name} = ${suggestion}`;
103+
}
104+
return suggestion.endsWith(';') ? suggestion.slice(0, -1) : suggestion;
105+
}
106+
107+
/**
108+
* @param member The TypeElement being checked
109+
* @param node The parent of member being checked
110+
*/
111+
function checkMember(member: TSESTree.TypeElement, node: TSESTree.Node) {
112+
if (
113+
(member.type === AST_NODE_TYPES.TSCallSignatureDeclaration ||
114+
member.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
115+
typeof member.returnType !== 'undefined'
116+
) {
117+
const suggestion = renderSuggestion(member, node);
118+
const fixStart =
119+
node.type === AST_NODE_TYPES.TSTypeLiteral
120+
? node.range[0]
121+
: sourceCode
122+
.getTokens(node)
123+
.filter(
124+
token =>
125+
token.type === AST_TOKEN_TYPES.Keyword &&
126+
token.value === 'interface'
127+
)[0].range[0];
128+
129+
context.report({
130+
node: member,
131+
messageId: 'functionTypeOverCallableType',
132+
data: {
133+
type:
134+
node.type === AST_NODE_TYPES.TSTypeLiteral
135+
? 'Type literal'
136+
: 'Interface',
137+
sigSuggestion: suggestion
138+
},
139+
fix(fixer) {
140+
return fixer.replaceTextRange(
141+
[fixStart, node.range[1]],
142+
suggestion
143+
);
144+
}
145+
});
146+
}
147+
}
148+
149+
return {
150+
TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration) {
151+
if (!hasOneSupertype(node) && node.body.body.length === 1) {
152+
checkMember(node.body.body[0], node);
153+
}
154+
},
155+
'TSTypeLiteral[members.length = 1]'(node: TSESTree.TSTypeLiteral) {
156+
checkMember(node.members[0], node);
157+
}
158+
};
159+
}
160+
});

packages/eslint-plugin/tests/rules/no-unnecessary-type-assertion.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ type Foo = number;
3737
const foo = (3 + 5) as Foo;`,
3838
options: [{ typesToIgnore: ['Foo'] }]
3939
},
40+
{
41+
code: `const foo = (3 + 5) as any;`,
42+
options: [{ typesToIgnore: ['any'] }]
43+
},
44+
{
45+
code: `((Syntax as any).ArrayExpression = 'foo')`,
46+
options: [{ typesToIgnore: ['any'] }]
47+
},
48+
{
49+
code: `const foo = (3 + 5) as string;`,
50+
options: [{ typesToIgnore: ['string'] }]
51+
},
4052
{
4153
code: `
4254
type Foo = number;

0 commit comments

Comments
 (0)
0