diff --git a/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts b/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts index 55d9748332c8d..8951ac8887814 100644 --- a/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts +++ b/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts @@ -4,30 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; import * as ts from 'typescript'; /** - * Disallow bracket notation for accessing properties that are valid identifiers, - * especially private members (starting with underscore). Bracket notation should - * only be used for properties with special characters or computed property names. - * - * Bad: obj['_privateMember'] - * Bad: obj['normalProperty'] - * Good: obj._privateMember // TypeScript will catch private access - * Good: obj.normalProperty - * Good: obj['property-with-dashes'] - * Good: obj[computedKey] + * Prefer dot notation for identifier properties, including TypeScript private members. + * Allow bracket notation for environment-variable names accessed directly through process.env. */ export default new class NoBracketNotationForIdentifiers implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: 'problem', docs: { - description: 'Disallow bracket notation for accessing properties that are valid identifiers' + description: 'Disallow bracket notation for identifier properties except on process.env' }, messages: { - noBracketNotation: 'Use dot notation instead of bracket notation for property \'{{property}}\'. Bracket notation bypasses TypeScript\'s type checking and access modifiers.' + noBracketNotation: 'Use dot notation instead of bracket notation for property \'{{property}}\'.' }, schema: [], fixable: 'code' @@ -47,7 +40,7 @@ export default new class NoBracketNotationForIdentifiers implements eslint.Rule. } return { - MemberExpression(node: any) { + MemberExpression(node: ESTree.MemberExpression) { const memberExpr = node as TSESTree.MemberExpression; // Only check computed member expressions (bracket notation) @@ -65,6 +58,13 @@ export default new class NoBracketNotationForIdentifiers implements eslint.Rule. return; } + const receiver = memberExpr.object; + if (receiver.type === 'MemberExpression' && !receiver.computed + && receiver.object.type === 'Identifier' && receiver.object.name === 'process' + && receiver.property.type === 'Identifier' && receiver.property.name === 'env') { + return; + } + const propertyName = memberExpr.property.value; // If it's a valid identifier, report it diff --git a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts index cfb1bac25a5b7..2a45c9dc2a691 100644 --- a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts +++ b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts @@ -20,6 +20,15 @@ new RuleTester().run('code-no-bracket-notation-for-identifiers', rule, { 'object[`property`];', String.raw`object["\u0061"];`, String.raw`object["a\x62"];`, + 'process.env["ProgramW6432"];', + 'process.env["PROGRAMFILES"];', + 'process.env["https_proxy"];', + 'process.env["PATH"] = "value";', + 'delete process.env["PATH"];', + 'process.env.PATH;', + 'process.env?.["PATH"];', + 'process?.env?.["PATH"];', + '(process.env)["PATH"];', ], invalid: [ { @@ -76,5 +85,19 @@ new RuleTester().run('code-no-bracket-notation-for-identifiers', rule, { output: null, errors: [{ messageId: 'noBracketNotation', data: { property: 'property' } }], }, + ...[ + { code: 'opts["f"];', output: 'opts.f;', property: 'f' }, + { code: 'env["PATH"];', output: 'env.PATH;', property: 'PATH' }, + { code: 'safeProcess.env["PATH"];', output: 'safeProcess.env.PATH;', property: 'PATH' }, + { code: 'process.versions["node"];', output: 'process.versions.node;', property: 'node' }, + { code: 'process.env.nested["PATH"];', output: 'process.env.nested.PATH;', property: 'PATH' }, + { code: 'other.process.env["PATH"];', output: 'other.process.env.PATH;', property: 'PATH' }, + { code: 'process[key]["PATH"];', output: 'process[key].PATH;', property: 'PATH' }, + { code: 'const env = process.env; env["PATH"];', output: 'const env = process.env; env.PATH;', property: 'PATH' }, + ].map(({ code, output, property }) => ({ + code, + output, + errors: [{ messageId: 'noBracketNotation', data: { property } }], + })), ], });