@blumintinc/eslint-plugin-blumint 1.19.32 → 1.20.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
CHANGED
|
@@ -196,6 +196,7 @@ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.
|
|
|
196
196
|
| [no-redundant-param-types](docs/rules/no-redundant-param-types.md) | Disallow redundant parameter type annotations | ✅ | | | 🔧 | | |
|
|
197
197
|
| [no-redundant-this-params](docs/rules/no-redundant-this-params.md) | Disallow passing class instance members (this.foo) into class instance methods; access the member from this inside the method instead. | ✅ | | | | | |
|
|
198
198
|
| [no-redundant-usecallback-wrapper](docs/rules/no-redundant-usecallback-wrapper.md) | Prevent wrapping already memoized/stable callbacks from hooks/contexts in an extra useCallback() | ✅ | | | 🔧 | | |
|
|
199
|
+
| [no-render-function-components](docs/rules/no-render-function-components.md) | 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 | ✅ | | | | | |
|
|
199
200
|
| [no-res-error-status-in-onrequest](docs/rules/no-res-error-status-in-onrequest.md) | Forbid sending 4xx/5xx Express responses inside onRequest handlers; throw structured HttpsError instances instead so the wrapper can format, log, and map errors consistently. | ✅ | | | | | |
|
|
200
201
|
| [no-restricted-properties-fix](docs/rules/no-restricted-properties-fix.md) | Disallow certain properties on certain objects, with special handling for Object.keys() and Object.values() | | | | 🔧 | | |
|
|
201
202
|
| [no-satisfies-in-frontend-bundle](docs/rules/no-satisfies-in-frontend-bundle.md) | Disallow the `satisfies` operator in files that reach the frontend webpack bundle (Next.js 12 SWC cannot parse it) | ✅ | | | | | |
|
package/lib/index.js
CHANGED
|
@@ -196,6 +196,7 @@ const no_single_dismiss_dialog_button_1 = require("./rules/no-single-dismiss-dia
|
|
|
196
196
|
const no_portal_inside_tooltip_1 = require("./rules/no-portal-inside-tooltip");
|
|
197
197
|
const no_satisfies_in_frontend_bundle_1 = require("./rules/no-satisfies-in-frontend-bundle");
|
|
198
198
|
const prefer_utility_function_own_file_1 = require("./rules/prefer-utility-function-own-file");
|
|
199
|
+
const no_render_function_components_1 = require("./rules/no-render-function-components");
|
|
199
200
|
const NO_FRONTEND_IMPORTS_FROM_FUNCTIONS_MESSAGE = 'Backend Cloud Functions (.f.ts under functions/) must not import frontend modules from the repo root src/**. Frontend code can depend on browser-only APIs and bundling it into Cloud Functions breaks server execution; move shared logic into functions/src or a shared package.';
|
|
200
201
|
function noFrontendImportsFromFunctionsPatterns(pattern) {
|
|
201
202
|
return [
|
|
@@ -222,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
|
|
|
222
223
|
module.exports = {
|
|
223
224
|
meta: {
|
|
224
225
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
225
|
-
version: '1.
|
|
226
|
+
version: '1.20.0',
|
|
226
227
|
},
|
|
227
228
|
parseOptions: {
|
|
228
229
|
ecmaVersion: 2020,
|
|
@@ -424,6 +425,7 @@ module.exports = {
|
|
|
424
425
|
'@blumintinc/blumint/no-portal-inside-tooltip': 'error',
|
|
425
426
|
'@blumintinc/blumint/no-satisfies-in-frontend-bundle': 'error',
|
|
426
427
|
'@blumintinc/blumint/prefer-utility-function-own-file': 'error',
|
|
428
|
+
'@blumintinc/blumint/no-render-function-components': 'error',
|
|
427
429
|
},
|
|
428
430
|
/**
|
|
429
431
|
* Depth-specific overrides block only import strings that traverse to the
|
|
@@ -700,6 +702,7 @@ module.exports = {
|
|
|
700
702
|
'no-portal-inside-tooltip': no_portal_inside_tooltip_1.noPortalInsideTooltip,
|
|
701
703
|
'no-satisfies-in-frontend-bundle': no_satisfies_in_frontend_bundle_1.noSatisfiesInFrontendBundle,
|
|
702
704
|
'prefer-utility-function-own-file': prefer_utility_function_own_file_1.preferUtilityFunctionOwnFile,
|
|
705
|
+
'no-render-function-components': no_render_function_components_1.noRenderFunctionComponents,
|
|
703
706
|
},
|
|
704
707
|
};
|
|
705
708
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type RuleOptions = {
|
|
3
|
+
renderPropNames?: string[];
|
|
4
|
+
allowNames?: string[];
|
|
5
|
+
};
|
|
6
|
+
type Options = [RuleOptions?];
|
|
7
|
+
export declare const noRenderFunctionComponents: TSESLint.RuleModule<"renderFunctionComponent", Options, TSESLint.RuleListener>;
|
|
8
|
+
export {};
|
|
@@ -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
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.0",
|
|
4
|
+
"date": "2026-07-24T15:36:40.953Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-render-function-components",
|
|
8
|
+
"changeType": "feat",
|
|
9
|
+
"issues": [
|
|
10
|
+
1345
|
|
11
|
+
],
|
|
12
|
+
"summary": "flag render* functions returning JSX consumed as plain calls (closes #1345)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.19.32",
|
|
4
18
|
"date": "2026-07-24T00:36:05.744Z",
|