Skip to content

Commit

Permalink
feat(eslint-plugin): no-inferrable-types: Support more primitives (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed May 9, 2019
1 parent 4cd5590 commit 4e193ca
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 129 deletions.
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

0 comments on commit 4e193ca

Please sign in to comment.