@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 +1 -1
- package/lib/rules/global-const-style.js +21 -4
- package/lib/rules/no-misleading-boolean-prefixes.js +19 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +61 -13
- package/lib/rules/prefer-flat-transform-each-keys.js +54 -21
- package/package.json +1 -1
- package/release-manifest.json +44 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
278
|
+
const returned = unwrapToObjectExpression(node.argument);
|
|
279
|
+
if (!returned)
|
|
233
280
|
return;
|
|
234
281
|
if (!isInTargetTransform(node))
|
|
235
282
|
return;
|
|
236
|
-
analyzeReturnedObject(
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
115
|
-
decl.id.name
|
|
116
|
-
|
|
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 (
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|