@blumintinc/eslint-plugin-blumint 1.20.191 → 1.20.193

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.191',
226
+ version: '1.20.193',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -423,6 +423,19 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
423
423
  // breakage.
424
424
  if (node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier)
425
425
  return;
426
+ // The same unwritable-remedy shape, one key form over: `Memoize`
427
+ // declares its `propertyKey` parameter as `string`, so decorating a
428
+ // symbol-keyed getter — `get [Symbol.iterator]()` — is TS1241, and the
429
+ // `--fix` edit turns compiling code into code that does not compile.
430
+ // A computed key that IS a string literal (`get ['name']()`) is an
431
+ // ordinary string key and stays in scope; anything else cannot be shown
432
+ // to be a string without the checker, and a false negative is the
433
+ // cheaper error here.
434
+ if (node.computed &&
435
+ !(node.key.type === utils_1.AST_NODE_TYPES.Literal &&
436
+ typeof node.key.value === 'string')) {
437
+ return;
438
+ }
426
439
  const classBody = node.parent;
427
440
  // The same reasoning, one level out: under `experimentalDecorators` a
428
441
  // decorator is rejected on EVERY member of a class EXPRESSION — TS1206
@@ -2,32 +2,104 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceTypescriptMarkdownCodeBlocks = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
- const FENCE = '```';
6
- function isFenceLine(text, fenceIndex, indent) {
7
- const lineEnd = text.indexOf('\n', fenceIndex);
8
- const afterFence = lineEnd === -1
9
- ? text.slice(fenceIndex + FENCE.length)
10
- : text.slice(fenceIndex + FENCE.length, lineEnd);
11
- return (afterFence.trim().length === 0 &&
12
- text.slice(text.lastIndexOf('\n', fenceIndex - 1) + 1, fenceIndex) ===
13
- indent);
5
+ const BACKTICK = '`';
6
+ const TILDE = '~';
7
+ /** Only a run of exactly three backticks is labelable; longer runs are declined. */
8
+ const LABELABLE_FENCE_LENGTH = 3;
9
+ /** CommonMark opens a fenced block on a run of three or more of either marker. */
10
+ const MIN_FENCE_LENGTH = 3;
11
+ /**
12
+ * CommonMark allows a fence to be indented at most three columns. At four or
13
+ * more the line opens an indented code block, so its backticks are literal
14
+ * document content that must never be rewritten.
15
+ */
16
+ const MAX_FENCE_INDENT_COLUMNS = 3;
17
+ /** CommonMark advances a tab to the next multiple of four when measuring indent. */
18
+ const TAB_STOP = 4;
19
+ function splitLines(text) {
20
+ const lines = [];
21
+ let start = 0;
22
+ for (;;) {
23
+ const terminator = text.indexOf('\n', start);
24
+ const end = terminator === -1 ? text.length : terminator;
25
+ lines.push({ start, end, text: text.slice(start, end) });
26
+ if (terminator === -1) {
27
+ return lines;
28
+ }
29
+ start = terminator + 1;
30
+ }
14
31
  }
15
- function findClosingFence(text, startIndex, indent) {
16
- let searchIndex = startIndex;
17
- while (searchIndex < text.length) {
18
- const candidate = text.indexOf(FENCE, searchIndex);
19
- if (candidate === -1) {
20
- return null;
32
+ /**
33
+ * Reads the fence a line opens, or null when the line cannot open one.
34
+ * Indentation is measured in columns rather than characters so that a tab is
35
+ * treated as the four-column indent CommonMark says it is.
36
+ */
37
+ function readFence(line) {
38
+ let indentColumns = 0;
39
+ let offset = 0;
40
+ while (offset < line.text.length) {
41
+ const char = line.text[offset];
42
+ if (char === ' ') {
43
+ indentColumns += 1;
21
44
  }
22
- if (isFenceLine(text, candidate, indent)) {
23
- return candidate;
45
+ else if (char === '\t') {
46
+ indentColumns += TAB_STOP - (indentColumns % TAB_STOP);
47
+ }
48
+ else {
49
+ break;
50
+ }
51
+ offset += 1;
52
+ }
53
+ if (indentColumns > MAX_FENCE_INDENT_COLUMNS) {
54
+ return null;
55
+ }
56
+ const marker = line.text[offset];
57
+ if (marker !== BACKTICK && marker !== TILDE) {
58
+ return null;
59
+ }
60
+ let runLength = 0;
61
+ while (line.text[offset + runLength] === marker) {
62
+ runLength += 1;
63
+ }
64
+ if (runLength < MIN_FENCE_LENGTH) {
65
+ return null;
66
+ }
67
+ return {
68
+ runStart: line.start + offset,
69
+ marker,
70
+ runLength,
71
+ indent: line.text.slice(0, offset),
72
+ infoString: line.text.slice(offset + runLength),
73
+ };
74
+ }
75
+ /**
76
+ * The line a block closes on, per CommonMark: a run of at least the opening
77
+ * length, of the SAME marker, at a fence indent, with nothing but whitespace
78
+ * after it. This locates the block's END, which is a separate question from
79
+ * whether the rule is willing to LABEL it.
80
+ */
81
+ function findFenceCloser(lines, fromLine, opener) {
82
+ for (let index = fromLine; index < lines.length; index++) {
83
+ const fence = readFence(lines[index]);
84
+ if (fence !== null &&
85
+ fence.marker === opener.marker &&
86
+ fence.runLength >= opener.runLength &&
87
+ fence.infoString.trim().length === 0) {
88
+ return { line: index, fence };
24
89
  }
25
- searchIndex = candidate + FENCE.length;
26
90
  }
27
91
  return null;
28
92
  }
29
- function isIndentOnly(value) {
30
- return /^[\t ]*$/.test(value);
93
+ /**
94
+ * The rule labels only a block it can delimit exactly: three backticks closed
95
+ * by three backticks at the same indent. A longer closing run or a differently
96
+ * indented one is left unlabeled by design — but the block is still SKIPPED
97
+ * whole, because a block the rule declines to label is a block it must not
98
+ * read.
99
+ */
100
+ function isExactlyDelimited(opener, closer) {
101
+ return (closer.runLength === LABELABLE_FENCE_LENGTH &&
102
+ closer.indent === opener.indent);
31
103
  }
32
104
  exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
33
105
  name: 'enforce-typescript-markdown-code-blocks',
@@ -53,45 +125,51 @@ exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
53
125
  Program() {
54
126
  const sourceCode = context.sourceCode;
55
127
  const text = sourceCode.getText();
128
+ const lines = splitLines(text);
56
129
  let index = 0;
57
- while (index < text.length) {
58
- const openingFence = text.indexOf(FENCE, index);
59
- if (openingFence === -1) {
60
- break;
61
- }
62
- const lineStart = text.lastIndexOf('\n', openingFence - 1) + 1;
63
- const indent = text.slice(lineStart, openingFence);
64
- if (!isIndentOnly(indent)) {
65
- index = openingFence + FENCE.length;
130
+ while (index < lines.length) {
131
+ const openingLine = lines[index];
132
+ const fence = readFence(openingLine);
133
+ if (fence === null) {
134
+ index += 1;
66
135
  continue;
67
136
  }
68
- const lineEnd = text.indexOf('\n', openingFence + FENCE.length);
69
- if (lineEnd === -1) {
70
- break;
71
- }
72
- const infoString = text.slice(openingFence + FENCE.length, lineEnd);
73
- const closingFence = findClosingFence(text, lineEnd + 1, indent);
74
- if (closingFence === null) {
75
- index = lineEnd + 1;
76
- continue;
137
+ const closing = findFenceCloser(lines, index + 1, fence);
138
+ // An unclosed fence runs to the end of the file, so everything after
139
+ // it is the block's literal content and there is nothing left to
140
+ // scan. Resuming on the next line would read the block's interior.
141
+ if (closing === null) {
142
+ return;
77
143
  }
78
- const content = text.slice(lineEnd + 1, closingFence);
79
- const hasContent = content.trim().length > 0;
80
- const hasLanguage = infoString.trim().length > 0;
81
- if (!hasLanguage && hasContent) {
82
- const locStart = sourceCode.getLocFromIndex(openingFence);
83
- const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
84
- context.report({
85
- loc: {
86
- start: locStart,
87
- end: sourceCode.getLocFromIndex(lineEnd),
88
- },
89
- messageId: 'missingLanguageSpecifier',
90
- data: { line: locStart.line },
91
- fix: (fixer) => fixer.replaceTextRange([openingFence + FENCE.length, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
92
- });
144
+ // A tilde fence and a run of four or more backticks are blocks this
145
+ // rule declines to label, and declining to label a block means
146
+ // declining to read it: its interior is literal text, triple
147
+ // backticks included. So is the interior of a triple-backtick block
148
+ // this rule cannot delimit exactly. Every one of them is skipped from
149
+ // its opening line to past its closing line.
150
+ const labelable = fence.marker === BACKTICK &&
151
+ fence.runLength === LABELABLE_FENCE_LENGTH &&
152
+ isExactlyDelimited(fence, closing.fence);
153
+ if (labelable) {
154
+ const content = text.slice(openingLine.end + 1, lines[closing.line].start);
155
+ const hasContent = content.trim().length > 0;
156
+ const hasLanguage = fence.infoString.trim().length > 0;
157
+ if (!hasLanguage && hasContent) {
158
+ const lineEnd = openingLine.end;
159
+ const locStart = sourceCode.getLocFromIndex(fence.runStart);
160
+ const hasCarriageReturn = lineEnd > 0 && text[lineEnd - 1] === '\r';
161
+ context.report({
162
+ loc: {
163
+ start: locStart,
164
+ end: sourceCode.getLocFromIndex(lineEnd),
165
+ },
166
+ messageId: 'missingLanguageSpecifier',
167
+ data: { line: locStart.line },
168
+ fix: (fixer) => fixer.replaceTextRange([fence.runStart + LABELABLE_FENCE_LENGTH, lineEnd], hasCarriageReturn ? 'typescript\r' : 'typescript'),
169
+ });
170
+ }
93
171
  }
94
- index = closingFence + FENCE.length;
172
+ index = closing.line + 1;
95
173
  }
96
174
  },
97
175
  };
@@ -4,6 +4,6 @@ type Options = [
4
4
  functionPatterns?: string[];
5
5
  }
6
6
  ];
7
- type MessageIds = 'noDirectFunctionState' | 'noDirectFunctionStateAssertion';
7
+ type MessageIds = 'noDirectFunctionState' | 'noDirectFunctionStateAssertion' | 'invalidFunctionPattern';
8
8
  export declare const noDirectFunctionState: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
9
9
  export {};
@@ -198,22 +198,33 @@ function isDefinitelySafeArg(argNode) {
198
198
  }
199
199
  }
200
200
  /**
201
- * Checks whether an identifier name matches any of the function-naming patterns
202
- * (e.g. onClose, handler, fn, callback).
201
+ * Compiles the configured function-naming patterns once, separating the ones
202
+ * that do not compile from the ones that do.
203
+ *
204
+ * Swallowing an uncompilable pattern makes the consumer's allowlist silently
205
+ * inert: the rule then reports the very code they wrote the pattern to exclude,
206
+ * with nothing anywhere saying why. Returning the rejects lets `create` report
207
+ * them, which is what the sibling pattern-compiling rules already do.
203
208
  */
204
- function matchesFunctionPattern(name, patterns) {
209
+ function compileFunctionPatterns(patterns) {
210
+ const matchers = [];
211
+ const invalid = [];
205
212
  for (const pattern of patterns) {
206
213
  try {
207
- const regex = new RegExp(`^${pattern}$`);
208
- if (regex.test(name)) {
209
- return true;
210
- }
214
+ matchers.push(new RegExp(`^${pattern}$`));
211
215
  }
212
216
  catch {
213
- // Ignore invalid regex patterns
217
+ invalid.push(pattern);
214
218
  }
215
219
  }
216
- return false;
220
+ return { matchers, invalid };
221
+ }
222
+ /**
223
+ * Checks whether an identifier name matches any of the function-naming patterns
224
+ * (e.g. onClose, handler, fn, callback).
225
+ */
226
+ function matchesFunctionPattern(name, matchers) {
227
+ return matchers.some((matcher) => matcher.test(name));
217
228
  }
218
229
  /**
219
230
  * Extracts the identifier name from an argument node for pattern matching.
@@ -296,12 +307,16 @@ exports.noDirectFunctionState = (0, createRule_1.createRule)({
296
307
  'Why it matters: The function will be called with the previous state value and its return value stored — a silent bug with no error. ' +
297
308
  'How to fix: Give the asserted value a name, then store that name through a thunk: const value = {{argText}}; {{setterName}}(() => value). ' +
298
309
  'The assertion is hoisted out because a thunk that returned it would be an arrow returning a cast, which no-type-assertion-returns reports.',
310
+ invalidFunctionPattern: 'What\u2019s wrong: "{{pattern}}" in functionPatterns is not a valid regular expression, so it was dropped. ' +
311
+ 'Why it matters: the rule silently stops honouring that entry, and reports the very code the pattern was written to exclude. ' +
312
+ 'How to fix: correct the pattern in your ESLint configuration.',
299
313
  },
300
314
  },
301
315
  defaultOptions: [{ functionPatterns: DEFAULT_FUNCTION_PATTERNS }],
302
316
  create(context) {
303
317
  const options = context.options[0] ?? {};
304
318
  const functionPatterns = options.functionPatterns ?? DEFAULT_FUNCTION_PATTERNS;
319
+ const { matchers: functionPatternMatchers, invalid: invalidPatterns } = compileFunctionPatterns(functionPatterns);
305
320
  /**
306
321
  * Maps setter-variable names to whether the corresponding useState has
307
322
  * an explicit function type parameter. This is populated as we encounter
@@ -309,6 +324,15 @@ exports.noDirectFunctionState = (0, createRule_1.createRule)({
309
324
  */
310
325
  const setterFunctionTyped = new Map();
311
326
  return {
327
+ Program(node) {
328
+ for (const pattern of invalidPatterns) {
329
+ context.report({
330
+ node,
331
+ messageId: 'invalidFunctionPattern',
332
+ data: { pattern },
333
+ });
334
+ }
335
+ },
312
336
  VariableDeclarator(node) {
313
337
  // Look for `const [state, setter] = useState<T>(...)` or
314
338
  // `const [state, setter] = React.useState<T>(...)`.
@@ -383,7 +407,8 @@ exports.noDirectFunctionState = (0, createRule_1.createRule)({
383
407
  // No explicit function type. Fall back to heuristic: name pattern match
384
408
  // or scope-level binding to a function.
385
409
  const argName = getArgName(arg);
386
- if (argName && matchesFunctionPattern(argName, functionPatterns)) {
410
+ if (argName &&
411
+ matchesFunctionPattern(argName, functionPatternMatchers)) {
387
412
  reportAndFix(node, arg, setterName, context);
388
413
  return;
389
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.191",
3
+ "version": "1.20.193",
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.193",
4
+ "date": "2026-08-30T05:32:04.963Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-memoize-getters",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2215
11
+ ],
12
+ "summary": "decline a getter whose computed key is not a string literal"
13
+ },
14
+ {
15
+ "name": "no-direct-function-state",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 2218
19
+ ],
20
+ "summary": "report an uncompilable functionPatterns entry instead of dropping it (closes #2218)"
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ "version": "1.20.192",
26
+ "date": "2026-08-30T01:20:08.891Z",
27
+ "rules": [
28
+ {
29
+ "name": "enforce-typescript-markdown-code-blocks",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 2213
33
+ ],
34
+ "summary": "never write inside a declined block (closes #2213)"
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "1.20.191",
4
40
  "date": "2026-08-29T19:38:08.123Z",