@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.0
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/README.md +28 -5
- package/lib/index.js +7 -1
- package/lib/rules/consistent-callback-naming.js +26 -0
- package/lib/rules/dynamic-https-errors.js +5 -26
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
- package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- package/lib/rules/enforce-memoize-async.js +1 -4
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-verb-noun-naming.js +3 -0
- package/lib/rules/global-const-style.js +9 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
- package/lib/rules/memo-nested-react-components.js +143 -8
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-circular-references.js +145 -482
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-entire-object-hook-deps.js +132 -97
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +1 -3
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/react-memoize-literals.js +87 -1
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +15 -0
- package/lib/utils/ASTHelpers.js +48 -0
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -252,18 +252,134 @@ const getVariableName = (node) => {
|
|
|
252
252
|
}
|
|
253
253
|
return null;
|
|
254
254
|
};
|
|
255
|
-
const
|
|
255
|
+
const isEnclosingFunction = (node) => {
|
|
256
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
257
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
258
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Nearest ancestor function whose body directly contains `node`. Climbs from
|
|
262
|
+
* the node's parent so a function node itself is not treated as its own
|
|
263
|
+
* enclosing function.
|
|
264
|
+
*/
|
|
265
|
+
const findEnclosingFunction = (node) => {
|
|
256
266
|
let parent = node.parent;
|
|
257
267
|
while (parent) {
|
|
258
|
-
if (parent
|
|
259
|
-
parent
|
|
260
|
-
parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
261
|
-
return true;
|
|
268
|
+
if (isEnclosingFunction(parent)) {
|
|
269
|
+
return parent;
|
|
262
270
|
}
|
|
263
271
|
parent = parent.parent;
|
|
264
272
|
}
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
const isInsideFunction = (node) => {
|
|
276
|
+
return findEnclosingFunction(node) !== null;
|
|
277
|
+
};
|
|
278
|
+
const isPascalCaseName = (name) => /^[A-Z]/.test(name);
|
|
279
|
+
/**
|
|
280
|
+
* A return value is a *component* (vs. rendered output) when it is a
|
|
281
|
+
* memo()/forwardRef() call, a PascalCase identifier (returned component
|
|
282
|
+
* reference), or an inline function that itself creates a component. Such a
|
|
283
|
+
* value identifies the surrounding function as an HOC factory rather than a
|
|
284
|
+
* render body.
|
|
285
|
+
*/
|
|
286
|
+
const returnExpressionIsComponent = (expression, reactImports) => {
|
|
287
|
+
if (!expression) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const unwrapped = unwrapExpression(expression);
|
|
291
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
292
|
+
if (isMemoCall(unwrapped, reactImports) ||
|
|
293
|
+
isForwardRefCall(unwrapped, reactImports)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
299
|
+
return isPascalCaseName(unwrapped.name);
|
|
300
|
+
}
|
|
301
|
+
if (isFunctionExpression(unwrapped)) {
|
|
302
|
+
return Boolean(functionCreatesComponent(unwrapped, reactImports));
|
|
303
|
+
}
|
|
265
304
|
return false;
|
|
266
305
|
};
|
|
306
|
+
const returnExpressionIsJsx = (expression) => {
|
|
307
|
+
if (!expression) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const unwrapped = unwrapExpression(expression);
|
|
311
|
+
return (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
312
|
+
unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Direct return-statement arguments of a function, traversing control flow
|
|
316
|
+
* (if/switch/try/loops) but NOT descending into nested function definitions—
|
|
317
|
+
* returns inside nested functions belong to those functions, not this one.
|
|
318
|
+
*/
|
|
319
|
+
const collectDirectReturnExpressions = (fn) => {
|
|
320
|
+
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
321
|
+
return [fn.body];
|
|
322
|
+
}
|
|
323
|
+
const returns = [];
|
|
324
|
+
const visitStatement = (statement) => {
|
|
325
|
+
switch (statement.type) {
|
|
326
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
327
|
+
returns.push(statement.argument);
|
|
328
|
+
break;
|
|
329
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
330
|
+
statement.body.forEach(visitStatement);
|
|
331
|
+
break;
|
|
332
|
+
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
333
|
+
visitStatement(statement.consequent);
|
|
334
|
+
if (statement.alternate) {
|
|
335
|
+
visitStatement(statement.alternate);
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement:
|
|
339
|
+
for (const switchCase of statement.cases) {
|
|
340
|
+
switchCase.consequent.forEach(visitStatement);
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
344
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
345
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
346
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
347
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
348
|
+
visitStatement(statement.body);
|
|
349
|
+
break;
|
|
350
|
+
case utils_1.AST_NODE_TYPES.TryStatement:
|
|
351
|
+
visitStatement(statement.block);
|
|
352
|
+
if (statement.handler) {
|
|
353
|
+
visitStatement(statement.handler.body);
|
|
354
|
+
}
|
|
355
|
+
if (statement.finalizer) {
|
|
356
|
+
visitStatement(statement.finalizer);
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
default:
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
fn.body.body.forEach(visitStatement);
|
|
364
|
+
return returns;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* True when the nearest enclosing function is an HOC factory—it returns a
|
|
368
|
+
* component (memo/forwardRef/component reference) and never returns JSX. Such
|
|
369
|
+
* a function runs once per call, so components defined inside it have stable
|
|
370
|
+
* identities and must NOT be flagged. A function that returns JSX is a render
|
|
371
|
+
* body, where nested components DO remount and remain flagged.
|
|
372
|
+
*/
|
|
373
|
+
const isInsideHocFactory = (node, reactImports) => {
|
|
374
|
+
const enclosing = findEnclosingFunction(node);
|
|
375
|
+
if (!enclosing) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const returns = collectDirectReturnExpressions(enclosing);
|
|
379
|
+
const returnsComponent = returns.some((expression) => returnExpressionIsComponent(expression, reactImports));
|
|
380
|
+
const returnsJsx = returns.some((expression) => returnExpressionIsJsx(expression));
|
|
381
|
+
return returnsComponent && !returnsJsx;
|
|
382
|
+
};
|
|
267
383
|
exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
268
384
|
name: 'memo-nested-react-components',
|
|
269
385
|
meta: {
|
|
@@ -343,7 +459,18 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
343
459
|
return;
|
|
344
460
|
}
|
|
345
461
|
}
|
|
346
|
-
const
|
|
462
|
+
const variableName = getVariableName(node);
|
|
463
|
+
// A non-PascalCase binding (e.g. renderHit) is a render callback used
|
|
464
|
+
// with a render={...} prop, not a component—skip it.
|
|
465
|
+
if (variableName && !isPascalCaseName(variableName)) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Inside an HOC factory the binding has a stable identity, so it does
|
|
469
|
+
// not remount on re-render and must not be flagged.
|
|
470
|
+
if (isInsideHocFactory(node, reactImports)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const componentName = variableName ??
|
|
347
474
|
(isFunctionExpression(callback) &&
|
|
348
475
|
callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
|
|
349
476
|
? callback.id.name
|
|
@@ -356,10 +483,14 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
356
483
|
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
357
484
|
return;
|
|
358
485
|
// Only check if name starts with uppercase (convention for components)
|
|
359
|
-
if (
|
|
486
|
+
if (!isPascalCaseName(node.id.name))
|
|
360
487
|
return;
|
|
361
488
|
if (!isInsideFunction(node))
|
|
362
489
|
return;
|
|
490
|
+
// Inside an HOC factory the component has a stable identity (the factory
|
|
491
|
+
// runs once per call), so it does not remount and must not be flagged.
|
|
492
|
+
if (isInsideHocFactory(node, reactImports))
|
|
493
|
+
return;
|
|
363
494
|
// Skip if it's already a hook call (handled by CallExpression visitor)
|
|
364
495
|
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
365
496
|
const hook = isHookCall(node.init.callee);
|
|
@@ -375,10 +506,14 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
375
506
|
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
376
507
|
},
|
|
377
508
|
FunctionDeclaration(node) {
|
|
378
|
-
if (!node.id ||
|
|
509
|
+
if (!node.id || !isPascalCaseName(node.id.name))
|
|
379
510
|
return;
|
|
380
511
|
if (!isInsideFunction(node))
|
|
381
512
|
return;
|
|
513
|
+
// Inside an HOC factory the component has a stable identity (the factory
|
|
514
|
+
// runs once per call), so it does not remount and must not be flagged.
|
|
515
|
+
if (isInsideHocFactory(node, reactImports))
|
|
516
|
+
return;
|
|
382
517
|
const componentMatch = functionCreatesComponent(node, reactImports);
|
|
383
518
|
if (!componentMatch)
|
|
384
519
|
return;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noArrayLengthInDeps = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
// React hooks to check
|
|
7
8
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
8
9
|
const DEFAULT_HASH_IMPORT = {
|
|
@@ -58,7 +59,7 @@ function getLastPropertyName(expr) {
|
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
function generateUniqueName(base, taken) {
|
|
61
|
-
|
|
62
|
+
const candidate = `${base}Hash`;
|
|
62
63
|
if (!taken.has(candidate))
|
|
63
64
|
return candidate;
|
|
64
65
|
let i = 2;
|
|
@@ -120,6 +121,69 @@ function isUseMemoImported(sourceCode) {
|
|
|
120
121
|
}
|
|
121
122
|
return false;
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Hook callback = first function-typed argument (the effect/factory/memo fn).
|
|
126
|
+
* Deps array is the LAST argument; the callback precedes it.
|
|
127
|
+
*/
|
|
128
|
+
function getHookCallback(node) {
|
|
129
|
+
for (const arg of node.arguments) {
|
|
130
|
+
if (arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
131
|
+
arg.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
132
|
+
return arg;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function rangeContains(outer, inner) {
|
|
138
|
+
return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* True when `identifier` is the object of a non-computed `.length` access,
|
|
142
|
+
* i.e. the reference reads only the array's length, not its contents.
|
|
143
|
+
*/
|
|
144
|
+
function isLengthAccessOf(identifier) {
|
|
145
|
+
const parent = identifier.parent;
|
|
146
|
+
return (!!parent &&
|
|
147
|
+
parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
148
|
+
!parent.computed &&
|
|
149
|
+
parent.object === identifier &&
|
|
150
|
+
parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
151
|
+
parent.property.name === 'length');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decide whether a `<array>.length` dependency is safe to keep (suppress the
|
|
155
|
+
* report) by inspecting how the hook callback body uses the array binding.
|
|
156
|
+
*
|
|
157
|
+
* Safe (suppress) only when the array is referenced at least once inside the
|
|
158
|
+
* callback body AND every such reference is the object of a `.length` access —
|
|
159
|
+
* then depending on `.length` correctly avoids reruns on content changes.
|
|
160
|
+
*
|
|
161
|
+
* Returns false (keep reporting) for any non-`.length` use (element access,
|
|
162
|
+
* spread, iteration/method calls, bare reference, passed as an argument), when
|
|
163
|
+
* the array is never referenced in the body, and whenever the base cannot be
|
|
164
|
+
* confidently resolved (complex member-chain bases) — never hide a real bug.
|
|
165
|
+
*/
|
|
166
|
+
function isLengthOnlyUsage(context, callback, baseExpr) {
|
|
167
|
+
// Only resolvable for a bare identifier base (e.g. `items` in `items.length`).
|
|
168
|
+
// Member-chain bases (`a.b`, `data?.items`) have no single binding to track.
|
|
169
|
+
if (baseExpr.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// Resolve the binding the DEP refers to (from the deps-array identifier's
|
|
173
|
+
// scope), so a binding shadowed inside the callback body is not mistaken for
|
|
174
|
+
// it — the outer binding then has zero body references and we keep reporting.
|
|
175
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, baseExpr);
|
|
176
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, baseExpr.name);
|
|
177
|
+
if (!variable) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const bodyReferences = variable.references.filter((ref) => rangeContains(callback.body, ref.identifier));
|
|
181
|
+
// No body usage → not the content-vs-length bug, but keep current behavior.
|
|
182
|
+
if (bodyReferences.length === 0) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return bodyReferences.every((ref) => isLengthAccessOf(ref.identifier));
|
|
186
|
+
}
|
|
123
187
|
function isStableHashImported(sourceCode, hashSource, hashImportName) {
|
|
124
188
|
const program = sourceCode.ast;
|
|
125
189
|
for (const node of program.body) {
|
|
@@ -188,15 +252,22 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
188
252
|
return;
|
|
189
253
|
// Collect .length deps
|
|
190
254
|
const lengthDeps = [];
|
|
255
|
+
const callback = getHookCallback(node);
|
|
191
256
|
for (const el of depsArg.elements) {
|
|
192
257
|
if (!el)
|
|
193
258
|
continue;
|
|
194
259
|
if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
195
260
|
continue;
|
|
196
261
|
const member = getLengthMember(el);
|
|
197
|
-
if (member)
|
|
198
|
-
|
|
262
|
+
if (!member)
|
|
263
|
+
continue;
|
|
264
|
+
// Suppress when the callback body reads only `<array>.length`, never
|
|
265
|
+
// the array's contents — then `.length` is the correct dependency.
|
|
266
|
+
if (callback &&
|
|
267
|
+
isLengthOnlyUsage(context, callback, getBaseExpression(member))) {
|
|
268
|
+
continue;
|
|
199
269
|
}
|
|
270
|
+
lengthDeps.push({ element: el, member });
|
|
200
271
|
}
|
|
201
272
|
if (lengthDeps.length === 0)
|
|
202
273
|
return;
|