@blumintinc/eslint-plugin-blumint 1.19.32 → 1.20.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/README.md +1 -0
- package/lib/index.js +4 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +240 -3
- package/lib/rules/enforce-firestore-facade.js +181 -4
- package/lib/rules/no-render-function-components.d.ts +8 -0
- package/lib/rules/no-render-function-components.js +269 -0
- package/lib/rules/react-memoize-literals.js +481 -10
- package/package.json +1 -1
- package/release-manifest.json +45 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noRenderFunctionComponents = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
/**
|
|
8
|
+
* Slot names that legitimately consume a `render*` callback BY REFERENCE.
|
|
9
|
+
* These belong to external libraries (MUI DataGrid `renderCell`, Autocomplete
|
|
10
|
+
* `renderInput`/`renderOption`, Algolia widgets' `render`, etc.) that invoke the
|
|
11
|
+
* callback themselves. Passing a function into one of these slots is NOT the
|
|
12
|
+
* anti-pattern this rule targets, so those wirings are exempt. The list is
|
|
13
|
+
* always applied; user-provided `renderPropNames` are added to (not replaced),
|
|
14
|
+
* so custom library slots can be exempted without losing the defaults.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_RENDER_PROP_NAMES = [
|
|
17
|
+
'renderCell',
|
|
18
|
+
'renderHeader',
|
|
19
|
+
'renderValue',
|
|
20
|
+
'renderInput',
|
|
21
|
+
'renderOption',
|
|
22
|
+
'renderTags',
|
|
23
|
+
'renderGroup',
|
|
24
|
+
'render',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Array iteration methods that accept a callback. Passing a `render*` function
|
|
28
|
+
* BY REFERENCE into one of these (e.g. `items.map(renderItem)`) consumes it as a
|
|
29
|
+
* plain function, which is the anti-pattern: the returned JSX is inlined instead
|
|
30
|
+
* of mounting as its own component.
|
|
31
|
+
*/
|
|
32
|
+
const ARRAY_ITERATION_METHODS = new Set([
|
|
33
|
+
'map',
|
|
34
|
+
'flatMap',
|
|
35
|
+
'forEach',
|
|
36
|
+
'reduce',
|
|
37
|
+
'reduceRight',
|
|
38
|
+
'filter',
|
|
39
|
+
'find',
|
|
40
|
+
'findIndex',
|
|
41
|
+
'findLast',
|
|
42
|
+
'findLastIndex',
|
|
43
|
+
'some',
|
|
44
|
+
'every',
|
|
45
|
+
]);
|
|
46
|
+
// Literal `render` prefix followed by an uppercase letter. Deliberately excludes
|
|
47
|
+
// bare `render`, `renderer`/`rendered` (lowercase continuation), PascalCase
|
|
48
|
+
// `Render*` (require-memo's domain), and `useRender*` hooks (no-jsx-in-hooks').
|
|
49
|
+
const RENDER_FN_NAME = /^render[A-Z]/;
|
|
50
|
+
function isFunctionLikeInit(node) {
|
|
51
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
52
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
53
|
+
}
|
|
54
|
+
function getJsxAttributeName(name) {
|
|
55
|
+
if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
56
|
+
return name.name;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
exports.noRenderFunctionComponents = (0, createRule_1.createRule)({
|
|
61
|
+
name: 'no-render-function-components',
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'problem',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Disallow declaring a local `render*` function that returns JSX and consuming it as a plain function (direct call or array-iteration callback); author it as a real component rendered with JSX instead',
|
|
66
|
+
recommended: 'error',
|
|
67
|
+
},
|
|
68
|
+
schema: [
|
|
69
|
+
{
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
renderPropNames: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: { type: 'string' },
|
|
75
|
+
},
|
|
76
|
+
allowNames: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
items: { type: 'string' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
messages: {
|
|
85
|
+
renderFunctionComponent: 'Local function "{{name}}" returns JSX and is consumed as a plain function (called directly or passed to an array method like .map). ' +
|
|
86
|
+
'A function that takes inputs and returns JSX IS a React component, so it should be a PascalCase functional component rendered as JSX (e.g. <{{suggestion}} {...props} />), not invoked like a helper. ' +
|
|
87
|
+
'Calling it directly forfeits React reconciliation and key semantics, isolated hook scope, a memoization boundary, and DevTools visibility — the elements are inlined into the parent instead of mounting as their own component. ' +
|
|
88
|
+
'Extract "{{name}}" into a real component named "{{suggestion}}" and render it with JSX.',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
defaultOptions: [{}],
|
|
92
|
+
create(context, [options]) {
|
|
93
|
+
const userRenderPropNames = options?.renderPropNames ?? [];
|
|
94
|
+
const renderPropNames = new Set([
|
|
95
|
+
...DEFAULT_RENDER_PROP_NAMES,
|
|
96
|
+
...userRenderPropNames,
|
|
97
|
+
]);
|
|
98
|
+
const allowNamePatterns = (options?.allowNames ?? []).map((pattern) => new RegExp(pattern));
|
|
99
|
+
const candidates = [];
|
|
100
|
+
function isAllowed(name) {
|
|
101
|
+
return allowNamePatterns.some((pattern) => pattern.test(name));
|
|
102
|
+
}
|
|
103
|
+
function findVariable(node, name) {
|
|
104
|
+
const declared = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, node);
|
|
105
|
+
return declared.find((variable) => variable.name === name) ?? null;
|
|
106
|
+
}
|
|
107
|
+
// A plain function invocation: renderFoo(...) where the identifier is the callee.
|
|
108
|
+
function isDirectCall(id) {
|
|
109
|
+
const parent = id.parent;
|
|
110
|
+
return (!!parent &&
|
|
111
|
+
parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
112
|
+
parent.callee === id);
|
|
113
|
+
}
|
|
114
|
+
// A by-reference pass into an array-iteration method: arr.map(renderFoo).
|
|
115
|
+
function isArrayIterationPass(id) {
|
|
116
|
+
const parent = id.parent;
|
|
117
|
+
if (!parent || parent.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (!parent.arguments.includes(id)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const callee = parent.callee;
|
|
124
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
125
|
+
callee.computed ||
|
|
126
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return ARRAY_ITERATION_METHODS.has(callee.property.name);
|
|
130
|
+
}
|
|
131
|
+
// A by-reference pass into a render-prop slot: <X renderCell={renderFoo} />
|
|
132
|
+
// or { renderCell: renderFoo }. The exemption anchors on the slot name, not
|
|
133
|
+
// the function's own name.
|
|
134
|
+
function isRenderPropSlotReference(id) {
|
|
135
|
+
const parent = id.parent;
|
|
136
|
+
if (!parent) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (parent.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
140
|
+
const attribute = parent.parent;
|
|
141
|
+
if (attribute &&
|
|
142
|
+
attribute.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
143
|
+
attribute.value === parent) {
|
|
144
|
+
const attrName = getJsxAttributeName(attribute.name);
|
|
145
|
+
if (attrName && renderPropNames.has(attrName)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (parent.type === utils_1.AST_NODE_TYPES.Property && parent.value === id) {
|
|
151
|
+
const key = parent.key;
|
|
152
|
+
let keyName = null;
|
|
153
|
+
if (!parent.computed && key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
154
|
+
keyName = key.name;
|
|
155
|
+
}
|
|
156
|
+
else if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
157
|
+
typeof key.value === 'string') {
|
|
158
|
+
keyName = key.value;
|
|
159
|
+
}
|
|
160
|
+
if (keyName && renderPropNames.has(keyName)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function isExportSpecifierReference(id) {
|
|
167
|
+
const parent = id.parent;
|
|
168
|
+
return !!parent && parent.type === utils_1.AST_NODE_TYPES.ExportSpecifier;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
VariableDeclarator(node) {
|
|
172
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const name = node.id.name;
|
|
176
|
+
if (!RENDER_FN_NAME.test(name) || isAllowed(name)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!node.init) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const init = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(node.init);
|
|
183
|
+
if (!isFunctionLikeInit(init)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!ASTHelpers_1.ASTHelpers.returnsJSX(init, context)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const declaration = node.parent;
|
|
190
|
+
const isExported = !!declaration &&
|
|
191
|
+
declaration.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
192
|
+
!!declaration.parent &&
|
|
193
|
+
(declaration.parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
194
|
+
declaration.parent.type ===
|
|
195
|
+
utils_1.AST_NODE_TYPES.ExportDefaultDeclaration);
|
|
196
|
+
candidates.push({
|
|
197
|
+
name,
|
|
198
|
+
reportNode: node.id,
|
|
199
|
+
variable: findVariable(node, name),
|
|
200
|
+
isExported,
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
FunctionDeclaration(node) {
|
|
204
|
+
if (!node.id) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const name = node.id.name;
|
|
208
|
+
if (!RENDER_FN_NAME.test(name) || isAllowed(name)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!ASTHelpers_1.ASTHelpers.returnsJSX(node, context)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const isExported = !!node.parent &&
|
|
215
|
+
(node.parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
216
|
+
node.parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration);
|
|
217
|
+
candidates.push({
|
|
218
|
+
name,
|
|
219
|
+
reportNode: node.id,
|
|
220
|
+
variable: findVariable(node, name),
|
|
221
|
+
isExported,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
'Program:exit'() {
|
|
225
|
+
for (const candidate of candidates) {
|
|
226
|
+
const references = candidate.variable
|
|
227
|
+
? candidate.variable.references
|
|
228
|
+
: [];
|
|
229
|
+
let hasViolatingConsumption = false;
|
|
230
|
+
let isWiredToSlot = false;
|
|
231
|
+
let exportedViaSpecifier = false;
|
|
232
|
+
for (const reference of references) {
|
|
233
|
+
// Skip the declaration write and any reassignments — only reads are
|
|
234
|
+
// consumption sites.
|
|
235
|
+
if (typeof reference.isRead === 'function' && !reference.isRead()) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const id = reference.identifier;
|
|
239
|
+
if (isDirectCall(id) || isArrayIterationPass(id)) {
|
|
240
|
+
hasViolatingConsumption = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (isRenderPropSlotReference(id)) {
|
|
244
|
+
isWiredToSlot = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (isExportSpecifierReference(id)) {
|
|
248
|
+
exportedViaSpecifier = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const isExported = candidate.isExported || exportedViaSpecifier;
|
|
253
|
+
const shouldFlag = hasViolatingConsumption || (isExported && !isWiredToSlot);
|
|
254
|
+
if (shouldFlag) {
|
|
255
|
+
context.report({
|
|
256
|
+
node: candidate.reportNode,
|
|
257
|
+
messageId: 'renderFunctionComponent',
|
|
258
|
+
data: {
|
|
259
|
+
name: candidate.name,
|
|
260
|
+
suggestion: candidate.name.slice('render'.length),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
//# sourceMappingURL=no-render-function-components.js.map
|