@blumintinc/eslint-plugin-blumint 1.20.155 → 1.20.157

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.155',
226
+ version: '1.20.157',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -55,7 +55,10 @@ const IN_EXCEPTIONS = [
55
55
  'increases',
56
56
  'increasing',
57
57
  'increasingly',
58
+ 'increment',
58
59
  'incremental',
60
+ 'incremented',
61
+ 'incrementing',
59
62
  'increments',
60
63
  'incriminating',
61
64
  'incubation',
@@ -190,8 +193,11 @@ const IN_EXCEPTIONS = [
190
193
  'inhaling',
191
194
  'inherent',
192
195
  'inherently',
196
+ 'inherit',
193
197
  'inheritance',
194
198
  'inherited',
199
+ 'inheriting',
200
+ 'inherits',
195
201
  'initial',
196
202
  'initially',
197
203
  'initials',
@@ -219,6 +225,10 @@ const IN_EXCEPTIONS = [
219
225
  'inland',
220
226
  'inlay',
221
227
  'inlet',
228
+ 'inline',
229
+ 'inlined',
230
+ 'inlines',
231
+ 'inlining',
222
232
  'inmate',
223
233
  'inmates',
224
234
  'inn',
@@ -294,6 +304,10 @@ const IN_EXCEPTIONS = [
294
304
  'instances',
295
305
  'instant',
296
306
  'instantaneous',
307
+ 'instantiate',
308
+ 'instantiated',
309
+ 'instantiates',
310
+ 'instantiating',
297
311
  'instantly',
298
312
  'instead',
299
313
  'instigated',
@@ -340,6 +354,11 @@ const IN_EXCEPTIONS = [
340
354
  'intake',
341
355
  'integer',
342
356
  'integers',
357
+ 'integrate',
358
+ 'integrated',
359
+ 'integrates',
360
+ 'integrating',
361
+ 'integration',
343
362
  'intel',
344
363
  'intellect',
345
364
  'intellectual',
@@ -1806,6 +1806,49 @@ function moveSegment(segments, fromIndex, toIndex) {
1806
1806
  next.splice(toIndex < fromIndex ? toIndex : toIndex - 1, 0, moved.replace(/[ \t]+$/u, ''));
1807
1807
  return next;
1808
1808
  }
1809
+ const LINE_TERMINATORS = new Set(['\n', '\r', '\u2028', '\u2029']);
1810
+ /**
1811
+ * Whether text appended to `text` would start on a fresh line.
1812
+ *
1813
+ * Trailing spaces and tabs are skipped: a segment ends with the indentation that
1814
+ * belonged to whatever followed it, and that indentation still leaves an appended
1815
+ * segment on a line of its own.
1816
+ */
1817
+ function endsWithLineTerminator(text) {
1818
+ let cursor = text.length - 1;
1819
+ while (cursor >= 0 && (text[cursor] === ' ' || text[cursor] === '\t')) {
1820
+ cursor -= 1;
1821
+ }
1822
+ return cursor >= 0 && LINE_TERMINATORS.has(text[cursor]);
1823
+ }
1824
+ /**
1825
+ * The line break the source itself uses, so a separator restored below matches its
1826
+ * neighbours instead of mixing endings into a CRLF file.
1827
+ */
1828
+ function lineBreakOf(text) {
1829
+ const index = text.indexOf('\n');
1830
+ return index > 0 && text[index - 1] === '\r' ? '\r\n' : '\n';
1831
+ }
1832
+ /**
1833
+ * Concatenates reordered segments, restoring the statement separation the source
1834
+ * carried positionally.
1835
+ *
1836
+ * A segment runs up to where the next one began, so it normally already ends with the
1837
+ * line break that separated them. Two do not: the block's last segment stops at `}` or
1838
+ * at the end of the file, and a statement sharing a line with its predecessor never had
1839
+ * a break in front of it. Once a reordering moves such a segment away from the end,
1840
+ * appending the next segment directly runs two statements together — and where the
1841
+ * first ends in a `//` comment that is not a formatting blemish, since the appended
1842
+ * statement becomes comment text and vanishes from the program (#2023).
1843
+ *
1844
+ * Only the joins are separated. Nothing is appended after the final segment, whose
1845
+ * successor is the untouched text outside the replaced range.
1846
+ */
1847
+ function joinSegments(segments, lineBreak) {
1848
+ return segments.reduce((text, segment, index) => index === 0 || endsWithLineTerminator(text)
1849
+ ? text + segment
1850
+ : text + lineBreak + segment, '');
1851
+ }
1809
1852
  /**
1810
1853
  * Emits an entire reordering as one fix by permuting the block's text segments.
1811
1854
  *
@@ -1827,7 +1870,7 @@ function buildReorderFix(body, moves, parent, sourceCode, fixer) {
1827
1870
  if (first > last) {
1828
1871
  return null;
1829
1872
  }
1830
- return fixer.replaceTextRange([bounds[first], bounds[last + 1]], reordered.slice(first, last + 1).join(''));
1873
+ return fixer.replaceTextRange([bounds[first], bounds[last + 1]], joinSegments(reordered.slice(first, last + 1), lineBreakOf(sourceCode.getText())));
1831
1874
  }
1832
1875
  /**
1833
1876
  * A fix is emitted only for a reordering the detector scores at zero relocatable
@@ -27,6 +27,7 @@ exports.preferNullishCoalescingBooleanProps = void 0;
27
27
  const utils_1 = require("@typescript-eslint/utils");
28
28
  const ts = __importStar(require("typescript"));
29
29
  const createRule_1 = require("../utils/createRule");
30
+ const replacementSegments_1 = require("../utils/replacementSegments");
30
31
  const BOOLEAN_PROP_REGEX = /^(is|has|should|can|will|do|does|did|was|were|enable|disable)/;
31
32
  function isBooleanType(type, checker) {
32
33
  if (type.isUnion()) {
@@ -531,6 +532,68 @@ function needsSelfParens(node, sourceCode) {
531
532
  }
532
533
  return !isParenthesized(node, sourceCode);
533
534
  }
535
+ /**
536
+ * Comments the rewrite would delete.
537
+ *
538
+ * The fix rebuilds the whole expression from `getText` of each operand, so every
539
+ * comment written inside the node but outside both operand ranges has no anchor
540
+ * in the replacement: the trivia around the `||` operator, and anything sitting
541
+ * inside source-level parentheses that the rebuild discards. Comments nested
542
+ * within an operand travel with that operand's own text and are never stranded.
543
+ */
544
+ function strandedComments(node, sourceCode) {
545
+ const enclosedBy = (operand) => (comment) => comment.range[0] >= operand.range[0] &&
546
+ comment.range[1] <= operand.range[1];
547
+ const inLeft = enclosedBy(node.left);
548
+ const inRight = enclosedBy(node.right);
549
+ return sourceCode
550
+ .getCommentsInside(node)
551
+ .filter((comment) => !inLeft(comment) && !inRight(comment));
552
+ }
553
+ function groupStrandedComments(node, sourceCode) {
554
+ const operator = sourceCode.getTokenAfter(node.left, {
555
+ filter: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '||',
556
+ });
557
+ const groups = {
558
+ leading: [],
559
+ beforeOperator: [],
560
+ afterOperator: [],
561
+ trailing: [],
562
+ };
563
+ for (const comment of strandedComments(node, sourceCode)) {
564
+ if (comment.range[1] <= node.left.range[0]) {
565
+ groups.leading.push(comment);
566
+ }
567
+ else if (comment.range[0] >= node.right.range[1]) {
568
+ groups.trailing.push(comment);
569
+ }
570
+ else if (operator && comment.range[1] <= operator.range[0]) {
571
+ groups.beforeOperator.push(comment);
572
+ }
573
+ else {
574
+ groups.afterOperator.push(comment);
575
+ }
576
+ }
577
+ return groups;
578
+ }
579
+ /**
580
+ * `return`, `throw` and `yield` forbid a LineTerminator between themselves and
581
+ * their operand, so a carried comment that demands its own line cannot sit
582
+ * between one of them and the rewritten expression: a line comment there ends
583
+ * the statement through ASI, and so does a block comment carrying a line
584
+ * terminator, which the grammar reads AS a LineTerminator (#1963). Such a
585
+ * comment rides ahead of the keyword instead.
586
+ *
587
+ * The keyword token is the anchor rather than the start of its line because an
588
+ * insertion point immediately before a token is always a token boundary, while a
589
+ * line start can fall inside a multi-line template literal and would be written
590
+ * into the template's text.
591
+ */
592
+ const RESTRICTED_KEYWORDS = new Set(['return', 'throw', 'yield']);
593
+ function restrictedKeywordBefore(node, sourceCode) {
594
+ const before = sourceCode.getTokenBefore(node);
595
+ return before && RESTRICTED_KEYWORDS.has(before.value) ? before : null;
596
+ }
534
597
  exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
535
598
  name: 'prefer-nullish-coalescing-boolean-props',
536
599
  meta: {
@@ -582,10 +645,67 @@ exports.preferNullishCoalescingBooleanProps = (0, createRule_1.createRule)({
582
645
  right: rightText,
583
646
  },
584
647
  fix(fixer) {
585
- const replacement = `${parenthesizeLogical(leftText, node.left)} ?? ${parenthesizeLogical(rightText, node.right)}`;
586
- return fixer.replaceText(node, needsSelfParens(node, sourceCode)
587
- ? `(${replacement})`
588
- : replacement);
648
+ const groups = groupStrandedComments(node, sourceCode);
649
+ // Whether the replacement is parenthesized is decided by the
650
+ // expression's context exactly as it is without comments:
651
+ // parentheses are tokens, so letting a carried comment add them
652
+ // would make the comment change the emitted program.
653
+ const selfParens = needsSelfParens(node, sourceCode);
654
+ const text = sourceCode.getText();
655
+ const toSegment = (comment) => ({
656
+ text: text.slice(comment.range[0], comment.range[1]),
657
+ breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
658
+ });
659
+ // A carried comment's only anchor is the line the expression
660
+ // opens on, which can start mid-line.
661
+ const startLine = sourceCode.lines[node.loc.start.line - 1] ?? '';
662
+ const indent = /^[\t ]*/.exec(startLine)?.[0] ?? '';
663
+ // Inside parentheses a newline can never trigger ASI, so every
664
+ // comment can ride within the replacement on a line of its own.
665
+ // Without them, a leading comment that demands its own line
666
+ // moves ahead of a restricted keyword when one governs the
667
+ // expression; everywhere else a line break before the
668
+ // expression is inert.
669
+ const keyword = selfParens
670
+ ? null
671
+ : restrictedKeywordBefore(node, sourceCode);
672
+ const hoisted = keyword
673
+ ? groups.leading.filter(replacementSegments_1.requiresOwnLine)
674
+ : [];
675
+ const segments = [
676
+ ...groups.leading
677
+ .filter((comment) => !hoisted.includes(comment))
678
+ .map(toSegment),
679
+ {
680
+ text: parenthesizeLogical(leftText, node.left),
681
+ breakAfter: false,
682
+ },
683
+ ...groups.beforeOperator.map(toSegment),
684
+ { text: '??', breakAfter: false },
685
+ ...groups.afterOperator.map(toSegment),
686
+ {
687
+ text: parenthesizeLogical(rightText, node.right),
688
+ breakAfter: false,
689
+ },
690
+ ...groups.trailing.map(toSegment),
691
+ ];
692
+ if (selfParens) {
693
+ return fixer.replaceText(node, (0, replacementSegments_1.joinSegments)(segments, indent));
694
+ }
695
+ const body = (0, replacementSegments_1.joinSegmentBody)(segments, indent);
696
+ const trailing = segments[segments.length - 1].breakAfter
697
+ ? `\n${indent}`
698
+ : '';
699
+ const replacement = fixer.replaceText(node, `${body}${trailing}`);
700
+ if (!keyword || hoisted.length === 0) {
701
+ return replacement;
702
+ }
703
+ return [
704
+ fixer.insertTextBefore(keyword, hoisted
705
+ .map((comment) => `${text.slice(comment.range[0], comment.range[1])}\n${indent}`)
706
+ .join('')),
707
+ replacement,
708
+ ];
589
709
  },
590
710
  });
591
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.155",
3
+ "version": "1.20.157",
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.20.157",
4
+ "date": "2026-08-16T06:24:21.626Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-positive-naming",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2025
11
+ ],
12
+ "summary": "treat the inherit/increment/integrate/instantiate/inline families as in- exceptions (closes #2025)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.156",
18
+ "date": "2026-08-15T18:39:54.919Z",
19
+ "rules": [
20
+ {
21
+ "name": "logical-top-to-bottom-grouping",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2023
25
+ ],
26
+ "summary": "separate reordered statements so a relocated one cannot land inside a trailing // comment (closes #2023)"
27
+ },
28
+ {
29
+ "name": "prefer-nullish-coalescing-boolean-props",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 2024
33
+ ],
34
+ "summary": "carry the comments stranded between the operands instead of deleting them (closes #2024)"
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "1.20.155",
4
40
  "date": "2026-08-15T14:21:55.491Z",