Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 81 additions & 18 deletions packages/eslint-plugin/docs/rules/no-inferrable-types.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading