@blumintinc/eslint-plugin-blumint 1.20.165 → 1.20.167

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.165',
226
+ version: '1.20.167',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -822,12 +822,55 @@ function isPropertyComplex(prop, checker, tsNode, ts, treatAnyAsComplex, parentT
822
822
  }
823
823
  return shouldTreatAnyAsComplex(prop, propType, ts, treatAnyAsComplex, parentTypeFlags, checker);
824
824
  }
825
+ /**
826
+ * Returns true when `declaration` was written by a dependency rather than by the
827
+ * component's author: a declaration file, or any file under `node_modules`.
828
+ * Keys on the same `getSourceFile().fileName` inspection the React and DOM
829
+ * origin gates use.
830
+ */
831
+ function isExternalDeclaration(declaration) {
832
+ const sourceFile = declaration.getSourceFile?.();
833
+ if (!sourceFile)
834
+ return false;
835
+ const fileName = sourceFile.fileName ?? '';
836
+ return (sourceFile.isDeclarationFile === true ||
837
+ /\.d\.[cm]?ts$/.test(fileName) ||
838
+ /[/\\]node_modules[/\\]/.test(fileName));
839
+ }
840
+ /**
841
+ * Returns true when every declaration site of `prop` lives in a dependency.
842
+ *
843
+ * `checker.getPropertiesOfType` returns INHERITED members, so a props type that
844
+ * extends or intersects a library interface (MUI's `TypographyProps`, React's
845
+ * `HTMLAttributes`, …) surfaces that library's entire surface. Demanding the
846
+ * author name those in `compareDeeply` prescribes a remedy nobody can act on —
847
+ * the component neither declares nor receives them, and the lists run past a
848
+ * hundred names — so the only available exit is a blanket rule disable.
849
+ *
850
+ * The gate is `every` rather than "first declaration", so a prop the author
851
+ * redeclares alongside the library's (the intersection of two same-named
852
+ * members yields one symbol carrying BOTH declarations) still counts as
853
+ * authored and is still reported.
854
+ *
855
+ * A prop with no declarations at all is treated as authored. Synthesized
856
+ * symbols reach here from mapped and generic types over the component's own
857
+ * props, and reporting them preserves the rule's core behaviour; a missing
858
+ * declaration is not evidence of a dependency.
859
+ */
860
+ function isExternallyDeclaredProperty(prop) {
861
+ const declarations = prop.declarations;
862
+ if (!declarations || declarations.length === 0)
863
+ return false;
864
+ return declarations.every(isExternalDeclaration);
865
+ }
825
866
  function getComplexPropertiesFromType(type, checker, tsNode, ts, treatAnyAsComplex = false, parentTypeFlags = 0) {
826
867
  const properties = checker.getPropertiesOfType(type);
827
868
  const complexProps = [];
828
869
  for (const prop of properties) {
829
870
  if (isReservedReactPropName(prop.name))
830
871
  continue;
872
+ if (isExternallyDeclaredProperty(prop))
873
+ continue;
831
874
  if (isPropertyComplex(prop, checker, tsNode, ts, treatAnyAsComplex, parentTypeFlags)) {
832
875
  complexProps.push(prop.name);
833
876
  }
@@ -119,8 +119,8 @@ function getTypeReferenceName(node) {
119
119
  /**
120
120
  * Recursively check if a TS type node composes with the given propsTypeName
121
121
  * via Pick/Omit (at any level of intersection / Readonly wrapping, union arm,
122
- * named-alias indirection, or nested in a TSTypeLiteral property's type
123
- * annotation).
122
+ * array or tuple element, named-alias indirection, or nested in a TSTypeLiteral
123
+ * property's type annotation).
124
124
  *
125
125
  * `scope` (when supplied) is the node the alias lookup walks outward from, which
126
126
  * enables resolving a locally-declared named type alias to its definition, so
@@ -185,6 +185,46 @@ function typeNodeComposesWithProps(typeNode, propsTypeName, scope, seenAliases =
185
185
  // case, where every arm composes with the single shared child.
186
186
  return typeNode.types.some((t) => typeNodeComposesWithProps(t, propsTypeName, scope, seenAliases));
187
187
  }
188
+ case utils_1.AST_NODE_TYPES.TSArrayType: {
189
+ // A list renderer's composition lives in the ELEMENT type: a parent
190
+ // declaring `buttons: readonly ActionButtonProps[]` and spreading one
191
+ // element onto each child forwards the child's ENTIRE prop object, which
192
+ // is the same DRY guarantee a direct `Pick`/`Omit` prop gives — the
193
+ // composition is simply one level of indirection away, inside the array.
194
+ // The child's contract belongs to the element type there, so demanding a
195
+ // whole-props Pick/Omit on the parent would name a composition a list
196
+ // renderer must not have (issue #2038). This is the array analogue of the
197
+ // union-arm unwrapping issue #1343 established.
198
+ return typeNodeComposesWithProps(typeNode.elementType, propsTypeName, scope, seenAliases);
199
+ }
200
+ case utils_1.AST_NODE_TYPES.TSTupleType: {
201
+ // A tuple is a fixed-length list, and each slot is handed to a child
202
+ // exactly as an array element is, so any slot that composes composes.
203
+ return typeNode.elementTypes.some((element) => typeNodeComposesWithProps(element, propsTypeName, scope, seenAliases));
204
+ }
205
+ case utils_1.AST_NODE_TYPES.TSNamedTupleMember: {
206
+ // A tuple slot's label names the slot, never its surface.
207
+ return typeNodeComposesWithProps(typeNode.elementType, propsTypeName, scope, seenAliases);
208
+ }
209
+ case utils_1.AST_NODE_TYPES.TSOptionalType:
210
+ case utils_1.AST_NODE_TYPES.TSRestType: {
211
+ // `?` and `...` govern how many slots a tuple carries, not what a slot
212
+ // carries, so the decorated type is the surface to test.
213
+ return typeNodeComposesWithProps(typeNode.typeAnnotation, propsTypeName, scope, seenAliases);
214
+ }
215
+ case utils_1.AST_NODE_TYPES.TSTypeOperator: {
216
+ // `readonly T[]` describes the same prop surface as `T[]`: the modifier
217
+ // constrains mutation of the container, never the shape of its elements.
218
+ //
219
+ // Only `readonly` unwraps. `keyof ChildProps` is the child's KEY union — a
220
+ // set of strings that hands the child nothing — and `unique symbol` is a
221
+ // nominal token, so crediting either would let a parent that merely names
222
+ // the child's keys pass as composing with its props.
223
+ if (typeNode.operator !== 'readonly' || !typeNode.typeAnnotation) {
224
+ return false;
225
+ }
226
+ return typeNodeComposesWithProps(typeNode.typeAnnotation, propsTypeName, scope, seenAliases);
227
+ }
188
228
  case utils_1.AST_NODE_TYPES.TSTypeLiteral: {
189
229
  // Check property signatures for nested composition
190
230
  // e.g. { iconProps?: Omit<GradientIconButtonProps, 'IconComponent'> }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.165",
3
+ "version": "1.20.167",
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.20.167",
4
+ "date": "2026-08-18T04:28:02.499Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-props-composition",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2038
11
+ ],
12
+ "summary": "credit composition through an array element (closes #2038)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.166",
18
+ "date": "2026-08-18T03:18:38.188Z",
19
+ "rules": [
20
+ {
21
+ "name": "memo-compare-deeply-complex-props",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2037
25
+ ],
26
+ "summary": "exempt props declared by a dependency (closes #2037)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.165",
4
32
  "date": "2026-08-18T00:07:35.027Z",