@blumintinc/eslint-plugin-blumint 1.20.72 → 1.20.74
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
|
@@ -7,6 +7,7 @@ exports.enforceBooleanNamingPrefixes = void 0;
|
|
|
7
7
|
const utils_1 = require("@typescript-eslint/utils");
|
|
8
8
|
const pluralize_1 = __importDefault(require("pluralize"));
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
10
11
|
// Default approved boolean prefixes. Some less common prefixes (e.g., 'are',
|
|
11
12
|
// 'includes') stay allowed for flexibility even though the user-facing message
|
|
12
13
|
// highlights only the most common ones. Underscore-prefixed names are also
|
|
@@ -226,6 +227,27 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
226
227
|
}
|
|
227
228
|
return false;
|
|
228
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Recognize `Boolean(x)` — the explicit spelling of `!!x` — as producing a
|
|
232
|
+
* primitive boolean.
|
|
233
|
+
*
|
|
234
|
+
* The callee name alone cannot decide this. A local binding, a parameter or
|
|
235
|
+
* an import named `Boolean` shadows the global and may return anything, so
|
|
236
|
+
* the identifier is resolved through the scope chain at the call site: only
|
|
237
|
+
* an unresolved reference, or one reaching a definition-less global, is the
|
|
238
|
+
* built-in. `new Boolean(x)` is deliberately not covered here — a
|
|
239
|
+
* `NewExpression` builds a Boolean wrapper *object*, which is always truthy
|
|
240
|
+
* and never a primitive boolean.
|
|
241
|
+
*/
|
|
242
|
+
function isGlobalBooleanCall(callExpression) {
|
|
243
|
+
const { callee } = callExpression;
|
|
244
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
245
|
+
callee.name !== 'Boolean') {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(ASTHelpers_1.ASTHelpers.getScope(context, callee), 'Boolean');
|
|
249
|
+
return !variable || variable.defs.length === 0;
|
|
250
|
+
}
|
|
229
251
|
/**
|
|
230
252
|
* Check if a node is initialized with a boolean value
|
|
231
253
|
*/
|
|
@@ -308,6 +330,12 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
308
330
|
// Check for function calls that might return boolean
|
|
309
331
|
if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
310
332
|
node.init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
333
|
+
// A coercion through the global `Boolean` is as definitive as `!!x`,
|
|
334
|
+
// and its callee carries no approved prefix for the name heuristic
|
|
335
|
+
// below to recognize.
|
|
336
|
+
if (isGlobalBooleanCall(node.init)) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
311
339
|
const calleeName = node.init.callee.name;
|
|
312
340
|
const lowerCallee = calleeName.toLowerCase();
|
|
313
341
|
// For assert*-style utilities, only treat as boolean if we can confirm boolean return type
|
|
@@ -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,
|
|
@@ -701,6 +701,85 @@ function buildHookImportFix(fixer, program, hookName) {
|
|
|
701
701
|
? fixer.insertTextBefore(anchor, statement)
|
|
702
702
|
: fixer.insertTextAfterRange([0, 0], statement);
|
|
703
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* Scope kinds whose bindings are established once per module evaluation:
|
|
706
|
+
* globals, imports and module-level declarations. Such a value is identical on
|
|
707
|
+
* every render, so it can never belong in a dependency array.
|
|
708
|
+
*/
|
|
709
|
+
const MODULE_LEVEL_SCOPE_TYPES = new Set(['global', 'module']);
|
|
710
|
+
/**
|
|
711
|
+
* True when `inner` lies entirely inside `outer`'s source range.
|
|
712
|
+
*/
|
|
713
|
+
function isRangeWithin(inner, outer) {
|
|
714
|
+
return inner[0] >= outer[0] && inner[1] <= outer[1];
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* True when a reference appears purely in type position (an annotation or a
|
|
718
|
+
* `satisfies`/`as` target inside the literal). Types erase at compile time, so
|
|
719
|
+
* such a name never becomes a dependency however it resolves. The flags are read
|
|
720
|
+
* defensively: an analyzer that omits them leaves the reference classified as a
|
|
721
|
+
* value, which keeps the suggestion — the conservative direction.
|
|
722
|
+
*/
|
|
723
|
+
function isTypeOnlyReference(reference) {
|
|
724
|
+
const flags = reference;
|
|
725
|
+
return flags.isTypeReference === true && flags.isValueReference === false;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* True when a reference names a value that can differ between renders, i.e. one
|
|
729
|
+
* bound in a scope INSIDE the module and OUTSIDE the literal: a prop, a local, a
|
|
730
|
+
* destructured value, another hook's result.
|
|
731
|
+
*
|
|
732
|
+
* Everything else is unusable as a dependency. An unresolved name is a global.
|
|
733
|
+
* A module- or global-scoped binding is fixed for the module's lifetime. A
|
|
734
|
+
* binding whose own scope sits inside the literal — an inline function's
|
|
735
|
+
* parameters, its locals, its `arguments` — is not closed over at all.
|
|
736
|
+
*/
|
|
737
|
+
function isRenderScopeReference(reference, literalRange) {
|
|
738
|
+
const variable = reference.resolved;
|
|
739
|
+
if (!variable) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
if (MODULE_LEVEL_SCOPE_TYPES.has(variable.scope.type)) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return !isRangeWithin(variable.scope.block.range, literalRange);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* True when the literal reads at least one value a dependency array could hold.
|
|
749
|
+
*
|
|
750
|
+
* Answered from RESOLVED scope references rather than identifier names, so
|
|
751
|
+
* shadowing, destructuring and imports are all accounted for exactly as the
|
|
752
|
+
* scope analyzer sees them.
|
|
753
|
+
*
|
|
754
|
+
* The walk starts at the literal's own scope and descends into every scope
|
|
755
|
+
* nested inside it — a callback buried in an object property closes over the
|
|
756
|
+
* component's scope just as a property value would — while the range filter
|
|
757
|
+
* keeps the literal's siblings out of the answer.
|
|
758
|
+
*/
|
|
759
|
+
function closesOverRenderScopeValue(node, scope) {
|
|
760
|
+
const literalRange = node.range;
|
|
761
|
+
const pending = [scope];
|
|
762
|
+
while (pending.length > 0) {
|
|
763
|
+
const current = pending.pop();
|
|
764
|
+
for (const reference of current.references) {
|
|
765
|
+
if (!isRangeWithin(reference.identifier.range, literalRange)) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (isTypeOnlyReference(reference)) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (isRenderScopeReference(reference, literalRange)) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
for (const child of current.childScopes) {
|
|
776
|
+
if (isRangeWithin(child.block.range, literalRange)) {
|
|
777
|
+
pending.push(child);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
704
783
|
/**
|
|
705
784
|
* Builds memoization suggestions with dependency placeholders for developers.
|
|
706
785
|
* @param node Literal node to wrap.
|
|
@@ -725,6 +804,19 @@ function buildMemoSuggestions(node, descriptor, sourceCode, context) {
|
|
|
725
804
|
memoHook: descriptor.memoHook,
|
|
726
805
|
},
|
|
727
806
|
fix(fixer) {
|
|
807
|
+
// The wrap writes an EMPTY dependency array for the author to fill in.
|
|
808
|
+
// A literal that closes over nothing has nothing to fill it with, and
|
|
809
|
+
// `enforce-global-constants` forbids precisely that shape — a useMemo
|
|
810
|
+
// over an object literal with empty deps — while offering no fixer of
|
|
811
|
+
// its own. Accepting the suggestion would therefore trade this report
|
|
812
|
+
// for a permanent, non-autofixable one. Hoisting is the correct branch
|
|
813
|
+
// when nothing is closed over, and the report's own message already
|
|
814
|
+
// prescribes it, so decline rather than emit a state the author cannot
|
|
815
|
+
// complete (the #1417 principle, applied here as in the shadowed-hook
|
|
816
|
+
// guard below).
|
|
817
|
+
if (!closesOverRenderScopeValue(node, ASTHelpers_1.ASTHelpers.getScope(context, node))) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
728
820
|
// The wrapper is only correct if the hook name resolves to React's
|
|
729
821
|
// hook. A shadowing local/parameter would silently call that value
|
|
730
822
|
// instead, and an import of the same name from another module would
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.74",
|
|
4
|
+
"date": "2026-08-02T06:07:00.009Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1605
|
|
11
|
+
],
|
|
12
|
+
"summary": "split the rename on case boundaries so it is idempotent (closes #1605)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.73",
|
|
18
|
+
"date": "2026-08-02T05:14:39.301Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1602
|
|
25
|
+
],
|
|
26
|
+
"summary": "recognise a direct Boolean(...) initializer (closes #1602)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "react-memoize-literals",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1600
|
|
33
|
+
],
|
|
34
|
+
"summary": "decline the memo suggestion when the literal closes over nothing (closes #1600)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.72",
|
|
4
40
|
"date": "2026-08-02T03:33:22.821Z",
|