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): no-inferrable-types: Support more primitives #442

Merged
merged 4 commits into from May 9, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
99 changes: 81 additions & 18 deletions packages/eslint-plugin/docs/rules/no-inferrable-types.md
Expand Up @@ -9,23 +9,59 @@ and properties where the type can be easily inferred from its value.

## Options

This rule has an options object:
This rule accepts the following options:

```json
{
"ignoreProperties": false,
"ignoreParameters": false
```ts
interface Options {
ignoreParameters?: boolean;
ignoreProperties?: boolean;
}
```

### Default

When none of the options are truthy, the following patterns are valid:
The default options are:

```JSON
{
"ignoreParameters": true,
"ignoreProperties": true,
}
```

With these options, the following patterns are valid:

```ts
const foo = 5;
const bar = true;
const baz = 'str';
const a = 10n;
const a = -10n;
const a = BigInt(10);
const a = -BigInt(10);
const a = false;
const a = true;
const a = Boolean(null);
const a = !0;
const a = 10;
const a = +10;
const a = -10;
const a = Number('1');
const a = +Number('1');
const a = -Number('1');
const a = Infinity;
const a = +Infinity;
const a = -Infinity;
const a = NaN;
const a = +NaN;
const a = -NaN;
const a = null;
const a = /a/;
const a = RegExp('a');
const a = new RegExp('a');
const a = 'str';
const a = `str`;
const a = String(1);
const a = Symbol('a');
const a = undefined;
const a = void someValue;

class Foo {
prop = 5;
Expand All @@ -39,9 +75,36 @@ function fn(a: number, b: boolean, c: string) {}
The following are invalid:

```ts
const foo: number = 5;
const bar: boolean = true;
const baz: string = 'str';
const a: bigint = 10n;
const a: bigint = -10n;
const a: bigint = BigInt(10);
const a: bigint = -BigInt(10);
const a: boolean = false;
const a: boolean = true;
const a: boolean = Boolean(null);
const a: boolean = !0;
const a: number = 10;
const a: number = +10;
const a: number = -10;
const a: number = Number('1');
const a: number = +Number('1');
const a: number = -Number('1');
const a: number = Infinity;
const a: number = +Infinity;
const a: number = -Infinity;
const a: number = NaN;
const a: number = +NaN;
const a: number = -NaN;
const a: null = null;
const a: RegExp = /a/;
const a: RegExp = RegExp('a');
const a: RegExp = new RegExp('a');
const a: string = 'str';
const a: string = `str`;
const a: string = String(1);
const a: symbol = Symbol('a');
const a: undefined = undefined;
const a: undefined = void someValue;

class Foo {
prop: number = 5;
Expand All @@ -50,23 +113,23 @@ class Foo {
function fn(a: number = 5, b: boolean = true) {}
```

### `ignoreProperties`
### `ignoreParameters`

When set to true, the following pattern is considered valid:

```ts
class Foo {
prop: number = 5;
function foo(a: number = 5, b: boolean = true) {
// ...
}
```

### `ignoreParameters`
### `ignoreProperties`

When set to true, the following pattern is considered valid:

```ts
function foo(a: number = 5, b: boolean = true) {
// ...
class Foo {
prop: number = 5;
}
```

Expand Down
167 changes: 116 additions & 51 deletions packages/eslint-plugin/src/rules/no-inferrable-types.ts
Expand Up @@ -47,60 +47,135 @@ export default util.createRule<Options, MessageIds>({
},
],
create(context, [{ ignoreParameters, ignoreProperties }]) {
function isFunctionCall(init: TSESTree.Expression, callName: string) {
return (
init.type === AST_NODE_TYPES.CallExpression &&
init.callee.type === AST_NODE_TYPES.Identifier &&
init.callee.name === callName
);
}
function isLiteral(init: TSESTree.Expression, typeName: string) {
return (
init.type === AST_NODE_TYPES.Literal && typeof init.value === typeName
);
}
function isIdentifier(init: TSESTree.Expression, ...names: string[]) {
return (
init.type === AST_NODE_TYPES.Identifier && names.includes(init.name)
);
}
function hasUnaryPrefix(
init: TSESTree.Expression,
...operators: string[]
): init is TSESTree.UnaryExpression {
return (
init.type === AST_NODE_TYPES.UnaryExpression &&
operators.includes(init.operator)
);
}

type Keywords =
| TSESTree.TSBigIntKeyword
| TSESTree.TSBooleanKeyword
| TSESTree.TSNumberKeyword
| TSESTree.TSNullKeyword
| TSESTree.TSStringKeyword
| TSESTree.TSSymbolKeyword
| TSESTree.TSUndefinedKeyword
| TSESTree.TSTypeReference;
const keywordMap = {
[AST_NODE_TYPES.TSBigIntKeyword]: 'bigint',
[AST_NODE_TYPES.TSBooleanKeyword]: 'boolean',
[AST_NODE_TYPES.TSNumberKeyword]: 'number',
[AST_NODE_TYPES.TSNullKeyword]: 'null',
[AST_NODE_TYPES.TSStringKeyword]: 'string',
[AST_NODE_TYPES.TSSymbolKeyword]: 'symbol',
[AST_NODE_TYPES.TSUndefinedKeyword]: 'undefined',
};

/**
* Returns whether a node has an inferrable value or not
* @param node the node to check
* @param init the initializer
*/
function isInferrable(
node: TSESTree.TSTypeAnnotation,
annotation: TSESTree.TypeNode,
init: TSESTree.Expression,
): boolean {
if (
node.type !== AST_NODE_TYPES.TSTypeAnnotation ||
!node.typeAnnotation
) {
return false;
}
): annotation is Keywords {
switch (annotation.type) {
case AST_NODE_TYPES.TSBigIntKeyword: {
// note that bigint cannot have + prefixed to it
const unwrappedInit = hasUnaryPrefix(init, '-')
? init.argument
: init;

return (
isFunctionCall(unwrappedInit, 'BigInt') ||
unwrappedInit.type === AST_NODE_TYPES.BigIntLiteral
);
}

case AST_NODE_TYPES.TSBooleanKeyword:
return (
hasUnaryPrefix(init, '!') ||
isFunctionCall(init, 'Boolean') ||
isLiteral(init, 'boolean')
);

const annotation = node.typeAnnotation;
case AST_NODE_TYPES.TSNumberKeyword: {
const unwrappedInit = hasUnaryPrefix(init, '+', '-')
? init.argument
: init;

if (annotation.type === AST_NODE_TYPES.TSStringKeyword) {
if (init.type === AST_NODE_TYPES.Literal) {
return typeof init.value === 'string';
return (
isIdentifier(unwrappedInit, 'Infinity', 'NaN') ||
isFunctionCall(unwrappedInit, 'Number') ||
isLiteral(unwrappedInit, 'number')
);
}
return false;
}

if (annotation.type === AST_NODE_TYPES.TSBooleanKeyword) {
return init.type === AST_NODE_TYPES.Literal;
}
case AST_NODE_TYPES.TSNullKeyword:
return init.type === AST_NODE_TYPES.Literal && init.value === null;

case AST_NODE_TYPES.TSStringKeyword:
return (
isFunctionCall(init, 'String') ||
isLiteral(init, 'string') ||
init.type === AST_NODE_TYPES.TemplateLiteral
);

if (annotation.type === AST_NODE_TYPES.TSNumberKeyword) {
// Infinity is special
if (
(init.type === AST_NODE_TYPES.UnaryExpression &&
init.operator === '-' &&
init.argument.type === AST_NODE_TYPES.Identifier &&
init.argument.name === 'Infinity') ||
(init.type === AST_NODE_TYPES.Identifier && init.name === 'Infinity')
) {
return true;
case AST_NODE_TYPES.TSSymbolKeyword:
return isFunctionCall(init, 'Symbol');

case AST_NODE_TYPES.TSTypeReference: {
if (
annotation.typeName.type === AST_NODE_TYPES.Identifier &&
annotation.typeName.name === 'RegExp'
) {
const isRegExpLiteral =
init.type === AST_NODE_TYPES.Literal &&
init.value instanceof RegExp;
const isRegExpNewCall =
init.type === AST_NODE_TYPES.NewExpression &&
init.callee.type === 'Identifier' &&
init.callee.name === 'RegExp';
const isRegExpCall = isFunctionCall(init, 'RegExp');

return isRegExpLiteral || isRegExpCall || isRegExpNewCall;
}

return false;
}

return (
init.type === AST_NODE_TYPES.Literal && typeof init.value === 'number'
);
case AST_NODE_TYPES.TSUndefinedKeyword:
return (
hasUnaryPrefix(init, 'void') || isIdentifier(init, 'undefined')
);
}

return false;
}

/**
* Reports an inferrable type declaration, if any
* @param node the node being visited
* @param typeNode the type annotation node
* @param initNode the initializer node
*/
function reportInferrableType(
node:
Expand All @@ -114,25 +189,15 @@ export default util.createRule<Options, MessageIds>({
return;
}

if (!isInferrable(typeNode, initNode)) {
if (!isInferrable(typeNode.typeAnnotation, initNode)) {
return;
}

let type = null;
if (typeNode.typeAnnotation.type === AST_NODE_TYPES.TSBooleanKeyword) {
type = 'boolean';
} else if (
typeNode.typeAnnotation.type === AST_NODE_TYPES.TSNumberKeyword
) {
type = 'number';
} else if (
typeNode.typeAnnotation.type === AST_NODE_TYPES.TSStringKeyword
) {
type = 'string';
} else {
// shouldn't happen...
return;
}
const type =
typeNode.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference
? // TODO - if we add more references
'RegExp'
: keywordMap[typeNode.typeAnnotation.type];

context.report({
node,
Expand Down