@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 CHANGED
@@ -218,7 +218,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
218
218
  module.exports = {
219
219
  meta: {
220
220
  name: '@blumintinc/eslint-plugin-blumint',
221
- version: '1.18.6',
221
+ version: '1.18.8',
222
222
  },
223
223
  parseOptions: {
224
224
  ecmaVersion: 2020,
@@ -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.type === 'MethodDefinition' ||
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 restOfName = name.replace(pattern, '');
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 resolved value: conditional branches (`sx={c ? {…} : {…}}`),
435
- * logical fallbacks (`sx={c && {…}}`), array entries (MUI accepts `sx` arrays),
436
- * and expression wrappers (`{…} as const`, parentheses). Any other parent a
437
- * function call, an object property, a spread means the reference is observed
438
- * or transformed before reaching the attribute, so the walk stops and the
439
- * literal stays reported. This keeps the exemption tied to genuine style values
440
- * without silencing unrelated literals that merely appear deeper in the tree.
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' || node.type === 'PropertyDefinition');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.18.6",
3
+ "version": "1.18.8",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",