Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-config",
"comment": "Enable the rule that prefers ECMAScript private class members.",
"type": "minor"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"changes": [
{
"packageName": "@rushstack/eslint-plugin",
"comment": "Add a rule that requires ECMAScript private syntax for class fields, methods, and accessors.",
"type": "minor"
}
]
}
3 changes: 3 additions & 0 deletions eslint/eslint-config/flat/profile/_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ const commonConfig = [
// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/no-new-null': 'warn',

// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/prefer-ecmascript-private-members': 'warn',

// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/typedef-var': 'warn',

Expand Down
3 changes: 3 additions & 0 deletions eslint/eslint-config/profile/_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ function buildRules(profile) {
// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/no-new-null': 'warn',

// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/prefer-ecmascript-private-members': 'warn',

// RATIONALE: See the @rushstack/eslint-plugin documentation
'@rushstack/typedef-var': 'warn',

Expand Down
35 changes: 35 additions & 0 deletions eslint/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,41 @@ enum E {
let e: E._PrivateMember = E._PrivateMember; // okay, because _PrivateMember is declared by E
```

## `@rushstack/prefer-ecmascript-private-members`

Require ECMAScript private syntax for fields, methods, and accessors declared with TypeScript's `private`
modifier.

#### Rule Details

ECMAScript `#` members provide runtime privacy. TypeScript's `private` modifier is erased during compilation,
allowing the member to be accessed through JavaScript, bracket notation, or type assertions.

This rule applies to class fields, methods, and accessors. Private constructors and constructor parameter
properties are not affected. The rule does not provide an autofix because converting a member requires updating
every reference and may change runtime behavior for reflection or objects created without invoking the
constructor.

#### Examples

The following pattern is considered a problem:

```ts
class Example {
private value: string = ''; // error
private calculate(): number {} // error
}
```

The following pattern is NOT considered a problem:

```ts
class Example {
#value: string = '';
#calculate(): number {}
}
```

## `@rushstack/normalized-imports`

Require relative import paths to be written in a normalized minimal form and autofix unnecessary directory traversals.
Expand Down
6 changes: 5 additions & 1 deletion eslint/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { normalizedImportsRule } from './normalized-imports';
import { typedefVar } from './typedef-var';
import { importRequiresChunkNameRule } from './import-requires-chunk-name';
import { pairReactDomRenderUnmountRule } from './pair-react-dom-render-unmount';
import { preferEcmascriptPrivateMembersRule } from './prefer-ecmascript-private-members';

interface IPlugin {
rules: { [ruleName: string]: TSESLint.RuleModule<string, unknown[]> };
Expand Down Expand Up @@ -52,7 +53,10 @@ const plugin: IPlugin = {
'import-requires-chunk-name': importRequiresChunkNameRule,

// Full name: "@rushstack/pair-react-dom-render-unmount"
'pair-react-dom-render-unmount': pairReactDomRenderUnmountRule
'pair-react-dom-render-unmount': pairReactDomRenderUnmountRule,

// Full name: "@rushstack/prefer-ecmascript-private-members"
'prefer-ecmascript-private-members': preferEcmascriptPrivateMembersRule
}
};

Expand Down
44 changes: 44 additions & 0 deletions eslint/eslint-plugin/src/prefer-ecmascript-private-members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { TSESLint, TSESTree } from '@typescript-eslint/utils';

type MessageIds = 'use-ecmascript-private-member';
type Options = [];

const preferEcmascriptPrivateMembersRule: TSESLint.RuleModule<MessageIds, Options> = {
defaultOptions: [],
meta: {
type: 'suggestion',
messages: {
'use-ecmascript-private-member':
'Use ECMAScript private syntax ("#member") instead of the TypeScript "private" modifier.'
},
schema: [],
docs: {
description: 'Require ECMAScript private syntax for private class fields, methods, and accessors',
recommended: 'recommended',
url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin'
} as TSESLint.RuleMetaDataDocs
},
create: (context: TSESLint.RuleContext<MessageIds, Options>) => ({
PropertyDefinition(node: TSESTree.PropertyDefinition): void {
if (node.accessibility === 'private') {
context.report({
node,
messageId: 'use-ecmascript-private-member'
});
}
},
MethodDefinition(node: TSESTree.MethodDefinition): void {
if (node.accessibility === 'private' && node.kind !== 'constructor') {
context.report({
node,
messageId: 'use-ecmascript-private-member'
});
}
}
})
};

export { preferEcmascriptPrivateMembersRule };
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { RuleTester } from '@typescript-eslint/rule-tester';

import { preferEcmascriptPrivateMembersRule } from '../prefer-ecmascript-private-members';
import { getRuleTesterWithoutProject } from './ruleTester';

const ruleTester: RuleTester = getRuleTesterWithoutProject();

ruleTester.run('prefer-ecmascript-private-members', preferEcmascriptPrivateMembersRule, {
invalid: [
{
code: 'class Example { private value: string = ""; }',
errors: [{ messageId: 'use-ecmascript-private-member' }]
},
{
code: 'class Example { private static readonly values: Set<string> = new Set(); }',
errors: [{ messageId: 'use-ecmascript-private-member' }]
},
{
code: 'class Example { private optional?: string; private assigned!: string; }',
errors: [
{ messageId: 'use-ecmascript-private-member' },
{ messageId: 'use-ecmascript-private-member' }
]
},
{
code: 'class Example { declare private value: string; }',
errors: [{ messageId: 'use-ecmascript-private-member' }]
},
{
code: 'class Example { private ["value"]: string = ""; }',
errors: [{ messageId: 'use-ecmascript-private-member' }]
},
{
code: 'class Example { private calculate(): number { return 1; } }',
errors: [{ messageId: 'use-ecmascript-private-member' }]
},
{
code: [
'class Example {',
' private get value(): string { return ""; }',
' private set value(value: string) {}',
'}'
].join('\n'),
errors: [
{ messageId: 'use-ecmascript-private-member' },
{ messageId: 'use-ecmascript-private-member' }
]
}
],
valid: [
{
code: 'class Example { #value: string = ""; static #values: Set<string> = new Set(); }'
},
{
code: [
'class Example {',
' #calculate(): number { return 1; }',
' get #value(): string { return ""; }',
' set #value(value: string) {}',
'}'
].join('\n')
},
{
code: 'class Example { public value: string = ""; protected otherValue: string = ""; }'
},
{
code: [
'class Example {',
' public constructor(private readonly parameter: string) {}',
'}'
].join('\n')
},
{
code: 'class Example { private constructor() {} }'
}
]
});