@blumintinc/eslint-plugin-blumint 1.20.192 → 1.20.194
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 +1 -1
- package/lib/rules/enforce-memoize-getters.js +13 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +119 -1
- package/lib/rules/no-direct-function-state.d.ts +1 -1
- package/lib/rules/no-direct-function-state.js +35 -10
- package/package.json +1 -1
- package/release-manifest.json +36 -0
package/lib/index.js
CHANGED
|
@@ -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
|
|
@@ -16,6 +16,10 @@ const MIN_FENCE_LENGTH = 3;
|
|
|
16
16
|
const MAX_FENCE_INDENT_COLUMNS = 3;
|
|
17
17
|
/** CommonMark advances a tab to the next multiple of four when measuring indent. */
|
|
18
18
|
const TAB_STOP = 4;
|
|
19
|
+
/** The three bullet characters CommonMark accepts for an unordered list item. */
|
|
20
|
+
const BULLET_MARKERS = new Set(['-', '+', '*']);
|
|
21
|
+
/** CommonMark caps an ordered list marker at nine digits before its delimiter. */
|
|
22
|
+
const MAX_ORDERED_MARKER_DIGITS = 9;
|
|
19
23
|
function splitLines(text) {
|
|
20
24
|
const lines = [];
|
|
21
25
|
let start = 0;
|
|
@@ -72,6 +76,106 @@ function readFence(line) {
|
|
|
72
76
|
infoString: line.text.slice(offset + runLength),
|
|
73
77
|
};
|
|
74
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads the list marker at `offset`, including the whitespace that separates
|
|
81
|
+
* it from the item's content, or 0 when no marker starts there. CommonMark
|
|
82
|
+
* requires that separator, which is what keeps `*emphasis*` and the thematic
|
|
83
|
+
* break `***` from reading as list items.
|
|
84
|
+
*/
|
|
85
|
+
function readListMarker(text, offset) {
|
|
86
|
+
let cursor = offset;
|
|
87
|
+
const char = text[cursor];
|
|
88
|
+
if (char !== undefined && BULLET_MARKERS.has(char)) {
|
|
89
|
+
cursor += 1;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
let digits = 0;
|
|
93
|
+
while (digits < MAX_ORDERED_MARKER_DIGITS &&
|
|
94
|
+
text[cursor + digits] >= '0' &&
|
|
95
|
+
text[cursor + digits] <= '9') {
|
|
96
|
+
digits += 1;
|
|
97
|
+
}
|
|
98
|
+
const delimiter = text[cursor + digits];
|
|
99
|
+
if (digits === 0 || (delimiter !== '.' && delimiter !== ')')) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
cursor += digits + 1;
|
|
103
|
+
}
|
|
104
|
+
if (text[cursor] !== ' ' && text[cursor] !== '\t') {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
while (text[cursor] === ' ' || text[cursor] === '\t') {
|
|
108
|
+
cursor += 1;
|
|
109
|
+
}
|
|
110
|
+
return cursor - offset;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Reads a fence opened on the same line as one or more list markers, as in
|
|
114
|
+
* "- ```ts". A list item's content begins after its marker, so the backticks
|
|
115
|
+
* open a block there exactly as they would at the head of a line.
|
|
116
|
+
*
|
|
117
|
+
* `readFence` cannot see one, because it stops at the first character that is
|
|
118
|
+
* neither a space nor a tab. Without this the scanner walks INTO such a block
|
|
119
|
+
* and mistakes its closing fence — which is nothing but spaces and backticks —
|
|
120
|
+
* for an opening one, appending a language to literal document content.
|
|
121
|
+
*
|
|
122
|
+
* The block is skipped whole rather than labeled: a fix would have to be
|
|
123
|
+
* written past the marker, and labeling a block this rule cannot delimit at
|
|
124
|
+
* the head of a line is the false-negative direction it deliberately keeps.
|
|
125
|
+
*/
|
|
126
|
+
function readListItemFence(line) {
|
|
127
|
+
let indentColumns = 0;
|
|
128
|
+
let offset = 0;
|
|
129
|
+
let sawMarker = false;
|
|
130
|
+
for (;;) {
|
|
131
|
+
while (offset < line.text.length) {
|
|
132
|
+
const char = line.text[offset];
|
|
133
|
+
if (char === ' ') {
|
|
134
|
+
indentColumns += 1;
|
|
135
|
+
}
|
|
136
|
+
else if (char === '\t') {
|
|
137
|
+
indentColumns += TAB_STOP - (indentColumns % TAB_STOP);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
offset += 1;
|
|
143
|
+
}
|
|
144
|
+
if (indentColumns > MAX_FENCE_INDENT_COLUMNS) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const markerLength = readListMarker(line.text, offset);
|
|
148
|
+
if (markerLength === 0) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
offset += markerLength;
|
|
152
|
+
// The item's content starts a fresh indent budget, so a fence sitting at
|
|
153
|
+
// the content column is at indent zero however deep the nesting is.
|
|
154
|
+
indentColumns = 0;
|
|
155
|
+
sawMarker = true;
|
|
156
|
+
}
|
|
157
|
+
if (!sawMarker) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const marker = line.text[offset];
|
|
161
|
+
if (marker !== BACKTICK && marker !== TILDE) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
let runLength = 0;
|
|
165
|
+
while (line.text[offset + runLength] === marker) {
|
|
166
|
+
runLength += 1;
|
|
167
|
+
}
|
|
168
|
+
if (runLength < MIN_FENCE_LENGTH) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
runStart: line.start + offset,
|
|
173
|
+
marker,
|
|
174
|
+
runLength,
|
|
175
|
+
indent: line.text.slice(0, offset),
|
|
176
|
+
infoString: line.text.slice(offset + runLength),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
75
179
|
/**
|
|
76
180
|
* The line a block closes on, per CommonMark: a run of at least the opening
|
|
77
181
|
* length, of the SAME marker, at a fence indent, with nothing but whitespace
|
|
@@ -131,7 +235,21 @@ exports.enforceTypescriptMarkdownCodeBlocks = (0, createRule_1.createRule)({
|
|
|
131
235
|
const openingLine = lines[index];
|
|
132
236
|
const fence = readFence(openingLine);
|
|
133
237
|
if (fence === null) {
|
|
134
|
-
|
|
238
|
+
const listFence = readListItemFence(openingLine);
|
|
239
|
+
if (listFence === null) {
|
|
240
|
+
index += 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// A fence opened on a list marker's line is one this rule declines
|
|
244
|
+
// to label, and declining to label a block means declining to read
|
|
245
|
+
// it. Skipping it whole is what keeps its closing fence — spaces
|
|
246
|
+
// and backticks, indistinguishable from an opener — out of the
|
|
247
|
+
// walk.
|
|
248
|
+
const listClosing = findFenceCloser(lines, index + 1, listFence);
|
|
249
|
+
if (listClosing === null) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
index = listClosing.line + 1;
|
|
135
253
|
continue;
|
|
136
254
|
}
|
|
137
255
|
const closing = findFenceCloser(lines, index + 1, fence);
|
|
@@ -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
|
-
*
|
|
202
|
-
*
|
|
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
|
|
209
|
+
function compileFunctionPatterns(patterns) {
|
|
210
|
+
const matchers = [];
|
|
211
|
+
const invalid = [];
|
|
205
212
|
for (const pattern of patterns) {
|
|
206
213
|
try {
|
|
207
|
-
|
|
208
|
-
if (regex.test(name)) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
214
|
+
matchers.push(new RegExp(`^${pattern}$`));
|
|
211
215
|
}
|
|
212
216
|
catch {
|
|
213
|
-
|
|
217
|
+
invalid.push(pattern);
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
|
-
return
|
|
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 &&
|
|
410
|
+
if (argName &&
|
|
411
|
+
matchesFunctionPattern(argName, functionPatternMatchers)) {
|
|
387
412
|
reportAndFix(node, arg, setterName, context);
|
|
388
413
|
return;
|
|
389
414
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.194",
|
|
4
|
+
"date": "2026-08-30T09:11:52.116Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-typescript-markdown-code-blocks",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2220
|
|
11
|
+
],
|
|
12
|
+
"summary": "skip a fence opened on a list marker's line (closes #2220)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.193",
|
|
18
|
+
"date": "2026-08-30T05:32:04.963Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-memoize-getters",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2215
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline a getter whose computed key is not a string literal"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "no-direct-function-state",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2218
|
|
33
|
+
],
|
|
34
|
+
"summary": "report an uncompilable functionPatterns entry instead of dropping it (closes #2218)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.192",
|
|
4
40
|
"date": "2026-08-30T01:20:08.891Z",
|