@blumintinc/eslint-plugin-blumint 1.20.185 → 1.20.186
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/enforce-dynamic-firebase-imports.js +27 -5
- package/lib/rules/enforce-firestore-doc-ref-generic.js +14 -2
- package/lib/rules/enforce-positive-naming.js +36 -9
- package/lib/rules/enforce-props-argument-name.js +105 -75
- package/lib/rules/enforce-props-naming-consistency.js +110 -44
- package/lib/rules/logical-top-to-bottom-grouping.js +30 -20
- package/lib/rules/no-empty-dependency-use-callbacks.js +6 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +71 -14
- package/lib/rules/no-unmemoized-memo-without-props.js +6 -1
- package/lib/rules/no-unused-props.js +62 -20
- package/lib/rules/parallelize-async-operations.js +33 -0
- package/lib/rules/prefer-utility-function-own-file.js +23 -15
- package/lib/rules/prevent-children-clobber.js +5 -1
- package/lib/rules/react-memoize-literals.js +57 -31
- package/lib/rules/react-usememo-should-be-component.js +105 -58
- package/lib/rules/require-memo.js +4 -2
- package/lib/rules/require-props-composition.js +186 -22
- package/lib/rules/vertically-group-related-functions.js +10 -0
- package/lib/utils/ASTHelpers.d.ts +38 -0
- package/lib/utils/ASTHelpers.js +103 -47
- package/package.json +1 -1
- package/release-manifest.json +179 -0
package/lib/index.js
CHANGED
|
@@ -154,13 +154,23 @@ const prologueLengthOf = (statements) => {
|
|
|
154
154
|
return first === -1 ? statements.length : first;
|
|
155
155
|
};
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
|
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
|
-
*
|
|
34
|
-
* autofix.
|
|
33
|
+
* The identifier a parameter declares, whatever wrappers it carries.
|
|
35
34
|
*
|
|
36
|
-
* A
|
|
37
|
-
* constructor
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
current.type !== utils_1.AST_NODE_TYPES.
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
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 &&
|