@blumintinc/eslint-plugin-blumint 1.20.113 → 1.20.115

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.113',
226
+ version: '1.20.115',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -4,6 +4,7 @@ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").R
4
4
  functionTypes: string[];
5
5
  }[], {
6
6
  TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration): void;
7
+ TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration): void;
7
8
  TSTypeReference(node: TSESTree.TSTypeReference): void;
8
9
  'Program:exit'(): void;
9
10
  }>;
@@ -53,17 +53,63 @@ exports.default = (0, createRule_1.createRule)({
53
53
  ...NON_SERIALIZABLE_TYPES,
54
54
  ...(options.additionalNonSerializableTypes || []),
55
55
  ]);
56
- const typeAliasMap = new Map();
56
+ /**
57
+ * Interfaces sit here alongside aliases because a request contract is as
58
+ * often an `interface` as a `type`, and resolving only one of the two makes
59
+ * the rule's coverage depend on which keyword the author reached for.
60
+ */
61
+ const declarations = new Map();
62
+ /**
63
+ * A node reports at most once. Resolution follows references, so a type
64
+ * reachable by two paths (`{ x: Inner; y: Inner }`) would otherwise produce
65
+ * duplicate reports at one location.
66
+ */
67
+ const reported = new Set();
57
68
  function isNonSerializableType(typeName) {
58
69
  return allNonSerializableTypes.has(typeName);
59
70
  }
60
- function checkTypeNode(node, propName) {
71
+ /**
72
+ * The rightmost identifier of a possibly-namespaced name, so
73
+ * `admin.firestore.Timestamp` is matched as `Timestamp`. The whole rule
74
+ * keys off simple names, and a namespaced spelling of a type is the same
75
+ * type — reading only `Identifier.name` leaves it `undefined` and silently
76
+ * exempts every firebase-admin-style reference.
77
+ */
78
+ function simpleTypeNameOf(entity) {
79
+ if (!entity)
80
+ return undefined;
81
+ if (entity.type === utils_1.AST_NODE_TYPES.Identifier)
82
+ return entity.name;
83
+ if (entity.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
84
+ return simpleTypeNameOf(entity.right);
85
+ }
86
+ return undefined;
87
+ }
88
+ const propertyNameOf = (member) => {
89
+ const { key } = member;
90
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier)
91
+ return key.name;
92
+ if (key.type === utils_1.AST_NODE_TYPES.Literal)
93
+ return String(key.value);
94
+ return undefined;
95
+ };
96
+ function checkMembers(members, seen, propName) {
97
+ for (const member of members) {
98
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature)
99
+ continue;
100
+ checkTypeNode(member.typeAnnotation, propertyNameOf(member) ?? propName, seen);
101
+ }
102
+ }
103
+ function checkTypeNode(node, propName, seen = new Set()) {
61
104
  if (!node)
62
105
  return;
63
106
  switch (node.type) {
64
107
  case utils_1.AST_NODE_TYPES.TSTypeReference: {
65
- const typeName = node.typeName.name;
66
- if (isNonSerializableType(typeName)) {
108
+ const typeName = simpleTypeNameOf(node.typeName);
109
+ if (typeName &&
110
+ isNonSerializableType(typeName) &&
111
+ !reported.has(node)) {
112
+ reported.add(node);
67
113
  context.report({
68
114
  node,
69
115
  messageId: propName
@@ -75,28 +121,51 @@ exports.default = (0, createRule_1.createRule)({
75
121
  },
76
122
  });
77
123
  }
124
+ /**
125
+ * A reference to a locally declared type is followed so a payload
126
+ * assembled from named parts is inspected as a whole. `seen` is
127
+ * path-scoped rather than global: a self-referential type must not
128
+ * loop, but a type legitimately used by two siblings must still be
129
+ * checked under each of them.
130
+ */
131
+ if (typeName && !seen.has(typeName)) {
132
+ const declaration = declarations.get(typeName);
133
+ if (declaration) {
134
+ seen.add(typeName);
135
+ if (declaration.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
136
+ checkTypeNode(declaration.typeAnnotation, propName, seen);
137
+ }
138
+ else {
139
+ checkMembers(declaration.body.body, seen, propName);
140
+ }
141
+ seen.delete(typeName);
142
+ }
143
+ }
78
144
  // Check type parameters of generic types (like Array<T>)
79
145
  if (node.typeParameters) {
80
- node.typeParameters.params.forEach((param) => checkTypeNode(param, propName));
146
+ node.typeParameters.params.forEach((param) => checkTypeNode(param, propName, seen));
81
147
  }
82
148
  break;
83
149
  }
84
150
  case utils_1.AST_NODE_TYPES.TSArrayType:
85
- checkTypeNode(node.elementType, propName);
151
+ checkTypeNode(node.elementType, propName, seen);
86
152
  break;
87
153
  case utils_1.AST_NODE_TYPES.TSTypeAnnotation:
88
- checkTypeNode(node.typeAnnotation, propName);
154
+ checkTypeNode(node.typeAnnotation, propName, seen);
89
155
  break;
90
156
  case utils_1.AST_NODE_TYPES.TSTypeLiteral:
91
- node.members.forEach((member) => {
92
- if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
93
- const propertyName = member.key.name;
94
- checkTypeNode(member.typeAnnotation, propertyName);
95
- }
96
- });
157
+ checkMembers(node.members, seen, propName);
97
158
  break;
98
159
  case utils_1.AST_NODE_TYPES.TSUnionType:
99
- node.types.forEach((type) => checkTypeNode(type, propName));
160
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
161
+ node.types.forEach((type) => checkTypeNode(type, propName, seen));
162
+ break;
163
+ case utils_1.AST_NODE_TYPES.TSTupleType:
164
+ node.elementTypes.forEach((type) => checkTypeNode(type, propName, seen));
165
+ break;
166
+ // `readonly T[]` wraps its operand rather than replacing it.
167
+ case utils_1.AST_NODE_TYPES.TSTypeOperator:
168
+ checkTypeNode(node.typeAnnotation, propName, seen);
100
169
  break;
101
170
  }
102
171
  }
@@ -109,35 +178,30 @@ exports.default = (0, createRule_1.createRule)({
109
178
  const wrapperReferences = [];
110
179
  return {
111
180
  TSTypeAliasDeclaration(node) {
112
- typeAliasMap.set(node.id.name, node);
181
+ declarations.set(node.id.name, node);
182
+ },
183
+ TSInterfaceDeclaration(node) {
184
+ declarations.set(node.id.name, node);
113
185
  },
114
186
  TSTypeReference(node) {
115
- const typeName = node.typeName.name;
116
- if (options.functionTypes.includes(typeName) &&
187
+ const typeName = simpleTypeNameOf(node.typeName);
188
+ if (typeName &&
189
+ options.functionTypes.includes(typeName) &&
117
190
  node.typeParameters?.params[0]) {
118
191
  wrapperReferences.push(node);
119
192
  }
120
193
  },
121
194
  'Program:exit'() {
122
195
  for (const node of wrapperReferences) {
123
- const typeParam = node.typeParameters?.params[0];
124
- if (!typeParam)
125
- continue;
126
- if (typeParam.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
127
- const referencedTypeName = typeParam.typeName.name;
128
- const typeAlias = typeAliasMap.get(referencedTypeName);
129
- /**
130
- * A reference that names no local alias is still a type in its own
131
- * right — `CallableRequest<Timestamp>` is the plainest form of the
132
- * violation. Checking the node itself lets the non-serializable
133
- * lookup and the generic descent apply; an unrecognized name simply
134
- * yields no report, so an imported request type stays silent.
135
- */
136
- checkTypeNode(typeAlias ? typeAlias.typeAnnotation : typeParam);
137
- }
138
- else {
139
- checkTypeNode(typeParam);
140
- }
196
+ /**
197
+ * The payload is handed to `checkTypeNode` whatever its shape: it
198
+ * resolves a reference to a local declaration itself, and a reference
199
+ * that names none is still a type in its own right —
200
+ * `CallableRequest<Timestamp>` is the plainest form of the violation.
201
+ * An unrecognized name yields no report, so an imported request type
202
+ * stays silent.
203
+ */
204
+ checkTypeNode(node.typeParameters?.params[0]);
141
205
  }
142
206
  },
143
207
  };
@@ -1,4 +1,2 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- type MessageIds = 'useFastDeepEqual' | 'addFastDeepEqualImport';
3
- export declare const fastDeepEqualOverMicrodiff: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
- export {};
2
+ export declare const fastDeepEqualOverMicrodiff: TSESLint.RuleModule<"useFastDeepEqual", [], TSESLint.RuleListener>;
@@ -89,9 +89,6 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
89
89
  useFastDeepEqual: "What's wrong: This code uses `{{diffName}}(...).length` as a deep equality check.\n" +
90
90
  'Why it matters: `{{diffName}}` allocates a full change list (paths, types, values) before you compare it to zero, which hides the boolean intent and wastes memory/time.\n' +
91
91
  'How to fix: Call `{{fastEqualName}}(left, right)` for equality (or prefix with `!` for inequality) using the same two arguments instead of counting diff length.',
92
- addFastDeepEqualImport: "What's wrong: This file checks equality via `{{diffName}}(...).length` but does not import a deep-equality function.\n" +
93
- 'Why it matters: Without `@blumintinc/fast-deep-equal`, equality checks keep building diff entries just to count them, adding overhead and obscuring intent.\n' +
94
- 'How to fix: Add a default import from `@blumintinc/fast-deep-equal` as `{{fastEqualName}}` and use `{{fastEqualName}}(a, b)` for equality checks.',
95
92
  },
96
93
  },
97
94
  defaultOptions: [],
@@ -13,20 +13,47 @@ const isJsxElement = (node) => {
13
13
  node.type === utils_1.AST_NODE_TYPES.JSXFragment ||
14
14
  node.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer);
15
15
  };
16
+ /**
17
+ * A type reference's name as dotted segments.
18
+ *
19
+ * A qualified name nests to the LEFT, so `React.JSX.Element` is a
20
+ * TSQualifiedName whose own `left` is another TSQualifiedName. Matching one
21
+ * nesting level at a time therefore recognizes `JSX.Element` but not the
22
+ * React-namespaced spelling of the same type; comparing whole paths recognizes
23
+ * both.
24
+ */
25
+ const qualifiedSegments = (typeName) => {
26
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
27
+ return [typeName.name];
28
+ }
29
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
30
+ const left = qualifiedSegments(typeName.left);
31
+ return left && [...left, typeName.right.name];
32
+ }
33
+ return null;
34
+ };
35
+ /** JSX-producing type paths, written without the optional `React` qualifier. */
36
+ const JSX_TYPE_PATHS = new Set([
37
+ 'JSX',
38
+ 'JSX.Element',
39
+ 'ReactNode',
40
+ 'ReactElement',
41
+ ]);
16
42
  const isJsxReturnType = (node) => {
17
- if (node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
18
- const typeName = node.typeAnnotation.typeName;
19
- if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
20
- return ['JSX', 'ReactNode', 'ReactElement'].includes(typeName.name);
21
- }
22
- if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
23
- return (typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
24
- typeName.left.name === 'JSX' &&
25
- typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
26
- typeName.right.name === 'Element');
27
- }
43
+ if (node.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
44
+ return false;
28
45
  }
29
- return false;
46
+ const segments = qualifiedSegments(node.typeAnnotation.typeName);
47
+ if (!segments) {
48
+ return false;
49
+ }
50
+ /**
51
+ * `React.` re-exports the same declarations, so it carries no meaning for
52
+ * this question. Every other qualifier names a type from a different module
53
+ * and must not match — `Foo.ReactNode` is not React's.
54
+ */
55
+ const path = segments[0] === 'React' ? segments.slice(1) : segments;
56
+ return path.length > 0 && JSX_TYPE_PATHS.has(path.join('.'));
30
57
  };
31
58
  const containsJsxInBlockStatement = (node) => {
32
59
  const variablesWithJsx = new Set();
@@ -1,4 +1,2 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- type MessageIds = 'preferFragment' | 'addFragmentImport';
3
- export declare const preferFragmentComponent: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
- export {};
2
+ export declare const preferFragmentComponent: TSESLint.RuleModule<"preferFragment", [], TSESLint.RuleListener>;
@@ -121,7 +121,6 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
121
121
  schema: [],
122
122
  messages: {
123
123
  preferFragment: 'Prefer Fragment imported from react over {{type}}. Shorthand fragments block props like "key" and mixing fragment styles makes JSX harder to refactor. Import { Fragment } from "react" and wrap the children with <Fragment>...</Fragment> so fragment usage stays explicit.',
124
- addFragmentImport: "Fragment is used but not imported from react. Without an explicit import the fixer leaves <Fragment> undefined and the React dependency implicit. Add `import { Fragment } from 'react'` alongside your other React imports so the file compiles.",
125
124
  },
126
125
  },
127
126
  defaultOptions: [],
@@ -1,9 +1,8 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- type MessageIds = 'preferNextDynamic' | 'addNextDynamicImport' | 'removeUseDynamicImport';
3
2
  type Options = [
4
3
  {
5
4
  useDynamicSources?: string[];
6
5
  }?
7
6
  ];
8
- export declare const preferNextDynamic: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
7
+ export declare const preferNextDynamic: TSESLint.RuleModule<"preferNextDynamic", Options, TSESLint.RuleListener>;
9
8
  export {};
@@ -206,8 +206,6 @@ exports.preferNextDynamic = (0, createRule_1.createRule)({
206
206
  ],
207
207
  messages: {
208
208
  preferNextDynamic: 'Component "{{componentName}}" is created with useDynamic(import(...)), which bypasses Next.js dynamic() handling for client-only components and leaves SSR control to a custom wrapper. Wrap the import in dynamic(() => import(...), { ssr: false }) so Next.js manages code-splitting and disables server rendering safely.',
209
- addNextDynamicImport: "The auto-fix will replace useDynamic(import(...)) with dynamic(() => import(...), { ssr: false }), which references Next.js's dynamic function. Without importing dynamic from 'next/dynamic', the fixed code will throw a ReferenceError at runtime when the module loads. Add `import dynamic from 'next/dynamic'` at the top of the file to make the dynamic identifier available.",
210
- removeUseDynamicImport: 'Remove the unused useDynamic import after migrating to dynamic(); leaving the custom hook imported invites accidental reuse and keeps dead code in the bundle.',
211
209
  },
212
210
  },
213
211
  defaultOptions: [{}],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.113",
3
+ "version": "1.20.115",
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.115",
4
+ "date": "2026-08-05T18:41:41.153Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-jsx-in-hooks",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1753
11
+ ],
12
+ "summary": "detect React-qualified JSX return types (closes #1753)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.114",
18
+ "date": "2026-08-05T18:16:40.596Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-serializable-params",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1751
25
+ ],
26
+ "summary": "resolve namespaced names, interfaces and reference chains (closes #1751)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.113",
4
32
  "date": "2026-08-05T17:37:08.020Z",