@blumintinc/eslint-plugin-blumint 1.20.11 → 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
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;
|
|
@@ -13,6 +13,26 @@ const DEFAULT_OPTIONS = {
|
|
|
13
13
|
additionalSubjectExtensions: [],
|
|
14
14
|
};
|
|
15
15
|
const normalizeExtension = (extension) => extension.startsWith('.') ? extension : `.${extension}`;
|
|
16
|
+
/**
|
|
17
|
+
* A suite may be split by concern into `Subject.<qualifier>.test.tsx` files that
|
|
18
|
+
* sit beside `Subject.tsx`. Progressively dropping trailing dot-segments lets
|
|
19
|
+
* such a test resolve to its subject at any qualifier depth, while never
|
|
20
|
+
* crossing a directory boundary — a genuinely misplaced test still reports.
|
|
21
|
+
* Empty prefixes are dropped so a dotfile stem cannot match a bare `.ts`.
|
|
22
|
+
*/
|
|
23
|
+
const subjectBaseNamesFor = (stem) => {
|
|
24
|
+
const segments = stem.split('.');
|
|
25
|
+
const baseNames = [];
|
|
26
|
+
for (let depth = segments.length; depth > 0; depth--) {
|
|
27
|
+
const baseName = segments.slice(0, depth).join('.');
|
|
28
|
+
if (baseName) {
|
|
29
|
+
baseNames.push(baseName);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// An extensionless stem (a file named exactly ".test.ts") leaves nothing to
|
|
33
|
+
// probe; keep it so the report still names what was looked for.
|
|
34
|
+
return baseNames.length > 0 ? baseNames : [stem];
|
|
35
|
+
};
|
|
16
36
|
exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
17
37
|
name: 'test-file-location-enforcement',
|
|
18
38
|
meta: {
|
|
@@ -57,17 +77,22 @@ exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
|
57
77
|
}
|
|
58
78
|
const directory = path_1.default.dirname(filename);
|
|
59
79
|
const testFileName = path_1.default.basename(filename);
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const hasSibling =
|
|
80
|
+
const stem = testFileName.replace(TEST_FILE_PATTERN, '');
|
|
81
|
+
const baseNames = subjectBaseNamesFor(stem);
|
|
82
|
+
const hasSibling = baseNames.some((baseName) => subjectExtensions.some((extension) => fs_1.default.existsSync(path_1.default.join(directory, `${baseName}${extension}`))));
|
|
63
83
|
if (hasSibling) {
|
|
64
84
|
return;
|
|
65
85
|
}
|
|
66
86
|
const relativePath = path_1.default.isAbsolute(filename)
|
|
67
87
|
? path_1.default.relative(process.cwd(), filename) || filename
|
|
68
88
|
: filename;
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
// Naming the shortest prefix alongside the full stem keeps the guidance
|
|
90
|
+
// honest: either subject name satisfies the rule.
|
|
91
|
+
const reportedBaseNames = baseNames.length > 1
|
|
92
|
+
? [baseNames[0], baseNames[baseNames.length - 1]]
|
|
93
|
+
: baseNames;
|
|
94
|
+
const expectedNames = reportedBaseNames
|
|
95
|
+
.flatMap((baseName) => subjectExtensions.map((extension) => `"${baseName}${extension}"`))
|
|
71
96
|
.join(' or ');
|
|
72
97
|
context.report({
|
|
73
98
|
node,
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
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
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.12",
|
|
18
|
+
"date": "2026-07-28T22:32:01.811Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "test-file-location-enforcement",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1372
|
|
25
|
+
],
|
|
26
|
+
"summary": "accept suite-qualifier test files (closes #1372)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.11",
|
|
4
32
|
"date": "2026-07-28T20:40:15.882Z",
|