@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.
Files changed (28) hide show
  1. package/lib/index.js +1 -1
  2. package/lib/rules/class-methods-read-top-to-bottom.js +37 -9
  3. package/lib/rules/consistent-callback-naming.js +393 -21
  4. package/lib/rules/enforce-assert-safe-object-key.js +59 -28
  5. package/lib/rules/enforce-assert-throws.js +51 -26
  6. package/lib/rules/enforce-boolean-naming-prefixes.js +47 -18
  7. package/lib/rules/enforce-centralized-mock-firestore.js +6 -11
  8. package/lib/rules/enforce-early-destructuring.js +12 -3
  9. package/lib/rules/enforce-firestore-doc-ref-generic.js +35 -21
  10. package/lib/rules/enforce-memoize-async.js +68 -23
  11. package/lib/rules/enforce-memoize-getters.js +44 -5
  12. package/lib/rules/no-passthrough-getters.js +50 -7
  13. package/lib/rules/parallelize-async-operations.js +53 -2
  14. package/lib/rules/prefer-docsetter-setall.js +28 -7
  15. package/lib/rules/prefer-getter-over-parameterless-method.js +111 -50
  16. package/lib/rules/prefer-map-over-conditional-dispatch.js +182 -20
  17. package/lib/rules/prefer-use-deep-compare-memo.js +6 -3
  18. package/lib/rules/prefer-utility-function-over-private-static.js +39 -4
  19. package/lib/rules/require-memoize-jsx-returners.js +64 -10
  20. package/lib/rules/semantic-function-prefixes.js +29 -4
  21. package/lib/utils/ASTHelpers.d.ts +14 -0
  22. package/lib/utils/ASTHelpers.js +67 -0
  23. package/lib/utils/graph/ClassGraphBuilder.d.ts +15 -1
  24. package/lib/utils/graph/ClassGraphBuilder.js +47 -6
  25. package/lib/utils/importInsertion.d.ts +17 -3
  26. package/lib/utils/importInsertion.js +24 -7
  27. package/package.json +1 -1
  28. package/release-manifest.json +180 -0
package/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.139',
226
+ version: '1.20.141',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- function getMemberName(member) {
7
- if (member.type === 'MethodDefinition' ||
8
- member.type === 'PropertyDefinition' ||
9
- member.type === 'TSAbstractMethodDefinition' ||
10
- member.type === 'TSAbstractPropertyDefinition' ||
11
- member.type === 'TSAbstractAccessorProperty') {
12
- return member.key.type === 'Identifier' ? member.key.name : null;
13
- }
14
- return null;
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
- const stack = [program];
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
- return names;
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
- return variable.scope.childScopes.some((child) => child.set.has(newName));
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
- // Check class methods and object methods
652
- 'MethodDefinition, Property'(node) {
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 canFix = isEmittableName(newName) &&
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
- return canFix ? fixer.replaceText(key, newName) : null;
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' &&