-
Notifications
You must be signed in to change notification settings - Fork 4
/
prefer-hash-private.js
132 lines (126 loc) · 4.43 KB
/
prefer-hash-private.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* @param {import("eslint").Rule.Node} node
*/
function getEnclosingClass(node) {
for (let current = node; current; current = current.parent) {
if (current.type === "ClassDeclaration") {
return current;
} else if (current.type === "FunctionDeclaration") {
return undefined;
} else if (
current.type === "FunctionExpression" &&
current.parent?.parent?.type !== "ClassBody"
) {
return undefined;
}
}
return undefined;
}
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
fixable: "code",
hasSuggestions: true,
messages: {
preferHash:
"Prefer `{{newName}}` language feature over `private {{oldName}}` accessibility modifier",
rename: "Rename to {{newName}}",
},
},
create: (context) => {
/**
* @typedef ClassInfo
* @property {Set<import("estree").Identifier & import("eslint").Rule.NodeParentExtension>} privates
* @property {Map<string, import("estree").Identifier[]>} memberReferences
*/
/**
* @type {Map<import("estree").ClassDeclaration, ClassInfo>}
*/
const infoByClass = new Map();
return {
// Track any references to properties inside the class body, e.g. `this.foo`.
[`MemberExpression:has(ThisExpression.object) > Identifier.property`]: (
/** @type {import("estree").Identifier & { parent: import("estree").MemberExpression & import("eslint").Rule.Node }} */ node
) => {
if (node.parent.object.type !== "ThisExpression") {
// Avoid treating `this.foo.bar` as a reference to `private bar`.
// We'd prefer the selector to use `:has(> ThisExpression.object)`, but ESQuery doesn't support that syntax.
return;
}
const cls = getEnclosingClass(node);
if (!cls) {
return;
}
let info = infoByClass.get(cls);
if (!info) {
info = { privates: new Set(), memberReferences: new Map() };
infoByClass.set(cls, info);
}
let refs = info.memberReferences.get(node.name);
if (!refs) {
refs = [];
info.memberReferences.set(node.name, refs);
}
refs.push(node);
},
// Track any private properties or methods on the class, e.g. `private foo`.
[`:matches(PropertyDefinition, MethodDefinition[kind!="constructor"])[accessibility="private"] > Identifier.key`]:
(
/** @type {import("estree").Identifier & import("eslint").Rule.NodeParentExtension} */
node
) => {
const cls = getEnclosingClass(node);
if (!cls) {
throw new Error("Expected class around private definition");
}
let info = infoByClass.get(cls);
if (!info) {
info = { privates: new Set(), memberReferences: new Map() };
infoByClass.set(cls, info);
}
info.privates.add(node);
},
// Once we have processed all properties and references in the whole class, emit any errors
[`ClassDeclaration:exit`]: (node) => {
const info = infoByClass.get(node);
if (!info) {
return;
}
for (const privateIdentifier of info.privates) {
const refs = info.memberReferences.get(privateIdentifier.name) ?? [];
const newName = "#" + privateIdentifier.name.replace(/^_/, "");
context.report({
node: privateIdentifier,
messageId: "preferHash",
data: { oldName: privateIdentifier.name, newName },
suggest: [
{
messageId: "rename",
data: { newName },
*fix(fixer) {
const privateToken = context.sourceCode
.getTokens(privateIdentifier.parent)
.find(
(token) =>
token.type === "Keyword" && token.value === "private"
);
if (privateToken) {
yield fixer.removeRange([
privateToken.range[0],
privateToken.range[1] + 1,
]);
}
yield fixer.replaceText(privateIdentifier, newName);
for (const ref of refs) {
yield fixer.replaceText(ref, newName);
}
},
},
],
});
}
},
};
},
};