diff --git a/packages/eslint-plugin/docs/rules/no-inferrable-types.md b/packages/eslint-plugin/docs/rules/no-inferrable-types.md index be19fc439d6..9dbabe276c3 100644 --- a/packages/eslint-plugin/docs/rules/no-inferrable-types.md +++ b/packages/eslint-plugin/docs/rules/no-inferrable-types.md @@ -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; @@ -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; @@ -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; } ``` diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts index 94b8533108d..9ee66b44564 100644 --- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts +++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts @@ -47,50 +47,128 @@ export default util.createRule({ }, ], 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; @@ -98,9 +176,6 @@ export default util.createRule({ /** * 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: @@ -114,25 +189,15 @@ export default util.createRule({ 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, diff --git a/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts b/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts index a852f943cb5..007fe8042d9 100644 --- a/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts +++ b/packages/eslint-plugin/tests/rules/no-inferrable-types.test.ts @@ -1,5 +1,84 @@ import rule from '../../src/rules/no-inferrable-types'; -import { RuleTester } from '../RuleTester'; +import { RuleTester, InvalidTestCase } from '../RuleTester'; +import { + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, +} from '../../src/util'; + +type MessageIds = InferMessageIdsTypeFromRule; +type Options = InferOptionsTypeFromRule; + +function flatten(arr: T[][]): T[] { + return arr.reduce((acc, a) => acc.concat(a), []); +} +const testCases = [ + { + type: 'bigint', + code: ['10n', '-10n', 'BigInt(10)', '-BigInt(10)'], + }, + { + type: 'boolean', + code: ['false', 'true', 'Boolean(null)', '!0'], + }, + { + type: 'number', + code: [ + '10', + '+10', + '-10', + 'Number("1")', + '+Number("1")', + '-Number("1")', + 'Infinity', + '+Infinity', + '-Infinity', + 'NaN', + '+NaN', + '-NaN', + ], + }, + { + type: 'null', + code: ['null'], + }, + { + type: 'RegExp', + code: ['/a/', 'RegExp("a")', 'new RegExp("a")'], + }, + { + type: 'string', + code: ['"str"', "'str'", '`str`', 'String(1)'], + }, + { + type: 'symbol', + code: ['Symbol("a")'], + }, + { + type: 'undefined', + code: ['undefined', 'void someValue'], + }, +]; +const validTestCases = flatten( + testCases.map(c => c.code.map(code => `const a = ${code}`)), +); +const invalidTestCases: InvalidTestCase[] = flatten( + testCases.map(cas => + cas.code.map(code => ({ + code: `const a: ${cas.type} = ${code}`, + output: `const a = ${code}`, + errors: [ + { + messageId: 'noInferrableType', + data: { + type: cas.type, + }, + line: 1, + column: 7, + }, + ], + })), + ), +); const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', @@ -7,9 +86,7 @@ const ruleTester = new RuleTester({ ruleTester.run('no-inferrable-types', rule, { valid: [ - 'const a = 5', - 'const a = true', - "const a = 'foo'", + ...validTestCases, "const fn = (a = 5, b = true, c = 'foo') => {}", "const fn = function(a = 5, b = true, c = 'foo') {}", @@ -45,62 +122,8 @@ ruleTester.run('no-inferrable-types', rule, { ], invalid: [ - { - code: 'const a: number = 5', - output: 'const a = 5', - errors: [ - { - messageId: 'noInferrableType', - data: { - type: 'number', - }, - line: 1, - column: 7, - }, - ], - }, - { - code: 'const a: number = Infinity', - output: 'const a = Infinity', - errors: [ - { - messageId: 'noInferrableType', - data: { - type: 'number', - }, - line: 1, - column: 7, - }, - ], - }, - { - code: 'const a: boolean = true', - output: 'const a = true', - errors: [ - { - messageId: 'noInferrableType', - data: { - type: 'boolean', - }, - line: 1, - column: 7, - }, - ], - }, - { - code: "const a: string = 'foo'", - output: "const a = 'foo'", - errors: [ - { - messageId: 'noInferrableType', - data: { - type: 'string', - }, - line: 1, - column: 7, - }, - ], - }, + ...invalidTestCases, + { code: "const fn = (a: number = 5, b: boolean = true, c: string = 'foo') => {}", diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index e35384b6081..da1523fabc5 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -288,6 +288,7 @@ export type Expression = | JSXOpeningFragment | JSXSpreadChild | LogicalExpression + | NewExpression | RestElement | SequenceExpression | SpreadElement