@blumintinc/eslint-plugin-blumint 1.20.75 → 1.20.77

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.75',
226
+ version: '1.20.77',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -156,6 +156,17 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
156
156
  if (node.parent?.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
157
157
  return true;
158
158
  }
159
+ // An assertion whose parent is another assertion is an inner link of a chain
160
+ // (`x as unknown as T`). The chain expresses ONE violation, and only the
161
+ // outermost link describes the type that actually reaches the caller, so it
162
+ // alone carries the report — otherwise the author is told to fix a cast to
163
+ // "unknown", which is not actionable. Exempting inner links also makes the
164
+ // report count independent of body style: an expression-bodied arrow has no
165
+ // ReturnStatement node, so its chain yields a single report either way.
166
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
167
+ node.parent?.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
168
+ return true;
169
+ }
159
170
  // If the parent is an arrow function, we already handle it in ArrowFunctionExpression
160
171
  if (node.parent?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
161
172
  return true;
@@ -268,11 +279,9 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
268
279
  if (mergedOptions.allowAsConst && isAsConstAssertion(node.argument)) {
269
280
  return;
270
281
  }
271
- // For nested type assertions, only report the outermost one
272
- if (node.argument.expression.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
273
- reportTypeAssertion(node.argument);
274
- return;
275
- }
282
+ // The returned expression is the outermost link of any assertion chain, so
283
+ // reporting it here yields one report naming the final asserted type; inner
284
+ // links are exempted in shouldAllowTypeAssertion.
276
285
  reportTypeAssertion(node.argument);
277
286
  }
278
287
  // Check for type assertions using angle bracket syntax
@@ -147,23 +147,57 @@ function collectObjectForwardedFields(objectExpression, destructuredNames) {
147
147
  }
148
148
  return forwarded;
149
149
  }
150
- function getSingleTarget(fn) {
151
- const body = fn.body;
152
- // Concise arrow: `(props) => <X />`
153
- if (body.type === utils_1.AST_NODE_TYPES.JSXElement) {
150
+ /**
151
+ * Wrappers that exist only for the type checker: they compile away entirely, so
152
+ * the value they wrap is the value the function actually produces.
153
+ */
154
+ const TRANSPARENT_WRAPPERS = new Set([
155
+ utils_1.AST_NODE_TYPES.TSAsExpression,
156
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
157
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
158
+ ]);
159
+ /**
160
+ * Strips every type-only wrapper (`as T`, `as const`, `satisfies T`, `!`) from
161
+ * an expression. An assertion removes no reassembly — `{ a, b } as const` is
162
+ * the very same destructure-then-reassemble as `{ a, b }` — so classifying the
163
+ * target without stripping first turns an assertion into an accidental
164
+ * suppression. The loop is what handles chains such as `{...} as unknown as T`,
165
+ * and `enforce-object-literal-as-const` appends `as const` to exactly this
166
+ * shape, so wrapped literals are the common case rather than the exotic one.
167
+ */
168
+ function unwrapTransparent(node) {
169
+ let current = node;
170
+ while (TRANSPARENT_WRAPPERS.has(current.type)) {
171
+ current = current.expression;
172
+ }
173
+ return current;
174
+ }
175
+ /**
176
+ * Classifies an already-unwrapped expression as a spread target. The fix is
177
+ * anchored on the node returned here — the JSX opening element or the object
178
+ * literal's own braces — so any wrapper stripped on the way in keeps its source
179
+ * text untouched.
180
+ */
181
+ function classifyTarget(expression) {
182
+ if (expression.type === utils_1.AST_NODE_TYPES.JSXElement) {
154
183
  return {
155
184
  kind: 'jsx',
156
- openingElement: body.openingElement,
157
- jsxElement: body,
185
+ openingElement: expression.openingElement,
186
+ jsxElement: expression,
158
187
  };
159
188
  }
160
189
  // Concise arrow returning object literal: `({ x, y }) => ({ x, y, extra: 1 })`
161
190
  // The parser represents the parenthesized object as an ObjectExpression body.
162
- if (body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
163
- return { kind: 'object', expression: body };
191
+ if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
192
+ return { kind: 'object', expression };
164
193
  }
194
+ return null;
195
+ }
196
+ function getSingleTarget(fn) {
197
+ const body = fn.body;
198
+ // Concise arrow: `(props) => <X />`, `({ x, y }) => ({ x, y } as const)`.
165
199
  if (body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
166
- return null;
200
+ return classifyTarget(unwrapTransparent(body));
167
201
  }
168
202
  // Require exactly one statement in the block, which must be a return.
169
203
  if (body.body.length !== 1) {
@@ -173,30 +207,7 @@ function getSingleTarget(fn) {
173
207
  if (stmt.type !== utils_1.AST_NODE_TYPES.ReturnStatement || !stmt.argument) {
174
208
  return null;
175
209
  }
176
- const arg = stmt.argument;
177
- if (arg.type === utils_1.AST_NODE_TYPES.JSXElement) {
178
- return {
179
- kind: 'jsx',
180
- openingElement: arg.openingElement,
181
- jsxElement: arg,
182
- };
183
- }
184
- // Parenthesized JSX in arrow: `(props) => (<X />)` — arg may wrap with
185
- // TSAsExpression or similar. We unwrap one level of TSAsExpression / TSSatisfiesExpression.
186
- if ((arg.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
187
- arg.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) &&
188
- arg.expression.type === utils_1.AST_NODE_TYPES.JSXElement) {
189
- const jsx = arg.expression;
190
- return {
191
- kind: 'jsx',
192
- openingElement: jsx.openingElement,
193
- jsxElement: jsx,
194
- };
195
- }
196
- if (arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
197
- return { kind: 'object', expression: arg };
198
- }
199
- return null;
210
+ return classifyTarget(unwrapTransparent(stmt.argument));
200
211
  }
201
212
  /**
202
213
  * Checks whether any of the destructured names is used anywhere in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.75",
3
+ "version": "1.20.77",
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.77",
4
+ "date": "2026-08-02T08:02:01.009Z",
5
+ "rules": [
6
+ {
7
+ "name": "prefer-spread-over-reassembly",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1610
11
+ ],
12
+ "summary": "see through transparent type-assertion wrappers (closes #1610)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.76",
18
+ "date": "2026-08-02T07:09:34.794Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-type-assertion-returns",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1609
25
+ ],
26
+ "summary": "report a chained assertion once, on the outermost link (closes #1609)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.75",
4
32
  "date": "2026-08-02T06:40:23.306Z",