From 37ec2c2264add3e6ce20ac4e02d48644afda3fa8 Mon Sep 17 00:00:00 2001 From: Daniel Cassidy Date: Sat, 15 May 2021 22:13:12 +0100 Subject: [PATCH] feat(eslint-plugin): [dot-notation] optionally allow square bracket notation where an index signature exists in conjunction with `noPropertyAccessFromIndexSignature` (#3361) --- .../eslint-plugin/docs/rules/dot-notation.md | 24 ++++++++- .../eslint-plugin/src/rules/dot-notation.ts | 49 +++++++++++++++---- .../tests/rules/dot-notation.test.ts | 34 +++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 1 + 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/dot-notation.md b/packages/eslint-plugin/docs/rules/dot-notation.md index 56097090e2e..094e2e4fc67 100644 --- a/packages/eslint-plugin/docs/rules/dot-notation.md +++ b/packages/eslint-plugin/docs/rules/dot-notation.md @@ -3,7 +3,10 @@ ## 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. +It adds: + +- Support for optionally ignoring computed `private` and/or `protected` member access. +- Compatibility with TypeScript's `noPropertyAccessFromIndexSignature` option. ## How to use @@ -24,14 +27,18 @@ This rule adds the following options: interface Options extends BaseDotNotationOptions { allowPrivateClassPropertyAccess?: boolean; allowProtectedClassPropertyAccess?: boolean; + allowIndexSignaturePropertyAccess?: boolean; } const defaultOptions: Options = { ...baseDotNotationDefaultOptions, allowPrivateClassPropertyAccess: false, allowProtectedClassPropertyAccess: false, + allowIndexSignaturePropertyAccess: false, }; ``` +If the TypeScript compiler option `noPropertyAccessFromIndexSignature` is set to `true`, then this rule always allows the use of square bracket notation to access properties of types that have a `string` index signature, even if `allowIndexSignaturePropertyAccess` is `false`. + ### `allowPrivateClassPropertyAccess` Example of a correct code when `allowPrivateClassPropertyAccess` is set to `true` @@ -58,4 +65,19 @@ const x = new X(); x['protected_prop'] = 123; ``` +### `allowIndexSignaturePropertyAccess` + +Example of correct code when `allowIndexSignaturePropertyAccess` is set to `true` + +```ts +class X { + [key: string]: number; +} + +const x = new X(); +x['hello'] = 123; +``` + +If the TypeScript compiler option `noPropertyAccessFromIndexSignature` is set to `true`, then the above code is always allowed, even if `allowIndexSignaturePropertyAccess` is `false`. + Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/dot-notation.md) diff --git a/packages/eslint-plugin/src/rules/dot-notation.ts b/packages/eslint-plugin/src/rules/dot-notation.ts index 2c9dd9f2351..055b97ecab2 100644 --- a/packages/eslint-plugin/src/rules/dot-notation.ts +++ b/packages/eslint-plugin/src/rules/dot-notation.ts @@ -1,11 +1,12 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import * as ts from 'typescript'; +import * as tsutils from 'tsutils'; import baseRule from 'eslint/lib/rules/dot-notation'; import { - InferOptionsTypeFromRule, - InferMessageIdsTypeFromRule, createRule, getParserServices, + InferMessageIdsTypeFromRule, + InferOptionsTypeFromRule, } from '../util'; export type Options = InferOptionsTypeFromRule; @@ -42,6 +43,10 @@ export default createRule({ type: 'boolean', default: false, }, + allowIndexSignaturePropertyAccess: { + type: 'boolean', + default: false, + }, }, additionalProperties: false, }, @@ -53,32 +58,41 @@ export default createRule({ { allowPrivateClassPropertyAccess: false, allowProtectedClassPropertyAccess: false, + allowIndexSignaturePropertyAccess: false, allowKeywords: true, allowPattern: '', }, ], create(context, [options]) { const rules = baseRule.create(context); + + const { program, esTreeNodeToTSNodeMap } = getParserServices(context); + const typeChecker = program.getTypeChecker(); + const allowPrivateClassPropertyAccess = options.allowPrivateClassPropertyAccess; const allowProtectedClassPropertyAccess = options.allowProtectedClassPropertyAccess; - - const parserServices = getParserServices(context); - const typeChecker = parserServices.program.getTypeChecker(); + const allowIndexSignaturePropertyAccess = + (options.allowIndexSignaturePropertyAccess ?? false) || + tsutils.isCompilerOptionEnabled( + program.getCompilerOptions(), + 'noPropertyAccessFromIndexSignature', + ); return { MemberExpression(node: TSESTree.MemberExpression): void { if ( (allowPrivateClassPropertyAccess || - allowProtectedClassPropertyAccess) && + allowProtectedClassPropertyAccess || + allowIndexSignaturePropertyAccess) && node.computed ) { - // for perf reasons - only fetch the symbol if we have to - const objectSymbol = typeChecker.getSymbolAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(node.property), + // for perf reasons - only fetch symbols if we have to + const propertySymbol = typeChecker.getSymbolAtLocation( + esTreeNodeToTSNodeMap.get(node.property), ); - const modifierKind = objectSymbol?.getDeclarations()?.[0] + const modifierKind = propertySymbol?.getDeclarations()?.[0] ?.modifiers?.[0].kind; if ( (allowPrivateClassPropertyAccess && @@ -88,6 +102,21 @@ export default createRule({ ) { return; } + if ( + propertySymbol === undefined && + allowIndexSignaturePropertyAccess + ) { + const objectType = typeChecker.getTypeAtLocation( + esTreeNodeToTSNodeMap.get(node.object), + ); + const indexType = typeChecker.getIndexTypeOfType( + objectType, + ts.IndexKind.String, + ); + if (indexType != undefined) { + return; + } + } } rules.MemberExpression(node); }, diff --git a/packages/eslint-plugin/tests/rules/dot-notation.test.ts b/packages/eslint-plugin/tests/rules/dot-notation.test.ts index 0d1f755b482..cbdb2d52342 100644 --- a/packages/eslint-plugin/tests/rules/dot-notation.test.ts +++ b/packages/eslint-plugin/tests/rules/dot-notation.test.ts @@ -87,6 +87,18 @@ x['protected_prop'] = 123; `, options: [{ allowProtectedClassPropertyAccess: true }], }, + { + code: ` +class X { + prop: string; + [key: string]: number; +} + +const x = new X(); +x['hello'] = 3; + `, + options: [{ allowIndexSignaturePropertyAccess: true }], + }, ], invalid: [ { @@ -287,5 +299,27 @@ x.protected_prop = 123; `, errors: [{ messageId: 'useDot' }], }, + { + code: ` +class X { + prop: string; + [key: string]: number; +} + +const x = new X(); +x['prop'] = 'hello'; + `, + options: [{ allowIndexSignaturePropertyAccess: true }], + errors: [{ messageId: 'useDot' }], + output: ` +class X { + prop: string; + [key: string]: number; +} + +const x = new X(); +x.prop = 'hello'; + `, + }, ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 425956e1f25..d6677e9dc93 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -713,6 +713,7 @@ declare module 'eslint/lib/rules/dot-notation' { allowPattern?: string; allowPrivateClassPropertyAccess?: boolean; allowProtectedClassPropertyAccess?: boolean; + allowIndexSignaturePropertyAccess?: boolean; }, ], {