@blumintinc/eslint-plugin-blumint 1.16.0 → 1.16.1
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/require-memo.js +8 -0
- package/package.json +1 -1
- package/release-manifest.json +30 -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',
|
|
@@ -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,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.16.1",
|
|
4
|
+
"date": "2026-06-30T15:58:17.558Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-assert-safe-object-key",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1245
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat assertSafe-cached variables as validated (closes #1245)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-dynamic-imports",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1244
|
|
19
|
+
],
|
|
20
|
+
"summary": "restore libraries whitelist mode and exempt builtins/internal paths (closes #1244)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "require-memo",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1243
|
|
27
|
+
],
|
|
28
|
+
"summary": "exempt camelCase render-prop callbacks (closes #1243)"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"version": "1.16.0",
|
|
4
34
|
"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
|