@blumintinc/eslint-plugin-blumint 1.20.139 → 1.20.141
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/lib/index.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +37 -9
- package/lib/rules/consistent-callback-naming.js +393 -21
- package/lib/rules/enforce-assert-safe-object-key.js +59 -28
- package/lib/rules/enforce-assert-throws.js +51 -26
- package/lib/rules/enforce-boolean-naming-prefixes.js +47 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +6 -11
- package/lib/rules/enforce-early-destructuring.js +12 -3
- package/lib/rules/enforce-firestore-doc-ref-generic.js +35 -21
- package/lib/rules/enforce-memoize-async.js +68 -23
- package/lib/rules/enforce-memoize-getters.js +44 -5
- package/lib/rules/no-passthrough-getters.js +50 -7
- package/lib/rules/parallelize-async-operations.js +53 -2
- package/lib/rules/prefer-docsetter-setall.js +28 -7
- package/lib/rules/prefer-getter-over-parameterless-method.js +111 -50
- package/lib/rules/prefer-map-over-conditional-dispatch.js +182 -20
- package/lib/rules/prefer-use-deep-compare-memo.js +6 -3
- package/lib/rules/prefer-utility-function-over-private-static.js +39 -4
- package/lib/rules/require-memoize-jsx-returners.js +64 -10
- package/lib/rules/semantic-function-prefixes.js +29 -4
- package/lib/utils/ASTHelpers.d.ts +14 -0
- package/lib/utils/ASTHelpers.js +67 -0
- package/lib/utils/graph/ClassGraphBuilder.d.ts +15 -1
- package/lib/utils/graph/ClassGraphBuilder.js +47 -6
- package/lib/utils/importInsertion.d.ts +17 -3
- package/lib/utils/importInsertion.js +24 -7
- package/package.json +1 -1
- package/release-manifest.json +180 -0
package/lib/index.js
CHANGED
|
@@ -3,15 +3,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.classMethodsReadTopToBottom = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const ClassGraphBuilder_1 = require("../utils/graph/ClassGraphBuilder");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
// The source order and the sorted graph are matched by name, so both sides must
|
|
8
|
+
// derive names from the same function: any disagreement makes the two arrays
|
|
9
|
+
// differ in length and silently skips the whole class body.
|
|
10
|
+
const getMemberName = ClassGraphBuilder_1.classMemberNameOf;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the proposed order still declares every field above the initializer
|
|
13
|
+
* that reads it. Only field-to-field reads constrain the layout: methods (and
|
|
14
|
+
* private methods) are installed before any initializer runs, so relocating one
|
|
15
|
+
* is unobservable.
|
|
16
|
+
*/
|
|
17
|
+
function initializerReadsPrecedeDeclarations(node, sortedOrder, graph, className) {
|
|
18
|
+
const positionOf = new Map(sortedOrder.map((name, index) => [name, index]));
|
|
19
|
+
return node.body.every((member) => {
|
|
20
|
+
if (member.type !== 'PropertyDefinition' || !member.value) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const reader = getMemberName(member);
|
|
24
|
+
const readerPosition = reader === null ? undefined : positionOf.get(reader);
|
|
25
|
+
if (readerPosition === undefined) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return ASTHelpers_1.ASTHelpers.classMemberNamesReadEagerly(member.value, className)
|
|
29
|
+
.filter((name) => graph[name]?.type === 'property' && name !== reader)
|
|
30
|
+
.every((name) => {
|
|
31
|
+
const readPosition = positionOf.get(name);
|
|
32
|
+
return readPosition === undefined || readPosition < readerPosition;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
15
35
|
}
|
|
16
36
|
exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
17
37
|
name: 'class-methods-read-top-to-bottom',
|
|
@@ -71,6 +91,14 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
71
91
|
if (!allMembersRepresented) {
|
|
72
92
|
return;
|
|
73
93
|
}
|
|
94
|
+
// Field declaration order is observable where method order is not:
|
|
95
|
+
// methods are installed before any initializer runs, but a field read
|
|
96
|
+
// before its own declaration evaluates to `undefined` under the
|
|
97
|
+
// `private` spelling and throws under the ECMA `#` spelling. Bail
|
|
98
|
+
// rather than emit an order that changes what the class computes.
|
|
99
|
+
if (!initializerReadsPrecedeDeclarations(node, sortedOrder, graphBuilder.graph, className)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
74
102
|
for (let i = 0; i < actualOrder.length; i++) {
|
|
75
103
|
const actualMember = actualOrder[i];
|
|
76
104
|
const expectedMember = sortedOrder[i];
|
|
@@ -148,6 +148,100 @@ function isApiSurfaceValue(node) {
|
|
|
148
148
|
}
|
|
149
149
|
return false;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether `node` is a member of a class that `extends` or `implements`
|
|
153
|
+
* something.
|
|
154
|
+
*
|
|
155
|
+
* Such a member is one end of a contract whose other end this fixer does not
|
|
156
|
+
* own. Renaming only the implementation leaves the declaration behind and the
|
|
157
|
+
* class stops satisfying it: `TS2515` when an abstract base declares the member,
|
|
158
|
+
* `TS2420` when an `implements` clause does (Bug #1944). The heritage itself is
|
|
159
|
+
* the trigger rather than a matching declaration the rule can find, because the
|
|
160
|
+
* base is routinely imported — the declaration need not be in this file at all,
|
|
161
|
+
* and sibling implementors of the same contract certainly need not be. Erring
|
|
162
|
+
* toward withholding costs an autofix on members that override nothing; it buys
|
|
163
|
+
* a fixer that cannot turn a compiling file into a broken one. The violation is
|
|
164
|
+
* still reported, so the author renames both ends deliberately — the same policy
|
|
165
|
+
* that already withholds the rename for exported bindings and for members of an
|
|
166
|
+
* exported or returned object literal.
|
|
167
|
+
*/
|
|
168
|
+
function satisfiesDeclaredContract(node) {
|
|
169
|
+
const classNode = enclosingClass(node);
|
|
170
|
+
return (!!classNode &&
|
|
171
|
+
(!!classNode.superClass || (classNode.implements?.length ?? 0) > 0));
|
|
172
|
+
}
|
|
173
|
+
/** The class a member belongs to, when `node` is a class member. */
|
|
174
|
+
function enclosingClass(node) {
|
|
175
|
+
const body = node.parent;
|
|
176
|
+
if (body?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const classNode = body.parent;
|
|
180
|
+
return classNode?.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
181
|
+
classNode?.type === utils_1.AST_NODE_TYPES.ClassExpression
|
|
182
|
+
? classNode
|
|
183
|
+
: undefined;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* What `this` denotes at `node`: `classNode`'s instance, `classNode` itself
|
|
187
|
+
* (inside a `static` member), or nothing the rule can pin down.
|
|
188
|
+
*
|
|
189
|
+
* `this` is only rewritable evidence when it provably names the class whose
|
|
190
|
+
* member is being renamed. An arrow function inherits `this` lexically, so the
|
|
191
|
+
* walk passes through it; an ordinary function expression rebinds `this` to
|
|
192
|
+
* whatever calls it, so `this.handleClick` inside a callback may be some other
|
|
193
|
+
* object's member entirely and the rename must not touch it.
|
|
194
|
+
*/
|
|
195
|
+
function thisReceiverOf(node, classNode) {
|
|
196
|
+
const ownedBy = (member) => member.parent?.parent === classNode
|
|
197
|
+
? member.static
|
|
198
|
+
? 'static'
|
|
199
|
+
: 'instance'
|
|
200
|
+
: undefined;
|
|
201
|
+
let child = node;
|
|
202
|
+
let current = node.parent;
|
|
203
|
+
while (current) {
|
|
204
|
+
// A concise or block-bodied arrow keeps the enclosing `this`.
|
|
205
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
206
|
+
const owner = current.parent;
|
|
207
|
+
return (owner?.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
208
|
+
owner?.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
|
|
209
|
+
owner.value === current
|
|
210
|
+
? ownedBy(owner)
|
|
211
|
+
: undefined;
|
|
212
|
+
}
|
|
213
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (current.type === utils_1.AST_NODE_TYPES.PropertyDefinition) {
|
|
217
|
+
// Reached through arrows alone: a field initializer's `this` is the
|
|
218
|
+
// instance under construction.
|
|
219
|
+
return current.value === child ? ownedBy(current) : undefined;
|
|
220
|
+
}
|
|
221
|
+
if (current.type === utils_1.AST_NODE_TYPES.StaticBlock) {
|
|
222
|
+
return current.parent === classNode.body ? 'static' : undefined;
|
|
223
|
+
}
|
|
224
|
+
// A computed key, a decorator or a heritage expression evaluates outside
|
|
225
|
+
// the class body, so `this` there is not the instance.
|
|
226
|
+
if (current.type === utils_1.AST_NODE_TYPES.ClassBody ||
|
|
227
|
+
current.type === utils_1.AST_NODE_TYPES.Program) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
child = current;
|
|
231
|
+
current = current.parent;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
/** Whether a class body declares `name` twice — a `get`/`set` pair. */
|
|
236
|
+
function declaresNameTwice(body, name) {
|
|
237
|
+
const declarations = body.body.filter((member) => {
|
|
238
|
+
const key = member.key;
|
|
239
|
+
return (!member.computed &&
|
|
240
|
+
key?.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
241
|
+
key.name === name);
|
|
242
|
+
});
|
|
243
|
+
return declarations.length > 1;
|
|
244
|
+
}
|
|
151
245
|
/** The member names already declared alongside `node`, keyed by identifier. */
|
|
152
246
|
function siblingMemberNames(node) {
|
|
153
247
|
const names = new Set();
|
|
@@ -179,17 +273,7 @@ function siblingMemberNames(node) {
|
|
|
179
273
|
*/
|
|
180
274
|
function collectMemberReads(program, visitorKeys) {
|
|
181
275
|
const names = new Set();
|
|
182
|
-
|
|
183
|
-
const push = (value) => {
|
|
184
|
-
if (Array.isArray(value)) {
|
|
185
|
-
value.forEach(push);
|
|
186
|
-
}
|
|
187
|
-
else if (value && typeof value === 'object' && 'type' in value) {
|
|
188
|
-
stack.push(value);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
while (stack.length > 0) {
|
|
192
|
-
const node = stack.pop();
|
|
276
|
+
walkProgram(program, visitorKeys, (node) => {
|
|
193
277
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
194
278
|
if (!node.computed && node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
195
279
|
names.add(node.property.name);
|
|
@@ -210,11 +294,75 @@ function collectMemberReads(program, visitorKeys) {
|
|
|
210
294
|
}
|
|
211
295
|
}
|
|
212
296
|
}
|
|
297
|
+
});
|
|
298
|
+
return names;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Every node of the file, type nodes included.
|
|
302
|
+
*
|
|
303
|
+
* The walk is driven by `visitorKeys` rather than by the rule's own visitors so
|
|
304
|
+
* that a reference is found wherever it sits — including before the declaration
|
|
305
|
+
* it refers to, and inside a type position the default traversal would still
|
|
306
|
+
* reach but a hand-written recursion routinely forgets.
|
|
307
|
+
*/
|
|
308
|
+
function walkProgram(program, visitorKeys, visit) {
|
|
309
|
+
const stack = [program];
|
|
310
|
+
const push = (value) => {
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
value.forEach(push);
|
|
313
|
+
}
|
|
314
|
+
else if (value && typeof value === 'object' && 'type' in value) {
|
|
315
|
+
stack.push(value);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
while (stack.length > 0) {
|
|
319
|
+
const node = stack.pop();
|
|
320
|
+
visit(node);
|
|
213
321
|
for (const key of visitorKeys[node.type] ?? []) {
|
|
214
322
|
push(node[key]);
|
|
215
323
|
}
|
|
216
324
|
}
|
|
217
|
-
|
|
325
|
+
}
|
|
326
|
+
function collectMemberSites(program, visitorKeys) {
|
|
327
|
+
const reads = new Map();
|
|
328
|
+
const opaque = new Set();
|
|
329
|
+
let dynamicThisAccess = false;
|
|
330
|
+
walkProgram(program, visitorKeys, (node) => {
|
|
331
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
332
|
+
if (!node.computed && node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
333
|
+
const existing = reads.get(node.property.name);
|
|
334
|
+
if (existing) {
|
|
335
|
+
existing.push(node);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
reads.set(node.property.name, [node]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (node.computed &&
|
|
342
|
+
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
343
|
+
!(node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
344
|
+
typeof node.property.value === 'string')) {
|
|
345
|
+
dynamicThisAccess = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
349
|
+
typeof node.value === 'string') {
|
|
350
|
+
opaque.add(node.value);
|
|
351
|
+
}
|
|
352
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
353
|
+
opaque.add(node.right.name);
|
|
354
|
+
}
|
|
355
|
+
if (node.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
356
|
+
for (const property of node.properties) {
|
|
357
|
+
if (property.type === utils_1.AST_NODE_TYPES.Property &&
|
|
358
|
+
!property.computed &&
|
|
359
|
+
property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
360
|
+
opaque.add(property.key.name);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return { reads, opaque, dynamicThisAccess };
|
|
218
366
|
}
|
|
219
367
|
module.exports = (0, createRule_1.createRule)({
|
|
220
368
|
name: 'consistent-callback-naming',
|
|
@@ -385,6 +533,101 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
385
533
|
memberReads ??= collectMemberReads(sourceCode.ast, sourceCode.visitorKeys);
|
|
386
534
|
return memberReads.has(name);
|
|
387
535
|
}
|
|
536
|
+
// Built once per file, and only when a class member is actually a rename
|
|
537
|
+
// candidate.
|
|
538
|
+
let memberSites;
|
|
539
|
+
function fileMemberSites() {
|
|
540
|
+
const sourceCode = context.getSourceCode();
|
|
541
|
+
memberSites ??= collectMemberSites(sourceCode.ast, sourceCode.visitorKeys);
|
|
542
|
+
return memberSites;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Whether the class — and with it every value of its type — stays inside
|
|
546
|
+
* this file.
|
|
547
|
+
*
|
|
548
|
+
* A rename of a member another module can name is the JSX-prop problem in
|
|
549
|
+
* class clothing: the fixer edits one end of a contract whose readers it
|
|
550
|
+
* cannot see, and `import { C } from './c'; c.handleClick()` fails with
|
|
551
|
+
* TS2339 (Bug #1946). Export of the class is the obvious leak; a bare
|
|
552
|
+
* mention of the class name is the subtler one, since `export const c = new
|
|
553
|
+
* C()` hands the instance out without exporting the class at all. Neither is
|
|
554
|
+
* traceable across files, so both withhold.
|
|
555
|
+
*/
|
|
556
|
+
function classStaysModulePrivate(classNode) {
|
|
557
|
+
if (isApiSurfaceValue(classNode)) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const bindings = [...context.getDeclaredVariables(classNode)];
|
|
561
|
+
if (classNode.type === utils_1.AST_NODE_TYPES.ClassExpression) {
|
|
562
|
+
const declarator = classNode.parent;
|
|
563
|
+
if (declarator?.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
564
|
+
declarator.init !== classNode ||
|
|
565
|
+
declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
566
|
+
// A class expression passed to a call, stored on a property or
|
|
567
|
+
// returned is a value whose consumers the fixer cannot follow.
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
bindings.push(...context.getDeclaredVariables(declarator));
|
|
571
|
+
}
|
|
572
|
+
else if (bindings.length === 0) {
|
|
573
|
+
// An unnamed declaration is only reachable through an export.
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
return bindings.every((variable) => !isExportedBinding(variable) &&
|
|
577
|
+
!variable.references.some((ref) => !variable.identifiers.includes(ref.identifier)));
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* The reads a class-member rename must rewrite alongside the key, or `null`
|
|
581
|
+
* when the rename has to be withheld entirely.
|
|
582
|
+
*
|
|
583
|
+
* The reads live in this file, so unlike the JSX prop or the exported
|
|
584
|
+
* binding they are the fixer's to move — and they must move in the SAME fix,
|
|
585
|
+
* because a rename that reaches the declaration and not `this.handleClick()`
|
|
586
|
+
* turns a compiling file into TS2339 (Bug #1946). Any read the fixer cannot
|
|
587
|
+
* prove it owns — a computed access, a `super.` or instance-variable read, a
|
|
588
|
+
* `this` that some other object binds — collapses the whole rename to
|
|
589
|
+
* `null`: a partial rename is the very breakage this returns to prevent.
|
|
590
|
+
*
|
|
591
|
+
* A field (`handleClick = () => {}`) has exactly the binding sites a method
|
|
592
|
+
* has — the key, and `this.` reads of it — so it is answered here rather
|
|
593
|
+
* than on a parallel path that would have to re-derive every one of these
|
|
594
|
+
* withholdings (Bug #1949).
|
|
595
|
+
*/
|
|
596
|
+
function classMemberRenameTargets(node, name) {
|
|
597
|
+
const classNode = enclosingClass(node);
|
|
598
|
+
if (!classNode || node.computed) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
// A `get`/`set` pair declares the name twice, and this report covers one
|
|
602
|
+
// half of it: the getter is report-only, so renaming the setter alone
|
|
603
|
+
// would split the accessor in two and leave `obj.handleResize = x`
|
|
604
|
+
// assigning to a property that no longer has a setter.
|
|
605
|
+
if (declaresNameTwice(classNode.body, name)) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
// A `private` member is unnameable outside the class body, so its
|
|
609
|
+
// references are all in this file however far the class itself travels.
|
|
610
|
+
if (node.accessibility !== 'private' &&
|
|
611
|
+
!classStaysModulePrivate(classNode)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
const sites = fileMemberSites();
|
|
615
|
+
if (sites.dynamicThisAccess || sites.opaque.has(name)) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
// `this` in a static member is the class object, so a static member's
|
|
619
|
+
// reads and an instance member's reads are different members entirely.
|
|
620
|
+
const receiver = node.static ? 'static' : 'instance';
|
|
621
|
+
const targets = [];
|
|
622
|
+
for (const read of sites.reads.get(name) ?? []) {
|
|
623
|
+
if (read.object.type !== utils_1.AST_NODE_TYPES.ThisExpression ||
|
|
624
|
+
thisReceiverOf(read, classNode) !== receiver) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
targets.push(read.property);
|
|
628
|
+
}
|
|
629
|
+
return targets;
|
|
630
|
+
}
|
|
388
631
|
/**
|
|
389
632
|
* The variable a pattern identifier binds. `getDeclaredVariables` is
|
|
390
633
|
* authoritative — it is asked of the declaring ancestor (the
|
|
@@ -413,10 +656,35 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
413
656
|
variable.identifiers.some(namedByExportSpecifier) ||
|
|
414
657
|
variable.defs.some((def) => isExportedDeclaration(def.node)));
|
|
415
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Whether any scope from `from` up to — but NOT including — `until` binds
|
|
661
|
+
* `newName`.
|
|
662
|
+
*
|
|
663
|
+
* `until` is the scope the renamed declaration lives in, so it and
|
|
664
|
+
* everything above it is already answered by the upward walk in
|
|
665
|
+
* `isNameTaken`. What this adds is the span BETWEEN a reference and its
|
|
666
|
+
* declaration, which is where a binding of the new name captures the
|
|
667
|
+
* rewritten reference without colliding with anything the rename can see.
|
|
668
|
+
*/
|
|
669
|
+
function bindsNameBelow(from, until, newName) {
|
|
670
|
+
let scope = from;
|
|
671
|
+
while (scope && scope !== until) {
|
|
672
|
+
if (scope.set.has(newName)) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
scope = scope.upper;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
416
679
|
// A rename that collides with a name already visible where the binding (or
|
|
417
680
|
// any of its references) lives silently re-points those references at the
|
|
418
681
|
// other declaration.
|
|
419
|
-
function isNameTaken(variable, newName
|
|
682
|
+
function isNameTaken(variable, newName,
|
|
683
|
+
// The sites the fix will actually rewrite. The function path gathers a
|
|
684
|
+
// wider set than `variable.references` — a reference resolved through a
|
|
685
|
+
// sibling or child scope is still a site the fixer emits the new name at,
|
|
686
|
+
// so it is still a site the new name has to resolve correctly at.
|
|
687
|
+
references = variable.references) {
|
|
420
688
|
let scope = variable.scope;
|
|
421
689
|
while (scope) {
|
|
422
690
|
if (scope.set.has(newName)) {
|
|
@@ -424,7 +692,22 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
424
692
|
}
|
|
425
693
|
scope = scope.upper;
|
|
426
694
|
}
|
|
427
|
-
|
|
695
|
+
if (variable.scope.childScopes.some((child) => child.set.has(newName))) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The declaration site is not the only place the new name has to mean the
|
|
700
|
+
* renamed binding: the fix emits it at every reference too, and a scope
|
|
701
|
+
* BETWEEN a reference and the declaration that binds the new name takes
|
|
702
|
+
* that reference over (Bug #1948). Nothing else notices — the module scope
|
|
703
|
+
* still holds one `submit`, so a redeclaration check passes, and the
|
|
704
|
+
* emitted reference is well-typed against a real binding right up until it
|
|
705
|
+
* is called (`TS2349`). Withholding the whole rename is the only safe
|
|
706
|
+
* answer: alpha-renaming the intervening binding instead would mean owning
|
|
707
|
+
* ITS references as well, and a partial rename is exactly the breakage
|
|
708
|
+
* this prevents.
|
|
709
|
+
*/
|
|
710
|
+
return references.some((ref) => bindsNameBelow(ref.from, variable.scope, newName));
|
|
428
711
|
}
|
|
429
712
|
/**
|
|
430
713
|
* A `Property` inside an `ObjectPattern`. Its key names a property of the
|
|
@@ -620,16 +903,25 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
620
903
|
const leavesModule = declaredVariable
|
|
621
904
|
? isExportedBinding(declaredVariable)
|
|
622
905
|
: isExportedDeclaration(node);
|
|
906
|
+
// Remove 'handle' prefix and convert first character to lowercase
|
|
907
|
+
const newName = stripHandlePrefix(functionName);
|
|
908
|
+
// The rename is emitted at the declaration AND at every reference, so
|
|
909
|
+
// it is safe only where the new name resolves to this declaration at
|
|
910
|
+
// each of them. A binding of that name between a reference and the
|
|
911
|
+
// declaration silently captures the rewritten reference (Bug #1948),
|
|
912
|
+
// and without the binding itself there is no scope chain to ask, so
|
|
913
|
+
// the fix is withheld rather than guessed. The violation still
|
|
914
|
+
// reports; only the rename is withheld.
|
|
915
|
+
const capturesReference = !declaredVariable ||
|
|
916
|
+
isNameTaken(declaredVariable, newName, [...references]);
|
|
623
917
|
context.report({
|
|
624
918
|
node,
|
|
625
919
|
messageId: 'callbackFunctionPrefix',
|
|
626
920
|
data: { functionName },
|
|
627
921
|
fix(fixer) {
|
|
628
|
-
if (leavesModule) {
|
|
922
|
+
if (leavesModule || capturesReference) {
|
|
629
923
|
return null;
|
|
630
924
|
}
|
|
631
|
-
// Remove 'handle' prefix and convert first character to lowercase
|
|
632
|
-
const newName = stripHandlePrefix(functionName);
|
|
633
925
|
// `const handleDelete = fn` would become `const delete = fn`,
|
|
634
926
|
// which does not parse (Bug #1719).
|
|
635
927
|
if (!isEmittableName(newName)) {
|
|
@@ -648,8 +940,29 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
648
940
|
});
|
|
649
941
|
}
|
|
650
942
|
},
|
|
651
|
-
|
|
652
|
-
|
|
943
|
+
/**
|
|
944
|
+
* Class members (methods and fields) and object-literal members.
|
|
945
|
+
*
|
|
946
|
+
* A class FIELD is a subject here because it is one of the docs' own
|
|
947
|
+
* subjects — "a function, method, class property, or parameter" — and
|
|
948
|
+
* because covering only the method spelling leaves the rule evadable by a
|
|
949
|
+
* single token: `handleClick() {}` reported while `handleClick = () => {}`
|
|
950
|
+
* did not, so writing `=` silenced it without changing anything about the
|
|
951
|
+
* callback (Bug #1949).
|
|
952
|
+
*
|
|
953
|
+
* The field is judged on its NAME alone, not on whether it holds a
|
|
954
|
+
* function — the same question this arm already asks of an object-literal
|
|
955
|
+
* member, that the abstract-member arm asks of `abstract handleSubmit:
|
|
956
|
+
* (d: string) => string`, and that the variable arm asks of `const
|
|
957
|
+
* handleClickCount = 0`. The subject is the `handle<Something>` prefix
|
|
958
|
+
* itself, which describes no action whatever sits to the right of the
|
|
959
|
+
* `=`. Gating on a function-typed value would reinstate the same evasion
|
|
960
|
+
* one level down — `handleClick = makeHandler()` and `handleClick =
|
|
961
|
+
* deps.click` are the identical name behind a value the rule cannot
|
|
962
|
+
* always resolve, and a file parsed without type information resolves
|
|
963
|
+
* none of them.
|
|
964
|
+
*/
|
|
965
|
+
'MethodDefinition, PropertyDefinition, Property'(node) {
|
|
653
966
|
const key = node.key;
|
|
654
967
|
if (key.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
655
968
|
!key.name ||
|
|
@@ -657,6 +970,16 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
657
970
|
return;
|
|
658
971
|
}
|
|
659
972
|
const name = key.name;
|
|
973
|
+
// A computed key is an expression evaluated where the class is
|
|
974
|
+
// defined, so the identifier in `[handleClick] = fn` REFERENCES a
|
|
975
|
+
// binding and the member's own name is whatever that binding holds —
|
|
976
|
+
// something the rule cannot read. That binding is a subject in its own
|
|
977
|
+
// right and is reported at its declaration, so reporting the reference
|
|
978
|
+
// too would name a member that need not exist. The abstract-member arm
|
|
979
|
+
// skips a computed key for the same reason.
|
|
980
|
+
if (node.type === utils_1.AST_NODE_TYPES.PropertyDefinition && node.computed) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
660
983
|
// Skip autofixing for class parameters and getters
|
|
661
984
|
if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
662
985
|
node.kind === 'get') {
|
|
@@ -677,24 +1000,73 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
677
1000
|
// the key also replaces the value: `{ handleClick }` becomes
|
|
678
1001
|
// `{ click }`, which both renames the member and re-points it at a
|
|
679
1002
|
// binding that need not exist (Bug #1719).
|
|
680
|
-
const
|
|
1003
|
+
const isRenameable = isEmittableName(newName) &&
|
|
681
1004
|
// A sibling already holding the target name turns the rename into a
|
|
682
1005
|
// duplicate member: `{ click: a, handleClick: b }` would collapse to
|
|
683
1006
|
// two `click` keys, silently discarding the first.
|
|
684
1007
|
!siblingMemberNames(node.parent).has(newName) &&
|
|
685
1008
|
!(isProperty && node.shorthand) &&
|
|
1009
|
+
// A member of a class with heritage may be satisfying a declaration
|
|
1010
|
+
// the fixer cannot rewrite (Bug #1944).
|
|
1011
|
+
!satisfiesDeclaredContract(node) &&
|
|
1012
|
+
// A `declare` field defines nothing: it asserts that a property is
|
|
1013
|
+
// established somewhere the class body does not show — a base
|
|
1014
|
+
// constructor, a decorator, a framework assigning by name — so the
|
|
1015
|
+
// definition the rename must move with is out of the fixer's reach,
|
|
1016
|
+
// exactly as an abstract declaration's implementors are (Bug #1949).
|
|
1017
|
+
!(node.type === utils_1.AST_NODE_TYPES.PropertyDefinition && node.declare) &&
|
|
686
1018
|
!(isProperty &&
|
|
687
1019
|
node.parent &&
|
|
688
1020
|
(isApiSurfaceValue(node.parent) || isReadByName(name)));
|
|
1021
|
+
// A class member's readers are `this.handleClick` sites in this same
|
|
1022
|
+
// file, so the rename owns them and rewrites them in one fix; `null`
|
|
1023
|
+
// means at least one reference is beyond its reach (Bug #1946).
|
|
1024
|
+
const referenceTargets = isRenameable && !isProperty
|
|
1025
|
+
? classMemberRenameTargets(node, name)
|
|
1026
|
+
: [];
|
|
1027
|
+
const canFix = isRenameable && referenceTargets !== null;
|
|
689
1028
|
context.report({
|
|
690
1029
|
node: key,
|
|
691
1030
|
messageId: 'callbackFunctionPrefix',
|
|
692
1031
|
data: { functionName: name },
|
|
693
1032
|
fix(fixer) {
|
|
694
|
-
|
|
1033
|
+
if (!canFix || !referenceTargets) {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
// One fix carrying every edit: ESLint applies a fix atomically, so
|
|
1037
|
+
// the declaration and its readers cannot be rewritten apart — a
|
|
1038
|
+
// partial application would leave the file broken.
|
|
1039
|
+
return [key, ...referenceTargets].map((target) => fixer.replaceText(target, newName));
|
|
695
1040
|
},
|
|
696
1041
|
});
|
|
697
1042
|
},
|
|
1043
|
+
// The declaration half of a class contract: `abstract handleSubmit(): T`
|
|
1044
|
+
// and `abstract handleSubmit: () => T`. The docs scope the subject by what
|
|
1045
|
+
// the member IS — "a function, method, class property, or parameter" — and
|
|
1046
|
+
// an abstract member is a method or a class property, so silence here is
|
|
1047
|
+
// a gap rather than a carve-out (Bug #1944).
|
|
1048
|
+
//
|
|
1049
|
+
// Report-only, and permanently so: the declaration binds every implementor
|
|
1050
|
+
// of the class, and implementors live in files a single-file fixer cannot
|
|
1051
|
+
// see, let alone edit atomically. Interface and type-literal members
|
|
1052
|
+
// (`TSMethodSignature`, `TSPropertySignature`) are deliberately NOT
|
|
1053
|
+
// reported here — a member of a type is a prop declaration, which this
|
|
1054
|
+
// rule governs through `callbackPropPrefix`, whose remedy is the OPPOSITE
|
|
1055
|
+
// one (`onSubmit`, not `submit`). Reporting both on one declaration would
|
|
1056
|
+
// hand the author contradictory instructions.
|
|
1057
|
+
'TSAbstractMethodDefinition, TSAbstractPropertyDefinition'(node) {
|
|
1058
|
+
const key = node.key;
|
|
1059
|
+
if (node.computed ||
|
|
1060
|
+
key.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
1061
|
+
!hasHandlePrefix(key.name)) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
context.report({
|
|
1065
|
+
node: key,
|
|
1066
|
+
messageId: 'callbackFunctionPrefix',
|
|
1067
|
+
data: { functionName: key.name },
|
|
1068
|
+
});
|
|
1069
|
+
},
|
|
698
1070
|
// Check constructor parameters
|
|
699
1071
|
TSParameterProperty(node) {
|
|
700
1072
|
if (node.parameter.type === 'Identifier' &&
|