@blumintinc/eslint-plugin-blumint 1.20.65 → 1.20.67

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.
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferUnionFromConstArray = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * Matches Prettier's own default. The autofix authors a whole statement that a
8
+ * formatter owns, so a line it emits past this width is rewritten on the next
9
+ * `prettier --write` — and fails `prettier --check` in the meantime.
10
+ */
11
+ const DEFAULT_PRINT_WIDTH = 80;
6
12
  /**
7
13
  * Mechanically derive an UPPER_SNAKE constant base from a PascalCase/camelCase
8
14
  * type name by inserting `_` at every lowercase/digit-to-uppercase boundary and
@@ -43,6 +49,42 @@ function isNameInScope(scope, name) {
43
49
  }
44
50
  return false;
45
51
  }
52
+ /**
53
+ * The file's own nesting step, taken as the most common indentation increase
54
+ * between consecutive lines. Reading it from the source keeps emitted code in
55
+ * the author's units instead of assuming a two-space, space-indented file.
56
+ */
57
+ function indentUnitOf(text) {
58
+ const frequencies = new Map();
59
+ let previous = '';
60
+ for (const line of text.split('\n')) {
61
+ if (line.trim() === '') {
62
+ continue;
63
+ }
64
+ // A block comment's continuation lines align on the `*` one column in from
65
+ // the comment's own indentation. That is comment alignment, not a nesting
66
+ // step, and counting it makes any JSDoc-heavy file look 1-space indented.
67
+ if (line.trimStart().startsWith('*')) {
68
+ continue;
69
+ }
70
+ const match = /^[ \t]*/.exec(line);
71
+ const indent = match ? match[0] : '';
72
+ if (indent.length > previous.length && indent.startsWith(previous)) {
73
+ const delta = indent.slice(previous.length);
74
+ frequencies.set(delta, (frequencies.get(delta) ?? 0) + 1);
75
+ }
76
+ previous = indent;
77
+ }
78
+ let unit = ' ';
79
+ let best = 0;
80
+ for (const [delta, count] of frequencies) {
81
+ if (count > best) {
82
+ unit = delta;
83
+ best = count;
84
+ }
85
+ }
86
+ return unit;
87
+ }
46
88
  exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
47
89
  name: 'prefer-union-from-const-array',
48
90
  meta: {
@@ -52,14 +94,37 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
52
94
  recommended: 'error',
53
95
  },
54
96
  fixable: 'code',
55
- schema: [],
97
+ schema: [
98
+ {
99
+ type: 'object',
100
+ properties: {
101
+ printWidth: {
102
+ type: 'number',
103
+ minimum: 1,
104
+ },
105
+ },
106
+ additionalProperties: false,
107
+ },
108
+ ],
56
109
  messages: {
57
110
  preferDerivedUnion: 'Derive this union from an `as const` array: `const {{constName}} = [...] as const; type {{typeName}} = (typeof {{constName}})[number];`. This tethers the type to importable runtime values and prevents silent drift. Consider also naming each literal as its own exported constant so call sites never repeat magic strings.',
58
111
  },
59
112
  },
60
- defaultOptions: [],
61
- create(context) {
113
+ defaultOptions: [{}],
114
+ create(context, [options]) {
62
115
  const sourceCode = context.getSourceCode();
116
+ const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
117
+ ? options.printWidth
118
+ : DEFAULT_PRINT_WIDTH;
119
+ // Derived once per file rather than per fix: every fix in a file shares the
120
+ // author's nesting step.
121
+ let indentUnit = null;
122
+ const fileIndentUnit = () => {
123
+ if (indentUnit === null) {
124
+ indentUnit = indentUnitOf(sourceCode.getText());
125
+ }
126
+ return indentUnit;
127
+ };
63
128
  return {
64
129
  TSTypeAliasDeclaration(node) {
65
130
  const { typeAnnotation } = node;
@@ -103,12 +168,10 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
103
168
  messageId: 'preferDerivedUnion',
104
169
  data: { constName, typeName },
105
170
  fix(fixer) {
106
- const literals = members
107
- .map((member) => {
171
+ const literals = members.map((member) => {
108
172
  const literal = member.literal;
109
173
  return literal.raw ?? sourceCode.getText(literal);
110
- })
111
- .join(', ');
174
+ });
112
175
  const typeParams = node.typeParameters
113
176
  ? sourceCode.getText(node.typeParameters)
114
177
  : '';
@@ -124,8 +187,39 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
124
187
  .slice(0, fixNode.loc.start.column)
125
188
  .match(/^\s*/);
126
189
  const indent = indentMatch ? indentMatch[0] : '';
127
- const constDecl = `${constKeyword} ${constName} = [${literals}] as const;`;
128
- const typeDecl = `${typeKeyword} ${typeName}${typeParams} = (typeof ${constName})[number];`;
190
+ // A line break the fixer does not add is one Prettier adds later,
191
+ // so the fix must decide the same way Prettier does: keep the
192
+ // statement on one line while it fits, break it open past the print
193
+ // width. Over-wrapping is not the safe direction here — unlike an
194
+ // object literal, an array Prettier considers short enough is
195
+ // collapsed back onto one line, so a blanket wrap would trade this
196
+ // defect for its mirror image on every short union.
197
+ //
198
+ // The emitted const starts where the replaced node starts, so its
199
+ // column is the exact prefix length; the derived alias is emitted on
200
+ // a fresh line at `indent`.
201
+ const constStartColumn = fixNode.loc.start.column;
202
+ const inlineConst = `${constKeyword} ${constName} = [${literals.join(', ')}] as const;`;
203
+ // A literal whose raw text spans lines (a string written with a
204
+ // backslash line continuation) cannot sit on one line at all, so
205
+ // there is no width to compare — such an array always breaks open.
206
+ // Its raw text is emitted verbatim, leaving the interior line break
207
+ // and everything after it exactly where the author put it: that
208
+ // whitespace is string DATA, not layout.
209
+ const hasMultilineLiteral = literals.some((literal) => /[\r\n]/.test(literal));
210
+ const constDecl = !hasMultilineLiteral &&
211
+ constStartColumn + inlineConst.length <= printWidth
212
+ ? inlineConst
213
+ : `${constKeyword} ${constName} = [\n${literals
214
+ .map((literal) => `${indent}${fileIndentUnit()}${literal},`)
215
+ .join('\n')}\n${indent}] as const;`;
216
+ const inlineType = `${typeKeyword} ${typeName}${typeParams} = (typeof ${constName})[number];`;
217
+ // Prettier answers an over-long type alias by breaking after the
218
+ // `=`, the only break point the derived form has.
219
+ const typeDecl = typeParams.includes('\n') ||
220
+ indent.length + inlineType.length <= printWidth
221
+ ? inlineType
222
+ : `${typeKeyword} ${typeName}${typeParams} =\n${indent}${fileIndentUnit()}(typeof ${constName})[number];`;
129
223
  return fixer.replaceText(fixNode, `${constDecl}\n${indent}${typeDecl}`);
130
224
  },
131
225
  });
@@ -1,6 +1,32 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import type { TSESLint } from '@typescript-eslint/utils';
2
3
  import { RuleTester } from 'eslint';
3
4
  export declare const ruleTesterTs: ESLintUtils.RuleTester;
4
5
  export declare const ruleTesterJsx: ESLintUtils.RuleTester;
5
6
  export declare const ruleTesterJson: RuleTester;
6
7
  export declare const ruleTesterMarkdown: RuleTester;
8
+ type ParserOptionsCarrier = {
9
+ parserOptions?: TSESLint.ParserOptions;
10
+ };
11
+ /**
12
+ * Applies `parserOptions` to every case of a suite.
13
+ *
14
+ * The instances above are the only RuleTester instances the suite may use:
15
+ * corpus-harvesting probes collect cases by stubbing `run` on these exports, so
16
+ * a test file that constructs its own tester is invisible to every one of them
17
+ * and its rule silently drops out of the fixer, convergence, collision and
18
+ * crash sweeps. `src/tests/no-local-rule-tester.test.ts` enforces that.
19
+ *
20
+ * A file needing parser options the shared instances do not declare — ES module
21
+ * scope analysis, a type-aware `project` — attaches them per case rather than
22
+ * forking a tester. RuleTester deep merges each case's config over the tester's,
23
+ * and options a case declares itself still win over the ones applied here, so a
24
+ * case keeps any override it already had. Carrying the options on the case also
25
+ * means the harvested corpus records the parser configuration a snippet was
26
+ * written for, which a tester-level setting never exposes to a probe.
27
+ */
28
+ export declare const withParserOptions: <T extends string | ParserOptionsCarrier>(parserOptions: TSESLint.ParserOptions, cases: readonly T[]) => (Exclude<T, string> | {
29
+ code: string;
30
+ parserOptions: TSESLint.ParserOptions;
31
+ })[];
32
+ export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ruleTesterMarkdown = exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
3
+ exports.withParserOptions = exports.ruleTesterMarkdown = exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const eslint_1 = require("eslint");
6
6
  exports.ruleTesterTs = new utils_1.ESLintUtils.RuleTester({
@@ -23,4 +23,31 @@ exports.ruleTesterJson = new eslint_1.RuleTester({
23
23
  exports.ruleTesterMarkdown = new eslint_1.RuleTester({
24
24
  parser: require.resolve('markdown-eslint-parser'),
25
25
  });
26
+ /**
27
+ * Applies `parserOptions` to every case of a suite.
28
+ *
29
+ * The instances above are the only RuleTester instances the suite may use:
30
+ * corpus-harvesting probes collect cases by stubbing `run` on these exports, so
31
+ * a test file that constructs its own tester is invisible to every one of them
32
+ * and its rule silently drops out of the fixer, convergence, collision and
33
+ * crash sweeps. `src/tests/no-local-rule-tester.test.ts` enforces that.
34
+ *
35
+ * A file needing parser options the shared instances do not declare — ES module
36
+ * scope analysis, a type-aware `project` — attaches them per case rather than
37
+ * forking a tester. RuleTester deep merges each case's config over the tester's,
38
+ * and options a case declares itself still win over the ones applied here, so a
39
+ * case keeps any override it already had. Carrying the options on the case also
40
+ * means the harvested corpus records the parser configuration a snippet was
41
+ * written for, which a tester-level setting never exposes to a probe.
42
+ */
43
+ const withParserOptions = (parserOptions, cases) => cases.map((testCase) => typeof testCase === 'string'
44
+ ? { code: testCase, parserOptions }
45
+ : {
46
+ ...testCase,
47
+ parserOptions: {
48
+ ...parserOptions,
49
+ ...testCase.parserOptions,
50
+ },
51
+ });
52
+ exports.withParserOptions = withParserOptions;
26
53
  //# sourceMappingURL=ruleTester.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.65",
3
+ "version": "1.20.67",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,89 @@
1
1
  [
2
+ {
3
+ "version": "1.20.67",
4
+ "date": "2026-08-01T17:17:30.817Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-microdiff",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1571
11
+ ],
12
+ "summary": "guard the zero-argument JSON.stringify comparison (closes #1571)"
13
+ },
14
+ {
15
+ "name": "memo-nested-react-components",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1567
19
+ ],
20
+ "summary": "exempt lowercase-initial render-prop callbacks (closes #1567)"
21
+ },
22
+ {
23
+ "name": "no-compositing-layer-props",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1570
27
+ ],
28
+ "summary": "exempt mix-blend-mode's non-promoting values (closes #1570)"
29
+ },
30
+ {
31
+ "name": "no-jsx-in-hooks",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1572
35
+ ],
36
+ "summary": "guard the zero-argument .map() inside useMemo (closes #1572)"
37
+ },
38
+ {
39
+ "name": "optimize-object-boolean-conditions",
40
+ "changeType": "fix",
41
+ "issues": [
42
+ 1569
43
+ ],
44
+ "summary": "stop calling boolean-prefixed and primitive identifiers objects (closes #1569)"
45
+ },
46
+ {
47
+ "name": "prefer-getter-over-parameterless-method",
48
+ "changeType": "fix",
49
+ "issues": [
50
+ 1568
51
+ ],
52
+ "summary": "prescribe a remedy that clears preferGetterSideEffect (closes #1568)"
53
+ },
54
+ {
55
+ "name": "prefer-sx-prop-over-system-props",
56
+ "changeType": "fix",
57
+ "issues": [
58
+ 1565,
59
+ 1574
60
+ ],
61
+ "summary": "ignore block-comment lines when inferring the indent unit (closes #1574); wrap emitted sx object when it overflows the print width (closes #1565)"
62
+ },
63
+ {
64
+ "name": "prefer-union-from-const-array",
65
+ "changeType": "fix",
66
+ "issues": [
67
+ 1566
68
+ ],
69
+ "summary": "wrap the authored as-const array when it exceeds the print width (closes #1566)"
70
+ }
71
+ ]
72
+ },
73
+ {
74
+ "version": "1.20.66",
75
+ "date": "2026-08-01T13:53:31.680Z",
76
+ "rules": [
77
+ {
78
+ "name": "enforce-firestore-doc-ref-generic",
79
+ "changeType": "fix",
80
+ "issues": [
81
+ 1564
82
+ ],
83
+ "summary": "trace compat receivers through annotated lets and seeding callbacks (closes #1564)"
84
+ }
85
+ ]
86
+ },
2
87
  {
3
88
  "version": "1.20.65",
4
89
  "date": "2026-08-01T13:35:40.762Z",