@blumintinc/eslint-plugin-blumint 1.20.185 → 1.20.187

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 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.185',
226
+ version: '1.20.187',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -154,13 +154,23 @@ const prologueLengthOf = (statements) => {
154
154
  return first === -1 ? statements.length : first;
155
155
  };
156
156
  /**
157
- * The children of `node` that the async function's own execution reaches.
157
+ * Whether the async function's own execution stops AT `node` rather than
158
+ * running what it holds.
158
159
  *
159
160
  * A nested function, a class static block and a `namespace` body are cut, the
160
161
  * same boundary {@link referenceSitesOf} draws: what they hold runs when THEY
161
162
  * are invoked, which is not a moment the relocated `await` moves, so their
162
163
  * contents say nothing about the placement.
164
+ *
165
+ * The predicate is asked of every node a reach walk touches, the handed root
166
+ * included. The boundary belongs to the NODE, not to the edge it is reached
167
+ * by: cutting children alone lets a walk seeded ON one — a hoisted `function`
168
+ * declaration standing as a statement of the region, or an `if` test that IS a
169
+ * closure — run through it and credit its body's guards and reads to the
170
+ * region (#2172, #2173).
163
171
  */
172
+ const isReachBoundary = (node) => isFunctionNode(node) || AWAIT_OPAQUE_BODIES.has(node.type);
173
+ /** The children of `node` that the async function's own execution reaches. */
164
174
  const reachedChildrenOf = (node) => {
165
175
  const children = [];
166
176
  for (const [key, value] of Object.entries(node)) {
@@ -170,17 +180,23 @@ const reachedChildrenOf = (node) => {
170
180
  continue;
171
181
  }
172
182
  for (const child of Array.isArray(value) ? value : [value]) {
173
- if (ASTHelpers_1.ASTHelpers.isNode(child) &&
174
- !isFunctionNode(child) &&
175
- !AWAIT_OPAQUE_BODIES.has(child.type)) {
183
+ if (ASTHelpers_1.ASTHelpers.isNode(child) && !isReachBoundary(child)) {
176
184
  children.push(child);
177
185
  }
178
186
  }
179
187
  }
180
188
  return children;
181
189
  };
190
+ /**
191
+ * Visits every node the async function's own execution reaches from `root`.
192
+ *
193
+ * `root` takes the same cut as the nodes below it: a walk handed a boundary
194
+ * visits nothing, because the question the callers ask — what runs behind the
195
+ * relocated `await` — has one answer for a nested body whether it is reached
196
+ * as a child or handed in directly.
197
+ */
182
198
  const walkReached = (root, visit) => {
183
- const pending = [root];
199
+ const pending = isReachBoundary(root) ? [] : [root];
184
200
  while (pending.length > 0) {
185
201
  const node = pending.pop();
186
202
  visit(node);
@@ -211,6 +227,12 @@ const pathTextOf = (node) => {
211
227
  return null;
212
228
  };
213
229
  const readPathsInto = (node, paths) => {
230
+ // The handed node takes the same cut as the children below it: a test that IS
231
+ // a function literal (`if (() => busy)`) evaluates to the closure, never to
232
+ // what the closure would read when called.
233
+ if (isReachBoundary(node)) {
234
+ return;
235
+ }
214
236
  const path = pathTextOf(node);
215
237
  if (path !== null) {
216
238
  paths.add(path);
@@ -101,10 +101,22 @@ const childNodesOf = (node) => {
101
101
  }
102
102
  return children;
103
103
  };
104
- /** The type nodes a declaration states about the type it declares. */
104
+ /**
105
+ * The type nodes a declaration states about the type it declares.
106
+ *
107
+ * An alias hands over its whole right-hand side, so every type its members name
108
+ * is reached. The interface spelling has to hand over as much or the two answer
109
+ * differently about the same declaration: its heritage clauses state what it
110
+ * inherits, and its body states what it declares. Reading the clauses alone
111
+ * left a reference type written as an interface MEMBER invisible, so
112
+ * `as Schema['user']` reported a schema the file states in full — while the
113
+ * alias `prefer-type-over-interface --fix` rewrites that interface into was
114
+ * already exempt, making one fix pass the difference between reporting and
115
+ * silence (#2189).
116
+ */
105
117
  const statedTypeNodesOf = (declaration) => declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration
106
118
  ? [declaration.typeAnnotation]
107
- : declaration.extends ?? [];
119
+ : [...(declaration.extends ?? []), declaration.body];
108
120
  /**
109
121
  * The expression an optional link wraps, so a receiver spelled with `?.` is
110
122
  * read as the expression it actually evaluates.
@@ -1146,19 +1146,40 @@ function classifyFunctionReturn(fn) {
1146
1146
  function isExemptFromBooleanNaming(fn) {
1147
1147
  return classifyFunctionReturn(fn) !== 'boolean';
1148
1148
  }
1149
+ /**
1150
+ * Strips the type-only expression wrappers (`fn as T`, `fn satisfies T`, `fn!`,
1151
+ * `<T>fn`) that can sit between a name and the value it is bound to. None of
1152
+ * them changes what the value is at runtime, so a wrapped function's return
1153
+ * shape is the shape of the function inside. Reading only the outermost node
1154
+ * made the validator carve-out depend on the assertion's presence (#2174).
1155
+ */
1156
+ function unwrapTypeOnlyExpression(node) {
1157
+ let current = node;
1158
+ while (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
1159
+ current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
1160
+ current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
1161
+ current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
1162
+ current = current.expression;
1163
+ }
1164
+ return current;
1165
+ }
1149
1166
  /**
1150
1167
  * When a declarator/property value is a function, whether that function is
1151
- * exempt from boolean negative-naming.
1168
+ * exempt from boolean negative-naming. The value is unwrapped first: exemption
1169
+ * turns on what the function returns, never on how the binding is asserted.
1152
1170
  */
1153
1171
  function isExemptFunctionValue(node) {
1154
- return (!!node &&
1155
- (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
1156
- node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1157
- // `abstract isNotBlank(value?: string): string | true;` declares the
1158
- // validator without a body, and must be exempt on the same grounds as
1159
- // the implementation that satisfies it.
1160
- node.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) &&
1161
- isExemptFromBooleanNaming(node));
1172
+ if (!node) {
1173
+ return false;
1174
+ }
1175
+ const value = unwrapTypeOnlyExpression(node);
1176
+ return ((value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
1177
+ value.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1178
+ // `abstract isNotBlank(value?: string): string | true;` declares the
1179
+ // validator without a body, and must be exempt on the same grounds as
1180
+ // the implementation that satisfies it.
1181
+ value.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) &&
1182
+ isExemptFromBooleanNaming(value));
1162
1183
  }
1163
1184
  /**
1164
1185
  * A member declared with a function type but no initializer
@@ -1548,6 +1569,12 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
1548
1569
  !(node.typeAnnotation?.typeAnnotation.type ===
1549
1570
  utils_1.AST_NODE_TYPES.TSBooleanKeyword))
1550
1571
  return;
1572
+ // A signature has no value, so its function type is the only place its
1573
+ // return shape can live. Skipping the annotation here exempted a
1574
+ // validator declared as a class field while reporting the identical
1575
+ // member declared in an interface or type literal (#2175).
1576
+ if (isExemptFunctionTypeAnnotation(node.typeAnnotation))
1577
+ return;
1551
1578
  // Ensure we have a valid property name
1552
1579
  const propertyName = node.key.name;
1553
1580
  if (!propertyName)
@@ -95,6 +95,108 @@ const renameWouldCollide = (variable, newName) => {
95
95
  // name would shadow whatever those uses resolve to.
96
96
  return scopeSubtreeReferencesName(declarationScope, newName);
97
97
  };
98
+ /**
99
+ * The member name a `this.<x>` access reads, whatever its spelling.
100
+ *
101
+ * Keying the check on the dot spelling alone left `this['settings']` invisible,
102
+ * so the rename shipped and stranded it — the class no longer had the member the
103
+ * getter reads (#1881/#1882). A computed access with a static string is the SAME
104
+ * member as the dot form, and the fixer cannot rewrite it either, so it has to
105
+ * count. `null` marks a genuinely dynamic key, which names no member statically
106
+ * and therefore strands nothing.
107
+ */
108
+ const staticMemberName = (node) => {
109
+ if (!node.computed) {
110
+ return node.property.type === utils_1.AST_NODE_TYPES.Identifier
111
+ ? node.property.name
112
+ : null;
113
+ }
114
+ if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
115
+ typeof node.property.value === 'string') {
116
+ return node.property.value;
117
+ }
118
+ if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
119
+ node.property.expressions.length === 0 &&
120
+ node.property.quasis.length === 1) {
121
+ return node.property.quasis[0].value.cooked;
122
+ }
123
+ return null;
124
+ };
125
+ /**
126
+ * How far from its declaration a stranded read of a parameter property can sit.
127
+ *
128
+ * A parameter property declares a class FIELD as well as a constructor-local
129
+ * binding, and `private` is what confines the field's legal readers to the class
130
+ * body: for a private field the class node is a complete scan root. Every other
131
+ * visibility — `public`, `protected`, and a modifier-less `readonly`, which is
132
+ * public — publishes the field to the whole file, so `widget.settings` in a
133
+ * sibling function, or `this.settings` in a subclass declared elsewhere in the
134
+ * file, outlives a declaration-only rename and points at a member the class no
135
+ * longer has. Handing the scan the enclosing class hid exactly those reads, so
136
+ * the fixer corrupted the file it was fixing (#2177/#2178).
137
+ */
138
+ const parameterPropertyScanRoot = (param, enclosingClass) => {
139
+ if (param.accessibility === 'private') {
140
+ return enclosingClass;
141
+ }
142
+ let root = enclosingClass;
143
+ while (root.parent) {
144
+ root = root.parent;
145
+ }
146
+ return root;
147
+ };
148
+ /**
149
+ * Reports whether renaming a constructor parameter property is unsafe to
150
+ * autofix.
151
+ *
152
+ * A parameter property (`private readonly settings: FooProps`) declares BOTH a
153
+ * constructor-local binding and a `this.settings` class field. The scope
154
+ * analyzer only models the binding, so a scope-driven rename rewrites the
155
+ * declaration while leaving every member read pointing at a name that no longer
156
+ * exists (Issue #1358). Since the field half of the rename cannot be resolved
157
+ * through scope analysis, the fix is withheld whenever the name occurs anywhere
158
+ * under `scanRoot` other than at its declaration.
159
+ *
160
+ * The member check is object-agnostic on purpose: `widget.settings` reads the
161
+ * same field as `this.settings`, and the fixer can rewrite neither.
162
+ */
163
+ const parameterPropertyRenameIsUnsafe = (scanRoot, name, declarationId) => {
164
+ let unsafe = false;
165
+ const visit = (node) => {
166
+ if (unsafe) {
167
+ return;
168
+ }
169
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
170
+ staticMemberName(node) === name) {
171
+ unsafe = true;
172
+ return;
173
+ }
174
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
175
+ node.name === name &&
176
+ node !== declarationId) {
177
+ unsafe = true;
178
+ return;
179
+ }
180
+ for (const key of Object.keys(node)) {
181
+ if (key === 'parent') {
182
+ continue;
183
+ }
184
+ const value = node[key];
185
+ if (Array.isArray(value)) {
186
+ for (const child of value) {
187
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
188
+ visit(child);
189
+ }
190
+ }
191
+ }
192
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
193
+ visit(value);
194
+ }
195
+ }
196
+ };
197
+ visit(scanRoot);
198
+ return unsafe;
199
+ };
98
200
  exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
99
201
  name: 'enforce-props-argument-name',
100
202
  meta: {
@@ -340,78 +442,6 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
340
442
  }
341
443
  });
342
444
  }
343
- /**
344
- * The member name a `this.<x>` access reads, whatever its spelling.
345
- *
346
- * Keying the check on the dot spelling alone left `this['settings']`
347
- * invisible, so the rename shipped and stranded it — the class no longer had
348
- * the member the getter reads (#1881). A computed access with a static
349
- * string is the SAME member as the dot form, and the fixer cannot rewrite it
350
- * either, so it has to count. `null` marks a genuinely dynamic key, which
351
- * names no member statically.
352
- */
353
- function staticMemberName(node) {
354
- if (!node.computed) {
355
- return node.property.type === utils_1.AST_NODE_TYPES.Identifier
356
- ? node.property.name
357
- : null;
358
- }
359
- if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
360
- typeof node.property.value === 'string') {
361
- return node.property.value;
362
- }
363
- if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
364
- node.property.expressions.length === 0 &&
365
- node.property.quasis.length === 1) {
366
- return node.property.quasis[0].value.cooked;
367
- }
368
- return null;
369
- }
370
- // Determine whether renaming a constructor parameter property is unsafe to
371
- // autofix. A parameter property (`private readonly foo: T`) creates BOTH a
372
- // constructor-local binding and a `this.foo` class field, so a
373
- // declaration-only rename would leave dangling references: plain `foo`
374
- // usages inside the constructor (e.g. `super(foo)`) and `this.foo` accesses
375
- // anywhere in the class. Mirrors #1123 — when a rename cannot be applied
376
- // everywhere syntactically, emit no fix rather than corrupt the code.
377
- function parameterPropertyRenameIsUnsafe(classNode, name, declarationId) {
378
- let unsafe = false;
379
- const visit = (node) => {
380
- if (unsafe) {
381
- return;
382
- }
383
- if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
384
- node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
385
- staticMemberName(node) === name) {
386
- unsafe = true;
387
- return;
388
- }
389
- if (node.type === utils_1.AST_NODE_TYPES.Identifier &&
390
- node.name === name &&
391
- node !== declarationId) {
392
- unsafe = true;
393
- return;
394
- }
395
- for (const key of Object.keys(node)) {
396
- if (key === 'parent') {
397
- continue;
398
- }
399
- const value = node[key];
400
- if (Array.isArray(value)) {
401
- for (const child of value) {
402
- if (ASTHelpers_1.ASTHelpers.isNode(child)) {
403
- visit(child);
404
- }
405
- }
406
- }
407
- else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
408
- visit(value);
409
- }
410
- }
411
- };
412
- visit(classNode);
413
- return unsafe;
414
- }
415
445
  // Find the class (declaration or expression) that owns a method definition.
416
446
  function getEnclosingClass(node) {
417
447
  const classBody = node.parent;
@@ -467,11 +497,11 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
467
497
  fix: (fixer) => {
468
498
  // A parameter-property rename touches both the parameter and
469
499
  // the `this.<name>` field; refuse to autofix when the name is
470
- // referenced elsewhere, since a declaration-only rename would
471
- // leave dangling references.
500
+ // referenced anywhere within reach of that field, since a
501
+ // declaration-only rename would leave dangling references.
472
502
  if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
473
503
  enclosingClass &&
474
- parameterPropertyRenameIsUnsafe(enclosingClass, id.name, id)) {
504
+ parameterPropertyRenameIsUnsafe(parameterPropertyScanRoot(param, enclosingClass), id.name, id)) {
475
505
  return null;
476
506
  }
477
507
  return buildParameterRenameFixes(fixer, method, id, suggestedName);
@@ -30,27 +30,50 @@ const getEnclosingClass = (node) => {
30
30
  return null;
31
31
  };
32
32
  /**
33
- * Reports whether renaming a constructor parameter property is unsafe to
34
- * autofix.
33
+ * The identifier a parameter declares, whatever wrappers it carries.
35
34
  *
36
- * A parameter property (`private readonly settings: FooProps`) declares BOTH a
37
- * constructor-local binding and a `this.settings` class field. The scope
38
- * analyzer only models the binding, so a scope-driven rename silently rewrites
39
- * the field declaration while leaving every `this.settings` access and every
40
- * plain `settings` use the analyzer attributes elsewhere pointing at a name
41
- * that no longer exists (Issue #1358). Since the field half of the rename
42
- * cannot be resolved through scope analysis, the fix is withheld whenever the
43
- * name occurs anywhere in the class other than at its declaration.
35
+ * A default value nests the identifier in an `AssignmentPattern`, and a
36
+ * constructor parameter property nests it in a `TSParameterProperty` possibly
37
+ * both at once (`private readonly alpha: AProps = fallback`). Keying the
38
+ * multi-Props deferral counter on a bare `Identifier` dropped every defaulted
39
+ * parameter, so a two-Props signature counted as one and this rule reported a
40
+ * rename to `props` that its authoritative sibling contradicts with a prefixed
41
+ * name (#2180). Mirrors `getIdFromParam` in enforce-props-argument-name so both
42
+ * rules count the same parameters.
44
43
  */
44
+ const getIdFromParam = (param) => {
45
+ if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
46
+ return param;
47
+ }
48
+ if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
49
+ param.left.type === utils_1.AST_NODE_TYPES.Identifier) {
50
+ return param.left;
51
+ }
52
+ if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
53
+ const inner = param.parameter;
54
+ if (inner.type === utils_1.AST_NODE_TYPES.Identifier) {
55
+ return inner;
56
+ }
57
+ if (inner.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
58
+ inner.left.type === utils_1.AST_NODE_TYPES.Identifier) {
59
+ return inner.left;
60
+ }
61
+ }
62
+ if (param.type === utils_1.AST_NODE_TYPES.RestElement &&
63
+ param.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
64
+ return param.argument;
65
+ }
66
+ return null;
67
+ };
45
68
  /**
46
69
  * The member name a `this.<x>` access reads, whatever its spelling.
47
70
  *
48
71
  * Keying the check on the dot spelling alone left `this['settings']` invisible,
49
72
  * so the rename shipped and stranded it — the class no longer had the member the
50
- * getter reads (#1882, the sibling of #1881). A computed access with a static
51
- * string is the SAME member as the dot form, and the fixer cannot rewrite it
52
- * either, so it has to count. `null` marks a genuinely dynamic key, which names
53
- * no member statically and therefore strands nothing.
73
+ * getter reads (#1881/#1882). A computed access with a static string is the SAME
74
+ * member as the dot form, and the fixer cannot rewrite it either, so it has to
75
+ * count. `null` marks a genuinely dynamic key, which names no member statically
76
+ * and therefore strands nothing.
54
77
  */
55
78
  const staticMemberName = (node) => {
56
79
  if (!node.computed) {
@@ -69,14 +92,51 @@ const staticMemberName = (node) => {
69
92
  }
70
93
  return null;
71
94
  };
72
- const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
95
+ /**
96
+ * How far from its declaration a stranded read of a parameter property can sit.
97
+ *
98
+ * A parameter property declares a class FIELD as well as a constructor-local
99
+ * binding, and `private` is what confines the field's legal readers to the class
100
+ * body: for a private field the class node is a complete scan root. Every other
101
+ * visibility — `public`, `protected`, and a modifier-less `readonly`, which is
102
+ * public — publishes the field to the whole file, so `widget.settings` in a
103
+ * sibling function, or `this.settings` in a subclass declared elsewhere in the
104
+ * file, outlives a declaration-only rename and points at a member the class no
105
+ * longer has. Handing the scan the enclosing class hid exactly those reads, so
106
+ * the fixer corrupted the file it was fixing (#2177/#2178).
107
+ */
108
+ const parameterPropertyScanRoot = (param, enclosingClass) => {
109
+ if (param.accessibility === 'private') {
110
+ return enclosingClass;
111
+ }
112
+ let root = enclosingClass;
113
+ while (root.parent) {
114
+ root = root.parent;
115
+ }
116
+ return root;
117
+ };
118
+ /**
119
+ * Reports whether renaming a constructor parameter property is unsafe to
120
+ * autofix.
121
+ *
122
+ * A parameter property (`private readonly settings: FooProps`) declares BOTH a
123
+ * constructor-local binding and a `this.settings` class field. The scope
124
+ * analyzer only models the binding, so a scope-driven rename rewrites the
125
+ * declaration while leaving every member read pointing at a name that no longer
126
+ * exists (Issue #1358). Since the field half of the rename cannot be resolved
127
+ * through scope analysis, the fix is withheld whenever the name occurs anywhere
128
+ * under `scanRoot` other than at its declaration.
129
+ *
130
+ * The member check is object-agnostic on purpose: `widget.settings` reads the
131
+ * same field as `this.settings`, and the fixer can rewrite neither.
132
+ */
133
+ const parameterPropertyRenameIsUnsafe = (scanRoot, name, declarationId) => {
73
134
  let unsafe = false;
74
135
  const visit = (node) => {
75
136
  if (unsafe) {
76
137
  return;
77
138
  }
78
139
  if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
79
- node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
80
140
  staticMemberName(node) === name) {
81
141
  unsafe = true;
82
142
  return;
@@ -104,7 +164,7 @@ const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
104
164
  }
105
165
  }
106
166
  };
107
- visit(classNode);
167
+ visit(scanRoot);
108
168
  return unsafe;
109
169
  };
110
170
  exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
@@ -133,6 +193,20 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
133
193
  }
134
194
  return null;
135
195
  }
196
+ // Whether a parameter carries a `*Props` type, counted for the multi-Props
197
+ // deferral. The identifier is unwrapped from its default value and/or
198
+ // parameter-property wrapper first: a parameter this misses is a Props
199
+ // parameter this rule cannot see, which turns a multi-Props signature — the
200
+ // case it defers to enforce-props-argument-name — into a lone parameter it
201
+ // renames to `props` against the sibling's prefixed suggestion (#2180).
202
+ function isPropsTypedParam(param) {
203
+ const id = getIdFromParam(param);
204
+ if (!id || !id.typeAnnotation || !id.typeAnnotation.typeAnnotation) {
205
+ return false;
206
+ }
207
+ const typeName = getTypeName(id.typeAnnotation.typeAnnotation);
208
+ return !!typeName && typeName.endsWith('Props');
209
+ }
136
210
  // Check if a parameter should be named "props"
137
211
  function shouldBeNamedProps(param) {
138
212
  // Only check non-destructured parameters
@@ -205,14 +279,7 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
205
279
  return;
206
280
  }
207
281
  // Skip functions with multiple parameters that have Props types
208
- const propsTypeParams = node.params.filter((param) => {
209
- if (param.type !== utils_1.AST_NODE_TYPES.Identifier)
210
- return false;
211
- if (!param.typeAnnotation || !param.typeAnnotation.typeAnnotation)
212
- return false;
213
- const typeName = getTypeName(param.typeAnnotation.typeAnnotation);
214
- return typeName && typeName.endsWith('Props');
215
- });
282
+ const propsTypeParams = node.params.filter(isPropsTypedParam);
216
283
  if (propsTypeParams.length > 1) {
217
284
  return; // Skip functions with multiple Props parameters
218
285
  }
@@ -236,28 +303,27 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
236
303
  }
237
304
  const constructor = node.value;
238
305
  // Skip constructors with multiple parameters that have Props types
239
- const propsTypeParams = constructor.params.filter((param) => {
240
- if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
241
- if (!param.typeAnnotation || !param.typeAnnotation.typeAnnotation)
242
- return false;
243
- const typeName = getTypeName(param.typeAnnotation.typeAnnotation);
244
- return typeName && typeName.endsWith('Props');
245
- }
246
- else if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty &&
247
- param.parameter.type === utils_1.AST_NODE_TYPES.Identifier) {
248
- if (!param.parameter.typeAnnotation ||
249
- !param.parameter.typeAnnotation.typeAnnotation)
250
- return false;
251
- const typeName = getTypeName(param.parameter.typeAnnotation.typeAnnotation);
252
- return typeName && typeName.endsWith('Props');
253
- }
254
- return false;
255
- });
306
+ const propsTypeParams = constructor.params.filter(isPropsTypedParam);
256
307
  if (propsTypeParams.length > 1) {
257
308
  return; // Skip constructors with multiple Props parameters
258
309
  }
259
310
  const enclosingClass = getEnclosingClass(node);
311
+ // When the enclosing class extends a base class, a constructor parameter
312
+ // property (e.g. `private readonly fullProps: SubProps`) cannot be safely
313
+ // renamed to `props`: the base class may already declare a private `props`
314
+ // field/parameter-property, so the rename would create a TS2415 private-
315
+ // field collision, and it would also leave `super(fullProps)` /
316
+ // `this.fullProps` references dangling. enforce-props-argument-name — the
317
+ // authoritative rule this one defers to — treats a distinct name on a
318
+ // subclass parameter property as intentional (#1276), so reporting it here
319
+ // would make that carve-out void in the shipped config and hand the
320
+ // consumer an error it cannot satisfy (#2179).
321
+ const classExtendsBase = !!enclosingClass?.superClass;
260
322
  for (const param of constructor.params) {
323
+ if (classExtendsBase &&
324
+ param.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
325
+ continue;
326
+ }
261
327
  if (shouldBeNamedProps(param) &&
262
328
  param.type === utils_1.AST_NODE_TYPES.Identifier &&
263
329
  !isPropsNameWithPrefix(param.name)) {
@@ -280,9 +346,9 @@ exports.enforcePropsNamingConsistency = (0, createRule_1.createRule)({
280
346
  fix: (fixer) => {
281
347
  // A parameter property also declares a `this.<name>` field the
282
348
  // scope analyzer does not model, so renaming it is only safe
283
- // when the name appears nowhere else in the class.
349
+ // when the name appears nowhere else within reach of that field.
284
350
  if (!enclosingClass ||
285
- parameterPropertyRenameIsUnsafe(enclosingClass, declarationId.name, declarationId)) {
351
+ parameterPropertyRenameIsUnsafe(parameterPropertyScanRoot(param, enclosingClass), declarationId.name, declarationId)) {
286
352
  return null;
287
353
  }
288
354
  return buildParameterRenameFixes(fixer, constructor, declarationId);
@@ -1297,27 +1297,37 @@ function collectFunctionBodyDependencies(fn, dependencies, context) {
1297
1297
  includeFunctionCaptures: true,
1298
1298
  });
1299
1299
  let resolved = true;
1300
- traverseAst(fn.body, {
1301
- skipFunctions: true,
1302
- visit(current) {
1303
- if (current.type !== utils_1.AST_NODE_TYPES.CallExpression &&
1304
- current.type !== utils_1.AST_NODE_TYPES.ChainExpression) {
1305
- return undefined;
1306
- }
1307
- const callExpression = current.type === utils_1.AST_NODE_TYPES.CallExpression
1308
- ? current
1309
- : extractCallExpression(current);
1310
- if (!callExpression) {
1300
+ const resolveCallsIn = (region) => {
1301
+ traverseAst(region, {
1302
+ skipFunctions: true,
1303
+ visit(current) {
1304
+ if (current.type !== utils_1.AST_NODE_TYPES.CallExpression &&
1305
+ current.type !== utils_1.AST_NODE_TYPES.ChainExpression) {
1306
+ return undefined;
1307
+ }
1308
+ const callExpression = current.type === utils_1.AST_NODE_TYPES.CallExpression
1309
+ ? current
1310
+ : extractCallExpression(current);
1311
+ if (!callExpression) {
1312
+ return undefined;
1313
+ }
1314
+ const nestedResolved = collectCalleeDependencies(context.body, callExpression.callee, dependencies, context.callIndex, context.visitedCallees);
1315
+ if (!nestedResolved) {
1316
+ resolved = false;
1317
+ return { skipChildren: true };
1318
+ }
1311
1319
  return undefined;
1312
- }
1313
- const nestedResolved = collectCalleeDependencies(context.body, callExpression.callee, dependencies, context.callIndex, context.visitedCallees);
1314
- if (!nestedResolved) {
1315
- resolved = false;
1316
- return { skipChildren: true };
1317
- }
1318
- return undefined;
1319
- },
1320
- });
1320
+ },
1321
+ });
1322
+ };
1323
+ // Parameter initializers run on entry, so a call sitting in a default reaches
1324
+ // the callee's captures exactly as a call in the body does. Restricting this
1325
+ // walk to the body contributes the callee's NAME without any of its captures,
1326
+ // and the reordering fix then hoists the effect above a binding that callee
1327
+ // reads -- a TDZ ReferenceError at runtime with no lint-visible symptom.
1328
+ // `collectFunctionCaptures` above spans `fn.params` for the same reason.
1329
+ fn.params.forEach((param) => resolveCallsIn(param));
1330
+ resolveCallsIn(fn.body);
1321
1331
  return resolved;
1322
1332
  }
1323
1333
  function resolveValueForIdentifier(body, name, beforeIndex) {
@@ -661,7 +661,12 @@ exports.noEmptyDependencyUseCallbacks = (0, createRule_1.createRule)({
661
661
  const callback = getCallbackArg(callExpression);
662
662
  if (!callback)
663
663
  return;
664
- if (ASTHelpers_1.ASTHelpers.returnsJSX(callback.body, context))
664
+ // The exemption is for a callback that RENDERS. Handing `callback.body`
665
+ // let a callback returning a FUNCTION claim it, because returnsJSX
666
+ // unwraps a handed function instead of judging it as a value — so
667
+ // `useCallback(() => () => <div/>, [])` was exempt while the identical
668
+ // block-bodied spelling reported (#2192).
669
+ if (ASTHelpers_1.ASTHelpers.returnsJSX(callback, context))
665
670
  return;
666
671
  const extraTypeRoots = [];
667
672
  if (callExpression.parent &&