@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +1 -1
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +44 -18
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +66 -23
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.js +9 -3
- package/lib/rules/no-async-foreach.js +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +527 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.js +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -1
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +67 -12
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +465 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -2
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +508 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.js +6 -4
- package/lib/rules/parallelize-async-operations.js +143 -59
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.js +88 -13
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +155 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +18 -10
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoNestedReactComponents = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const CALLBACK_HOOKS = new Set([
|
|
8
|
+
'useCallback',
|
|
9
|
+
'useDeepCompareCallback',
|
|
10
|
+
'useMemo',
|
|
11
|
+
'useDeepCompareMemo',
|
|
12
|
+
]);
|
|
13
|
+
const collectReactImports = (sourceCode) => {
|
|
14
|
+
const reactImports = { namespace: null, named: {} };
|
|
15
|
+
for (const statement of sourceCode.ast.body) {
|
|
16
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
|
|
17
|
+
continue;
|
|
18
|
+
if (statement.source.value !== 'react')
|
|
19
|
+
continue;
|
|
20
|
+
for (const specifier of statement.specifiers) {
|
|
21
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
22
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
23
|
+
const importedName = specifier.imported.name;
|
|
24
|
+
if (importedName === 'createElement' ||
|
|
25
|
+
importedName === 'memo' ||
|
|
26
|
+
importedName === 'forwardRef') {
|
|
27
|
+
reactImports.named[importedName] = specifier.local.name;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
31
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
32
|
+
reactImports.namespace = specifier.local.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return reactImports;
|
|
37
|
+
};
|
|
38
|
+
const calleeMatchesReactMember = (callee, reactImports, member) => {
|
|
39
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return reactImports.named[member] === callee.name;
|
|
41
|
+
}
|
|
42
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
43
|
+
!callee.computed &&
|
|
44
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
45
|
+
callee.property.name === member &&
|
|
46
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
47
|
+
reactImports.namespace === callee.object.name) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
const isFunctionExpression = (node) => {
|
|
53
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
54
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
55
|
+
};
|
|
56
|
+
const unwrapExpression = (expression) => {
|
|
57
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
58
|
+
return unwrapExpression(expression.expression);
|
|
59
|
+
}
|
|
60
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
61
|
+
return unwrapExpression(expression.expression);
|
|
62
|
+
}
|
|
63
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
64
|
+
return unwrapExpression(expression.expression);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* ParenthesizedExpression appears in the parsed AST even though the type
|
|
68
|
+
* is missing from AST_NODE_TYPES. This assertion safely unwraps to keep
|
|
69
|
+
* traversal consistent.
|
|
70
|
+
*/
|
|
71
|
+
if (expression.type ===
|
|
72
|
+
'ParenthesizedExpression') {
|
|
73
|
+
return unwrapExpression(expression.expression);
|
|
74
|
+
}
|
|
75
|
+
return expression;
|
|
76
|
+
};
|
|
77
|
+
const isReactCreateElementCall = (node, reactImports) => {
|
|
78
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'createElement');
|
|
79
|
+
};
|
|
80
|
+
const isHookCall = (callee) => {
|
|
81
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
82
|
+
if (CALLBACK_HOOKS.has(callee.name)) {
|
|
83
|
+
return { name: callee.name };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
88
|
+
!callee.computed &&
|
|
89
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
90
|
+
CALLBACK_HOOKS.has(callee.property.name)) {
|
|
91
|
+
return { name: callee.property.name };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
const isForwardRefCall = (node, reactImports) => {
|
|
96
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'forwardRef');
|
|
97
|
+
};
|
|
98
|
+
const isMemoCall = (node, reactImports) => {
|
|
99
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'memo');
|
|
100
|
+
};
|
|
101
|
+
const filterPresentResults = (results) => {
|
|
102
|
+
return results.filter((result) => Boolean(result));
|
|
103
|
+
};
|
|
104
|
+
const areAllComponentsCallbacks = (components) => {
|
|
105
|
+
return components.every((result) => result.componentIsCallback);
|
|
106
|
+
};
|
|
107
|
+
const createMergedResult = (components) => {
|
|
108
|
+
return {
|
|
109
|
+
found: true,
|
|
110
|
+
// Auto-fix wraps the callback in memo() and swaps the hook. That is only valid when
|
|
111
|
+
// every branch is itself a component callback (returns JSX/createElement directly).
|
|
112
|
+
// If any branch yields a component factory, the transformation changes behavior.
|
|
113
|
+
componentIsCallback: areAllComponentsCallbacks(components),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const mergeComponentResults = (...results) => {
|
|
117
|
+
const present = filterPresentResults(results);
|
|
118
|
+
if (!present.length) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return createMergedResult(present);
|
|
122
|
+
};
|
|
123
|
+
const expressionCreatesComponent = (expression, reactImports) => {
|
|
124
|
+
if (!expression) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const unwrapped = unwrapExpression(expression);
|
|
128
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
129
|
+
unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
130
|
+
return { found: true, componentIsCallback: true };
|
|
131
|
+
}
|
|
132
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
133
|
+
isReactCreateElementCall(unwrapped, reactImports)) {
|
|
134
|
+
return { found: true, componentIsCallback: true };
|
|
135
|
+
}
|
|
136
|
+
switch (unwrapped.type) {
|
|
137
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
138
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression: {
|
|
139
|
+
const inner = functionCreatesComponent(unwrapped, reactImports);
|
|
140
|
+
return inner ? { found: true, componentIsCallback: false } : null;
|
|
141
|
+
}
|
|
142
|
+
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
143
|
+
if (isForwardRefCall(unwrapped, reactImports) ||
|
|
144
|
+
isMemoCall(unwrapped, reactImports)) {
|
|
145
|
+
const firstArg = unwrapped.arguments[0];
|
|
146
|
+
if (firstArg && isFunctionExpression(firstArg)) {
|
|
147
|
+
const inner = functionCreatesComponent(firstArg, reactImports);
|
|
148
|
+
if (inner) {
|
|
149
|
+
return { found: true, componentIsCallback: false };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression: {
|
|
156
|
+
const cons = expressionCreatesComponent(unwrapped.consequent, reactImports);
|
|
157
|
+
const alt = expressionCreatesComponent(unwrapped.alternate, reactImports);
|
|
158
|
+
return mergeComponentResults(cons, alt);
|
|
159
|
+
}
|
|
160
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression: {
|
|
161
|
+
const left = expressionCreatesComponent(unwrapped.left, reactImports);
|
|
162
|
+
const right = expressionCreatesComponent(unwrapped.right, reactImports);
|
|
163
|
+
return mergeComponentResults(left, right);
|
|
164
|
+
}
|
|
165
|
+
case utils_1.AST_NODE_TYPES.SequenceExpression: {
|
|
166
|
+
const last = unwrapped.expressions[unwrapped.expressions.length - 1];
|
|
167
|
+
return expressionCreatesComponent(last || null, reactImports);
|
|
168
|
+
}
|
|
169
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression: {
|
|
170
|
+
const matches = unwrapped.elements
|
|
171
|
+
.map((element) => element && element.type !== utils_1.AST_NODE_TYPES.SpreadElement
|
|
172
|
+
? expressionCreatesComponent(element, reactImports)
|
|
173
|
+
: null)
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
return mergeComponentResults(...matches);
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const statementCreatesComponent = (node, reactImports) => {
|
|
182
|
+
switch (node.type) {
|
|
183
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
184
|
+
return expressionCreatesComponent(node.argument, reactImports);
|
|
185
|
+
case utils_1.AST_NODE_TYPES.IfStatement: {
|
|
186
|
+
const cons = node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
187
|
+
? blockCreatesComponent(node.consequent, reactImports)
|
|
188
|
+
: statementCreatesComponent(node.consequent, reactImports);
|
|
189
|
+
const alt = node.alternate
|
|
190
|
+
? node.alternate.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
191
|
+
? blockCreatesComponent(node.alternate, reactImports)
|
|
192
|
+
: statementCreatesComponent(node.alternate, reactImports)
|
|
193
|
+
: null;
|
|
194
|
+
return mergeComponentResults(cons, alt);
|
|
195
|
+
}
|
|
196
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement: {
|
|
197
|
+
const matches = [];
|
|
198
|
+
for (const switchCase of node.cases) {
|
|
199
|
+
for (const consequent of switchCase.consequent) {
|
|
200
|
+
matches.push(statementCreatesComponent(consequent, reactImports));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return mergeComponentResults(...matches);
|
|
204
|
+
}
|
|
205
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
206
|
+
return blockCreatesComponent(node, reactImports);
|
|
207
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
208
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
209
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
210
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
211
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement: {
|
|
212
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
213
|
+
return blockCreatesComponent(node.body, reactImports);
|
|
214
|
+
}
|
|
215
|
+
return statementCreatesComponent(node.body, reactImports);
|
|
216
|
+
}
|
|
217
|
+
case utils_1.AST_NODE_TYPES.TryStatement: {
|
|
218
|
+
const blockMatch = blockCreatesComponent(node.block, reactImports);
|
|
219
|
+
const handlerMatch = node.handler?.body
|
|
220
|
+
? blockCreatesComponent(node.handler.body, reactImports)
|
|
221
|
+
: null;
|
|
222
|
+
const finalizerMatch = node.finalizer
|
|
223
|
+
? blockCreatesComponent(node.finalizer, reactImports)
|
|
224
|
+
: null;
|
|
225
|
+
return mergeComponentResults(blockMatch, handlerMatch, finalizerMatch);
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const blockCreatesComponent = (block, reactImports) => {
|
|
232
|
+
const matches = [];
|
|
233
|
+
for (const statement of block.body) {
|
|
234
|
+
matches.push(statementCreatesComponent(statement, reactImports));
|
|
235
|
+
}
|
|
236
|
+
return mergeComponentResults(...matches);
|
|
237
|
+
};
|
|
238
|
+
const functionCreatesComponent = (node, reactImports) => {
|
|
239
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
240
|
+
return blockCreatesComponent(node.body, reactImports);
|
|
241
|
+
}
|
|
242
|
+
return expressionCreatesComponent(node.body, reactImports);
|
|
243
|
+
};
|
|
244
|
+
const shouldIgnoreFile = (filename, patterns) => {
|
|
245
|
+
return patterns.some((pattern) => (0, minimatch_1.minimatch)(filename, pattern));
|
|
246
|
+
};
|
|
247
|
+
const getVariableName = (node) => {
|
|
248
|
+
if (node.parent &&
|
|
249
|
+
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
250
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
251
|
+
return node.parent.id.name;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
const isInsideFunction = (node) => {
|
|
256
|
+
let parent = node.parent;
|
|
257
|
+
while (parent) {
|
|
258
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
259
|
+
parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
260
|
+
parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
parent = parent.parent;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
268
|
+
name: 'memo-nested-react-components',
|
|
269
|
+
meta: {
|
|
270
|
+
type: 'suggestion',
|
|
271
|
+
docs: {
|
|
272
|
+
description: 'Disallow React components defined in render bodies, hooks, or passed as props',
|
|
273
|
+
recommended: 'error',
|
|
274
|
+
},
|
|
275
|
+
schema: [
|
|
276
|
+
{
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
ignorePatterns: {
|
|
280
|
+
type: 'array',
|
|
281
|
+
items: { type: 'string' },
|
|
282
|
+
default: [],
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
messages: {
|
|
289
|
+
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inline inside {{locationDescription}}.
|
|
290
|
+
|
|
291
|
+
Why it matters: Inline components get new identities when their containing scope re-renders, causing React to unmount and remount them—dropping state, replaying animations, and causing UI flashes. Wrapping with memo() does NOT fix this—memo() only prevents re-renders when props change, not when the component identity itself changes.
|
|
292
|
+
|
|
293
|
+
How to fix:
|
|
294
|
+
1. Define the component at MODULE SCOPE in its own file, wrapped with memo()
|
|
295
|
+
2. Use React Context and/or directly provide props to supply any dynamic data the component needs
|
|
296
|
+
3. Pass the stable, imported component reference to props like CatalogWrapper
|
|
297
|
+
|
|
298
|
+
Don't pass inline function components to component-type props (*Wrapper, *Component).
|
|
299
|
+
Render-prop callbacks (e.g., render={...}) are fine; this rule targets component-type props only.
|
|
300
|
+
|
|
301
|
+
See: https://react.dev/learn/your-first-component#nesting-and-organizing-components`,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
defaultOptions: [{}],
|
|
305
|
+
create(context, [options]) {
|
|
306
|
+
const ignorePatterns = options?.ignorePatterns ?? [];
|
|
307
|
+
const filename = context.filename ?? context.getFilename();
|
|
308
|
+
if (ignorePatterns.length && shouldIgnoreFile(filename, ignorePatterns)) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
312
|
+
const reactImports = collectReactImports(sourceCode);
|
|
313
|
+
const reportNestedComponentViolation = (node, componentName, locationDescription) => {
|
|
314
|
+
context.report({
|
|
315
|
+
node,
|
|
316
|
+
messageId: 'memoizeNestedComponent',
|
|
317
|
+
data: {
|
|
318
|
+
componentName,
|
|
319
|
+
locationDescription,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
CallExpression(node) {
|
|
325
|
+
const hook = isHookCall(node.callee);
|
|
326
|
+
if (!hook)
|
|
327
|
+
return;
|
|
328
|
+
const callback = node.arguments[0];
|
|
329
|
+
if (!callback || callback.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const componentMatch = isFunctionExpression(callback)
|
|
333
|
+
? functionCreatesComponent(callback, reactImports)
|
|
334
|
+
: expressionCreatesComponent(callback, reactImports);
|
|
335
|
+
if (!componentMatch) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// For useMemo, it only counts as a component if it returns a function
|
|
339
|
+
if (hook.name.includes('useMemo') ||
|
|
340
|
+
hook.name.includes('useDeepCompareMemo')) {
|
|
341
|
+
if (componentMatch.componentIsCallback) {
|
|
342
|
+
// Returns JSX directly, so it's an element, not a component
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const componentName = getVariableName(node) ??
|
|
347
|
+
(isFunctionExpression(callback) &&
|
|
348
|
+
callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
|
|
349
|
+
? callback.id.name
|
|
350
|
+
: 'component');
|
|
351
|
+
reportNestedComponentViolation(node, componentName, `${hook.name}()`);
|
|
352
|
+
},
|
|
353
|
+
VariableDeclarator(node) {
|
|
354
|
+
if (!node.init)
|
|
355
|
+
return;
|
|
356
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
357
|
+
return;
|
|
358
|
+
// Only check if name starts with uppercase (convention for components)
|
|
359
|
+
if (!/^[A-Z]/.test(node.id.name))
|
|
360
|
+
return;
|
|
361
|
+
if (!isInsideFunction(node))
|
|
362
|
+
return;
|
|
363
|
+
// Skip if it's already a hook call (handled by CallExpression visitor)
|
|
364
|
+
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
365
|
+
const hook = isHookCall(node.init.callee);
|
|
366
|
+
if (hook)
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const componentMatch = expressionCreatesComponent(node.init, reactImports);
|
|
370
|
+
if (!componentMatch)
|
|
371
|
+
return;
|
|
372
|
+
// JSX elements assigned to variables are fine, only report function definitions
|
|
373
|
+
if (componentMatch.componentIsCallback)
|
|
374
|
+
return;
|
|
375
|
+
reportNestedComponentViolation(node, node.id.name, 'a render body');
|
|
376
|
+
},
|
|
377
|
+
FunctionDeclaration(node) {
|
|
378
|
+
if (!node.id || !/^[A-Z]/.test(node.id.name))
|
|
379
|
+
return;
|
|
380
|
+
if (!isInsideFunction(node))
|
|
381
|
+
return;
|
|
382
|
+
const componentMatch = functionCreatesComponent(node, reactImports);
|
|
383
|
+
if (!componentMatch)
|
|
384
|
+
return;
|
|
385
|
+
reportNestedComponentViolation(node.id, node.id.name, 'a render body');
|
|
386
|
+
},
|
|
387
|
+
JSXAttribute(node) {
|
|
388
|
+
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
389
|
+
return;
|
|
390
|
+
const attrName = node.name.name;
|
|
391
|
+
// Check if it's a component-type prop
|
|
392
|
+
if (!/(Wrapper|Component|Template|Header|Footer)$/.test(attrName)) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!node.value ||
|
|
396
|
+
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const expression = node.value.expression;
|
|
400
|
+
if (expression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression)
|
|
401
|
+
return;
|
|
402
|
+
const componentMatch = expressionCreatesComponent(expression, reactImports);
|
|
403
|
+
if (!componentMatch)
|
|
404
|
+
return;
|
|
405
|
+
// For props, we only report if it's a function (component definition)
|
|
406
|
+
if (componentMatch.componentIsCallback) {
|
|
407
|
+
// It's a JSX element passed directly, which is usually fine
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
reportNestedComponentViolation(node, attrName, `the "${attrName}" prop`);
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
//# sourceMappingURL=memo-nested-react-components.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'alwaysTrueCondition' | 'alwaysFalseCondition';
|
|
2
|
-
export declare const noAlwaysTrueFalseConditions:
|
|
3
|
+
export declare const noAlwaysTrueFalseConditions: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|
|
@@ -13,16 +13,69 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
13
13
|
},
|
|
14
14
|
schema: [],
|
|
15
15
|
messages: {
|
|
16
|
-
alwaysTrueCondition: '
|
|
17
|
-
alwaysFalseCondition: '
|
|
16
|
+
alwaysTrueCondition: 'What\'s wrong → Condition "{{condition}}" is always true.\nWhy it matters → The guarded branch runs every time, which can hide logic errors and leave redundant checks.\nHow to fix → Remove the check, or rewrite "{{condition}}" so it depends on runtime values instead of constants.',
|
|
17
|
+
alwaysFalseCondition: 'What\'s wrong → Condition "{{condition}}" is always false.\nWhy it matters → The guarded branch is unreachable, which leaves misleading or dead code.\nHow to fix → Remove the unreachable branch, or adjust "{{condition}}" so it can evaluate to true when intended.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
|
+
const sourceCode = context.getSourceCode();
|
|
23
|
+
const normalizedConditionText = (node) => sourceCode.getText(node).replace(/\s+/g, ' ').trim();
|
|
24
|
+
const conditionTextFor = (node) => ({
|
|
25
|
+
condition: normalizedConditionText(node),
|
|
26
|
+
});
|
|
22
27
|
// Track nodes that have already been reported to prevent duplicate reports
|
|
23
28
|
const reportedNodes = new Set();
|
|
24
29
|
// Track parent nodes that have been evaluated to prevent duplicate reports on children
|
|
25
30
|
const evaluatedParentNodes = new Set();
|
|
31
|
+
// Sentinel distinguishes "no const literal resolved" from valid literal values (including null/undefined)
|
|
32
|
+
const NOT_FOUND = Symbol('literal-not-found');
|
|
33
|
+
/**
|
|
34
|
+
* Walks the current scope chain to locate a variable by name so identifier
|
|
35
|
+
* lookups can resolve const initializers even when declared in parent scopes.
|
|
36
|
+
*/
|
|
37
|
+
function findVariableInScopes(name) {
|
|
38
|
+
let currentScope = context.getScope();
|
|
39
|
+
while (currentScope) {
|
|
40
|
+
const variable = currentScope.variables.find((variableItem) => variableItem.name === name);
|
|
41
|
+
if (variable) {
|
|
42
|
+
return variable;
|
|
43
|
+
}
|
|
44
|
+
currentScope = currentScope.upper;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function isConstIdentifierDefinition(def, identifierName) {
|
|
49
|
+
return (def.type === 'Variable' &&
|
|
50
|
+
def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
51
|
+
def.node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
52
|
+
def.node.id.name === identifierName &&
|
|
53
|
+
def.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
54
|
+
def.parent.kind === 'const');
|
|
55
|
+
}
|
|
56
|
+
function findConstIdentifierDefinition(variable, identifierName) {
|
|
57
|
+
return variable.defs.find((def) => isConstIdentifierDefinition(def, identifierName));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves an identifier to the literal it is initialized with when declared
|
|
61
|
+
* as a const. Returns the NOT_FOUND sentinel when the binding is absent,
|
|
62
|
+
* not a const, or not initialized with a literal value.
|
|
63
|
+
*/
|
|
64
|
+
function resolveIdentifierValue(identifier) {
|
|
65
|
+
const variable = findVariableInScopes(identifier.name);
|
|
66
|
+
if (!variable) {
|
|
67
|
+
return NOT_FOUND;
|
|
68
|
+
}
|
|
69
|
+
const definition = findConstIdentifierDefinition(variable, identifier.name);
|
|
70
|
+
if (!definition) {
|
|
71
|
+
return NOT_FOUND;
|
|
72
|
+
}
|
|
73
|
+
const initializer = definition.node.init;
|
|
74
|
+
if (initializer?.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
75
|
+
return initializer.value;
|
|
76
|
+
}
|
|
77
|
+
return NOT_FOUND;
|
|
78
|
+
}
|
|
26
79
|
/**
|
|
27
80
|
* Checks if a literal value is always truthy or falsy
|
|
28
81
|
*/
|
|
@@ -1309,6 +1362,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1309
1362
|
context.report({
|
|
1310
1363
|
node,
|
|
1311
1364
|
messageId: 'alwaysTrueCondition',
|
|
1365
|
+
data: conditionTextFor(node),
|
|
1312
1366
|
});
|
|
1313
1367
|
reportedNodes.add(node);
|
|
1314
1368
|
}
|
|
@@ -1316,6 +1370,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1316
1370
|
context.report({
|
|
1317
1371
|
node,
|
|
1318
1372
|
messageId: 'alwaysFalseCondition',
|
|
1373
|
+
data: conditionTextFor(node),
|
|
1319
1374
|
});
|
|
1320
1375
|
reportedNodes.add(node);
|
|
1321
1376
|
}
|
|
@@ -1372,12 +1427,14 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1372
1427
|
context.report({
|
|
1373
1428
|
node: node.test,
|
|
1374
1429
|
messageId: 'alwaysTrueCondition',
|
|
1430
|
+
data: conditionTextFor(node.test),
|
|
1375
1431
|
});
|
|
1376
1432
|
}
|
|
1377
1433
|
else {
|
|
1378
1434
|
context.report({
|
|
1379
1435
|
node: node.test,
|
|
1380
1436
|
messageId: 'alwaysFalseCondition',
|
|
1437
|
+
data: conditionTextFor(node.test),
|
|
1381
1438
|
});
|
|
1382
1439
|
}
|
|
1383
1440
|
reportedNodes.add(node.test);
|
|
@@ -1385,14 +1442,17 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
|
|
|
1385
1442
|
}
|
|
1386
1443
|
// Special cases for tests with variables
|
|
1387
1444
|
if (switchStatement.discriminant.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1388
|
-
node.test.type === utils_1.AST_NODE_TYPES.Identifier
|
|
1389
|
-
node.test
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1445
|
+
node.test.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
1446
|
+
const identifierValue = resolveIdentifierValue(node.test);
|
|
1447
|
+
if (identifierValue !== NOT_FOUND &&
|
|
1448
|
+
!reportedNodes.has(node.test)) {
|
|
1449
|
+
const messageId = identifierValue === switchStatement.discriminant.value
|
|
1450
|
+
? 'alwaysTrueCondition'
|
|
1451
|
+
: 'alwaysFalseCondition';
|
|
1393
1452
|
context.report({
|
|
1394
1453
|
node: node.test,
|
|
1395
|
-
messageId
|
|
1454
|
+
messageId,
|
|
1455
|
+
data: conditionTextFor(node.test),
|
|
1396
1456
|
});
|
|
1397
1457
|
reportedNodes.add(node.test);
|
|
1398
1458
|
}
|
|
@@ -141,7 +141,7 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
141
141
|
meta: {
|
|
142
142
|
type: 'problem',
|
|
143
143
|
docs: {
|
|
144
|
-
description: '
|
|
144
|
+
description: 'Detects array.length entries in React hook dependency arrays because length ignores content changes; auto-fixes by memoizing stableHash(array) with useMemo and depending on the hash instead.',
|
|
145
145
|
recommended: 'error',
|
|
146
146
|
},
|
|
147
147
|
fixable: 'code',
|
|
@@ -162,7 +162,7 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
162
162
|
},
|
|
163
163
|
],
|
|
164
164
|
messages: {
|
|
165
|
-
noArrayLengthInDeps: '
|
|
165
|
+
noArrayLengthInDeps: 'Dependency array includes length-based entries ({{dependencies}}). Array length only changes when items are added or removed, so hooks miss updates when array contents change at the same size. Memoize a stableHash of each array with useMemo and depend on that hash so the hook reruns when contents change.',
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
defaultOptions: [{}],
|
|
@@ -200,12 +200,18 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
|
|
|
200
200
|
}
|
|
201
201
|
if (lengthDeps.length === 0)
|
|
202
202
|
return;
|
|
203
|
+
const sourceCode = context.getSourceCode();
|
|
204
|
+
const dependencies = lengthDeps
|
|
205
|
+
.map(({ element }) => sourceCode.getText(element))
|
|
206
|
+
.join(', ');
|
|
203
207
|
// Report once on the dependency array
|
|
204
208
|
context.report({
|
|
205
209
|
node: depsArg,
|
|
206
210
|
messageId: 'noArrayLengthInDeps',
|
|
211
|
+
data: {
|
|
212
|
+
dependencies,
|
|
213
|
+
},
|
|
207
214
|
fix(fixer) {
|
|
208
|
-
const sourceCode = context.getSourceCode();
|
|
209
215
|
const fixes = [];
|
|
210
216
|
const hostFn = findEnclosingFunction(node) ?? sourceCode.ast;
|
|
211
217
|
const declaredBases = ensureWeakMapEntry(perFuncDeclaredBases, hostFn, () => new Set());
|