@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.
@@ -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