@blumintinc/eslint-plugin-blumint 1.19.28 → 1.19.30
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
|
@@ -27,6 +27,21 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
27
27
|
// Regular expression to match standalone "id" surrounded by whitespace or punctuation
|
|
28
28
|
// This ensures we only match "id" as a word, not as part of another word
|
|
29
29
|
const idRegex = /(^|\s|[.,;:!?'"()\[\]{}])id(\s|$|[.,;:!?'"()\[\]{}])/g;
|
|
30
|
+
// DOM / Testing-Library APIs whose first argument is an attribute NAME
|
|
31
|
+
// (code), not user-facing text. A literal like 'id' passed here is a DOM
|
|
32
|
+
// attribute name; flagging or rewriting it to 'ID' breaks the call.
|
|
33
|
+
const ATTRIBUTE_NAME_METHODS = new Set([
|
|
34
|
+
'getAttribute',
|
|
35
|
+
'setAttribute',
|
|
36
|
+
'hasAttribute',
|
|
37
|
+
'removeAttribute',
|
|
38
|
+
'getAttributeNode',
|
|
39
|
+
'getAttributeNS',
|
|
40
|
+
'setAttributeNS',
|
|
41
|
+
'hasAttributeNS',
|
|
42
|
+
'removeAttributeNS',
|
|
43
|
+
'toHaveAttribute',
|
|
44
|
+
]);
|
|
30
45
|
/**
|
|
31
46
|
* Check if a node is in a context that should be excluded from the rule
|
|
32
47
|
* (e.g., parameter names, property names, type definitions)
|
|
@@ -82,6 +97,24 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
82
97
|
if (node.parent && node.parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
83
98
|
return true;
|
|
84
99
|
}
|
|
100
|
+
// Check if the node is the attribute-name argument of a DOM / jest-dom
|
|
101
|
+
// attribute API call, e.g. element.getAttribute('id') or
|
|
102
|
+
// expect(el).toHaveAttribute('id', ...). The attribute name is code, not
|
|
103
|
+
// user-facing text. For the *NS variants the name is the second argument
|
|
104
|
+
// (the first is the namespace URI); otherwise it is the first argument.
|
|
105
|
+
if (node.parent &&
|
|
106
|
+
node.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
107
|
+
node.parent.callee &&
|
|
108
|
+
node.parent.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
109
|
+
node.parent.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
110
|
+
ATTRIBUTE_NAME_METHODS.has(node.parent.callee.property.name)) {
|
|
111
|
+
const nameArgIndex = node.parent.callee.property.name.endsWith('NS')
|
|
112
|
+
? 1
|
|
113
|
+
: 0;
|
|
114
|
+
if (node.parent.arguments[nameArgIndex] === node) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
85
118
|
// Check if the node is a string literal used for property access
|
|
86
119
|
// This handles cases like obj['id'] or OverwolfGame['id']
|
|
87
120
|
if (node.parent &&
|
|
@@ -112,10 +112,18 @@ function getTypeReferenceName(node) {
|
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
114
|
* Recursively check if a TS type node composes with the given propsTypeName
|
|
115
|
-
* via Pick/Omit (at any level of intersection / Readonly wrapping,
|
|
116
|
-
* in a TSTypeLiteral property's type
|
|
115
|
+
* via Pick/Omit (at any level of intersection / Readonly wrapping, union arm,
|
|
116
|
+
* named-alias indirection, or nested in a TSTypeLiteral property's type
|
|
117
|
+
* annotation).
|
|
118
|
+
*
|
|
119
|
+
* `program` (when supplied) enables resolving a locally-declared named type
|
|
120
|
+
* alias to its definition, so composition can be seen through named union arms
|
|
121
|
+
* and shared bases. `seenAliases` guards against recursive-alias cycles; each
|
|
122
|
+
* descent *through* an alias extends a copy of the set so that sibling paths
|
|
123
|
+
* (e.g. two union arms sharing a base) each resolve the shared alias
|
|
124
|
+
* independently.
|
|
117
125
|
*/
|
|
118
|
-
function typeNodeComposesWithProps(typeNode, propsTypeName) {
|
|
126
|
+
function typeNodeComposesWithProps(typeNode, propsTypeName, program, seenAliases = new Set()) {
|
|
119
127
|
switch (typeNode.type) {
|
|
120
128
|
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
121
129
|
// A direct reference to the child's whole props type (bare `ChildProps`
|
|
@@ -131,20 +139,45 @@ function typeNodeComposesWithProps(typeNode, propsTypeName) {
|
|
|
131
139
|
// Also recurse into type params (e.g. Readonly<Pick<XProps, ...>>)
|
|
132
140
|
if (typeNode.typeParameters) {
|
|
133
141
|
for (const param of typeNode.typeParameters.params) {
|
|
134
|
-
if (typeNodeComposesWithProps(param, propsTypeName)) {
|
|
142
|
+
if (typeNodeComposesWithProps(param, propsTypeName, program, seenAliases)) {
|
|
135
143
|
return true;
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
147
|
+
// Resolve a locally-declared named type alias to its definition and
|
|
148
|
+
// recurse. This lets composition be seen through named union arms and
|
|
149
|
+
// shared bases (issue #1343): `RowActionableProps` → `RowBaseProps & {…}`
|
|
150
|
+
// → `Pick<MenuItemProps, …>`. Only in-file aliases resolve; imported
|
|
151
|
+
// names (e.g. MenuItemProps) return null and are left as-is.
|
|
152
|
+
if (program) {
|
|
153
|
+
const aliasName = getTypeReferenceName(typeNode);
|
|
154
|
+
if (aliasName && !seenAliases.has(aliasName)) {
|
|
155
|
+
const alias = findPropsTypeAliasByName(program, aliasName);
|
|
156
|
+
if (alias) {
|
|
157
|
+
const nextSeen = new Set(seenAliases);
|
|
158
|
+
nextSeen.add(aliasName);
|
|
159
|
+
if (typeNodeComposesWithProps(alias.typeAnnotation, propsTypeName, program, nextSeen)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
139
165
|
return false;
|
|
140
166
|
}
|
|
141
167
|
case utils_1.AST_NODE_TYPES.TSIntersectionType: {
|
|
142
|
-
// Check each member of an intersection (A & B & C)
|
|
143
|
-
|
|
168
|
+
// Check each member of an intersection (A & B & C) — the whole
|
|
169
|
+
// intersection composes if any member does.
|
|
170
|
+
return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName, program, seenAliases));
|
|
144
171
|
}
|
|
145
172
|
case utils_1.AST_NODE_TYPES.TSUnionType: {
|
|
146
|
-
//
|
|
147
|
-
|
|
173
|
+
// A union (A | B) composes if ANY arm composes. `.some` (not `.every`) is
|
|
174
|
+
// deliberate: a discriminated union commonly renders a *different* child
|
|
175
|
+
// per arm (issue #1343's EditableBoolean: `Omit<SwitchProps>` on one arm,
|
|
176
|
+
// `Omit<CheckboxProps>` on the other). Requiring every arm to compose with
|
|
177
|
+
// every rendered child would flag that legitimate pattern — a false
|
|
178
|
+
// positive the repo prefers to avoid. `.some` still passes the target
|
|
179
|
+
// case, where every arm composes with the single shared child.
|
|
180
|
+
return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName, program, seenAliases));
|
|
148
181
|
}
|
|
149
182
|
case utils_1.AST_NODE_TYPES.TSTypeLiteral: {
|
|
150
183
|
// Check property signatures for nested composition
|
|
@@ -152,7 +185,7 @@ function typeNodeComposesWithProps(typeNode, propsTypeName) {
|
|
|
152
185
|
return typeNode.members.some((member) => {
|
|
153
186
|
if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
154
187
|
member.typeAnnotation) {
|
|
155
|
-
return typeNodeComposesWithProps(member.typeAnnotation.typeAnnotation, propsTypeName);
|
|
188
|
+
return typeNodeComposesWithProps(member.typeAnnotation.typeAnnotation, propsTypeName, program, seenAliases);
|
|
156
189
|
}
|
|
157
190
|
return false;
|
|
158
191
|
});
|
|
@@ -498,7 +531,7 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
|
|
|
498
531
|
const missingComposition = [];
|
|
499
532
|
for (const dep of depComponents) {
|
|
500
533
|
const expectedPropsType = toPropsTypeName(dep);
|
|
501
|
-
let composes = typeNodeComposesWithProps(propsTypeNode, expectedPropsType);
|
|
534
|
+
let composes = typeNodeComposesWithProps(propsTypeNode, expectedPropsType, prog);
|
|
502
535
|
// Inverse composition: the child derives its props FROM this parent's
|
|
503
536
|
// props type (e.g. `Omit<ParentProps, 'children'>`, often with no named
|
|
504
537
|
// ChildProps at all). The parent is then the single shared source of
|
|
@@ -508,7 +541,7 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
|
|
|
508
541
|
if (!composes && propsTypeName) {
|
|
509
542
|
const depPropsSource = getDependencyPropsSourceType(prog, dep);
|
|
510
543
|
if (depPropsSource &&
|
|
511
|
-
typeNodeComposesWithProps(depPropsSource, propsTypeName)) {
|
|
544
|
+
typeNodeComposesWithProps(depPropsSource, propsTypeName, prog)) {
|
|
512
545
|
composes = true;
|
|
513
546
|
}
|
|
514
547
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.30",
|
|
4
|
+
"date": "2026-07-23T22:28:34.611Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "require-props-composition",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1343
|
|
11
|
+
],
|
|
12
|
+
"summary": "resolve named aliases through union arms (closes #1343)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.29",
|
|
18
|
+
"date": "2026-07-23T21:23:38.334Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-id-capitalization",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1337
|
|
25
|
+
],
|
|
26
|
+
"summary": "exempt DOM attribute-name arguments (closes #1337)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.28",
|
|
4
32
|
"date": "2026-07-23T17:25:39.744Z",
|