diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index a0b38fead9c..f6e70ef98ac 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -182,6 +182,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | |
| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | |
| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
+| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: |
| [`@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 | | | |
diff --git a/packages/eslint-plugin/docs/rules/dot-notation.md b/packages/eslint-plugin/docs/rules/dot-notation.md
new file mode 100644
index 00000000000..f827b003e57
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/dot-notation.md
@@ -0,0 +1,46 @@
+# enforce dot notation whenever possible (`dot-notation`)
+
+## Rule Details
+
+This rule extends the base [`eslint/dot-notation`](https://eslint.org/docs/rules/dot-notation) rule.
+It adds support for optionally ignoring computed `private` member access.
+
+## How to use
+
+```cjson
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "dot-notation": "off",
+ "@typescript-eslint/dot-notation": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/dot-notation`](https://eslint.org/docs/rules/dot-notation#options) options.
+This rule adds the following options:
+
+```ts
+interface Options extends BaseDotNotationOptions {
+ allowPrivateClassPropertyAccess?: boolean;
+}
+const defaultOptions: Options = {
+ ...baseDotNotationDefaultOptions,
+ allowPrivateClassPropertyAccess: false,
+};
+```
+
+### `allowPrivateClassPropertyAccess`
+
+Example of a correct code when `allowPrivateClassPropertyAccess` is set to `true`
+
+```ts
+class X {
+ private priv_prop = 123;
+}
+
+const x = new X();
+x['priv_prop'] = 123;
+```
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/dot-notation.md)
diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json
index 7a6a217140a..0e103162f5f 100644
--- a/packages/eslint-plugin/src/configs/all.json
+++ b/packages/eslint-plugin/src/configs/all.json
@@ -15,6 +15,8 @@
"@typescript-eslint/consistent-type-definitions": "error",
"default-param-last": "off",
"@typescript-eslint/default-param-last": "error",
+ "dot-notation": "off",
+ "@typescript-eslint/dot-notation": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
diff --git a/packages/eslint-plugin/src/rules/dot-notation.ts b/packages/eslint-plugin/src/rules/dot-notation.ts
new file mode 100644
index 00000000000..fb710187d29
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/dot-notation.ts
@@ -0,0 +1,80 @@
+import { TSESTree } from '@typescript-eslint/experimental-utils';
+import * as ts from 'typescript';
+import baseRule from 'eslint/lib/rules/dot-notation';
+import {
+ InferOptionsTypeFromRule,
+ InferMessageIdsTypeFromRule,
+ createRule,
+ getParserServices,
+} from '../util';
+
+export type Options = InferOptionsTypeFromRule;
+export type MessageIds = InferMessageIdsTypeFromRule;
+
+export default createRule({
+ name: 'dot-notation',
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'enforce dot notation whenever possible',
+ category: 'Best Practices',
+ recommended: false,
+ extendsBaseRule: true,
+ requiresTypeChecking: true,
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ allowKeywords: {
+ type: 'boolean',
+ default: true,
+ },
+ allowPattern: {
+ type: 'string',
+ default: '',
+ },
+ allowPrivateClassPropertyAccess: {
+ tyoe: 'boolean',
+ default: false,
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ fixable: baseRule.meta.fixable,
+ messages: baseRule.meta.messages,
+ },
+ defaultOptions: [
+ {
+ allowPrivateClassPropertyAccess: false,
+ allowKeywords: true,
+ allowPattern: '',
+ },
+ ],
+ create(context, [options]) {
+ const rules = baseRule.create(context);
+ const allowPrivateClassPropertyAccess =
+ options.allowPrivateClassPropertyAccess;
+
+ const parserServices = getParserServices(context);
+ const typeChecker = parserServices.program.getTypeChecker();
+
+ return {
+ MemberExpression(node: TSESTree.MemberExpression): void {
+ const objectSymbol = typeChecker.getSymbolAtLocation(
+ parserServices.esTreeNodeToTSNodeMap.get(node.property),
+ );
+
+ if (
+ allowPrivateClassPropertyAccess &&
+ objectSymbol?.declarations[0]?.modifiers?.[0].kind ===
+ ts.SyntaxKind.PrivateKeyword
+ ) {
+ return;
+ }
+ rules.MemberExpression(node);
+ },
+ };
+ },
+});
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index b6652f79eb4..2d8e6830c4f 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -12,6 +12,7 @@ import commaSpacing from './comma-spacing';
import consistentTypeAssertions from './consistent-type-assertions';
import consistentTypeDefinitions from './consistent-type-definitions';
import defaultParamLast from './default-param-last';
+import dotNotation from './dot-notation';
import explicitFunctionReturnType from './explicit-function-return-type';
import explicitMemberAccessibility from './explicit-member-accessibility';
import explicitModuleBoundaryTypes from './explicit-module-boundary-types';
@@ -114,6 +115,7 @@ export default {
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
'default-param-last': defaultParamLast,
+ 'dot-notation': dotNotation,
'explicit-function-return-type': explicitFunctionReturnType,
'explicit-member-accessibility': explicitMemberAccessibility,
'explicit-module-boundary-types': explicitModuleBoundaryTypes,
diff --git a/packages/eslint-plugin/tests/rules/dot-notation.test.ts b/packages/eslint-plugin/tests/rules/dot-notation.test.ts
new file mode 100644
index 00000000000..797b111cb17
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/dot-notation.test.ts
@@ -0,0 +1,259 @@
+import rule from '../../src/rules/dot-notation';
+import { RuleTester, getFixturesRootDir } from '../RuleTester';
+
+const rootPath = getFixturesRootDir();
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ sourceType: 'module',
+ tsconfigRootDir: rootPath,
+ project: './tsconfig.json',
+ },
+});
+
+/**
+ * Quote a string in "double quotes" because it’s painful
+ * with a double-quoted string literal
+ */
+function q(str: string): string {
+ return `"${str}"`;
+}
+
+ruleTester.run('dot-notation', rule, {
+ valid: [
+ // baseRule
+
+ 'a.b;',
+ 'a.b.c;',
+ "a['12'];",
+ 'a[b];',
+ 'a[0];',
+ { code: 'a.b.c;', options: [{ allowKeywords: false }] },
+ { code: 'a.arguments;', options: [{ allowKeywords: false }] },
+ { code: 'a.let;', options: [{ allowKeywords: false }] },
+ { code: 'a.yield;', options: [{ allowKeywords: false }] },
+ { code: 'a.eval;', options: [{ allowKeywords: false }] },
+ { code: 'a[0];', options: [{ allowKeywords: false }] },
+ { code: "a['while'];", options: [{ allowKeywords: false }] },
+ { code: "a['true'];", options: [{ allowKeywords: false }] },
+ { code: "a['null'];", options: [{ allowKeywords: false }] },
+ { code: 'a[true];', options: [{ allowKeywords: false }] },
+ { code: 'a[null];', options: [{ allowKeywords: false }] },
+ { code: 'a.true;', options: [{ allowKeywords: true }] },
+ { code: 'a.null;', options: [{ allowKeywords: true }] },
+ {
+ code: "a['snake_case'];",
+ options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }],
+ },
+ {
+ code: "a['lots_of_snake_case'];",
+ options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }],
+ },
+ { code: 'a[`time${range}`];', parserOptions: { ecmaVersion: 6 } },
+ {
+ code: 'a[`while`];',
+ options: [{ allowKeywords: false }],
+ parserOptions: { ecmaVersion: 6 },
+ },
+ { code: 'a[`time range`];', parserOptions: { ecmaVersion: 6 } },
+ 'a.true;',
+ 'a.null;',
+ 'a[undefined];',
+ 'a[void 0];',
+ 'a[b()];',
+ { code: 'a[/(?0)/];', parserOptions: { ecmaVersion: 2018 } },
+
+ {
+ code: `
+class X {
+ private priv_prop = 123;
+}
+
+const x = new X();
+x['priv_prop'] = 123;
+ `,
+ options: [{ allowPrivateClassPropertyAccess: true }],
+ },
+ ],
+ invalid: [
+ {
+ code: `
+class X {
+ private priv_prop = 123;
+}
+
+const x = new X();
+x['priv_prop'] = 123;
+ `,
+ options: [{ allowPrivateClassPropertyAccess: false }],
+ output: `
+class X {
+ private priv_prop = 123;
+}
+
+const x = new X();
+x.priv_prop = 123;
+ `,
+ errors: [{ messageId: 'useDot' }],
+ },
+ {
+ code: `
+class X {
+ public pub_prop = 123;
+}
+
+const x = new X();
+x['pub_prop'] = 123;
+ `,
+ output: `
+class X {
+ public pub_prop = 123;
+}
+
+const x = new X();
+x.pub_prop = 123;
+ `,
+ errors: [{ messageId: 'useDot' }],
+ },
+ // baseRule
+
+ // {
+ // code: 'a.true;',
+ // output: "a['true'];",
+ // options: [{ allowKeywords: false }],
+ // errors: [{ messageId: "useBrackets", data: { key: "true" } }],
+ // },
+ {
+ code: "a['true'];",
+ output: 'a.true;',
+ errors: [{ messageId: 'useDot', data: { key: q('true') } }],
+ },
+ {
+ code: "a['time'];",
+ output: 'a.time;',
+ parserOptions: { ecmaVersion: 6 },
+ errors: [{ messageId: 'useDot', data: { key: '"time"' } }],
+ },
+ {
+ code: 'a[null];',
+ output: 'a.null;',
+ errors: [{ messageId: 'useDot', data: { key: 'null' } }],
+ },
+ {
+ code: 'a[true];',
+ output: 'a.true;',
+ errors: [{ messageId: 'useDot', data: { key: 'true' } }],
+ },
+ {
+ code: 'a[false];',
+ output: 'a.false;',
+ errors: [{ messageId: 'useDot', data: { key: 'false' } }],
+ },
+ {
+ code: "a['b'];",
+ output: 'a.b;',
+ errors: [{ messageId: 'useDot', data: { key: q('b') } }],
+ },
+ {
+ code: "a.b['c'];",
+ output: 'a.b.c;',
+ errors: [{ messageId: 'useDot', data: { key: q('c') } }],
+ },
+ {
+ code: "a['_dangle'];",
+ output: 'a._dangle;',
+ options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }],
+ errors: [{ messageId: 'useDot', data: { key: q('_dangle') } }],
+ },
+ {
+ code: "a['SHOUT_CASE'];",
+ output: 'a.SHOUT_CASE;',
+ options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }],
+ errors: [{ messageId: 'useDot', data: { key: q('SHOUT_CASE') } }],
+ },
+ {
+ code: 'a\n' + " ['SHOUT_CASE'];",
+ output: 'a\n' + ' .SHOUT_CASE;',
+ errors: [
+ {
+ messageId: 'useDot',
+ data: { key: q('SHOUT_CASE') },
+ line: 2,
+ column: 4,
+ },
+ ],
+ },
+ {
+ code:
+ 'getResource()\n' +
+ ' .then(function(){})\n' +
+ ' ["catch"](function(){})\n' +
+ ' .then(function(){})\n' +
+ ' ["catch"](function(){});',
+ output:
+ 'getResource()\n' +
+ ' .then(function(){})\n' +
+ ' .catch(function(){})\n' +
+ ' .then(function(){})\n' +
+ ' .catch(function(){});',
+ errors: [
+ {
+ messageId: 'useDot',
+ data: { key: q('catch') },
+ line: 3,
+ column: 6,
+ },
+ {
+ messageId: 'useDot',
+ data: { key: q('catch') },
+ line: 5,
+ column: 6,
+ },
+ ],
+ },
+ {
+ code: 'foo\n' + ' .while;',
+ output: 'foo\n' + ' ["while"];',
+ options: [{ allowKeywords: false }],
+ errors: [{ messageId: 'useBrackets', data: { key: 'while' } }],
+ },
+ {
+ code: "foo[/* comment */ 'bar'];",
+ output: null, // Not fixed due to comment
+ errors: [{ messageId: 'useDot', data: { key: q('bar') } }],
+ },
+ {
+ code: "foo['bar' /* comment */];",
+ output: null, // Not fixed due to comment
+ errors: [{ messageId: 'useDot', data: { key: q('bar') } }],
+ },
+ {
+ code: "foo['bar'];",
+ output: 'foo.bar;',
+ errors: [{ messageId: 'useDot', data: { key: q('bar') } }],
+ },
+ {
+ code: 'foo./* comment */ while;',
+ output: null, // Not fixed due to comment
+ options: [{ allowKeywords: false }],
+ errors: [{ messageId: 'useBrackets', data: { key: 'while' } }],
+ },
+ {
+ code: 'foo[null];',
+ output: 'foo.null;',
+ errors: [{ messageId: 'useDot', data: { key: 'null' } }],
+ },
+ {
+ code: "foo['bar'] instanceof baz;",
+ output: 'foo.bar instanceof baz;',
+ errors: [{ messageId: 'useDot', data: { key: q('bar') } }],
+ },
+ {
+ code: 'let.if();',
+ output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration
+ options: [{ allowKeywords: false }],
+ errors: [{ messageId: 'useBrackets', data: { key: 'if' } }],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts
index eb2d1cd9c60..0bac88823e2 100644
--- a/packages/eslint-plugin/typings/eslint-rules.d.ts
+++ b/packages/eslint-plugin/typings/eslint-rules.d.ts
@@ -640,3 +640,22 @@ declare module 'eslint/lib/rules/init-declarations' {
>;
export = rule;
}
+
+declare module 'eslint/lib/rules/dot-notation' {
+ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
+
+ const rule: TSESLint.RuleModule<
+ 'useDot' | 'useBrackets',
+ [
+ {
+ allowKeywords?: boolean;
+ allowPattern?: string;
+ allowPrivateClassPropertyAccess?: boolean;
+ },
+ ],
+ {
+ MemberExpression(node: TSESTree.MemberExpression): void;
+ }
+ >;
+ export = rule;
+}