@blumintinc/eslint-plugin-blumint 1.16.1 → 1.16.2

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
@@ -196,7 +196,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
196
196
  module.exports = {
197
197
  meta: {
198
198
  name: '@blumintinc/eslint-plugin-blumint',
199
- version: '1.16.1',
199
+ version: '1.16.2',
200
200
  },
201
201
  parseOptions: {
202
202
  ecmaVersion: 2020,
@@ -368,6 +368,18 @@ function statementReferencesAny(statement, names) {
368
368
  }
369
369
  return false;
370
370
  }
371
+ /**
372
+ * Find the index of the first statement that references any of the given names,
373
+ * searching forward from afterIndex.
374
+ */
375
+ function findFirstUsageIndex(body, names, afterIndex) {
376
+ for (let cursor = afterIndex; cursor < body.length; cursor += 1) {
377
+ if (statementReferencesAny(body[cursor], names)) {
378
+ return cursor;
379
+ }
380
+ }
381
+ return -1;
382
+ }
371
383
  function collectAssignedNamesFromPattern(target, names) {
372
384
  if (TYPE_EXPRESSION_WRAPPERS.has(target.type) &&
373
385
  'expression' in target) {
@@ -671,10 +683,13 @@ function findEarliestSafeIndex(body, startIndex, dependencies, { allowHooks }) {
671
683
  }
672
684
  function getStartWithComments(statement, sourceCode) {
673
685
  const comments = sourceCode.getCommentsBefore(statement);
674
- if (comments.length === 0) {
675
- return statement.range[0];
686
+ const start = comments.length === 0 ? statement.range[0] : comments[0].range[0];
687
+ const text = sourceCode.getText();
688
+ let cursor = start - 1;
689
+ while (cursor >= 0 && (text[cursor] === ' ' || text[cursor] === '\t')) {
690
+ cursor -= 1;
676
691
  }
677
- return comments[0].range[0];
692
+ return cursor + 1;
678
693
  }
679
694
  function getNextStart(body, index, parent, sourceCode) {
680
695
  const nextStatement = body[index + 1];
@@ -907,42 +922,95 @@ function isSiblingSourceDerivation(statement, sourceNodes, sourceDeclarators) {
907
922
  return !sourceDeclarators.has(name);
908
923
  });
909
924
  }
925
+ /**
926
+ * Restrict late-declaration candidates to simple variables with at most an Identifier or
927
+ * Literal initializer. This ensures they are pure values that do not have side effects or
928
+ * change execution order when moved closer to their usage. More complex initializers are
929
+ * excluded to maintain temporal safety.
930
+ */
931
+ function isLateDeclarationCandidate(statement) {
932
+ if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
933
+ statement.declarations.length !== 1) {
934
+ return false;
935
+ }
936
+ const [declarator] = statement.declarations;
937
+ return (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier &&
938
+ (!declarator.init ||
939
+ declarator.init.type === utils_1.AST_NODE_TYPES.Identifier ||
940
+ declarator.init.type === utils_1.AST_NODE_TYPES.Literal));
941
+ }
942
+ const LOOP_TYPES = new Set([
943
+ utils_1.AST_NODE_TYPES.ForStatement,
944
+ utils_1.AST_NODE_TYPES.ForInStatement,
945
+ utils_1.AST_NODE_TYPES.ForOfStatement,
946
+ utils_1.AST_NODE_TYPES.WhileStatement,
947
+ utils_1.AST_NODE_TYPES.DoWhileStatement,
948
+ ]);
949
+ /**
950
+ * Treat a loop as an ordering barrier for late-declaration moves when it mutates any
951
+ * tracked variable inside the loop body.
952
+ *
953
+ * Variables that are declared before a loop and mutated inside it (accumulators,
954
+ * counters, collectors) must remain before the loop so their declaration is visible
955
+ * across all iterations. Moving them inside would reset them on every iteration,
956
+ * changing program semantics. This guard applies regardless of whether the variable
957
+ * is also read after the loop.
958
+ *
959
+ * Assumptions:
960
+ * - nameSet contains the name(s) being tracked for late declaration.
961
+ * - body is the array of statements in the current block.
962
+ * - usageIndex is the index of the first statement that references the variable(s).
963
+ */
964
+ function isMutatedInLoop(body, usageIndex, nameSet) {
965
+ const firstUsage = body[usageIndex];
966
+ if (!firstUsage) {
967
+ return false;
968
+ }
969
+ if (!LOOP_TYPES.has(firstUsage.type)) {
970
+ return false;
971
+ }
972
+ return statementMutatesAny(firstUsage, nameSet);
973
+ }
910
974
  function handleLateDeclarations(ruleContext, body, parent) {
911
975
  const { sourceCode } = ruleContext;
912
976
  body.forEach((statement, index) => {
913
- if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration ||
914
- statement.declarations.length !== 1) {
977
+ if (!isLateDeclarationCandidate(statement)) {
915
978
  return;
916
979
  }
917
980
  const [declarator] = statement.declarations;
918
- if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
919
- (declarator.init &&
920
- declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier &&
921
- declarator.init.type !== utils_1.AST_NODE_TYPES.Literal)) {
922
- return;
923
- }
924
981
  const name = declarator.id.name;
925
982
  const dependencies = new Set();
926
983
  if (declarator.init && declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
927
984
  dependencies.add(declarator.init.name);
928
985
  }
929
986
  const nameSet = new Set([name]);
930
- let usageIndex = -1;
931
- for (let cursor = index + 1; cursor < body.length; cursor += 1) {
932
- if (statementReferencesAny(body[cursor], nameSet)) {
933
- usageIndex = cursor;
934
- break;
935
- }
936
- }
987
+ const usageIndex = findFirstUsageIndex(body, nameSet, index + 1);
937
988
  if (usageIndex === -1 || usageIndex <= index + 1) {
938
989
  return;
939
990
  }
991
+ // Loop mutations create a dependency barrier: declarations that precede loops and are
992
+ // mutated inside them cannot be safely moved, as the declaration must be visible across
993
+ // all iterations. Prevent false positives for this pattern.
994
+ if (isMutatedInLoop(body, usageIndex, nameSet)) {
995
+ return;
996
+ }
940
997
  const intervening = body.slice(index + 1, usageIndex);
941
998
  // Only move across pure declarations that do not mention the placeholder or its initializer dependencies to avoid changing closure timing or TDZ behavior.
942
- const crossesImpureOrTracked = intervening.some((stmt) => {
999
+ const crossesImpureOrTracked = intervening.some((stmt, i) => {
943
1000
  if (!isPureDeclaration(stmt, { allowHooks: false })) {
944
1001
  return true;
945
1002
  }
1003
+ // Do not hop over another declaration that is used at the same index or earlier
1004
+ // if it is also a candidate for being moved.
1005
+ // This prevents circular swapping of related declarations (like resolve/reject pairs).
1006
+ if (isLateDeclarationCandidate(stmt)) {
1007
+ const declaredNames = getDeclaredNames(stmt);
1008
+ const firstUsageOfIntervening = findFirstUsageIndex(body, declaredNames, index + 1 + i + 1);
1009
+ if (firstUsageOfIntervening !== -1 &&
1010
+ firstUsageOfIntervening <= usageIndex) {
1011
+ return true;
1012
+ }
1013
+ }
946
1014
  if (statementDeclaresAny(stmt, nameSet) ||
947
1015
  statementMutatesAny(stmt, nameSet)) {
948
1016
  return true;
@@ -352,6 +352,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
352
352
  /[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
353
353
  return true;
354
354
  }
355
+ // Abbreviation markers (str, num, int, bool, arr, obj, fn, func) are
356
+ // short enough that the raw-character boundary heuristic below can fire
357
+ // on them as substrings inside real English words (e.g. "int" inside
358
+ // "Mint", "str" inside "stream"). For these markers, require an exact
359
+ // match against a full camelCase token so that "Mint" → ["Mint"] never
360
+ // matches "int", while genuine Hungarian like intValue → ["int","Value"]
361
+ // still does.
362
+ if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
363
+ const segments = splitCamelSegments(variableName);
364
+ return segments.some((s) => s.toLowerCase() === normalizedMarker);
365
+ }
355
366
  // Check for word boundaries to avoid matching substrings
356
367
  // For example, avoid matching "int" in "points" or "str" in "stream"
357
368
  const markerIndex = normalizedVarName.indexOf(normalizedMarker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.16.2",
4
+ "date": "2026-06-30T22:17:37.457Z",
5
+ "rules": [
6
+ {
7
+ "name": "logical-top-to-bottom-grouping",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1247
11
+ ],
12
+ "summary": "restore loop-mutation guard and indentation-preserving idempotent autofix (closes #1247)"
13
+ },
14
+ {
15
+ "name": "no-hungarian",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1246
19
+ ],
20
+ "summary": "only flag abbreviation markers at camelCase token boundaries (closes #1246)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.16.1",
4
26
  "date": "2026-06-30T15:58:17.558Z",