@blumintinc/eslint-plugin-blumint 1.19.29 → 1.19.31

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.29',
225
+ version: '1.19.31',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -921,6 +921,122 @@ const BOOLEAN_POSITIVE_ALTERNATIVES = {
921
921
  WILL_NOT: ['WILL'],
922
922
  DOES_NOT: ['DOES'],
923
923
  };
924
+ /**
925
+ * Recognizes type nodes that denote a boolean-only value: the `boolean`
926
+ * keyword, a `true`/`false` literal type, or a union whose every arm is
927
+ * boolean. A validator's `string | true` return type is deliberately NOT
928
+ * boolean-only, so a predicate annotated that way is not treated as a boolean.
929
+ */
930
+ function isBooleanOnlyType(typeNode) {
931
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
932
+ return true;
933
+ }
934
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
935
+ typeNode.literal.type === utils_1.AST_NODE_TYPES.Literal &&
936
+ typeof typeNode.literal.value === 'boolean') {
937
+ return true;
938
+ }
939
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
940
+ return typeNode.types.every(isBooleanOnlyType);
941
+ }
942
+ return false;
943
+ }
944
+ /**
945
+ * Detects an expression that is definitively non-boolean — a string or number
946
+ * literal, or a template/object/array literal. Validator predicates return an
947
+ * error-message string on rejection (and `true` on success), so a string
948
+ * literal return is the syntactic tell that an `is`/`has`-prefixed value is not
949
+ * actually a boolean. Ambiguous expressions (calls, identifiers, comparisons,
950
+ * negations) are intentionally NOT treated as non-boolean, preserving the
951
+ * rule's existing flagging of genuine negatively-named booleans.
952
+ */
953
+ function isDefinitelyNonBooleanExpression(node) {
954
+ switch (node.type) {
955
+ case utils_1.AST_NODE_TYPES.Literal:
956
+ return typeof node.value === 'string' || typeof node.value === 'number';
957
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
958
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
959
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
960
+ return true;
961
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
962
+ return (isDefinitelyNonBooleanExpression(node.consequent) ||
963
+ isDefinitelyNonBooleanExpression(node.alternate));
964
+ default:
965
+ return false;
966
+ }
967
+ }
968
+ /**
969
+ * Yields the immediate AST-node children of `node`, skipping the `parent`
970
+ * back-reference so traversal only walks downward.
971
+ */
972
+ function childNodesOf(node) {
973
+ const children = [];
974
+ for (const key of Object.keys(node)) {
975
+ if (key === 'parent')
976
+ continue;
977
+ const value = node[key];
978
+ const candidates = Array.isArray(value) ? value : [value];
979
+ for (const candidate of candidates) {
980
+ if (candidate &&
981
+ typeof candidate === 'object' &&
982
+ typeof candidate.type === 'string') {
983
+ children.push(candidate);
984
+ }
985
+ }
986
+ }
987
+ return children;
988
+ }
989
+ /**
990
+ * Whether any `return` statement belonging to `fn`'s own body (not a nested
991
+ * function's) yields a definitively non-boolean value.
992
+ */
993
+ function blockReturnsNonBoolean(block) {
994
+ const stack = [block];
995
+ while (stack.length > 0) {
996
+ const current = stack.pop();
997
+ // Do not descend into nested function scopes: their returns belong to them.
998
+ if (current !== block &&
999
+ (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
1000
+ current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
1001
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
1002
+ continue;
1003
+ }
1004
+ if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
1005
+ current.argument &&
1006
+ isDefinitelyNonBooleanExpression(current.argument)) {
1007
+ return true;
1008
+ }
1009
+ for (const child of childNodesOf(current)) {
1010
+ stack.push(child);
1011
+ }
1012
+ }
1013
+ return false;
1014
+ }
1015
+ /**
1016
+ * Whether a function backing an `is`/`has`-prefixed name actually returns a
1017
+ * non-boolean value — e.g. a validator predicate returning `string | true`. An
1018
+ * explicit return-type annotation is authoritative; otherwise the body's own
1019
+ * `return` statements (or the concise-arrow expression) are inspected.
1020
+ */
1021
+ function functionReturnsNonBoolean(fn) {
1022
+ if (fn.returnType) {
1023
+ return !isBooleanOnlyType(fn.returnType.typeAnnotation);
1024
+ }
1025
+ if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
1026
+ return isDefinitelyNonBooleanExpression(fn.body);
1027
+ }
1028
+ return blockReturnsNonBoolean(fn.body);
1029
+ }
1030
+ /**
1031
+ * When a declarator/property value is a function, whether that function is a
1032
+ * non-boolean predicate that must be exempt from boolean negative-naming.
1033
+ */
1034
+ function isNonBooleanFunctionValue(node) {
1035
+ return (!!node &&
1036
+ (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
1037
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
1038
+ functionReturnsNonBoolean(node));
1039
+ }
924
1040
  exports.enforcePositiveNaming = (0, createRule_1.createRule)({
925
1041
  name: 'enforce-positive-naming',
926
1042
  meta: {
@@ -1152,6 +1268,12 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
1152
1268
  // Only check boolean-like variables
1153
1269
  if (!isBooleanLike(node.id) && !isBooleanLike(node))
1154
1270
  return;
1271
+ // A validator predicate (e.g. returns `string | true`) is name-prefixed
1272
+ // with `is`/`has` but is not a boolean, so its domain-correct negation
1273
+ // ("isNotBlank") must not be flagged. The name heuristic alone cannot
1274
+ // tell them apart; the initializer's return shape can.
1275
+ if (isNonBooleanFunctionValue(node.init))
1276
+ return;
1155
1277
  const variableName = node.id.name;
1156
1278
  const { isNegative, alternatives } = hasBooleanNegativeNaming(variableName);
1157
1279
  if (isNegative) {
@@ -1192,6 +1314,10 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
1192
1314
  // Only check boolean-returning functions
1193
1315
  if (!isBooleanLike(node.id || node))
1194
1316
  return;
1317
+ // Skip validator predicates that return a non-boolean value (e.g.
1318
+ // `string | true`), whose negation is the domain-correct term.
1319
+ if (functionReturnsNonBoolean(node))
1320
+ return;
1195
1321
  const { isNegative, alternatives } = hasBooleanNegativeNaming(functionName);
1196
1322
  if (isNegative) {
1197
1323
  context.report({
@@ -1213,6 +1339,9 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
1213
1339
  // Only check boolean-returning methods
1214
1340
  if (!isBooleanLike(node.key))
1215
1341
  return;
1342
+ // Skip validator predicates returning a non-boolean value.
1343
+ if (isNonBooleanFunctionValue(node.value))
1344
+ return;
1216
1345
  const methodName = node.key.name;
1217
1346
  const { isNegative, alternatives } = hasBooleanNegativeNaming(methodName);
1218
1347
  if (isNegative) {
@@ -1235,6 +1364,9 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
1235
1364
  // Only check boolean properties
1236
1365
  if (!isBooleanLike(node.key))
1237
1366
  return;
1367
+ // Skip validator predicates returning a non-boolean value.
1368
+ if (isNonBooleanFunctionValue(node.value))
1369
+ return;
1238
1370
  const propertyName = node.key.name;
1239
1371
  const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
1240
1372
  if (isNegative) {
@@ -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.29",
3
+ "version": "1.19.31",
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.31",
4
+ "date": "2026-07-24T00:26:32.378Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-positive-naming",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1344
11
+ ],
12
+ "summary": "don't flag non-boolean validator predicates (closes #1344)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.30",
18
+ "date": "2026-07-23T22:28:34.611Z",
19
+ "rules": [
20
+ {
21
+ "name": "require-props-composition",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1343
25
+ ],
26
+ "summary": "resolve named aliases through union arms (closes #1343)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.19.29",
4
32
  "date": "2026-07-23T21:23:38.334Z",