@blumintinc/eslint-plugin-blumint 1.18.6 → 1.18.8
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/class-methods-read-top-to-bottom.js +18 -5
- package/lib/rules/enforce-positive-naming.js +21 -2
- package/lib/rules/react-memoize-literals.js +24 -8
- package/lib/utils/graph/ClassGraphBuilder.js +9 -1
- package/package.json +1 -1
- package/release-manifest.json +36 -0
package/lib/index.js
CHANGED
|
@@ -5,7 +5,10 @@ const createRule_1 = require("../utils/createRule");
|
|
|
5
5
|
const ClassGraphBuilder_1 = require("../utils/graph/ClassGraphBuilder");
|
|
6
6
|
function getMemberName(member) {
|
|
7
7
|
if (member.type === 'MethodDefinition' ||
|
|
8
|
-
member.type === 'PropertyDefinition'
|
|
8
|
+
member.type === 'PropertyDefinition' ||
|
|
9
|
+
member.type === 'TSAbstractMethodDefinition' ||
|
|
10
|
+
member.type === 'TSAbstractPropertyDefinition' ||
|
|
11
|
+
member.type === 'TSAbstractAccessorProperty') {
|
|
9
12
|
return member.key.type === 'Identifier' ? member.key.name : null;
|
|
10
13
|
}
|
|
11
14
|
return null;
|
|
@@ -43,10 +46,7 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
43
46
|
const graphBuilder = new ClassGraphBuilder_1.ClassGraphBuilder(className, node);
|
|
44
47
|
const sortedOrder = graphBuilder.memberNamesSorted;
|
|
45
48
|
const actualOrder = node.body
|
|
46
|
-
.map((member) => member
|
|
47
|
-
member.type === 'PropertyDefinition'
|
|
48
|
-
? member.key.name
|
|
49
|
-
: null)
|
|
49
|
+
.map((member) => getMemberName(member))
|
|
50
50
|
.filter(Boolean);
|
|
51
51
|
// Check if we have the same number of methods in both arrays
|
|
52
52
|
// This prevents issues with similar method names being treated as duplicates
|
|
@@ -58,6 +58,19 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
58
58
|
if (uniqueMethodNames.size !== actualOrder.length) {
|
|
59
59
|
return; // Skip if there are actual duplicates
|
|
60
60
|
}
|
|
61
|
+
// Defense-in-depth: the fixer overwrites the ENTIRE class body from the
|
|
62
|
+
// tracked members. If any member is not tracked (unknown node type,
|
|
63
|
+
// non-Identifier/computed key, static block, index signature, etc.), it
|
|
64
|
+
// would be silently dropped by the rewrite. Bail rather than emit a body
|
|
65
|
+
// that deletes source the rule does not track.
|
|
66
|
+
const trackedNames = new Set(sortedOrder);
|
|
67
|
+
const allMembersRepresented = node.body.every((member) => {
|
|
68
|
+
const name = getMemberName(member);
|
|
69
|
+
return name !== null && trackedNames.has(name);
|
|
70
|
+
});
|
|
71
|
+
if (!allMembersRepresented) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
61
74
|
for (let i = 0; i < actualOrder.length; i++) {
|
|
62
75
|
const actualMember = actualOrder[i];
|
|
63
76
|
const expectedMember = sortedOrder[i];
|
|
@@ -729,6 +729,8 @@ const UN_EXCEPTIONS = [
|
|
|
729
729
|
'universes',
|
|
730
730
|
'universities',
|
|
731
731
|
'university',
|
|
732
|
+
'unknown',
|
|
733
|
+
'unknowns',
|
|
732
734
|
'unless',
|
|
733
735
|
'until',
|
|
734
736
|
'unto',
|
|
@@ -1063,16 +1065,33 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1063
1065
|
if (directMatch) {
|
|
1064
1066
|
return { isNegative: true, alternatives: directMatch };
|
|
1065
1067
|
}
|
|
1068
|
+
const restOfName = name.replace(pattern, '');
|
|
1069
|
+
const capitalizedRest = restOfName
|
|
1070
|
+
? restOfName.charAt(0).toUpperCase() + restOfName.slice(1)
|
|
1071
|
+
: '';
|
|
1066
1072
|
const alternatives = BOOLEAN_POSITIVE_ALTERNATIVES[key] || [];
|
|
1067
1073
|
if (alternatives.length > 0) {
|
|
1068
1074
|
// Suggest the positive version with the rest of the name
|
|
1069
|
-
const
|
|
1070
|
-
const suggestedAlternatives = alternatives.map((alt) => `${alt}${restOfName.charAt(0).toUpperCase() + restOfName.slice(1)}`);
|
|
1075
|
+
const suggestedAlternatives = alternatives.map((alt) => `${alt}${capitalizedRest}`);
|
|
1071
1076
|
return {
|
|
1072
1077
|
isNegative: true,
|
|
1073
1078
|
alternatives: suggestedAlternatives,
|
|
1074
1079
|
};
|
|
1075
1080
|
}
|
|
1081
|
+
// No hand-maintained mapping covers this boolean+negative prefix
|
|
1082
|
+
// combination (e.g. `hasUn`, `canNo`). Derive the positive form by
|
|
1083
|
+
// dropping the negative prefix while keeping the boolean prefix
|
|
1084
|
+
// (hasUnavailableItems -> hasAvailableItems), mirroring the
|
|
1085
|
+
// `is`-prefix behavior so the report always names a real
|
|
1086
|
+
// alternative rather than the "a positive alternative" placeholder.
|
|
1087
|
+
const matched = name.match(pattern);
|
|
1088
|
+
if (matched && capitalizedRest) {
|
|
1089
|
+
const positivePrefix = matched[0].slice(0, matched[0].length - prefix.length);
|
|
1090
|
+
return {
|
|
1091
|
+
isNegative: true,
|
|
1092
|
+
alternatives: [`${positivePrefix}${capitalizedRest}`],
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1076
1095
|
return {
|
|
1077
1096
|
isNegative: true,
|
|
1078
1097
|
alternatives: ['a positive alternative'],
|
|
@@ -430,14 +430,18 @@ function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
|
430
430
|
* style data consumed by a library rather than compared by reference (MUI `sx`
|
|
431
431
|
* or the standard `style` prop).
|
|
432
432
|
*
|
|
433
|
-
* Walks up from the literal only through positions where it remains the
|
|
434
|
-
* attribute's
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
433
|
+
* Walks up from the literal only through positions where it remains part of the
|
|
434
|
+
* attribute's style value: conditional branches (`sx={c ? {…} : {…}}`), logical
|
|
435
|
+
* fallbacks (`sx={c && {…}}`), array entries (MUI accepts `sx` arrays), nested
|
|
436
|
+
* object property values (`sx={{ display: { xs: 'none', md: 'inline' } }}` —
|
|
437
|
+
* MUI's responsive breakpoint syntax), and expression wrappers (`{…} as const`,
|
|
438
|
+
* parentheses). MUI reprocesses the entire `sx`/`style` object subtree on every
|
|
439
|
+
* render, so nested values benefit no more from referential stability than the
|
|
440
|
+
* top-level object does. Any other parent — a function call, a spread — means
|
|
441
|
+
* the reference is observed or transformed before reaching the attribute, so the
|
|
442
|
+
* walk stops and the literal stays reported. This keeps the exemption tied to
|
|
443
|
+
* genuine style values without silencing unrelated literals that merely appear
|
|
444
|
+
* deeper in the tree.
|
|
441
445
|
*/
|
|
442
446
|
function isStyleJSXAttributeValue(node) {
|
|
443
447
|
let current = node;
|
|
@@ -474,6 +478,18 @@ function isStyleJSXAttributeValue(node) {
|
|
|
474
478
|
}
|
|
475
479
|
break;
|
|
476
480
|
}
|
|
481
|
+
case utils_1.AST_NODE_TYPES.Property: {
|
|
482
|
+
if (parent.value !== current) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression: {
|
|
488
|
+
if (!parent.properties.some((property) => property === current)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
477
493
|
default: {
|
|
478
494
|
if (!isExpressionWrapper(parent) || parent.expression !== current) {
|
|
479
495
|
return false;
|
|
@@ -36,7 +36,11 @@ class ClassGraphBuilder {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
static isClassMember(node) {
|
|
39
|
-
return (node.type === 'MethodDefinition' ||
|
|
39
|
+
return (node.type === 'MethodDefinition' ||
|
|
40
|
+
node.type === 'PropertyDefinition' ||
|
|
41
|
+
node.type === 'TSAbstractMethodDefinition' ||
|
|
42
|
+
node.type === 'TSAbstractPropertyDefinition' ||
|
|
43
|
+
node.type === 'TSAbstractAccessorProperty');
|
|
40
44
|
}
|
|
41
45
|
addMemberToGraph(member) {
|
|
42
46
|
const name = member.key.name;
|
|
@@ -48,6 +52,10 @@ class ClassGraphBuilder {
|
|
|
48
52
|
if (member.type === 'MethodDefinition') {
|
|
49
53
|
return member.kind === 'constructor' ? 'constructor' : 'method';
|
|
50
54
|
}
|
|
55
|
+
// Abstract methods can never be constructors, so they are always methods.
|
|
56
|
+
if (member.type === 'TSAbstractMethodDefinition') {
|
|
57
|
+
return 'method';
|
|
58
|
+
}
|
|
51
59
|
return 'property';
|
|
52
60
|
}
|
|
53
61
|
static createGraphNode(name, type, accessibility, isStatic = false) {
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.18.8",
|
|
4
|
+
"date": "2026-07-10T21:29:12.501Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-positive-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1281
|
|
11
|
+
],
|
|
12
|
+
"summary": "whitelist \"unknown\" and derive real suggestions for non-is prefixes (closes #1281)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "react-memoize-literals",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1280
|
|
19
|
+
],
|
|
20
|
+
"summary": "exempt nested object literals inside sx/style values (closes #1280)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.18.7",
|
|
26
|
+
"date": "2026-07-10T02:31:44.580Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "class-methods-read-top-to-bottom",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1279
|
|
33
|
+
],
|
|
34
|
+
"summary": "preserve abstract members during autofix (closes #1279)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.18.6",
|
|
4
40
|
"date": "2026-07-09T08:45:37.007Z",
|