@blumintinc/eslint-plugin-blumint 1.20.76 → 1.20.78
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
|
@@ -34,6 +34,24 @@ exports.enforceFirestorePathUtils = (0, createRule_1.createRule)({
|
|
|
34
34
|
typeof node.value === 'string') ||
|
|
35
35
|
node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A `+` chain qualifies as an inline path only when a string literal or
|
|
39
|
+
* template literal appears somewhere in it: that literal is the hard-coded
|
|
40
|
+
* path fragment the rule exists to push behind a helper. The walk recurses
|
|
41
|
+
* because `'teams/' + teamId + '/members'` parses as a left-nested
|
|
42
|
+
* BinaryExpression, which leaves the literals below the outermost operands.
|
|
43
|
+
* A chain of opaque operands (`a + b`) constructs no path fragment inline,
|
|
44
|
+
* so it keeps the same indirection allowance as a bare variable.
|
|
45
|
+
*/
|
|
46
|
+
function isInlinePathExpression(node) {
|
|
47
|
+
if (isStringLiteralOrTemplate(node)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
51
|
+
node.operator === '+' &&
|
|
52
|
+
(isInlinePathExpression(node.left) ||
|
|
53
|
+
isInlinePathExpression(node.right)));
|
|
54
|
+
}
|
|
37
55
|
function isUtilityFunction(node) {
|
|
38
56
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
39
57
|
return false;
|
|
@@ -59,8 +77,9 @@ exports.enforceFirestorePathUtils = (0, createRule_1.createRule)({
|
|
|
59
77
|
if (isUtilityFunction(pathArg)) {
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
|
-
// Skip if it's a variable or other
|
|
63
|
-
|
|
80
|
+
// Skip if it's a variable or other expression that hides construction
|
|
81
|
+
// behind a name rather than assembling the path inline
|
|
82
|
+
if (!isInlinePathExpression(pathArg)) {
|
|
64
83
|
return;
|
|
65
84
|
}
|
|
66
85
|
// Skip test files
|
|
@@ -14,7 +14,7 @@ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requirePathUtil: 'Use a utility function for Realtime Database paths to ensure type safety and maintainability. Instead of `ref(
|
|
17
|
+
requirePathUtil: 'Use a utility function for Realtime Database paths to ensure type safety and maintainability. Instead of `admin.database().ref(`users/${userId}`)`, create and use a utility function: `const toUserPath = (id: string) => `users/${id}`; admin.database().ref(toUserPath(userId))`.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -59,6 +59,24 @@ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
|
59
59
|
typeof node.value === 'string') ||
|
|
60
60
|
node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A `+` chain qualifies as an inline path only when a string literal or
|
|
64
|
+
* template literal appears somewhere in it: that literal is the hard-coded
|
|
65
|
+
* path fragment the rule exists to push behind a helper. The walk recurses
|
|
66
|
+
* because `'users/' + userId + '/posts'` parses as a left-nested
|
|
67
|
+
* BinaryExpression, which leaves the literals below the outermost operands.
|
|
68
|
+
* A chain of opaque operands (`a + b`) constructs no path fragment inline,
|
|
69
|
+
* so it keeps the same indirection allowance as a bare variable.
|
|
70
|
+
*/
|
|
71
|
+
function isInlinePathExpression(node) {
|
|
72
|
+
if (isStringLiteralOrTemplate(node)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
76
|
+
node.operator === '+' &&
|
|
77
|
+
(isInlinePathExpression(node.left) ||
|
|
78
|
+
isInlinePathExpression(node.right)));
|
|
79
|
+
}
|
|
62
80
|
function isUtilityFunction(node) {
|
|
63
81
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
64
82
|
return false;
|
|
@@ -84,8 +102,9 @@ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
|
84
102
|
if (isUtilityFunction(pathArg)) {
|
|
85
103
|
return;
|
|
86
104
|
}
|
|
87
|
-
// Skip if it's a variable or other
|
|
88
|
-
|
|
105
|
+
// Skip if it's a variable or other expression that hides construction
|
|
106
|
+
// behind a name rather than assembling the path inline
|
|
107
|
+
if (!isInlinePathExpression(pathArg)) {
|
|
89
108
|
return;
|
|
90
109
|
}
|
|
91
110
|
// Skip test files
|
|
@@ -147,23 +147,57 @@ function collectObjectForwardedFields(objectExpression, destructuredNames) {
|
|
|
147
147
|
}
|
|
148
148
|
return forwarded;
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
157
|
-
jsxElement:
|
|
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 (
|
|
163
|
-
return { kind: 'object', expression
|
|
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
|
|
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
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.78",
|
|
4
|
+
"date": "2026-08-02T08:45:25.049Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-firestore-path-utils",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1611
|
|
11
|
+
],
|
|
12
|
+
"summary": "report concatenated inline paths (closes #1611)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-realtimedb-path-utils",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1612
|
|
19
|
+
],
|
|
20
|
+
"summary": "report concatenated inline paths (closes #1612)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.77",
|
|
26
|
+
"date": "2026-08-02T08:02:01.009Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "prefer-spread-over-reassembly",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1610
|
|
33
|
+
],
|
|
34
|
+
"summary": "see through transparent type-assertion wrappers (closes #1610)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.76",
|
|
4
40
|
"date": "2026-08-02T07:09:34.794Z",
|