From 89037d60b72652ad96692a121a6c9d3a7df6878c Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 11 Sep 2026 01:29:10 +0200 Subject: [PATCH 1/2] eslint: allow bracket notation for dictionary receivers Add exact receiver-path exceptions without requiring type-aware linting. Configure environment and scoped dictionary receivers, preserve checks for ordinary members, and cover the behavior with rule and configuration tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ode-no-bracket-notation-for-identifiers.ts | 71 +++++++-- ...odeNoBracketNotationForIdentifiers.test.ts | 147 +++++++++++++++++- eslint.config.js | 37 ++++- 3 files changed, 237 insertions(+), 18 deletions(-) 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 55d9748332c8d5..a1cb1fd4b76dec 100644 --- a/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts +++ b/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts @@ -4,37 +4,73 @@ *--------------------------------------------------------------------------------------------*/ 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 dictionary receivers explicitly configured by their exact path. */ 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 configured dictionary receivers' }, 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: [], + schema: [{ + type: 'object', + properties: { + allow: { + type: 'array', + items: { type: 'string', minLength: 1 }, + uniqueItems: true, + description: 'Exact receiver paths such as process.env or opts. Matches ignore optional chaining and TypeScript assertions, but do not infer aliases or match nested receivers.' + } + }, + additionalProperties: false + }], fixable: 'code' }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + const options = context.options[0] as { allow?: string[] } | undefined; + const allowedReceivers = new Set(options?.allow); + + function getReceiverPath(node: TSESTree.Node): string | undefined { + switch (node.type) { + case 'Identifier': + return node.name; + case 'ThisExpression': + return 'this'; + case 'ChainExpression': + case 'TSAsExpression': + case 'TSTypeAssertion': + case 'TSNonNullExpression': + case 'TSSatisfiesExpression': + return getReceiverPath(node.expression); + case 'MemberExpression': { + const object = getReceiverPath(node.object); + const property = !node.computed && node.property.type === 'Identifier' + ? node.property.name + : node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string' + ? node.property.value + : undefined; + if (object !== undefined && property !== undefined && property.length > 0 && !property.includes('.')) { + return `${object}.${property}`; + } + return undefined; + } + default: + return undefined; + } + } + /** * Check if a string is a valid JavaScript identifier */ @@ -47,7 +83,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) @@ -67,6 +103,13 @@ export default new class NoBracketNotationForIdentifiers implements eslint.Rule. const propertyName = memberExpr.property.value; + if (allowedReceivers.size > 0) { + const receiver = getReceiverPath(memberExpr.object); + if (receiver !== undefined && allowedReceivers.has(receiver)) { + return; + } + } + // If it's a valid identifier, report it if (isValidIdentifier(propertyName)) { context.report({ diff --git a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts index cfb1bac25a5b72..18e3d960656177 100644 --- a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts +++ b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RuleTester } from 'eslint'; -import { suite, test } from 'node:test'; +import assert from 'assert'; +import { ESLint, Linter, RuleTester } from 'eslint'; +import { before, suite, test } from 'node:test'; +import path from 'path'; +import tseslint from 'typescript-eslint'; import rule from '../../../.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts'; RuleTester.describe = suite; @@ -20,6 +23,33 @@ 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["_"] = "value";', + 'process.env.PATH;', + 'process?.env?.["PATH"];', + '(process?.env)?.["PATH"];', + 'process /* comment */ . env["PATH"];', + ].map(code => ({ code, options: [{ allow: ['process.env'] }] })), + ...[ + '(process.env as NodeJS.ProcessEnv)["PATH"];', + '(process.env)["PATH"];', + 'process.env!["PATH"];', + '(process.env satisfies NodeJS.ProcessEnv)["PATH"];', + ].map(code => ({ + code, + options: [{ allow: ['process.env'] }], + languageOptions: { parser: tseslint.parser }, + })), + { + code: 'opts["f"] || opts["g"] || opts["help"];', + options: [{ allow: ['opts'] }], + }, + { + code: 'env["PATH"]; safeProcess.env["PATH"]; configuration?.userEnv?.["PATH"]; this.args["help"];', + options: [{ allow: ['env', 'safeProcess.env', 'configuration.userEnv', 'this.args'] }], + }, ], invalid: [ { @@ -76,5 +106,118 @@ new RuleTester().run('code-no-bracket-notation-for-identifiers', rule, { output: null, errors: [{ messageId: 'noBracketNotation', data: { property: 'property' } }], }, + { + name: 'environment receivers are not implicitly exempt', + code: 'process.env["PATH"];', + output: 'process.env.PATH;', + errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], + }, + { + name: 'an empty allow list preserves enforcement', + code: 'process.env["PATH"];', + output: 'process.env.PATH;', + options: [{ allow: [] }], + errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], + }, + { + name: 'computed receiver segments do not exempt their own access', + code: 'process["env"]["PATH"];', + output: 'process.env["PATH"];', + options: [{ allow: ['process.env'] }], + errors: [{ messageId: 'noBracketNotation', data: { property: 'env' } }], + }, + ...[ + { code: 'object["_private"];', output: 'object._private;', property: '_private' }, + { code: 'object["PATH"];', output: 'object.PATH;', property: 'PATH' }, + { code: 'point["x"];', output: 'point.x;', property: 'x' }, + { code: 'opts["f"];', output: 'opts.f;', property: 'f' }, + { code: 'env["PATH"];', output: 'env.PATH;', property: 'PATH' }, + { code: 'other.env["PATH"];', output: 'other.env.PATH;', property: 'PATH' }, + { code: 'process.other["PATH"];', output: 'process.other.PATH;', property: 'PATH' }, + { 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.envs["PATH"];', output: 'process.envs.PATH;', property: 'PATH' }, + { code: 'process[key]["PATH"];', output: 'process[key].PATH;', property: 'PATH' }, + { code: 'getEnv()["PATH"];', output: 'getEnv().PATH;', property: 'PATH' }, + { code: '(condition ? process.env : other)["PATH"];', output: '(condition ? process.env : other).PATH;', property: 'PATH' }, + { code: 'const env = process.env; env["PATH"];', output: 'const env = process.env; env.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, + options: [{ allow: ['process.env'] }], + errors: [{ messageId: 'noBracketNotation', data: { property } }], + })), + { + name: 'a literal containing dots is not a receiver path', + code: 'object["process.env"]["PATH"];', + output: 'object["process.env"].PATH;', + options: [{ allow: ['object.process.env'] }], + errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], + }, + { + name: 'TypeScript private members remain checked with exceptions enabled', + code: 'class Service { private value = 1; } new Service()["value"];', + output: 'class Service { private value = 1; } new Service().value;', + options: [{ allow: ['process.env'] }], + languageOptions: { parser: tseslint.parser }, + errors: [{ messageId: 'noBracketNotation', data: { property: 'value' } }], + }, ], }); + +suite('bracket notation receiver configuration', () => { + const ruleId = 'local/code-no-bracket-notation-for-identifiers'; + let eslint: ESLint; + + before(async () => { + const { default: configuration }: { default: Linter.Config[] } = await import(new URL('../../../eslint.config.js', import.meta.url).href); + eslint = new ESLint({ + cwd: path.resolve(import.meta.dirname, '../../..'), + overrideConfigFile: true, + // Exercise receiver scoping independently of the temporary migration allowlist. + overrideConfig: configuration.map(config => config.rules?.[ruleId] ? { ...config, ignores: [] } : config), + }); + }); + + for (const { filePath, code, properties } of [ + { + filePath: 'src/bootstrap-cli.ts', + code: 'process.env["PATH"]; env["PATH"]; opts["f"]; service["_private"];', + properties: ['PATH', 'f', '_private'], + }, + { + filePath: 'src/vs/code/electron-browser/workbench/workbench.ts', + code: 'safeProcess.env["PATH"]; service["_private"];', + properties: ['_private'], + }, + { + filePath: 'src/vs/platform/shell/node/shellEnv.ts', + code: 'process.env["PATH"]; env["PATH"]; opts["f"]; service["_private"];', + properties: ['f', '_private'], + }, + { + filePath: 'src/vs/platform/windows/electron-main/windowsMainService.ts', + code: 'configuration?.userEnv?.["PATH"]; openConfig.userEnv["PATH"]; other.userEnv["PATH"]; service["_private"];', + properties: ['PATH', '_private'], + }, + { + filePath: 'test/smoke/test/index.js', + code: 'process.env["PATH"]; opts["f"] || opts["g"] || opts["help"]; options["grep"]; service["_private"];', + properties: ['grep', '_private'], + }, + { + filePath: 'src/vs/base/browser/dom.ts', + code: 'object["property"];', + properties: ['property'], + }, + ]) { + test(filePath, async () => { + const [result] = await eslint.lintText(code, { filePath }); + assert.deepStrictEqual( + result.messages.filter(message => message.fatal || message.ruleId === ruleId).map(message => message.message), + properties.map(property => `Use dot notation instead of bracket notation for property '${property}'.`) + ); + }); + } +}); diff --git a/eslint.config.js b/eslint.config.js index 5ae73c6f455bd3..404107fb56b88c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -36,6 +36,8 @@ const allowedBracketNotationFiles = fs.readFileSync(path.join(import.meta.dirnam .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); +const bracketNotationEnvironmentReceivers = ['process.env', 'safeProcess.env']; + export default defineConfig( // Global ignores { @@ -150,7 +152,7 @@ export default defineConfig( ] }, }, - // Disallow bracket notation for property names that can use dot notation. + // Environment variable names are dictionary keys, not API members. { files: [ '**/*.{js,cjs,mjs,ts,tsx,mts,cts}', @@ -161,7 +163,38 @@ export default defineConfig( 'local': pluginLocal, }, rules: { - 'local/code-no-bracket-notation-for-identifiers': 'warn', + 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: bracketNotationEnvironmentReceivers }], + }, + }, + // Keep exceptions for local dictionary names scoped to their consumers. + { + files: [ + 'src/vs/code/node/cli.ts', + 'src/vs/code/test/node/bootstrapESM.test.ts', + 'src/vs/platform/environment/common/environmentService.ts', + 'src/vs/platform/environment/node/argvHelper.ts', + 'src/vs/platform/product/common/product.ts', + 'src/vs/platform/shell/node/shellEnv.ts', + 'src/vs/platform/utilityProcess/electron-main/utilityProcess.ts', + 'src/vs/server/node/remoteTerminalChannel.ts', + ], + ignores: allowedBracketNotationFiles, + rules: { + 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'env'] }], + }, + }, + { + files: ['src/vs/platform/windows/electron-main/windowsMainService.ts'], + ignores: allowedBracketNotationFiles, + rules: { + 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'configuration.userEnv', 'openConfig.userEnv'] }], + }, + }, + { + files: ['test/smoke/test/index.js'], + ignores: allowedBracketNotationFiles, + rules: { + 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'opts'] }], }, }, // TS From 5a36e70a7ad959e6d4812ebbb826b645bf736e2b Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Fri, 11 Sep 2026 12:05:38 +0200 Subject: [PATCH 2/2] eslint: narrow bracket exception to process.env Replace configurable receiver matching and file-scoped overrides with a single built-in process.env check. Keep aliases and all other receivers under the existing rule. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ode-no-bracket-notation-for-identifiers.ts | 63 ++------ ...odeNoBracketNotationForIdentifiers.test.ts | 146 ++---------------- eslint.config.js | 37 +---- 3 files changed, 25 insertions(+), 221 deletions(-) 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 a1cb1fd4b76dec..8951ac88878142 100644 --- a/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts +++ b/.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts @@ -10,67 +10,24 @@ import * as ts from 'typescript'; /** * Prefer dot notation for identifier properties, including TypeScript private members. - * Allow bracket notation for dictionary receivers explicitly configured by their exact path. + * 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 identifier properties except on configured dictionary receivers' + description: 'Disallow bracket notation for identifier properties except on process.env' }, messages: { noBracketNotation: 'Use dot notation instead of bracket notation for property \'{{property}}\'.' }, - schema: [{ - type: 'object', - properties: { - allow: { - type: 'array', - items: { type: 'string', minLength: 1 }, - uniqueItems: true, - description: 'Exact receiver paths such as process.env or opts. Matches ignore optional chaining and TypeScript assertions, but do not infer aliases or match nested receivers.' - } - }, - additionalProperties: false - }], + schema: [], fixable: 'code' }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const options = context.options[0] as { allow?: string[] } | undefined; - const allowedReceivers = new Set(options?.allow); - - function getReceiverPath(node: TSESTree.Node): string | undefined { - switch (node.type) { - case 'Identifier': - return node.name; - case 'ThisExpression': - return 'this'; - case 'ChainExpression': - case 'TSAsExpression': - case 'TSTypeAssertion': - case 'TSNonNullExpression': - case 'TSSatisfiesExpression': - return getReceiverPath(node.expression); - case 'MemberExpression': { - const object = getReceiverPath(node.object); - const property = !node.computed && node.property.type === 'Identifier' - ? node.property.name - : node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string' - ? node.property.value - : undefined; - if (object !== undefined && property !== undefined && property.length > 0 && !property.includes('.')) { - return `${object}.${property}`; - } - return undefined; - } - default: - return undefined; - } - } - /** * Check if a string is a valid JavaScript identifier */ @@ -101,15 +58,15 @@ export default new class NoBracketNotationForIdentifiers implements eslint.Rule. return; } - const propertyName = memberExpr.property.value; - - if (allowedReceivers.size > 0) { - const receiver = getReceiverPath(memberExpr.object); - if (receiver !== undefined && allowedReceivers.has(receiver)) { - 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 if (isValidIdentifier(propertyName)) { context.report({ diff --git a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts index 18e3d960656177..2a45c9dc2a691a 100644 --- a/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts +++ b/build/lib/test/codeNoBracketNotationForIdentifiers.test.ts @@ -3,11 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import assert from 'assert'; -import { ESLint, Linter, RuleTester } from 'eslint'; -import { before, suite, test } from 'node:test'; -import path from 'path'; -import tseslint from 'typescript-eslint'; +import { RuleTester } from 'eslint'; +import { suite, test } from 'node:test'; import rule from '../../../.eslint-plugin-local/code-no-bracket-notation-for-identifiers.ts'; RuleTester.describe = suite; @@ -23,33 +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["_"] = "value";', - 'process.env.PATH;', - 'process?.env?.["PATH"];', - '(process?.env)?.["PATH"];', - 'process /* comment */ . env["PATH"];', - ].map(code => ({ code, options: [{ allow: ['process.env'] }] })), - ...[ - '(process.env as NodeJS.ProcessEnv)["PATH"];', - '(process.env)["PATH"];', - 'process.env!["PATH"];', - '(process.env satisfies NodeJS.ProcessEnv)["PATH"];', - ].map(code => ({ - code, - options: [{ allow: ['process.env'] }], - languageOptions: { parser: tseslint.parser }, - })), - { - code: 'opts["f"] || opts["g"] || opts["help"];', - options: [{ allow: ['opts'] }], - }, - { - code: 'env["PATH"]; safeProcess.env["PATH"]; configuration?.userEnv?.["PATH"]; this.args["help"];', - options: [{ allow: ['env', 'safeProcess.env', 'configuration.userEnv', 'this.args'] }], - }, + '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: [ { @@ -106,118 +85,19 @@ new RuleTester().run('code-no-bracket-notation-for-identifiers', rule, { output: null, errors: [{ messageId: 'noBracketNotation', data: { property: 'property' } }], }, - { - name: 'environment receivers are not implicitly exempt', - code: 'process.env["PATH"];', - output: 'process.env.PATH;', - errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], - }, - { - name: 'an empty allow list preserves enforcement', - code: 'process.env["PATH"];', - output: 'process.env.PATH;', - options: [{ allow: [] }], - errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], - }, - { - name: 'computed receiver segments do not exempt their own access', - code: 'process["env"]["PATH"];', - output: 'process.env["PATH"];', - options: [{ allow: ['process.env'] }], - errors: [{ messageId: 'noBracketNotation', data: { property: 'env' } }], - }, ...[ - { code: 'object["_private"];', output: 'object._private;', property: '_private' }, - { code: 'object["PATH"];', output: 'object.PATH;', property: 'PATH' }, - { code: 'point["x"];', output: 'point.x;', property: 'x' }, { code: 'opts["f"];', output: 'opts.f;', property: 'f' }, { code: 'env["PATH"];', output: 'env.PATH;', property: 'PATH' }, - { code: 'other.env["PATH"];', output: 'other.env.PATH;', property: 'PATH' }, - { code: 'process.other["PATH"];', output: 'process.other.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.envs["PATH"];', output: 'process.envs.PATH;', property: 'PATH' }, { code: 'process[key]["PATH"];', output: 'process[key].PATH;', property: 'PATH' }, - { code: 'getEnv()["PATH"];', output: 'getEnv().PATH;', property: 'PATH' }, - { code: '(condition ? process.env : other)["PATH"];', output: '(condition ? process.env : other).PATH;', property: 'PATH' }, { code: 'const env = process.env; env["PATH"];', output: 'const env = process.env; env.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, - options: [{ allow: ['process.env'] }], errors: [{ messageId: 'noBracketNotation', data: { property } }], })), - { - name: 'a literal containing dots is not a receiver path', - code: 'object["process.env"]["PATH"];', - output: 'object["process.env"].PATH;', - options: [{ allow: ['object.process.env'] }], - errors: [{ messageId: 'noBracketNotation', data: { property: 'PATH' } }], - }, - { - name: 'TypeScript private members remain checked with exceptions enabled', - code: 'class Service { private value = 1; } new Service()["value"];', - output: 'class Service { private value = 1; } new Service().value;', - options: [{ allow: ['process.env'] }], - languageOptions: { parser: tseslint.parser }, - errors: [{ messageId: 'noBracketNotation', data: { property: 'value' } }], - }, ], }); - -suite('bracket notation receiver configuration', () => { - const ruleId = 'local/code-no-bracket-notation-for-identifiers'; - let eslint: ESLint; - - before(async () => { - const { default: configuration }: { default: Linter.Config[] } = await import(new URL('../../../eslint.config.js', import.meta.url).href); - eslint = new ESLint({ - cwd: path.resolve(import.meta.dirname, '../../..'), - overrideConfigFile: true, - // Exercise receiver scoping independently of the temporary migration allowlist. - overrideConfig: configuration.map(config => config.rules?.[ruleId] ? { ...config, ignores: [] } : config), - }); - }); - - for (const { filePath, code, properties } of [ - { - filePath: 'src/bootstrap-cli.ts', - code: 'process.env["PATH"]; env["PATH"]; opts["f"]; service["_private"];', - properties: ['PATH', 'f', '_private'], - }, - { - filePath: 'src/vs/code/electron-browser/workbench/workbench.ts', - code: 'safeProcess.env["PATH"]; service["_private"];', - properties: ['_private'], - }, - { - filePath: 'src/vs/platform/shell/node/shellEnv.ts', - code: 'process.env["PATH"]; env["PATH"]; opts["f"]; service["_private"];', - properties: ['f', '_private'], - }, - { - filePath: 'src/vs/platform/windows/electron-main/windowsMainService.ts', - code: 'configuration?.userEnv?.["PATH"]; openConfig.userEnv["PATH"]; other.userEnv["PATH"]; service["_private"];', - properties: ['PATH', '_private'], - }, - { - filePath: 'test/smoke/test/index.js', - code: 'process.env["PATH"]; opts["f"] || opts["g"] || opts["help"]; options["grep"]; service["_private"];', - properties: ['grep', '_private'], - }, - { - filePath: 'src/vs/base/browser/dom.ts', - code: 'object["property"];', - properties: ['property'], - }, - ]) { - test(filePath, async () => { - const [result] = await eslint.lintText(code, { filePath }); - assert.deepStrictEqual( - result.messages.filter(message => message.fatal || message.ruleId === ruleId).map(message => message.message), - properties.map(property => `Use dot notation instead of bracket notation for property '${property}'.`) - ); - }); - } -}); diff --git a/eslint.config.js b/eslint.config.js index 404107fb56b88c..5ae73c6f455bd3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -36,8 +36,6 @@ const allowedBracketNotationFiles = fs.readFileSync(path.join(import.meta.dirnam .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); -const bracketNotationEnvironmentReceivers = ['process.env', 'safeProcess.env']; - export default defineConfig( // Global ignores { @@ -152,7 +150,7 @@ export default defineConfig( ] }, }, - // Environment variable names are dictionary keys, not API members. + // Disallow bracket notation for property names that can use dot notation. { files: [ '**/*.{js,cjs,mjs,ts,tsx,mts,cts}', @@ -163,38 +161,7 @@ export default defineConfig( 'local': pluginLocal, }, rules: { - 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: bracketNotationEnvironmentReceivers }], - }, - }, - // Keep exceptions for local dictionary names scoped to their consumers. - { - files: [ - 'src/vs/code/node/cli.ts', - 'src/vs/code/test/node/bootstrapESM.test.ts', - 'src/vs/platform/environment/common/environmentService.ts', - 'src/vs/platform/environment/node/argvHelper.ts', - 'src/vs/platform/product/common/product.ts', - 'src/vs/platform/shell/node/shellEnv.ts', - 'src/vs/platform/utilityProcess/electron-main/utilityProcess.ts', - 'src/vs/server/node/remoteTerminalChannel.ts', - ], - ignores: allowedBracketNotationFiles, - rules: { - 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'env'] }], - }, - }, - { - files: ['src/vs/platform/windows/electron-main/windowsMainService.ts'], - ignores: allowedBracketNotationFiles, - rules: { - 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'configuration.userEnv', 'openConfig.userEnv'] }], - }, - }, - { - files: ['test/smoke/test/index.js'], - ignores: allowedBracketNotationFiles, - rules: { - 'local/code-no-bracket-notation-for-identifiers': ['warn', { allow: [...bracketNotationEnvironmentReceivers, 'opts'] }], + 'local/code-no-bracket-notation-for-identifiers': 'warn', }, }, // TS