@aneuhold/eslint-config 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/rules/no-private-modifier/classFrame.ts +16 -3
- package/src/rules/no-private-modifier/classReports.ts +4 -0
- package/src/rules/no-private-modifier/no-private-modifier.md +11 -0
- package/src/rules/no-private-modifier/no-private-modifier.test.ts +10 -0
- package/src/rules/no-private-modifier/no-private-modifier.ts +3 -3
- package/src/rules/no-private-modifier/types.ts +1 -1
package/package.json
CHANGED
|
@@ -3,13 +3,16 @@ import { type ClassFrame, type ClassNode } from './types';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates a fresh frame for a class, scanning its members up-front for the
|
|
6
|
-
* private names eligible to convert and the `#names` already in use.
|
|
6
|
+
* private names eligible to convert and the `#names` already in use. Decorated
|
|
7
|
+
* members are excluded and their names blocked, since TypeScript rejects
|
|
8
|
+
* decorators on `#private` members (`TS1206`).
|
|
7
9
|
*
|
|
8
10
|
* @param classNode The class declaration or expression
|
|
9
11
|
*/
|
|
10
12
|
export const createClassFrame = (classNode: ClassNode): ClassFrame => {
|
|
11
13
|
const candidateNames = new Set<string>();
|
|
12
14
|
const existingPrivateNames = new Set<string>();
|
|
15
|
+
const decoratedNames = new Set<string>();
|
|
13
16
|
|
|
14
17
|
for (const member of classNode.body.body) {
|
|
15
18
|
if (
|
|
@@ -26,10 +29,20 @@ export const createClassFrame = (classNode: ClassNode): ClassFrame => {
|
|
|
26
29
|
member.key.type === AST_NODE_TYPES.Identifier &&
|
|
27
30
|
member.accessibility === 'private'
|
|
28
31
|
) {
|
|
29
|
-
|
|
32
|
+
if (member.decorators.length > 0) {
|
|
33
|
+
decoratedNames.add(member.key.name);
|
|
34
|
+
} else {
|
|
35
|
+
candidateNames.add(member.key.name);
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
// A name shared with a decorated member (such as a get/set pair where only
|
|
41
|
+
// one carries the decorator) has to stay put, so the pair keeps matching.
|
|
42
|
+
for (const name of decoratedNames) {
|
|
43
|
+
candidateNames.delete(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
return {
|
|
34
47
|
classNode,
|
|
35
48
|
className: classNode.id?.name ?? null,
|
|
@@ -37,7 +50,7 @@ export const createClassFrame = (classNode: ClassNode): ClassFrame => {
|
|
|
37
50
|
existingPrivateNames,
|
|
38
51
|
thisRefs: new Map(),
|
|
39
52
|
staticRefs: new Map(),
|
|
40
|
-
blocked:
|
|
53
|
+
blocked: decoratedNames,
|
|
41
54
|
rebindDepth: 0,
|
|
42
55
|
};
|
|
43
56
|
};
|
|
@@ -11,6 +11,7 @@ export type ClassReports = { reports: MemberReport[]; targets: FixTarget[] };
|
|
|
11
11
|
* Turns a fully-populated frame into the per-member reports and the set of
|
|
12
12
|
* fixable conversion targets. Each name claims its references once, so a
|
|
13
13
|
* get/set pair sharing a name doesn't rewrite the same reference twice.
|
|
14
|
+
* Decorated members are skipped entirely — neither reported nor converted.
|
|
14
15
|
*
|
|
15
16
|
* @param frame The class frame, after traversal has recorded its references
|
|
16
17
|
*/
|
|
@@ -33,6 +34,9 @@ export const collectClassReports = (frame: ClassFrame): ClassReports => {
|
|
|
33
34
|
if (member.type === AST_NODE_TYPES.MethodDefinition && member.kind === 'constructor') {
|
|
34
35
|
continue;
|
|
35
36
|
}
|
|
37
|
+
if (member.decorators.length > 0) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
const messageId: MessageId =
|
|
38
42
|
member.type === AST_NODE_TYPES.MethodDefinition ? 'privateMethod' : 'privateField';
|
|
@@ -27,6 +27,13 @@ is a concise, readable shorthand that declares and assigns the field in one
|
|
|
27
27
|
place, and `#private` has no equivalent. Flagging it would force the verbose
|
|
28
28
|
field-plus-assignment form for no real benefit, so the rule permits it.
|
|
29
29
|
|
|
30
|
+
**Decorated members are allowed.** TypeScript rejects decorators on `#private`
|
|
31
|
+
fields and methods with `TS1206: Decorators are not valid here.`, so a decorated
|
|
32
|
+
member has no `#` form to convert to. Reporting one would only force an
|
|
33
|
+
`eslint-disable` comment for something that cannot be fixed, so the rule skips
|
|
34
|
+
it — no report, and neither the declaration nor its `this.name` references are
|
|
35
|
+
rewritten.
|
|
36
|
+
|
|
30
37
|
## Autofix
|
|
31
38
|
|
|
32
39
|
None. Converting `private` to `#` requires renaming every reference
|
|
@@ -64,6 +71,10 @@ class Counter {
|
|
|
64
71
|
class Service {
|
|
65
72
|
// Parameter properties are allowed.
|
|
66
73
|
constructor(private readonly client: Client) {}
|
|
74
|
+
|
|
75
|
+
// Decorated members are allowed.
|
|
76
|
+
@WebSocketServer()
|
|
77
|
+
private server!: SocketServer;
|
|
67
78
|
}
|
|
68
79
|
```
|
|
69
80
|
|
|
@@ -27,6 +27,9 @@ ruleTester.run('no-private-modifier', noPrivateModifier, {
|
|
|
27
27
|
// Constructor parameter properties are allowed (no `#` shorthand exists).
|
|
28
28
|
{ code: `class Foo { constructor(private foo: string) {} }` },
|
|
29
29
|
{ code: `class Foo { constructor(private readonly foo: string) {} }` },
|
|
30
|
+
// Decorated members are allowed (decorators are invalid on `#private`).
|
|
31
|
+
{ code: `class Foo { @Inject() private client!: Client; }` },
|
|
32
|
+
{ code: `class Foo { @Log() private helper(): void {} }` },
|
|
30
33
|
// `public`/`protected`/no-modifier members are out of scope.
|
|
31
34
|
{ code: `class Foo { public count = 0; }` },
|
|
32
35
|
{ code: `class Foo { protected helper(): void {} }` },
|
|
@@ -95,6 +98,13 @@ ruleTester.run('no-private-modifier', noPrivateModifier, {
|
|
|
95
98
|
errors: [{ messageId: 'privateField' }],
|
|
96
99
|
output: null,
|
|
97
100
|
},
|
|
101
|
+
// A decorated member sits out the conversion entirely, references included,
|
|
102
|
+
// while an undecorated sibling still converts.
|
|
103
|
+
{
|
|
104
|
+
code: `class Foo { @Inject() private client!: Client; private count = 0; run() { this.client.send(this.count); } }`,
|
|
105
|
+
errors: [{ messageId: 'privateField' }],
|
|
106
|
+
output: `class Foo { @Inject() private client!: Client; #count = 0; run() { this.client.send(this.#count); } }`,
|
|
107
|
+
},
|
|
98
108
|
// A colliding `#count` already exists, so the rename is unsafe — report only.
|
|
99
109
|
{
|
|
100
110
|
code: `class Foo { #count = 1; private count = 2; }`,
|
|
@@ -10,9 +10,9 @@ import { type ClassFrame, type ClassNode } from './types';
|
|
|
10
10
|
* syntax rather than the TypeScript `private` accessibility modifier, and
|
|
11
11
|
* autofixes the conversion — declaration plus `this.x` / `ClassName.x`
|
|
12
12
|
* references — whenever it can do so safely. Covers instance and static fields,
|
|
13
|
-
* and methods (including accessors and getters/setters). Private constructors
|
|
14
|
-
*
|
|
15
|
-
* intentionally allowed, since the `#` form has no equivalent.
|
|
13
|
+
* and methods (including accessors and getters/setters). Private constructors,
|
|
14
|
+
* constructor parameter properties (`constructor(private foo)`), and decorated
|
|
15
|
+
* members are intentionally allowed, since the `#` form has no equivalent.
|
|
16
16
|
*
|
|
17
17
|
* References are gathered as ESLint makes its single pass over the file: a stack
|
|
18
18
|
* tracks the class currently being traversed, and a per-class counter tracks how
|
|
@@ -31,7 +31,7 @@ export type ClassFrame = {
|
|
|
31
31
|
thisRefs: Map<string, TSESTree.MemberExpression[]>;
|
|
32
32
|
/** `ClassName.name` accesses (statics) that an autofix would rewrite. */
|
|
33
33
|
staticRefs: Map<string, TSESTree.MemberExpression[]>;
|
|
34
|
-
/** Names
|
|
34
|
+
/** Names the fix can't safely rewrite, so won't be touched. */
|
|
35
35
|
blocked: Set<string>;
|
|
36
36
|
/**
|
|
37
37
|
* How many non-arrow functions are currently open inside this class without
|