@blumintinc/eslint-plugin-blumint 1.20.178 → 1.20.180
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
|
@@ -1316,7 +1316,8 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1316
1316
|
const reportWrittenKey = (written) => reportUseAssertSafe(written, context.sourceCode.getText(written));
|
|
1317
1317
|
/**
|
|
1318
1318
|
* Returns true when the identifier was initialized directly from an
|
|
1319
|
-
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)
|
|
1319
|
+
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)`, with any
|
|
1320
|
+
* wrappers that erase before the code runs read through.
|
|
1320
1321
|
* Only direct, single-step initializers count — transitive aliases
|
|
1321
1322
|
* (const b = a) are not followed so they continue to be flagged.
|
|
1322
1323
|
* findVariableInScope returns the nearest binding, so an inner variable
|
|
@@ -1328,11 +1329,20 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1328
1329
|
if (!variable)
|
|
1329
1330
|
return false;
|
|
1330
1331
|
return variable.defs.some((def) => {
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1332
|
+
// The initializer is read through the same peel the index site uses, so
|
|
1333
|
+
// the two arms agree about one expression: `assertSafe(rawKey) as
|
|
1334
|
+
// TokenEncoded` binds the very key `assertSafe(rawKey)` does, the
|
|
1335
|
+
// assertion having erased before the code runs, and `assertSafe?.(...)`
|
|
1336
|
+
// is the same validated key again since the chain guards only a nullish
|
|
1337
|
+
// callee. Demanding a bare call instead rejected the shape this rule's
|
|
1338
|
+
// own remedy produces in typed code, where `Object.keys` widens to
|
|
1339
|
+
// `string` and the record is keyed by a branded type (#2152).
|
|
1340
|
+
//
|
|
1341
|
+
// That peel includes `await`. assertSafe is synchronous, so awaiting it
|
|
1342
|
+
// resolves to the very value it validated — the same reason the index
|
|
1343
|
+
// arm has always exempted `m[await assertSafe(k)]`.
|
|
1334
1344
|
const init = def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && def.node.init
|
|
1335
|
-
?
|
|
1345
|
+
? unwrapWrittenKey(def.node.init)
|
|
1336
1346
|
: null;
|
|
1337
1347
|
return (!!init &&
|
|
1338
1348
|
init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -156,12 +156,25 @@ function isRealtimeType(node) {
|
|
|
156
156
|
function isRealtimeAnnotation(annotation) {
|
|
157
157
|
return isRealtimeType(annotation?.typeAnnotation);
|
|
158
158
|
}
|
|
159
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Whether an initializer constructs the Realtime Database batch manager.
|
|
161
|
+
*
|
|
162
|
+
* The wrappers TypeScript writes around a value — `as T`, `satisfies T`, `!`,
|
|
163
|
+
* `<T>` — restate the expression's TYPE; none of them changes which class is
|
|
164
|
+
* constructed, so the construction underneath one is what the carve-out reads.
|
|
165
|
+
* {@link isRealtimeType} already looks through the type-level wrappers, and
|
|
166
|
+
* keying this arm on a bare `NewExpression` drifted the two apart: a field
|
|
167
|
+
* initialized `new RealtimeBatchManager() as RealtimeBatchManager` fell through
|
|
168
|
+
* to the Firestore arm and was told to call `set(…, { merge: true })` on a
|
|
169
|
+
* manager that has no `set` method at all, which no spelling of the code
|
|
170
|
+
* satisfies (#2150).
|
|
171
|
+
*/
|
|
160
172
|
function isRealtimeInstance(node) {
|
|
161
|
-
|
|
173
|
+
const constructed = node ? unwrapAssertions(node) : null;
|
|
174
|
+
if (constructed?.type !== utils_1.AST_NODE_TYPES.NewExpression) {
|
|
162
175
|
return false;
|
|
163
176
|
}
|
|
164
|
-
const { callee } =
|
|
177
|
+
const { callee } = constructed;
|
|
165
178
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
166
179
|
return callee.name === REALTIME_BATCH_MANAGER;
|
|
167
180
|
}
|
|
@@ -46,11 +46,42 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
46
46
|
// A single identifier token — no whitespace, no punctuation. Prose is a
|
|
47
47
|
// phrase, so anything matching this is a name rather than displayed text.
|
|
48
48
|
const IDENTIFIER_TOKEN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
49
|
+
// Every value spelled as a string-literal TYPE in this file. A value
|
|
50
|
+
// literal with the same spelling is pinned by that type, so capitalizing
|
|
51
|
+
// the value on its own contradicts it (TS2322) while the type itself is
|
|
52
|
+
// code the rule must leave alone — there is no half of the pair that can
|
|
53
|
+
// be rewritten safely, so neither is. Membership is exact: a type
|
|
54
|
+
// `'id'` pins the literal `'id'`, not the prose `'Please enter your id.'`.
|
|
55
|
+
const literalTypeValues = new Set();
|
|
56
|
+
// A `TSLiteralType` can be written after the value it pins, so the verdict
|
|
57
|
+
// is only complete once the whole file has been walked. Candidates are
|
|
58
|
+
// therefore collected during traversal and reported at `Program:exit`.
|
|
59
|
+
const candidates = [];
|
|
60
|
+
/**
|
|
61
|
+
* A string literal that spells a TYPE is code, never displayed prose. It
|
|
62
|
+
* names a property (`Match['id']`), a discriminant, or a member of a
|
|
63
|
+
* literal union, so capitalizing it renames something the program refers
|
|
64
|
+
* to by that exact spelling and the fixed file stops compiling — an
|
|
65
|
+
* indexed-access key rewritten to `Match['ID']` is a TS2339 (#2153).
|
|
66
|
+
*
|
|
67
|
+
* The question is asked about the type position itself rather than by
|
|
68
|
+
* listing parent node types: every literal in a type is wrapped in a
|
|
69
|
+
* `TSLiteralType`, so one check covers indexed-access keys, mapped-type
|
|
70
|
+
* `as` clauses, tuple members, generic defaults and conditional-type
|
|
71
|
+
* branches at once. Enumerating parents instead leaves each spelling that
|
|
72
|
+
* is not on the list unguarded, which is how this defect shipped.
|
|
73
|
+
*/
|
|
74
|
+
function isTypePositionLiteral(node) {
|
|
75
|
+
return !!node.parent && node.parent.type === utils_1.AST_NODE_TYPES.TSLiteralType;
|
|
76
|
+
}
|
|
49
77
|
/**
|
|
50
78
|
* Check if a node is in a context that should be excluded from the rule
|
|
51
79
|
* (e.g., parameter names, property names, type definitions)
|
|
52
80
|
*/
|
|
53
81
|
function isExcludedContext(node) {
|
|
82
|
+
if (isTypePositionLiteral(node)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
54
85
|
// Check if the node is a property of an object pattern (destructuring)
|
|
55
86
|
if (node.parent &&
|
|
56
87
|
(node.parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
@@ -240,30 +271,51 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
240
271
|
const fixedText = value.replace(idRegex, (_match, prefix, suffix) => {
|
|
241
272
|
return `${prefix}ID${suffix}`;
|
|
242
273
|
});
|
|
243
|
-
|
|
244
|
-
node,
|
|
245
|
-
messageId: 'enforceIdCapitalization',
|
|
246
|
-
fix: (fixer) => {
|
|
247
|
-
// JSX text carries no delimiters, so its own text is the content.
|
|
248
|
-
if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
249
|
-
return fixer.replaceText(node, fixedText);
|
|
250
|
-
}
|
|
251
|
-
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
252
|
-
const replacement = fixStringLiteral(node, fixedText);
|
|
253
|
-
return replacement === null
|
|
254
|
-
? null
|
|
255
|
-
: fixer.replaceText(node, replacement);
|
|
256
|
-
}
|
|
257
|
-
// Any other node kind (a TemplateElement, say) is a fragment of a
|
|
258
|
-
// larger construct whose delimiters and `${}` expressions live
|
|
259
|
-
// outside this node; rebuilding it from the parsed value would
|
|
260
|
-
// destroy them, so report without a fix.
|
|
261
|
-
return null;
|
|
262
|
-
},
|
|
263
|
-
});
|
|
274
|
+
candidates.push({ node, fixedText });
|
|
264
275
|
}
|
|
265
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* A value literal whose exact spelling is also written as a type in this
|
|
279
|
+
* file is a token that type names, not prose: `const kind: 'id' = 'id'`
|
|
280
|
+
* only compiles while both halves agree, and the type half is off limits.
|
|
281
|
+
*/
|
|
282
|
+
function isPinnedByLiteralType(node) {
|
|
283
|
+
return (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
284
|
+
typeof node.value === 'string' &&
|
|
285
|
+
literalTypeValues.has(node.value));
|
|
286
|
+
}
|
|
287
|
+
function reportCandidate(node, fixedText) {
|
|
288
|
+
context.report({
|
|
289
|
+
node,
|
|
290
|
+
messageId: 'enforceIdCapitalization',
|
|
291
|
+
fix: (fixer) => {
|
|
292
|
+
// JSX text carries no delimiters, so its own text is the content.
|
|
293
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
294
|
+
return fixer.replaceText(node, fixedText);
|
|
295
|
+
}
|
|
296
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
297
|
+
const replacement = fixStringLiteral(node, fixedText);
|
|
298
|
+
return replacement === null
|
|
299
|
+
? null
|
|
300
|
+
: fixer.replaceText(node, replacement);
|
|
301
|
+
}
|
|
302
|
+
// Any other node kind (a TemplateElement, say) is a fragment of a
|
|
303
|
+
// larger construct whose delimiters and `${}` expressions live
|
|
304
|
+
// outside this node; rebuilding it from the parsed value would
|
|
305
|
+
// destroy them, so report without a fix.
|
|
306
|
+
return null;
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
266
310
|
return {
|
|
311
|
+
// Record the spellings the file's types claim before any verdict is
|
|
312
|
+
// emitted; a value literal matching one of them cannot be rewritten.
|
|
313
|
+
TSLiteralType(node) {
|
|
314
|
+
if (node.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
315
|
+
typeof node.literal.value === 'string') {
|
|
316
|
+
literalTypeValues.add(node.literal.value);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
267
319
|
// Check string literals
|
|
268
320
|
Literal(node) {
|
|
269
321
|
if (typeof node.value === 'string') {
|
|
@@ -274,6 +326,16 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
274
326
|
JSXText(node) {
|
|
275
327
|
checkForIdInString(node, node.value);
|
|
276
328
|
},
|
|
329
|
+
'Program:exit'() {
|
|
330
|
+
for (const { node, fixedText } of candidates) {
|
|
331
|
+
// JSX text is rendered, so no type can pin it — the check is scoped
|
|
332
|
+
// to literals, whose spelling a type can name.
|
|
333
|
+
if (isPinnedByLiteralType(node)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
reportCandidate(node, fixedText);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
277
339
|
// We don't need a separate handler for CallExpression since we already handle Literals
|
|
278
340
|
// The Literal handler will catch the string arguments in t("user.profile.id")
|
|
279
341
|
};
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.180",
|
|
4
|
+
"date": "2026-08-27T01:52:25.340Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-assert-safe-object-key",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2152
|
|
11
|
+
],
|
|
12
|
+
"summary": "read the validated binding through the shared peel (closes #2152)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-id-capitalization",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2153
|
|
19
|
+
],
|
|
20
|
+
"summary": "leave string literals that spell a type alone (closes #2153)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.179",
|
|
26
|
+
"date": "2026-08-26T23:40:36.587Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-firestore-set-merge",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2150
|
|
33
|
+
],
|
|
34
|
+
"summary": "unwrap assertions in the RealtimeDB carve-out (closes #2150)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.178",
|
|
4
40
|
"date": "2026-08-26T22:11:53.342Z",
|