@blumintinc/eslint-plugin-blumint 1.20.73 → 1.20.75

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.73',
226
+ version: '1.20.75',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -3,6 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@typescript-eslint/utils");
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const isUpperSnakeCase = (str) => /^[A-Z][A-Z0-9_]*$/.test(str);
6
+ /**
7
+ * Converts an identifier to UPPER_SNAKE_CASE by splitting on case *boundaries*.
8
+ *
9
+ * Idempotence is a correctness requirement, not a nicety: `--fix` re-lints its
10
+ * own output up to ten times per file, and a sibling rule can rewrite the same
11
+ * identifier in between (`enforce-react-type-naming` lowercases it), so a
12
+ * converter that re-separates what it already separated compounds every pass
13
+ * and writes an ever-growing, corrupted identifier into source (Issue #1605).
14
+ * Splitting on boundaries also keeps acronym runs intact, so `HTTPServer` reads
15
+ * as `HTTP_SERVER` rather than `H_T_T_P_SERVER`.
16
+ *
17
+ * The leading underscore is dropped because `_PRIVATE_THING` fails
18
+ * `isUpperSnakeCase`, which would leave the rule demanding a rename it can
19
+ * never satisfy.
20
+ */
21
+ const toUpperSnakeCase = (name) => name
22
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
23
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
24
+ .toUpperCase()
25
+ .replace(/^_/, '');
6
26
  // Jest mock handles produced by an `as` cast to a `jest.Mock*` type are
7
27
  // stateful test doubles that are reassigned/mutated through
8
28
  // `.mockImplementation()`, `.mockReturnValue()`, etc. They are not immutable
@@ -328,10 +348,7 @@ exports.default = (0, createRule_1.createRule)({
328
348
  // the `mockedX` idiom is intentional. The exemption gates only this
329
349
  // rename check — the `as const` logic above is untouched.
330
350
  if (!isUpperSnakeCase(name) && !isJestMockCast(init)) {
331
- const newName = name
332
- .replace(/([A-Z])/g, '_$1')
333
- .toUpperCase()
334
- .replace(/^_/, '');
351
+ const newName = toUpperSnakeCase(name);
335
352
  const idNode = declaration.id;
336
353
  context.report({
337
354
  node: declaration,
@@ -81,6 +81,25 @@ function isTsBooleanLike(typeNode) {
81
81
  }
82
82
  function isExpressionBooleanLike(expr) {
83
83
  switch (expr.type) {
84
+ // Assertion wrappers restate a type but never change the runtime value, so
85
+ // the expression beneath one still decides what the function returns
86
+ // (#1606). Recursing per level rather than unwrapping the whole chain at
87
+ // once is what lets a boolean declared at any level answer first, and it
88
+ // reaches through nesting such as `({...} as const)!`.
89
+ // `enforce-object-literal-as-const` ships in the same recommended config
90
+ // and appends `as const` to returned object literals by `--fix`, so
91
+ // without this the plugin's own fixer silences the report.
92
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
93
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
94
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
95
+ // A declared boolean-like type is the same promise an explicit return
96
+ // annotation makes, which the rule already accepts; `as const` names no
97
+ // type at all and falls through to the asserted expression.
98
+ if (isTsBooleanLike(expr.typeAnnotation))
99
+ return true;
100
+ return isExpressionBooleanLike(expr.expression);
101
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
102
+ return isExpressionBooleanLike(expr.expression);
84
103
  case utils_1.AST_NODE_TYPES.Literal:
85
104
  return typeof expr.value === 'boolean' ? true : 'non';
86
105
  case utils_1.AST_NODE_TYPES.TemplateLiteral:
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferFieldPathsInTransforms = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const minimatch_1 = require("minimatch");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  const createRule_1 = require("../utils/createRule");
7
8
  // Defaults aim to catch common BluMint aggregation container names
8
9
  const DEFAULT_CONTAINERS = ['*Aggregation', 'previews', '*Previews'];
@@ -19,8 +20,9 @@ function describeNestedPath(containerValue) {
19
20
  const firstKey = getPropertyName(prop);
20
21
  if (!firstKey)
21
22
  continue;
22
- if (isObjectExpression(prop.value)) {
23
- for (const child of prop.value.properties) {
23
+ const nestedValue = unwrapToObjectExpression(prop.value);
24
+ if (nestedValue) {
25
+ for (const child of nestedValue.properties) {
24
26
  if (child.type === utils_1.AST_NODE_TYPES.SpreadElement)
25
27
  continue;
26
28
  if (!isProperty(child))
@@ -44,6 +46,50 @@ function describeNestedPath(containerValue) {
44
46
  function isObjectExpression(node) {
45
47
  return !!node && node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
46
48
  }
49
+ /**
50
+ * Resolves the object literal a value ultimately is, seeing through assertion
51
+ * wrappers (`as const`, `as T`, `satisfies T`, `!`, `<T>x`), which nest.
52
+ *
53
+ * An assertion changes no runtime value, so the write shape a transform sends
54
+ * to Firestore — the only thing this rule judges — is identical with or without
55
+ * one. Matching the bare `ObjectExpression` alone made the rule blind to its own
56
+ * ecosystem: `enforce-object-literal-as-const` ships `'error'` in the same
57
+ * recommended config and is fixable, so `eslint --fix` appends `as const` to
58
+ * exactly these literals and silences the nested-write report (#1607).
59
+ *
60
+ * Unwrapping the whole chain rather than one level at a time is safe here
61
+ * because no wrapper's `typeAnnotation` carries information this rule reads;
62
+ * only the key names and value shapes underneath matter.
63
+ */
64
+ function unwrapToObjectExpression(node) {
65
+ if (!node)
66
+ return null;
67
+ const inner = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(node);
68
+ return isObjectExpression(inner) ? inner : null;
69
+ }
70
+ /**
71
+ * Assertion wrappers as seen from below, for walks that climb toward the
72
+ * declaration a function is bound to. A wrapped transform
73
+ * (`transformEach: ((doc) => ({...})) as Transform`) is still bound to that
74
+ * key, so the wrapper must not hide the binding.
75
+ */
76
+ const ASSERTION_WRAPPER_TYPES = new Set([
77
+ utils_1.AST_NODE_TYPES.TSAsExpression,
78
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
79
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
80
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
81
+ ]);
82
+ function bindingParentOf(node) {
83
+ let current = node;
84
+ let parent = current.parent ?? null;
85
+ while (parent &&
86
+ ASSERTION_WRAPPER_TYPES.has(parent.type) &&
87
+ parent.expression === current) {
88
+ current = parent;
89
+ parent = current.parent ?? null;
90
+ }
91
+ return parent;
92
+ }
47
93
  function isProperty(node) {
48
94
  return node.type === utils_1.AST_NODE_TYPES.Property;
49
95
  }
@@ -68,7 +114,7 @@ function isBoundToName(fn, name) {
68
114
  if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
69
115
  return isNamedFunction(fn, name);
70
116
  }
71
- const parent = fn.parent;
117
+ const parent = bindingParentOf(fn);
72
118
  if (!parent)
73
119
  return false;
74
120
  if (parent.type === utils_1.AST_NODE_TYPES.Property ||
@@ -112,15 +158,14 @@ function isTransformEachVaripotent(fn) {
112
158
  function hasDeeperThanOneLevelUnderContainer(containerObj) {
113
159
  for (const prop of containerObj.properties) {
114
160
  if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
115
- if (isObjectExpression(prop.argument)) {
161
+ if (unwrapToObjectExpression(prop.argument)) {
116
162
  return true;
117
163
  }
118
164
  continue;
119
165
  }
120
166
  if (!isProperty(prop))
121
167
  continue;
122
- const value = prop.value;
123
- if (isObjectExpression(value)) {
168
+ if (unwrapToObjectExpression(prop.value)) {
124
169
  // Any nested object literal implies depth >= 2 (even if it only spreads)
125
170
  return true;
126
171
  }
@@ -143,9 +188,10 @@ function analyzeReturnedObject(obj, context, containerNameMatches) {
143
188
  continue;
144
189
  if (!containerNameMatches(keyName))
145
190
  continue;
146
- const containerValue = top.value;
147
- if (!isObjectExpression(containerValue))
148
- continue; // only care if returning an object under the container
191
+ // only care if returning an object under the container
192
+ const containerValue = unwrapToObjectExpression(top.value);
193
+ if (!containerValue)
194
+ continue;
149
195
  if (hasDeeperThanOneLevelUnderContainer(containerValue)) {
150
196
  const nestedPath = describeNestedPath(containerValue) ?? 'nestedField';
151
197
  context.report({
@@ -229,18 +275,20 @@ exports.preferFieldPathsInTransforms = (0, createRule_1.createRule)({
229
275
  }
230
276
  return {
231
277
  ReturnStatement(node) {
232
- if (!node.argument || !isObjectExpression(node.argument))
278
+ const returned = unwrapToObjectExpression(node.argument);
279
+ if (!returned)
233
280
  return;
234
281
  if (!isInTargetTransform(node))
235
282
  return;
236
- analyzeReturnedObject(node.argument, context, containerNameMatches);
283
+ analyzeReturnedObject(returned, context, containerNameMatches);
237
284
  },
238
285
  ArrowFunctionExpression(node) {
239
286
  // Handle implicit returns: transformEach: doc => ({ ... })
240
287
  if (!isTransformEachFunction(node) || isTransformEachVaripotent(node))
241
288
  return;
242
- if (isObjectExpression(node.body)) {
243
- analyzeReturnedObject(node.body, context, containerNameMatches);
289
+ const returned = unwrapToObjectExpression(node.body);
290
+ if (returned) {
291
+ analyzeReturnedObject(returned, context, containerNameMatches);
244
292
  }
245
293
  },
246
294
  };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferFlatTransformEachKeys = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
5
6
  const createRule_1 = require("../utils/createRule");
6
7
  // Propagation strategy shape signals: an object literal with one of these
7
8
  // properties (besides transformEach) is treated as a propagation strategy.
@@ -15,6 +16,34 @@ const STRATEGY_SHAPE_KEYS = new Set([
15
16
  function isObjectExpression(node) {
16
17
  return !!node && node.type === utils_1.AST_NODE_TYPES.ObjectExpression;
17
18
  }
19
+ /**
20
+ * Strips assertion wrappers (`as const`, `as T`, `satisfies T`, `!`, `<T>x`),
21
+ * which nest, from an expression before its node type is inspected.
22
+ *
23
+ * An assertion changes no runtime value, so the write shape transformEach sends
24
+ * to Firestore — the only thing this rule judges — is identical with or without
25
+ * one. Matching bare node types alone leaves the rule blind to its own
26
+ * ecosystem: `enforce-object-literal-as-const` ships `'error'` in the same
27
+ * recommended config and is fixable, so `eslint --fix` appends `as const` to
28
+ * exactly these literals, which silences the nested-key report while the nested
29
+ * write shape survives (#1608).
30
+ *
31
+ * Unwrapping the whole chain rather than one level is safe because no wrapper's
32
+ * `typeAnnotation` carries information this rule reads; only key names and value
33
+ * shapes underneath matter.
34
+ */
35
+ function unwrapAssertions(node) {
36
+ return ASTHelpers_1.ASTHelpers.unwrapTSAssertions(node);
37
+ }
38
+ // Resolve the object literal an expression ultimately is, seeing through
39
+ // assertion wrappers. Returns null when the underlying expression is anything
40
+ // else (a call, a member expression, a binding reference).
41
+ function unwrapToObjectExpression(node) {
42
+ if (!node)
43
+ return null;
44
+ const inner = unwrapAssertions(node);
45
+ return isObjectExpression(inner) ? inner : null;
46
+ }
18
47
  function isProperty(node) {
19
48
  return node.type === utils_1.AST_NODE_TYPES.Property;
20
49
  }
@@ -79,7 +108,9 @@ function usesResolveSelf(obj) {
79
108
  const resolveAllProp = findProp(obj, 'resolveAll');
80
109
  if (!resolveAllProp)
81
110
  return false;
82
- const val = resolveAllProp.value;
111
+ // `resolveAll: resolveSelf as ResolveAllStrategy` still resolves self, so the
112
+ // exemption must survive an assertion on the reference.
113
+ const val = unwrapAssertions(resolveAllProp.value);
83
114
  return val.type === utils_1.AST_NODE_TYPES.Identifier && val.name === 'resolveSelf';
84
115
  }
85
116
  // Extract the effective "data" object from a return value:
@@ -89,8 +120,8 @@ function usesResolveSelf(obj) {
89
120
  function getDataObject(obj) {
90
121
  const afterDataProp = findProp(obj, 'afterData');
91
122
  if (afterDataProp) {
92
- const val = afterDataProp.value;
93
- if (isObjectExpression(val))
123
+ const val = unwrapToObjectExpression(afterDataProp.value);
124
+ if (val)
94
125
  return val;
95
126
  // afterData exists but its value isn't a literal (e.g. a variable) — skip.
96
127
  return null;
@@ -111,12 +142,13 @@ function resolveVariableBinding(returnArg, funcBody) {
111
142
  if (stmt.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
112
143
  continue;
113
144
  for (const decl of stmt.declarations) {
114
- if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
115
- decl.id.name === varName &&
116
- decl.init &&
117
- isObjectExpression(decl.init)) {
118
- return decl.init;
145
+ if (decl.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
146
+ decl.id.name !== varName) {
147
+ continue;
119
148
  }
149
+ const init = unwrapToObjectExpression(decl.init);
150
+ if (init)
151
+ return init;
120
152
  }
121
153
  }
122
154
  return null;
@@ -139,7 +171,7 @@ function checkDataObject(dataObj, report) {
139
171
  if (isDotNotationKey(keyName))
140
172
  continue;
141
173
  // Flag when the value is a nested object literal.
142
- if (isObjectExpression(prop.value)) {
174
+ if (unwrapToObjectExpression(prop.value)) {
143
175
  report(prop);
144
176
  }
145
177
  }
@@ -152,14 +184,11 @@ function analyzeBlockBody(body, report) {
152
184
  continue;
153
185
  if (!stmt.argument)
154
186
  continue;
155
- let retObj = null;
156
- if (isObjectExpression(stmt.argument)) {
157
- retObj = stmt.argument;
158
- }
159
- else {
160
- // Try single-binding pattern: const x = {...}; return x;
161
- retObj = resolveVariableBinding(stmt.argument, body);
162
- }
187
+ const returned = unwrapAssertions(stmt.argument);
188
+ // Try single-binding pattern: const x = {...}; return x;
189
+ const retObj = isObjectExpression(returned)
190
+ ? returned
191
+ : resolveVariableBinding(returned, body);
163
192
  if (!retObj)
164
193
  continue;
165
194
  const dataObj = getDataObject(retObj);
@@ -201,7 +230,10 @@ exports.preferFlatTransformEachKeys = (0, createRule_1.createRule)({
201
230
  const transformEachProp = findProp(node, 'transformEach');
202
231
  if (!transformEachProp)
203
232
  return;
204
- const fn = transformEachProp.value;
233
+ // A transform asserted at its binding
234
+ // (`transformEach: ((doc) => ({...})) as TransformEach`) is still the
235
+ // function the strategy runs.
236
+ const fn = unwrapAssertions(transformEachProp.value);
205
237
  const report = (violatingNode) => {
206
238
  context.report({
207
239
  node: violatingNode,
@@ -211,9 +243,10 @@ exports.preferFlatTransformEachKeys = (0, createRule_1.createRule)({
211
243
  if (fn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
212
244
  fn.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
213
245
  const body = fn.body;
214
- if (isObjectExpression(body)) {
215
- // Arrow function with implicit return: () => ({ ... })
216
- const dataObj = getDataObject(body);
246
+ // Arrow function with implicit return: () => ({ ... })
247
+ const implicitReturn = unwrapToObjectExpression(body);
248
+ if (implicitReturn) {
249
+ const dataObj = getDataObject(implicitReturn);
217
250
  if (dataObj)
218
251
  checkDataObject(dataObj, report);
219
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.73",
3
+ "version": "1.20.75",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,48 @@
1
1
  [
2
+ {
3
+ "version": "1.20.75",
4
+ "date": "2026-08-02T06:40:23.306Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-misleading-boolean-prefixes",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1606
11
+ ],
12
+ "summary": "see through assertion wrappers on a returned value (closes #1606)"
13
+ },
14
+ {
15
+ "name": "prefer-field-paths-in-transforms",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1607
19
+ ],
20
+ "summary": "see through assertion wrappers when classifying a transform return (closes #1607)"
21
+ },
22
+ {
23
+ "name": "prefer-flat-transform-each-keys",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1608
27
+ ],
28
+ "summary": "see through assertion wrappers when classifying a transform return (closes #1608)"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "version": "1.20.74",
34
+ "date": "2026-08-02T06:07:00.009Z",
35
+ "rules": [
36
+ {
37
+ "name": "global-const-style",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1605
41
+ ],
42
+ "summary": "split the rename on case boundaries so it is idempotent (closes #1605)"
43
+ }
44
+ ]
45
+ },
2
46
  {
3
47
  "version": "1.20.73",
4
48
  "date": "2026-08-02T05:14:39.301Z",