@blumintinc/eslint-plugin-blumint 1.16.0 → 1.16.2
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-assert-safe-object-key.js +29 -0
- package/lib/rules/enforce-dynamic-imports.d.ts +3 -0
- package/lib/rules/enforce-dynamic-imports.js +69 -19
- package/lib/rules/logical-top-to-bottom-grouping.js +87 -19
- package/lib/rules/no-hungarian.js +11 -0
- package/lib/rules/require-memo.js +8 -0
- package/package.json +1 -1
- package/release-manifest.json +52 -0
- package/lib/rules/prefer-memoized-props.d.ts +0 -3
- package/lib/rules/prefer-memoized-props.js +0 -445
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +0 -7
- package/lib/rules/prefer-nullish-coalescing-override.js +0 -188
- package/lib/rules/require-usememo-object-literals.d.ts +0 -4
- package/lib/rules/require-usememo-object-literals.js +0 -64
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.enforceAssertSafeObjectKey = 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
|
const DEFAULT_IMPORT_PATH = 'functions/src/util/assertSafe';
|
|
7
8
|
exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
8
9
|
name: 'enforce-assert-safe-object-key',
|
|
@@ -66,6 +67,29 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
66
67
|
return createFixes(fixer, node, expressionText);
|
|
67
68
|
},
|
|
68
69
|
});
|
|
70
|
+
/**
|
|
71
|
+
* Returns true when the identifier was initialized directly from an
|
|
72
|
+
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)`.
|
|
73
|
+
* Only direct, single-step initializers count — transitive aliases
|
|
74
|
+
* (const b = a) are not followed so they continue to be flagged.
|
|
75
|
+
* findVariableInScope returns the nearest binding, so an inner variable
|
|
76
|
+
* that shadows an outer assertSafe-initialized one is correctly not exempt.
|
|
77
|
+
*/
|
|
78
|
+
const isAssertSafeValidatedIdentifier = (node) => {
|
|
79
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
80
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, node.name);
|
|
81
|
+
if (!variable)
|
|
82
|
+
return false;
|
|
83
|
+
return variable.defs.some((def) => {
|
|
84
|
+
const init = def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator
|
|
85
|
+
? def.node.init
|
|
86
|
+
: null;
|
|
87
|
+
return (!!init &&
|
|
88
|
+
init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
89
|
+
init.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
90
|
+
init.callee.name === 'assertSafe');
|
|
91
|
+
});
|
|
92
|
+
};
|
|
69
93
|
return {
|
|
70
94
|
ImportDeclaration(node) {
|
|
71
95
|
// Check if assertSafe is already imported
|
|
@@ -193,6 +217,11 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
193
217
|
if (isLikelyArray) {
|
|
194
218
|
return;
|
|
195
219
|
}
|
|
220
|
+
// Variables initialized directly from assertSafe(...) are already
|
|
221
|
+
// validated — no need to double-wrap them.
|
|
222
|
+
if (isAssertSafeValidatedIdentifier(property)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
196
225
|
const propText = context.sourceCode.getText(property);
|
|
197
226
|
reportUseAssertSafe(property, propText);
|
|
198
227
|
return;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export declare const RULE_NAME = "enforce-dynamic-imports";
|
|
2
2
|
type Options = [
|
|
3
3
|
{
|
|
4
|
+
libraries?: string[];
|
|
4
5
|
ignoredLibraries?: string[];
|
|
6
|
+
internalPrefixes?: string[];
|
|
5
7
|
allowImportType?: boolean;
|
|
6
8
|
}
|
|
7
9
|
];
|
|
8
10
|
export declare const DEFAULT_IGNORED_LIBRARIES: string[];
|
|
11
|
+
export declare const DEFAULT_INTERNAL_PREFIXES: string[];
|
|
9
12
|
declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"dynamicImportRequired", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
10
13
|
export default _default;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
3
|
+
exports.DEFAULT_INTERNAL_PREFIXES = exports.DEFAULT_IGNORED_LIBRARIES = exports.RULE_NAME = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const minimatch_1 = require("minimatch");
|
|
6
|
+
const module_1 = require("module");
|
|
6
7
|
exports.RULE_NAME = 'enforce-dynamic-imports';
|
|
7
8
|
exports.DEFAULT_IGNORED_LIBRARIES = [
|
|
8
9
|
'react',
|
|
@@ -19,6 +20,14 @@ exports.DEFAULT_IGNORED_LIBRARIES = [
|
|
|
19
20
|
'clsx',
|
|
20
21
|
'tailwind-merge',
|
|
21
22
|
];
|
|
23
|
+
exports.DEFAULT_INTERNAL_PREFIXES = ['src/', 'functions/'];
|
|
24
|
+
// Pre-built set of Node.js core module names for O(1) lookup.
|
|
25
|
+
const NODE_BUILTINS = new Set(module_1.builtinModules);
|
|
26
|
+
// Returns true for any source that resolves to a Node builtin: bare name
|
|
27
|
+
// ('crypto'), node:-prefixed ('node:fs'), or path form ('fs/promises').
|
|
28
|
+
const isNodeBuiltin = (source) => source.startsWith('node:') ||
|
|
29
|
+
NODE_BUILTINS.has(source) ||
|
|
30
|
+
NODE_BUILTINS.has(source.split('/')[0]);
|
|
22
31
|
exports.default = (0, createRule_1.createRule)({
|
|
23
32
|
name: exports.RULE_NAME,
|
|
24
33
|
meta: {
|
|
@@ -31,12 +40,24 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
31
40
|
{
|
|
32
41
|
type: 'object',
|
|
33
42
|
properties: {
|
|
43
|
+
libraries: {
|
|
44
|
+
type: 'array',
|
|
45
|
+
items: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
34
49
|
ignoredLibraries: {
|
|
35
50
|
type: 'array',
|
|
36
51
|
items: {
|
|
37
52
|
type: 'string',
|
|
38
53
|
},
|
|
39
54
|
},
|
|
55
|
+
internalPrefixes: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
40
61
|
allowImportType: {
|
|
41
62
|
type: 'boolean',
|
|
42
63
|
},
|
|
@@ -51,29 +72,46 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
51
72
|
defaultOptions: [
|
|
52
73
|
{
|
|
53
74
|
ignoredLibraries: exports.DEFAULT_IGNORED_LIBRARIES,
|
|
75
|
+
internalPrefixes: exports.DEFAULT_INTERNAL_PREFIXES,
|
|
54
76
|
allowImportType: true,
|
|
55
77
|
},
|
|
56
78
|
],
|
|
57
79
|
create(context, [options]) {
|
|
58
|
-
const { ignoredLibraries = exports.DEFAULT_IGNORED_LIBRARIES, allowImportType = true, } = options;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
const { libraries, ignoredLibraries = exports.DEFAULT_IGNORED_LIBRARIES, internalPrefixes = exports.DEFAULT_INTERNAL_PREFIXES, allowImportType = true, } = options;
|
|
81
|
+
// When `libraries` is provided, the rule operates in whitelist mode:
|
|
82
|
+
// only the explicitly listed libraries are enforced. This preserves
|
|
83
|
+
// backwards-compatibility with pre-1.16.0 consumer configurations.
|
|
84
|
+
// When `libraries` is absent, enforce-by-default mode applies:
|
|
85
|
+
// all external imports are flagged unless in `ignoredLibraries`.
|
|
86
|
+
const isWhitelistMode = libraries !== undefined;
|
|
87
|
+
// Build an O(1) + glob matcher from a list of library patterns.
|
|
88
|
+
const buildMatcher = (list) => {
|
|
89
|
+
const exactSet = new Set();
|
|
90
|
+
const globs = [];
|
|
91
|
+
for (const lib of list) {
|
|
92
|
+
const mm = new minimatch_1.Minimatch(lib);
|
|
93
|
+
if (mm.hasMagic()) {
|
|
94
|
+
globs.push(mm);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
exactSet.add(lib);
|
|
98
|
+
}
|
|
68
99
|
}
|
|
69
|
-
|
|
70
|
-
const isIgnored = (source) => {
|
|
71
|
-
return (exactIgnored.has(source) ||
|
|
72
|
-
globIgnored.some((mm) => mm.match(source)));
|
|
100
|
+
return (source) => exactSet.has(source) || globs.some((mm) => mm.match(source));
|
|
73
101
|
};
|
|
102
|
+
// In whitelist mode, `libraries` is defined (checked above). In
|
|
103
|
+
// enforce-by-default mode, `ignoredLibraries` is used instead.
|
|
104
|
+
const isListedInWhitelist = buildMatcher(libraries ?? []);
|
|
105
|
+
const isIgnoredLibrary = buildMatcher(ignoredLibraries);
|
|
106
|
+
// A source is external only if it looks like an npm package specifier AND
|
|
107
|
+
// is not a known-internal path. Node builtins and configured internal
|
|
108
|
+
// prefixes (e.g. src/, functions/) are excluded to avoid false positives
|
|
109
|
+
// on TypeScript baseUrl imports and Node core modules.
|
|
74
110
|
const isExternal = (source) => {
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
return (/^[a-z0-9@]/i.test(source) &&
|
|
112
|
+
!source.startsWith('@/') &&
|
|
113
|
+
!isNodeBuiltin(source) &&
|
|
114
|
+
!internalPrefixes.some((prefix) => source.startsWith(prefix)));
|
|
77
115
|
};
|
|
78
116
|
return {
|
|
79
117
|
ImportDeclaration(node) {
|
|
@@ -88,8 +126,20 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
88
126
|
return;
|
|
89
127
|
}
|
|
90
128
|
}
|
|
91
|
-
|
|
92
|
-
if (
|
|
129
|
+
let shouldReport;
|
|
130
|
+
if (isWhitelistMode) {
|
|
131
|
+
// Whitelist mode: report only if the source matches an explicitly
|
|
132
|
+
// listed library. External detection and ignoredLibraries are not
|
|
133
|
+
// consulted, so unlisted npm packages are silently allowed.
|
|
134
|
+
shouldReport = isListedInWhitelist(importSource);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Enforce-by-default mode: report if the source is an external
|
|
138
|
+
// package and is not in the ignored list.
|
|
139
|
+
shouldReport =
|
|
140
|
+
isExternal(importSource) && !isIgnoredLibrary(importSource);
|
|
141
|
+
}
|
|
142
|
+
if (shouldReport) {
|
|
93
143
|
context.report({
|
|
94
144
|
node,
|
|
95
145
|
messageId: 'dynamicImportRequired',
|
|
@@ -368,6 +368,18 @@ function statementReferencesAny(statement, names) {
|
|
|
368
368
|
}
|
|
369
369
|
return false;
|
|
370
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Find the index of the first statement that references any of the given names,
|
|
373
|
+
* searching forward from afterIndex.
|
|
374
|
+
*/
|
|
375
|
+
function findFirstUsageIndex(body, names, afterIndex) {
|
|
376
|
+
for (let cursor = afterIndex; cursor < body.length; cursor += 1) {
|
|
377
|
+
if (statementReferencesAny(body[cursor], names)) {
|
|
378
|
+
return cursor;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return -1;
|
|
382
|
+
}
|
|
371
383
|
function collectAssignedNamesFromPattern(target, names) {
|
|
372
384
|
if (TYPE_EXPRESSION_WRAPPERS.has(target.type) &&
|
|
373
385
|
'expression' in target) {
|
|
@@ -671,10 +683,13 @@ function findEarliestSafeIndex(body, startIndex, dependencies, { allowHooks }) {
|
|
|
671
683
|
}
|
|
672
684
|
function getStartWithComments(statement, sourceCode) {
|
|
673
685
|
const comments = sourceCode.getCommentsBefore(statement);
|
|
674
|
-
|
|
675
|
-
|
|
686
|
+
const start = comments.length === 0 ? statement.range[0] : comments[0].range[0];
|
|
687
|
+
const text = sourceCode.getText();
|
|
688
|
+
let cursor = start - 1;
|
|
689
|
+
while (cursor >= 0 && (text[cursor] === ' ' || text[cursor] === '\t')) {
|
|
690
|
+
cursor -= 1;
|
|
676
691
|
}
|
|
677
|
-
return
|
|
692
|
+
return cursor + 1;
|
|
678
693
|
}
|
|
679
694
|
function getNextStart(body, index, parent, sourceCode) {
|
|
680
695
|
const nextStatement = body[index + 1];
|
|
@@ -907,42 +922,95 @@ function isSiblingSourceDerivation(statement, sourceNodes, sourceDeclarators) {
|
|
|
907
922
|
return !sourceDeclarators.has(name);
|
|
908
923
|
});
|
|
909
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Restrict late-declaration candidates to simple variables with at most an Identifier or
|
|
927
|
+
* Literal initializer. This ensures they are pure values that do not have side effects or
|
|
928
|
+
* change execution order when moved closer to their usage. More complex initializers are
|
|
929
|
+
* excluded to maintain temporal safety.
|
|
930
|
+
*/
|
|
931
|
+
function isLateDeclarationCandidate(statement) {
|
|
932
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
|
|
933
|
+
statement.declarations.length !== 1) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const [declarator] = statement.declarations;
|
|
937
|
+
return (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
938
|
+
(!declarator.init ||
|
|
939
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
940
|
+
declarator.init.type === utils_1.AST_NODE_TYPES.Literal));
|
|
941
|
+
}
|
|
942
|
+
const LOOP_TYPES = new Set([
|
|
943
|
+
utils_1.AST_NODE_TYPES.ForStatement,
|
|
944
|
+
utils_1.AST_NODE_TYPES.ForInStatement,
|
|
945
|
+
utils_1.AST_NODE_TYPES.ForOfStatement,
|
|
946
|
+
utils_1.AST_NODE_TYPES.WhileStatement,
|
|
947
|
+
utils_1.AST_NODE_TYPES.DoWhileStatement,
|
|
948
|
+
]);
|
|
949
|
+
/**
|
|
950
|
+
* Treat a loop as an ordering barrier for late-declaration moves when it mutates any
|
|
951
|
+
* tracked variable inside the loop body.
|
|
952
|
+
*
|
|
953
|
+
* Variables that are declared before a loop and mutated inside it (accumulators,
|
|
954
|
+
* counters, collectors) must remain before the loop so their declaration is visible
|
|
955
|
+
* across all iterations. Moving them inside would reset them on every iteration,
|
|
956
|
+
* changing program semantics. This guard applies regardless of whether the variable
|
|
957
|
+
* is also read after the loop.
|
|
958
|
+
*
|
|
959
|
+
* Assumptions:
|
|
960
|
+
* - nameSet contains the name(s) being tracked for late declaration.
|
|
961
|
+
* - body is the array of statements in the current block.
|
|
962
|
+
* - usageIndex is the index of the first statement that references the variable(s).
|
|
963
|
+
*/
|
|
964
|
+
function isMutatedInLoop(body, usageIndex, nameSet) {
|
|
965
|
+
const firstUsage = body[usageIndex];
|
|
966
|
+
if (!firstUsage) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (!LOOP_TYPES.has(firstUsage.type)) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
return statementMutatesAny(firstUsage, nameSet);
|
|
973
|
+
}
|
|
910
974
|
function handleLateDeclarations(ruleContext, body, parent) {
|
|
911
975
|
const { sourceCode } = ruleContext;
|
|
912
976
|
body.forEach((statement, index) => {
|
|
913
|
-
if (statement
|
|
914
|
-
statement.declarations.length !== 1) {
|
|
977
|
+
if (!isLateDeclarationCandidate(statement)) {
|
|
915
978
|
return;
|
|
916
979
|
}
|
|
917
980
|
const [declarator] = statement.declarations;
|
|
918
|
-
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
919
|
-
(declarator.init &&
|
|
920
|
-
declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
921
|
-
declarator.init.type !== utils_1.AST_NODE_TYPES.Literal)) {
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
981
|
const name = declarator.id.name;
|
|
925
982
|
const dependencies = new Set();
|
|
926
983
|
if (declarator.init && declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
927
984
|
dependencies.add(declarator.init.name);
|
|
928
985
|
}
|
|
929
986
|
const nameSet = new Set([name]);
|
|
930
|
-
|
|
931
|
-
for (let cursor = index + 1; cursor < body.length; cursor += 1) {
|
|
932
|
-
if (statementReferencesAny(body[cursor], nameSet)) {
|
|
933
|
-
usageIndex = cursor;
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
987
|
+
const usageIndex = findFirstUsageIndex(body, nameSet, index + 1);
|
|
937
988
|
if (usageIndex === -1 || usageIndex <= index + 1) {
|
|
938
989
|
return;
|
|
939
990
|
}
|
|
991
|
+
// Loop mutations create a dependency barrier: declarations that precede loops and are
|
|
992
|
+
// mutated inside them cannot be safely moved, as the declaration must be visible across
|
|
993
|
+
// all iterations. Prevent false positives for this pattern.
|
|
994
|
+
if (isMutatedInLoop(body, usageIndex, nameSet)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
940
997
|
const intervening = body.slice(index + 1, usageIndex);
|
|
941
998
|
// Only move across pure declarations that do not mention the placeholder or its initializer dependencies to avoid changing closure timing or TDZ behavior.
|
|
942
|
-
const crossesImpureOrTracked = intervening.some((stmt) => {
|
|
999
|
+
const crossesImpureOrTracked = intervening.some((stmt, i) => {
|
|
943
1000
|
if (!isPureDeclaration(stmt, { allowHooks: false })) {
|
|
944
1001
|
return true;
|
|
945
1002
|
}
|
|
1003
|
+
// Do not hop over another declaration that is used at the same index or earlier
|
|
1004
|
+
// if it is also a candidate for being moved.
|
|
1005
|
+
// This prevents circular swapping of related declarations (like resolve/reject pairs).
|
|
1006
|
+
if (isLateDeclarationCandidate(stmt)) {
|
|
1007
|
+
const declaredNames = getDeclaredNames(stmt);
|
|
1008
|
+
const firstUsageOfIntervening = findFirstUsageIndex(body, declaredNames, index + 1 + i + 1);
|
|
1009
|
+
if (firstUsageOfIntervening !== -1 &&
|
|
1010
|
+
firstUsageOfIntervening <= usageIndex) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
946
1014
|
if (statementDeclaresAny(stmt, nameSet) ||
|
|
947
1015
|
statementMutatesAny(stmt, nameSet)) {
|
|
948
1016
|
return true;
|
|
@@ -352,6 +352,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
352
352
|
/[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
|
|
353
353
|
return true;
|
|
354
354
|
}
|
|
355
|
+
// Abbreviation markers (str, num, int, bool, arr, obj, fn, func) are
|
|
356
|
+
// short enough that the raw-character boundary heuristic below can fire
|
|
357
|
+
// on them as substrings inside real English words (e.g. "int" inside
|
|
358
|
+
// "Mint", "str" inside "stream"). For these markers, require an exact
|
|
359
|
+
// match against a full camelCase token so that "Mint" → ["Mint"] never
|
|
360
|
+
// matches "int", while genuine Hungarian like intValue → ["int","Value"]
|
|
361
|
+
// still does.
|
|
362
|
+
if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
|
|
363
|
+
const segments = splitCamelSegments(variableName);
|
|
364
|
+
return segments.some((s) => s.toLowerCase() === normalizedMarker);
|
|
365
|
+
}
|
|
355
366
|
// Check for word boundaries to avoid matching substrings
|
|
356
367
|
// For example, avoid matching "int" in "points" or "str" in "stream"
|
|
357
368
|
const markerIndex = normalizedVarName.indexOf(normalizedMarker);
|
|
@@ -6,6 +6,11 @@ exports.requireMemo = void 0;
|
|
|
6
6
|
const utils_1 = require("@typescript-eslint/utils");
|
|
7
7
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
8
8
|
const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
|
|
9
|
+
// React's universal convention: only PascalCase-initial identifiers are
|
|
10
|
+
// treated as components. camelCase names are render-prop callbacks or plain
|
|
11
|
+
// helper functions that are invoked directly (e.g. MUI's renderCell(params)),
|
|
12
|
+
// NOT React components — wrapping them in memo() would break callers.
|
|
13
|
+
const startsWithUppercase = (name) => /^[A-Z]/.test(name);
|
|
9
14
|
function isFunction(node) {
|
|
10
15
|
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
11
16
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
@@ -33,18 +38,21 @@ function isHigherOrderFunctionReturningJSX(node, context) {
|
|
|
33
38
|
const isUnmemoizedArrowFunction = (parentNode) => {
|
|
34
39
|
return (parentNode.type === 'VariableDeclarator' &&
|
|
35
40
|
parentNode.id.type === 'Identifier' &&
|
|
41
|
+
startsWithUppercase(parentNode.id.name) &&
|
|
36
42
|
!isComponentExplicitlyUnmemoized(parentNode.id.name));
|
|
37
43
|
};
|
|
38
44
|
const isUnmemoizedFunctionComponent = (parentNode, node) => {
|
|
39
45
|
return (node.type === 'FunctionDeclaration' &&
|
|
40
46
|
parentNode.type === 'Program' &&
|
|
41
47
|
node.id &&
|
|
48
|
+
startsWithUppercase(node.id.name) &&
|
|
42
49
|
!isComponentExplicitlyUnmemoized(node.id.name));
|
|
43
50
|
};
|
|
44
51
|
const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
|
|
45
52
|
return (node.type === 'FunctionDeclaration' &&
|
|
46
53
|
parentNode.type === 'ExportNamedDeclaration' &&
|
|
47
54
|
node.id &&
|
|
55
|
+
startsWithUppercase(node.id.name) &&
|
|
48
56
|
!isComponentExplicitlyUnmemoized(node.id.name));
|
|
49
57
|
};
|
|
50
58
|
function isMemoImport(importPath) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.16.2",
|
|
4
|
+
"date": "2026-06-30T22:17:37.457Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "logical-top-to-bottom-grouping",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1247
|
|
11
|
+
],
|
|
12
|
+
"summary": "restore loop-mutation guard and indentation-preserving idempotent autofix (closes #1247)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-hungarian",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1246
|
|
19
|
+
],
|
|
20
|
+
"summary": "only flag abbreviation markers at camelCase token boundaries (closes #1246)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.16.1",
|
|
26
|
+
"date": "2026-06-30T15:58:17.558Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-assert-safe-object-key",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1245
|
|
33
|
+
],
|
|
34
|
+
"summary": "treat assertSafe-cached variables as validated (closes #1245)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "enforce-dynamic-imports",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1244
|
|
41
|
+
],
|
|
42
|
+
"summary": "restore libraries whitelist mode and exempt builtins/internal paths (closes #1244)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "require-memo",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1243
|
|
49
|
+
],
|
|
50
|
+
"summary": "exempt camelCase render-prop callbacks (closes #1243)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.16.0",
|
|
4
56
|
"date": "2026-06-29T19:34:27.787Z",
|
|
@@ -1,445 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.preferMemoizedProps = void 0;
|
|
4
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
const createRule_1 = require("../utils/createRule");
|
|
6
|
-
const HOOK_NAMES = new Set(['useMemo', 'useCallback']);
|
|
7
|
-
exports.preferMemoizedProps = (0, createRule_1.createRule)({
|
|
8
|
-
name: 'prefer-memoized-props',
|
|
9
|
-
meta: {
|
|
10
|
-
type: 'suggestion',
|
|
11
|
-
docs: {
|
|
12
|
-
description: 'Require memoizing reference props (objects, arrays, functions) inside React.memo components while avoiding unnecessary useMemo for pass-through values.',
|
|
13
|
-
recommended: 'error',
|
|
14
|
-
},
|
|
15
|
-
schema: [],
|
|
16
|
-
messages: {
|
|
17
|
-
memoizeReferenceProp: 'Prop "{{propName}}" in a React.memo component receives a {{kind}} that is recreated every render, so memoized children lose referential equality and re-render. Memoize this {{kind}} with useMemo/useCallback or hoist a stable constant so the prop reference stays stable.',
|
|
18
|
-
avoidPrimitiveMemo: 'useMemo around "{{value}}" only wraps a pass-through value. Primitives already compare by value, and wrapping existing references without creating new objects does not improve stability. Return the value directly instead of adding memoization noise.',
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
defaultOptions: [],
|
|
22
|
-
create(context) {
|
|
23
|
-
const sourceCode = context.getSourceCode();
|
|
24
|
-
const visitorKeys = sourceCode.visitorKeys;
|
|
25
|
-
const memoizedComponents = new WeakSet();
|
|
26
|
-
const memoScopes = [];
|
|
27
|
-
const componentStack = [];
|
|
28
|
-
const scopedFunctions = new WeakSet();
|
|
29
|
-
function pushMemoScope() {
|
|
30
|
-
memoScopes.push(new Map());
|
|
31
|
-
}
|
|
32
|
-
function popMemoScope() {
|
|
33
|
-
memoScopes.pop();
|
|
34
|
-
}
|
|
35
|
-
function currentMemoScope() {
|
|
36
|
-
return memoScopes[memoScopes.length - 1];
|
|
37
|
-
}
|
|
38
|
-
function declareFunctionBinding(name, node) {
|
|
39
|
-
const scope = currentMemoScope();
|
|
40
|
-
if (!scope)
|
|
41
|
-
return;
|
|
42
|
-
scope.set(name, node);
|
|
43
|
-
}
|
|
44
|
-
function resolveFunctionBinding(name) {
|
|
45
|
-
for (let i = memoScopes.length - 1; i >= 0; i -= 1) {
|
|
46
|
-
const binding = memoScopes[i].get(name);
|
|
47
|
-
if (binding) {
|
|
48
|
-
return binding;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
function hoistFunctionDeclarations(statements) {
|
|
54
|
-
const scope = currentMemoScope();
|
|
55
|
-
if (!scope)
|
|
56
|
-
return;
|
|
57
|
-
for (const statement of statements) {
|
|
58
|
-
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
59
|
-
statement.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
60
|
-
scope.set(statement.id.name, statement);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function registerFunctionVariable(node) {
|
|
65
|
-
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
66
|
-
return;
|
|
67
|
-
const { init } = node;
|
|
68
|
-
if (init &&
|
|
69
|
-
(init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
70
|
-
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
71
|
-
declareFunctionBinding(node.id.name, init);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function isReactMemoCallee(callee) {
|
|
75
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier && callee.name === 'memo') {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
79
|
-
!callee.computed &&
|
|
80
|
-
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
|
-
callee.property.name === 'memo') {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
function isHookCall(node, name) {
|
|
87
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
88
|
-
node.callee.name === name) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
return (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
92
|
-
!node.callee.computed &&
|
|
93
|
-
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
94
|
-
node.callee.property.name === name);
|
|
95
|
-
}
|
|
96
|
-
function collectMemoizedComponents(node) {
|
|
97
|
-
pushMemoScope();
|
|
98
|
-
if (node.type === utils_1.AST_NODE_TYPES.Program) {
|
|
99
|
-
hoistFunctionDeclarations(node.body);
|
|
100
|
-
}
|
|
101
|
-
function traverse(current) {
|
|
102
|
-
const isFunctionLike = current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
103
|
-
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
104
|
-
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
105
|
-
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
106
|
-
current.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
107
|
-
declareFunctionBinding(current.id.name, current);
|
|
108
|
-
}
|
|
109
|
-
let pushedScope = false;
|
|
110
|
-
if (current.type === utils_1.AST_NODE_TYPES.BlockStatement || isFunctionLike) {
|
|
111
|
-
pushMemoScope();
|
|
112
|
-
pushedScope = true;
|
|
113
|
-
if (isFunctionLike &&
|
|
114
|
-
'id' in current &&
|
|
115
|
-
current.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
116
|
-
declareFunctionBinding(current.id.name, current);
|
|
117
|
-
}
|
|
118
|
-
if (current.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
119
|
-
hoistFunctionDeclarations(current.body);
|
|
120
|
-
}
|
|
121
|
-
else if (isFunctionLike &&
|
|
122
|
-
current.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
123
|
-
hoistFunctionDeclarations(current.body.body);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
127
|
-
registerFunctionVariable(current);
|
|
128
|
-
}
|
|
129
|
-
if (current.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
130
|
-
isReactMemoCallee(current.callee)) {
|
|
131
|
-
const [firstArg] = current.arguments;
|
|
132
|
-
if (firstArg &&
|
|
133
|
-
(firstArg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
134
|
-
firstArg.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
135
|
-
memoizedComponents.add(firstArg);
|
|
136
|
-
}
|
|
137
|
-
else if (firstArg && firstArg.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
138
|
-
const binding = resolveFunctionBinding(firstArg.name);
|
|
139
|
-
if (binding) {
|
|
140
|
-
memoizedComponents.add(binding);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const keys = visitorKeys[current.type] ?? [];
|
|
145
|
-
for (const key of keys) {
|
|
146
|
-
const value = current[key];
|
|
147
|
-
if (Array.isArray(value)) {
|
|
148
|
-
for (const child of value) {
|
|
149
|
-
if (child && typeof child === 'object') {
|
|
150
|
-
traverse(child);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else if (value && typeof value === 'object') {
|
|
155
|
-
traverse(value);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (pushedScope) {
|
|
159
|
-
popMemoScope();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
traverse(node);
|
|
163
|
-
popMemoScope();
|
|
164
|
-
}
|
|
165
|
-
collectMemoizedComponents(sourceCode.ast);
|
|
166
|
-
function isMemoizedComponent(node) {
|
|
167
|
-
return memoizedComponents.has(node);
|
|
168
|
-
}
|
|
169
|
-
function pushComponent(node) {
|
|
170
|
-
componentStack.push({ node, scopes: [new Map()] });
|
|
171
|
-
}
|
|
172
|
-
function popComponent(node) {
|
|
173
|
-
const top = componentStack[componentStack.length - 1];
|
|
174
|
-
if (top && top.node === node) {
|
|
175
|
-
componentStack.pop();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function currentComponent() {
|
|
179
|
-
return componentStack[componentStack.length - 1];
|
|
180
|
-
}
|
|
181
|
-
function currentScope() {
|
|
182
|
-
const component = currentComponent();
|
|
183
|
-
if (!component)
|
|
184
|
-
return undefined;
|
|
185
|
-
return component.scopes[component.scopes.length - 1];
|
|
186
|
-
}
|
|
187
|
-
function pushScopeForNestedFunction(node) {
|
|
188
|
-
const component = currentComponent();
|
|
189
|
-
if (!component)
|
|
190
|
-
return;
|
|
191
|
-
component.scopes.push(new Map());
|
|
192
|
-
scopedFunctions.add(node);
|
|
193
|
-
}
|
|
194
|
-
function popScopeForNestedFunction(node) {
|
|
195
|
-
const component = currentComponent();
|
|
196
|
-
if (!component)
|
|
197
|
-
return;
|
|
198
|
-
if (!scopedFunctions.has(node))
|
|
199
|
-
return;
|
|
200
|
-
component.scopes.pop();
|
|
201
|
-
scopedFunctions.delete(node);
|
|
202
|
-
}
|
|
203
|
-
function unwrapExpression(expression) {
|
|
204
|
-
let current = expression;
|
|
205
|
-
while (true) {
|
|
206
|
-
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
207
|
-
current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
208
|
-
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
209
|
-
current = current.expression;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
213
|
-
current = current.expression;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
return current;
|
|
219
|
-
}
|
|
220
|
-
function getBindingKind(init) {
|
|
221
|
-
if (!init) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const unwrapped = unwrapExpression(init);
|
|
225
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
226
|
-
((unwrapped.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
227
|
-
HOOK_NAMES.has(unwrapped.callee.name)) ||
|
|
228
|
-
(unwrapped.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
229
|
-
!unwrapped.callee.computed &&
|
|
230
|
-
unwrapped.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
231
|
-
HOOK_NAMES.has(unwrapped.callee.property.name)))) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
235
|
-
return 'object';
|
|
236
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
237
|
-
return 'array';
|
|
238
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
239
|
-
unwrapped.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
240
|
-
return 'function';
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
function recordBinding(node) {
|
|
245
|
-
const scope = currentScope();
|
|
246
|
-
if (!scope)
|
|
247
|
-
return;
|
|
248
|
-
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier || !node.init) {
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const initializer = unwrapExpression(node.init);
|
|
252
|
-
const aliasKind = initializer.type === utils_1.AST_NODE_TYPES.Identifier
|
|
253
|
-
? findBindingKind(initializer.name)
|
|
254
|
-
: null;
|
|
255
|
-
const kind = aliasKind ?? getBindingKind(initializer);
|
|
256
|
-
if (kind)
|
|
257
|
-
scope.set(node.id.name, kind);
|
|
258
|
-
}
|
|
259
|
-
function findBindingKind(name) {
|
|
260
|
-
for (let i = componentStack.length - 1; i >= 0; i -= 1) {
|
|
261
|
-
const component = componentStack[i];
|
|
262
|
-
for (let j = component.scopes.length - 1; j >= 0; j -= 1) {
|
|
263
|
-
const binding = component.scopes[j].get(name);
|
|
264
|
-
if (binding)
|
|
265
|
-
return binding;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
function getAttributeName(name) {
|
|
271
|
-
if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
272
|
-
return name.name;
|
|
273
|
-
}
|
|
274
|
-
if (name.type === utils_1.AST_NODE_TYPES.JSXNamespacedName) {
|
|
275
|
-
return name.name.name;
|
|
276
|
-
}
|
|
277
|
-
return 'prop';
|
|
278
|
-
}
|
|
279
|
-
function reportReference(node, propName, kind) {
|
|
280
|
-
context.report({
|
|
281
|
-
node,
|
|
282
|
-
messageId: 'memoizeReferenceProp',
|
|
283
|
-
data: { propName, kind },
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
function isEnvironmentFreeLiteral(expression) {
|
|
287
|
-
const unwrapped = unwrapExpression(expression);
|
|
288
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.Literal)
|
|
289
|
-
return true;
|
|
290
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
291
|
-
unwrapped.expressions.length === 0) {
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
function isStableReference(expression) {
|
|
297
|
-
const unwrapped = unwrapExpression(expression);
|
|
298
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.Literal)
|
|
299
|
-
return true;
|
|
300
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
301
|
-
return unwrapped.expressions.every((expr) => isStableReference(expr));
|
|
302
|
-
}
|
|
303
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
304
|
-
return true;
|
|
305
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression)
|
|
306
|
-
return true;
|
|
307
|
-
if (unwrapped.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
308
|
-
unwrapped.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
function extractReturnExpression(callback) {
|
|
314
|
-
if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
315
|
-
return callback.body;
|
|
316
|
-
}
|
|
317
|
-
const [statement] = callback.body.body;
|
|
318
|
-
if (statement &&
|
|
319
|
-
statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
320
|
-
statement.argument) {
|
|
321
|
-
return statement.argument;
|
|
322
|
-
}
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
function handleUseMemo(node) {
|
|
326
|
-
if (!currentComponent() || !isHookCall(node, 'useMemo')) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const [callback, deps] = node.arguments;
|
|
330
|
-
if (!callback ||
|
|
331
|
-
(callback.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
332
|
-
callback.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const returnExpression = extractReturnExpression(callback);
|
|
336
|
-
if (!returnExpression)
|
|
337
|
-
return;
|
|
338
|
-
const dependencies = deps && deps.type === utils_1.AST_NODE_TYPES.ArrayExpression ? deps : null;
|
|
339
|
-
if (dependencies &&
|
|
340
|
-
dependencies.elements.length === 0 &&
|
|
341
|
-
!isEnvironmentFreeLiteral(returnExpression)) {
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (isStableReference(returnExpression)) {
|
|
345
|
-
const valueText = sourceCode.getText(returnExpression).slice(0, 80);
|
|
346
|
-
context.report({
|
|
347
|
-
node,
|
|
348
|
-
messageId: 'avoidPrimitiveMemo',
|
|
349
|
-
data: { value: valueText },
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function handleJSXAttribute(node) {
|
|
354
|
-
if (!currentComponent())
|
|
355
|
-
return;
|
|
356
|
-
if (!node.value ||
|
|
357
|
-
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const rawExpression = node.value.expression;
|
|
361
|
-
if (rawExpression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const expression = unwrapExpression(rawExpression);
|
|
365
|
-
const propName = getAttributeName(node.name);
|
|
366
|
-
if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
367
|
-
reportReference(expression, propName, 'object');
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (expression.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
371
|
-
reportReference(expression, propName, 'array');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
375
|
-
expression.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
376
|
-
reportReference(expression, propName, 'function');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
380
|
-
const bindingKind = findBindingKind(expression.name);
|
|
381
|
-
if (bindingKind) {
|
|
382
|
-
reportReference(expression, propName, bindingKind);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
FunctionDeclaration(node) {
|
|
388
|
-
if (isMemoizedComponent(node)) {
|
|
389
|
-
pushComponent(node);
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const scope = currentScope();
|
|
393
|
-
if (scope && node.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
394
|
-
scope.set(node.id.name, 'function');
|
|
395
|
-
}
|
|
396
|
-
if (currentComponent()) {
|
|
397
|
-
pushScopeForNestedFunction(node);
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
'FunctionDeclaration:exit'(node) {
|
|
401
|
-
if (isMemoizedComponent(node)) {
|
|
402
|
-
popComponent(node);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
popScopeForNestedFunction(node);
|
|
406
|
-
},
|
|
407
|
-
FunctionExpression(node) {
|
|
408
|
-
if (isMemoizedComponent(node)) {
|
|
409
|
-
pushComponent(node);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
if (currentComponent()) {
|
|
413
|
-
pushScopeForNestedFunction(node);
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
'FunctionExpression:exit'(node) {
|
|
417
|
-
if (isMemoizedComponent(node)) {
|
|
418
|
-
popComponent(node);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
popScopeForNestedFunction(node);
|
|
422
|
-
},
|
|
423
|
-
ArrowFunctionExpression(node) {
|
|
424
|
-
if (isMemoizedComponent(node)) {
|
|
425
|
-
pushComponent(node);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (currentComponent()) {
|
|
429
|
-
pushScopeForNestedFunction(node);
|
|
430
|
-
}
|
|
431
|
-
},
|
|
432
|
-
'ArrowFunctionExpression:exit'(node) {
|
|
433
|
-
if (isMemoizedComponent(node)) {
|
|
434
|
-
popComponent(node);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
popScopeForNestedFunction(node);
|
|
438
|
-
},
|
|
439
|
-
VariableDeclarator: recordBinding,
|
|
440
|
-
CallExpression: handleUseMemo,
|
|
441
|
-
JSXAttribute: handleJSXAttribute,
|
|
442
|
-
};
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
//# sourceMappingURL=prefer-memoized-props.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
/**
|
|
3
|
-
* This rule overrides the behavior of @typescript-eslint/prefer-nullish-coalescing
|
|
4
|
-
* to only suggest using the nullish coalescing operator when checking for null/undefined,
|
|
5
|
-
* not when intentionally checking for all falsy values.
|
|
6
|
-
*/
|
|
7
|
-
export declare const preferNullishCoalescingOverride: TSESLint.RuleModule<"preferNullishCoalescing", [], TSESLint.RuleListener>;
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.preferNullishCoalescingOverride = void 0;
|
|
4
|
-
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
const createRule_1 = require("../utils/createRule");
|
|
6
|
-
/**
|
|
7
|
-
* This rule overrides the behavior of @typescript-eslint/prefer-nullish-coalescing
|
|
8
|
-
* to only suggest using the nullish coalescing operator when checking for null/undefined,
|
|
9
|
-
* not when intentionally checking for all falsy values.
|
|
10
|
-
*/
|
|
11
|
-
exports.preferNullishCoalescingOverride = (0, createRule_1.createRule)({
|
|
12
|
-
name: 'prefer-nullish-coalescing-override',
|
|
13
|
-
meta: {
|
|
14
|
-
type: 'suggestion',
|
|
15
|
-
docs: {
|
|
16
|
-
description: 'Enforce using nullish coalescing operator instead of logical OR operator, but only when appropriate',
|
|
17
|
-
recommended: 'warn',
|
|
18
|
-
},
|
|
19
|
-
fixable: 'code',
|
|
20
|
-
schema: [],
|
|
21
|
-
messages: {
|
|
22
|
-
preferNullishCoalescing: 'Replace "{{left}} || {{right}}" with nullish coalescing when you only want a fallback for null or undefined. The logical OR operator treats "", 0, and false as missing values and silently swaps in the fallback, which hides valid data. Use "{{left}} ?? {{right}}" so only nullish inputs trigger the fallback.',
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
defaultOptions: [],
|
|
26
|
-
create(context) {
|
|
27
|
-
const contextWithSource = context;
|
|
28
|
-
const sourceCode = contextWithSource.sourceCode ?? contextWithSource.getSourceCode();
|
|
29
|
-
/**
|
|
30
|
-
* Checks if a node is in a boolean context where truthiness matters
|
|
31
|
-
*/
|
|
32
|
-
function isInBooleanContext(node) {
|
|
33
|
-
const parent = node.parent;
|
|
34
|
-
if (!parent)
|
|
35
|
-
return false;
|
|
36
|
-
switch (parent.type) {
|
|
37
|
-
// Direct boolean contexts
|
|
38
|
-
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
39
|
-
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
40
|
-
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
41
|
-
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
42
|
-
return parent.test === node;
|
|
43
|
-
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
44
|
-
return parent.test === node;
|
|
45
|
-
// Logical expressions (&&, ||, !)
|
|
46
|
-
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
47
|
-
return true;
|
|
48
|
-
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
49
|
-
return parent.operator === '!';
|
|
50
|
-
// JSX expressions that expect boolean values
|
|
51
|
-
case utils_1.AST_NODE_TYPES.JSXExpressionContainer:
|
|
52
|
-
return isJSXBooleanContext(parent);
|
|
53
|
-
default:
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Checks if a JSX expression container is in a boolean context
|
|
59
|
-
*/
|
|
60
|
-
function isJSXBooleanContext(jsxContainer) {
|
|
61
|
-
const parent = jsxContainer.parent;
|
|
62
|
-
if (!parent)
|
|
63
|
-
return false;
|
|
64
|
-
// Check if used as a child expression (conditional rendering or child selection).
|
|
65
|
-
// Other guards (rightOperandSuggestsFalsyHandling, etc.) will prevent unsafe conversions.
|
|
66
|
-
if (parent.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
67
|
-
parent.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Checks if the right operand suggests we want to handle all falsy values
|
|
74
|
-
*/
|
|
75
|
-
function rightOperandSuggestsFalsyHandling(right) {
|
|
76
|
-
// String literals suggest we want to handle empty strings too
|
|
77
|
-
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
78
|
-
typeof right.value === 'string') {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
// Number literals (especially 0) suggest we want to handle falsy numbers
|
|
82
|
-
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
83
|
-
typeof right.value === 'number') {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
// Boolean literals suggest boolean logic
|
|
87
|
-
if (right.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
88
|
-
typeof right.value === 'boolean') {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Checks if the left operand is likely to be used for boolean logic
|
|
95
|
-
*/
|
|
96
|
-
function leftOperandSuggestsBooleanLogic(left) {
|
|
97
|
-
// Variables with boolean-like names
|
|
98
|
-
if (left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
99
|
-
const name = left.name.toLowerCase();
|
|
100
|
-
const booleanPrefixes = [
|
|
101
|
-
'is',
|
|
102
|
-
'has',
|
|
103
|
-
'can',
|
|
104
|
-
'should',
|
|
105
|
-
'will',
|
|
106
|
-
'was',
|
|
107
|
-
'were',
|
|
108
|
-
'are',
|
|
109
|
-
];
|
|
110
|
-
const booleanSuffixes = [
|
|
111
|
-
'enabled',
|
|
112
|
-
'disabled',
|
|
113
|
-
'active',
|
|
114
|
-
'inactive',
|
|
115
|
-
'valid',
|
|
116
|
-
'invalid',
|
|
117
|
-
];
|
|
118
|
-
return (booleanPrefixes.some((prefix) => name.startsWith(prefix)) ||
|
|
119
|
-
booleanSuffixes.some((suffix) => name.endsWith(suffix)));
|
|
120
|
-
}
|
|
121
|
-
// Binary expressions that result in booleans
|
|
122
|
-
if (left.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
|
|
123
|
-
const comparisonOperators = [
|
|
124
|
-
'===',
|
|
125
|
-
'!==',
|
|
126
|
-
'==',
|
|
127
|
-
'!=',
|
|
128
|
-
'<',
|
|
129
|
-
'>',
|
|
130
|
-
'<=',
|
|
131
|
-
'>=',
|
|
132
|
-
];
|
|
133
|
-
return comparisonOperators.includes(left.operator);
|
|
134
|
-
}
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Checks if this is a variable assignment context where falsy handling is expected
|
|
139
|
-
*/
|
|
140
|
-
function isVariableAssignmentWithFalsyHandling(node) {
|
|
141
|
-
const parent = node.parent;
|
|
142
|
-
if (!parent)
|
|
143
|
-
return false;
|
|
144
|
-
// Variable declarations like: const name = username || 'Anonymous'
|
|
145
|
-
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
146
|
-
parent.init === node) {
|
|
147
|
-
return rightOperandSuggestsFalsyHandling(node.right);
|
|
148
|
-
}
|
|
149
|
-
// Assignment expressions like: this.name = username || 'Anonymous'
|
|
150
|
-
if (parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
151
|
-
parent.right === node) {
|
|
152
|
-
return rightOperandSuggestsFalsyHandling(node.right);
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Checks if this logical OR should be converted to nullish coalescing
|
|
158
|
-
*/
|
|
159
|
-
function shouldConvertToNullishCoalescing(node) {
|
|
160
|
-
void node;
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
void isInBooleanContext;
|
|
164
|
-
void leftOperandSuggestsBooleanLogic;
|
|
165
|
-
void rightOperandSuggestsFalsyHandling;
|
|
166
|
-
void isVariableAssignmentWithFalsyHandling;
|
|
167
|
-
return {
|
|
168
|
-
LogicalExpression(node) {
|
|
169
|
-
if (shouldConvertToNullishCoalescing(node)) {
|
|
170
|
-
const leftText = sourceCode.getText(node.left);
|
|
171
|
-
const rightText = sourceCode.getText(node.right);
|
|
172
|
-
context.report({
|
|
173
|
-
node,
|
|
174
|
-
messageId: 'preferNullishCoalescing',
|
|
175
|
-
data: {
|
|
176
|
-
left: leftText,
|
|
177
|
-
right: rightText,
|
|
178
|
-
},
|
|
179
|
-
fix(fixer) {
|
|
180
|
-
return fixer.replaceText(node, `${leftText} ?? ${rightText}`);
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
//# sourceMappingURL=prefer-nullish-coalescing-override.js.map
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireUseMemoObjectLiterals = void 0;
|
|
4
|
-
const createRule_1 = require("../utils/createRule");
|
|
5
|
-
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
|
-
exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
7
|
-
name: 'require-usememo-object-literals',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'suggestion',
|
|
10
|
-
docs: {
|
|
11
|
-
description: "Enforce using useMemo for inline object/array literals passed as props to JSX components to prevent unnecessary re-renders. When object/array literals are defined inline in JSX, they create new references on every render, causing child components to re-render even if the values haven't changed. Wrap them in useMemo to maintain referential equality.",
|
|
12
|
-
recommended: 'error',
|
|
13
|
-
},
|
|
14
|
-
messages: {
|
|
15
|
-
requireUseMemo: 'Inline {{literalType}} literal passed to {{componentName}} prop "{{propName}}" is recreated on every render, producing a new reference that forces child components to re-render even when values stay the same. Wrap the literal in useMemo (or hoist a memoized constant) so the prop reference remains stable between renders.',
|
|
16
|
-
},
|
|
17
|
-
schema: [],
|
|
18
|
-
},
|
|
19
|
-
defaultOptions: [],
|
|
20
|
-
create(context) {
|
|
21
|
-
return {
|
|
22
|
-
JSXAttribute(node) {
|
|
23
|
-
// Skip if the value is not an expression
|
|
24
|
-
if (!node.value || node.value.type !== 'JSXExpressionContainer') {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
// Skip if the prop name is 'sx' or ends with 'Sx'
|
|
28
|
-
const propName = node.name.name;
|
|
29
|
-
if (typeof propName === 'string' &&
|
|
30
|
-
(propName === 'sx' || propName.endsWith('Sx'))) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const { expression } = node.value;
|
|
34
|
-
// Check if the expression is an object or array literal
|
|
35
|
-
if ((expression.type === 'ObjectExpression' ||
|
|
36
|
-
expression.type === 'ArrayExpression') &&
|
|
37
|
-
// Ensure we're in a function component context
|
|
38
|
-
ASTHelpers_1.ASTHelpers.getAncestors(context, node).some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
|
|
39
|
-
ancestor.type === 'ArrowFunctionExpression' ||
|
|
40
|
-
ancestor.type === 'FunctionExpression')) {
|
|
41
|
-
// Check if the parent component name starts with an uppercase letter
|
|
42
|
-
// to ensure it's a React component
|
|
43
|
-
const jsxElement = node.parent;
|
|
44
|
-
const elementName = jsxElement.name.type === 'JSXIdentifier'
|
|
45
|
-
? jsxElement.name.name
|
|
46
|
-
: '';
|
|
47
|
-
if (elementName && /^[A-Z]/.test(elementName)) {
|
|
48
|
-
const literalType = expression.type === 'ObjectExpression' ? 'object' : 'array';
|
|
49
|
-
context.report({
|
|
50
|
-
node: expression,
|
|
51
|
-
messageId: 'requireUseMemo',
|
|
52
|
-
data: {
|
|
53
|
-
literalType,
|
|
54
|
-
propName: typeof propName === 'string' ? propName : 'prop',
|
|
55
|
-
componentName: elementName,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
//# sourceMappingURL=require-usememo-object-literals.js.map
|