@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
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.28',
225
+ version: '1.19.30',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -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, or nested
116
- * in a TSTypeLiteral property's type annotation).
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
- return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName));
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
- // Check each member of a union for union types, at least one member composes
147
- return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.28",
3
+ "version": "1.19.30",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",