@blumintinc/eslint-plugin-blumint 1.20.12 → 1.20.13
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/require-props-composition.js +84 -0
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -237,6 +237,88 @@ function collectJsxElementNames(node) {
|
|
|
237
237
|
visit(node);
|
|
238
238
|
return names;
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Collect the capitalized binding names introduced by a destructuring pattern,
|
|
242
|
+
* following renames (`{ Slot: Renderer }`), defaults (`{ Slot = Fallback }`)
|
|
243
|
+
* and nesting (`{ slots: { Item } }`).
|
|
244
|
+
*/
|
|
245
|
+
function collectPatternBindings(pattern, into) {
|
|
246
|
+
switch (pattern.type) {
|
|
247
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
248
|
+
if (/^[A-Z]/.test(pattern.name)) {
|
|
249
|
+
into.add(pattern.name);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
253
|
+
for (const property of pattern.properties) {
|
|
254
|
+
if (property.type === utils_1.AST_NODE_TYPES.Property) {
|
|
255
|
+
collectPatternBindings(property.value, into);
|
|
256
|
+
}
|
|
257
|
+
// A `...rest` element rebinds the remaining props under a single
|
|
258
|
+
// lowercase-by-convention name; it introduces no component slot.
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
262
|
+
collectPatternBindings(pattern.left, into);
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Names of JSX elements that resolve to one of the component's own props — a
|
|
270
|
+
* rendering strategy injected by the caller (`ViewComponent: ComponentType<T>`)
|
|
271
|
+
* rather than a fixed child.
|
|
272
|
+
*
|
|
273
|
+
* A props-parameter binding shadows every import, so such an element is not a
|
|
274
|
+
* dependency the parent can compose with: the concrete component is chosen per
|
|
275
|
+
* call site and the slot's accepted props are already constrained by the prop's
|
|
276
|
+
* own type annotation. Demanding `<Slot>Props` composition names a type that
|
|
277
|
+
* exists nowhere.
|
|
278
|
+
*/
|
|
279
|
+
function collectPropSlotNames(funcNode) {
|
|
280
|
+
const slots = new Set();
|
|
281
|
+
const propsParam = funcNode.params[0];
|
|
282
|
+
if (!propsParam)
|
|
283
|
+
return slots;
|
|
284
|
+
if (propsParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
285
|
+
collectPatternBindings(propsParam, slots);
|
|
286
|
+
return slots;
|
|
287
|
+
}
|
|
288
|
+
// `(props: Props) => ...` — the slot may be destructured out of `props` in
|
|
289
|
+
// the body instead of in the signature.
|
|
290
|
+
if (propsParam.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
291
|
+
return slots;
|
|
292
|
+
const propsName = propsParam.name;
|
|
293
|
+
function visit(node) {
|
|
294
|
+
if (!node || typeof node !== 'object')
|
|
295
|
+
return;
|
|
296
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
297
|
+
node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
298
|
+
node.init?.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
299
|
+
node.init.name === propsName) {
|
|
300
|
+
collectPatternBindings(node.id, slots);
|
|
301
|
+
}
|
|
302
|
+
for (const key of Object.keys(node)) {
|
|
303
|
+
if (key === 'parent')
|
|
304
|
+
continue;
|
|
305
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
306
|
+
const child = node[key];
|
|
307
|
+
if (Array.isArray(child)) {
|
|
308
|
+
for (const item of child) {
|
|
309
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
310
|
+
visit(item);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (child && typeof child === 'object' && 'type' in child) {
|
|
315
|
+
visit(child);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
visit(funcNode.body);
|
|
320
|
+
return slots;
|
|
321
|
+
}
|
|
240
322
|
/**
|
|
241
323
|
* Find the Props type alias node that corresponds to a component by name.
|
|
242
324
|
* Looks for `type <ComponentName>Props = ...` in the program body.
|
|
@@ -929,10 +1011,12 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
|
|
|
929
1011
|
// Collect all JSX element names used in the component body
|
|
930
1012
|
const body = funcNode.body ?? funcNode;
|
|
931
1013
|
const allJsxNames = collectJsxElementNames(body);
|
|
1014
|
+
const propSlots = collectPropSlotNames(funcNode);
|
|
932
1015
|
// Filter to non-excluded custom components
|
|
933
1016
|
const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) &&
|
|
934
1017
|
!isDecorativeIcon(name) &&
|
|
935
1018
|
name !== componentName &&
|
|
1019
|
+
!propSlots.has(name) &&
|
|
936
1020
|
!isZeroPropComponent(prog, name));
|
|
937
1021
|
if (depComponents.length < minDependencyCount) {
|
|
938
1022
|
return;
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.13",
|
|
4
|
+
"date": "2026-07-29T06:26:25.973Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "require-props-composition",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1374
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt caller-injected component prop slots (closes #1374)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.12",
|
|
4
18
|
"date": "2026-07-28T22:32:01.811Z",
|