diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 0428cff43fc..c4507267c6f 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -183,6 +183,7 @@ In these cases, we create what we call an extension rule; a rule within our plug | [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | +| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | | | [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | | diff --git a/packages/eslint-plugin/docs/rules/init-declarations.md b/packages/eslint-plugin/docs/rules/init-declarations.md new file mode 100644 index 00000000000..8888e2efef2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/init-declarations.md @@ -0,0 +1,22 @@ +# require or disallow initialization in variable declarations (`init-declarations`) + +## Rule Details + +This rule extends the base [`eslint/init-declarations`](https://eslint.org/docs/rules/init-declarations) rule. +It adds support for TypeScript's `declare` variables. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "init-declarations": "off", + "@typescript-eslint/init-declarations": ["error"] +} +``` + +## Options + +See [`eslint/init-declarations` options](https://eslint.org/docs/rules/init-declarations#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/init-declarations.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index ec65f143c59..99e6060d670 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -22,6 +22,8 @@ "@typescript-eslint/func-call-spacing": "error", "indent": "off", "@typescript-eslint/indent": "error", + "init-declarations": "off", + "@typescript-eslint/init-declarations": "error", "keyword-spacing": "off", "@typescript-eslint/keyword-spacing": "error", "@typescript-eslint/member-delimiter-style": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 63c402297e3..cbc3b8c68f9 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -87,6 +87,7 @@ import requireAwait from './require-await'; import restrictPlusOperands from './restrict-plus-operands'; import restrictTemplateExpressions from './restrict-template-expressions'; import returnAwait from './return-await'; +import initDeclarations from './init-declarations'; import semi from './semi'; import spaceBeforeFunctionParen from './space-before-function-paren'; import strictBooleanExpressions from './strict-boolean-expressions'; @@ -118,6 +119,7 @@ export default { 'func-call-spacing': funcCallSpacing, 'generic-type-naming': genericTypeNaming, indent: indent, + 'init-declarations': initDeclarations, 'interface-name-prefix': interfaceNamePrefix, 'keyword-spacing': keywordSpacing, 'member-delimiter-style': memberDelimiterStyle, diff --git a/packages/eslint-plugin/src/rules/init-declarations.ts b/packages/eslint-plugin/src/rules/init-declarations.ts new file mode 100644 index 00000000000..b4368527e0c --- /dev/null +++ b/packages/eslint-plugin/src/rules/init-declarations.ts @@ -0,0 +1,53 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/init-declarations'; +import { + InferOptionsTypeFromRule, + InferMessageIdsTypeFromRule, + createRule, +} from '../util'; + +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; + +export default createRule({ + name: 'init-declarations', + meta: { + type: 'suggestion', + docs: { + description: + 'require or disallow initialization in variable declarations', + category: 'Variables', + recommended: false, + extendsBaseRule: true, + }, + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: ['always'], + create(context) { + const rules = baseRule.create(context); + const mode = context.options[0] || 'always'; + + return { + 'VariableDeclaration:exit'(node: TSESTree.VariableDeclaration): void { + if (mode === 'always') { + if (node.declare) { + return; + } + if ( + node.parent?.type === AST_NODE_TYPES.TSModuleBlock && + node.parent.parent?.type === AST_NODE_TYPES.TSModuleDeclaration && + node.parent.parent?.declare + ) { + return; + } + } + + rules['VariableDeclaration:exit'](node); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/init-declarations.test.ts b/packages/eslint-plugin/tests/rules/init-declarations.test.ts new file mode 100644 index 00000000000..6f4c8f75ebb --- /dev/null +++ b/packages/eslint-plugin/tests/rules/init-declarations.test.ts @@ -0,0 +1,728 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; +import rule from '../../src/rules/init-declarations'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('init-declarations', rule, { + valid: [ + // checking compatibility with base rule + 'var foo = null;', + 'foo = true;', + ` +var foo = 1, + bar = false, + baz = {}; + `, + ` +function foo() { + var foo = 0; + var bar = []; +} + `, + 'var fn = function() {};', + 'var foo = (bar = 2);', + 'for (var i = 0; i < 1; i++) {}', + ` +for (var foo in []) { +} + `, + { + code: ` +for (var foo of []) { +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'let a = true;', + options: ['always'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'const a = {};', + options: ['always'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + let a = 1, + b = false; + if (a) { + let c = 3, + d = null; + } +} + `, + options: ['always'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + const a = 1, + b = true; + if (a) { + const c = 3, + d = null; + } +} + `, + options: ['always'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + let a = 1; + const b = false; + var c = true; +} + `, + options: ['always'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'var foo;', + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'var foo, bar, baz;', + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + var foo; + var bar; +} + `, + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'let a;', + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'const a = 1;', + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + let a, b; + if (a) { + let c, d; + } +} + `, + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + const a = 1, + b = true; + if (a) { + const c = 3, + d = null; + } +} + `, + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + let a; + const b = false; + var c; +} + `, + options: ['never'], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'for (var i = 0; i < 1; i++) {}', + options: ['never', { ignoreForLoopInit: true }], + }, + { + code: ` +for (var foo in []) { +} + `, + options: ['never', { ignoreForLoopInit: true }], + }, + { + code: ` +for (var foo of []) { +} + `, + options: ['never', { ignoreForLoopInit: true }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + var bar = 1; + let baz = 2; + const qux = 3; +} + `, + options: ['always'], + }, + + // typescript-eslint + { + code: 'declare const foo: number;', + options: ['always'], + }, + { + code: 'declare const foo: number;', + options: ['never'], + }, + { + code: ` +declare namespace myLib { + let numberOfGreetings: number; +} + `, + options: ['always'], + }, + { + code: ` +declare namespace myLib { + let numberOfGreetings: number; +} + `, + options: ['never'], + }, + { + code: ` +interface GreetingSettings { + greeting: string; + duration?: number; + color?: string; +} + `, + }, + { + code: ` +interface GreetingSettings { + greeting: string; + duration?: number; + color?: string; +} + `, + options: ['never'], + }, + 'type GreetingLike = string | (() => string) | Greeter;', + { + code: 'type GreetingLike = string | (() => string) | Greeter;', + options: ['never'], + }, + { + code: ` +function foo() { + var bar: string; +} + `, + options: ['never'], + }, + { + code: 'var bar: string;', + options: ['never'], + }, + { + code: ` +var bar: string = function(): string { + return 'string'; +}; + `, + options: ['always'], + }, + { + code: ` +var bar: string = function(arg1: stirng): string { + return 'string'; +}; + `, + options: ['always'], + }, + { + code: "function foo(arg1: string = 'string'): void {}", + options: ['never'], + }, + { + code: "const foo: string = 'hello';", + options: ['never'], + }, + { + code: ` +const class1 = class NAME { + constructor() { + var name1: string = 'hello'; + } +}; + `, + }, + { + code: ` +const class1 = class NAME { + static pi: number = 3.14; +}; + `, + }, + { + code: ` +const class1 = class NAME { + static pi: number = 3.14; +}; + `, + options: ['never'], + }, + { + code: ` +interface IEmployee { + empCode: number; + empName: string; + getSalary: (number) => number; // arrow function + getManagerName(number): string; +} + `, + }, + { + code: ` +interface IEmployee { + empCode: number; + empName: string; + getSalary: (number) => number; // arrow function + getManagerName(number): string; +} + `, + options: ['never'], + }, + { + code: "declare const foo: number = 'asd';", + options: ['always'], + }, + + { + code: "const foo: number = 'asd';", + options: ['always'], + }, + { + code: 'const foo: number;', + options: ['never'], + }, + { + code: ` +namespace myLib { + let numberOfGreetings: number; +} + `, + options: ['never'], + }, + { + code: ` +namespace myLib { + let numberOfGreetings: number = 2; +} + `, + options: ['always'], + }, + ], + invalid: [ + // checking compatibility with base rule + { + code: 'var foo;', + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'for (var a in []) var foo;', + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +var foo, + bar = false, + baz; + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + { + messageId: 'initialized', + data: { idName: 'baz' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + var foo = 0; + var bar; +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'bar' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + var foo; + var bar = foo; +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'let a;', + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'a' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + let a = 1, + b; + if (a) { + let c = 3, + d = null; + } +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'b' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + let a; + const b = false; + var c; +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'a' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + { + messageId: 'initialized', + data: { idName: 'c' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'var foo = (bar = 2);', + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'var foo = true;', + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +var foo, + bar = 5, + baz = 3; + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'bar' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + { + messageId: 'notInitialized', + data: { idName: 'baz' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + var foo; + var bar = foo; +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'bar' }, + + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'let a = 1;', + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'a' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + let a = 'foo', + b; + if (a) { + let c, d; + } +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'a' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + let a; + const b = false; + var c = 1; +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'c' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'for (var i = 0; i < 1; i++) {}', + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'i' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +for (var foo in []) { +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +for (var foo of []) { +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +function foo() { + var bar; +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'bar' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + + // typescript-eslint + { + code: "let arr: string[] = ['arr', 'ar'];", + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'arr' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'let arr: string = function() {};', + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'arr' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +const class1 = class NAME { + constructor() { + var name1: string = 'hello'; + } +}; + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'name1' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: 'let arr: string;', + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'arr' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: "declare var foo: number = 'asd';", + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'foo' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +namespace myLib { + let numberOfGreetings: number; +} + `, + options: ['always'], + errors: [ + { + messageId: 'initialized', + data: { idName: 'numberOfGreetings' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + { + code: ` +namespace myLib { + let numberOfGreetings: number = 2; +} + `, + options: ['never'], + errors: [ + { + messageId: 'notInitialized', + data: { idName: 'numberOfGreetings' }, + type: AST_NODE_TYPES.VariableDeclarator, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index ac8700d42e5..eb2d1cd9c60 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -622,3 +622,21 @@ declare module 'eslint/lib/rules/no-extra-semi' { >; export = rule; } + +declare module 'eslint/lib/rules/init-declarations' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'initialized' | 'notInitialized', + [ + 'always' | 'never', + { + ignoreForLoopInit?: boolean; + }?, + ], + { + 'VariableDeclaration:exit'(node: TSESTree.VariableDeclaration): void; + } + >; + export = rule; +}