@blumintinc/eslint-plugin-blumint 1.20.190 → 1.20.192
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
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 (
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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 <
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 =
|
|
172
|
+
index = closing.line + 1;
|
|
95
173
|
}
|
|
96
174
|
},
|
|
97
175
|
};
|
|
@@ -1278,19 +1278,63 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1278
1278
|
return variableByReference.get(identifier) ?? null;
|
|
1279
1279
|
}
|
|
1280
1280
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
* binding with no reader left in the file.
|
|
1281
|
+
* Every identifier that a hook dependency array holds as an entry.
|
|
1283
1282
|
*
|
|
1284
|
-
* why:
|
|
1283
|
+
* why: an entry is a recompute trigger rather than a consumer of the value,
|
|
1284
|
+
* so it cannot keep a declaration alive once the entries are pruned. The
|
|
1285
|
+
* set is built from the rule's own notion of a dependency array — the last
|
|
1286
|
+
* argument of a hook call — so it can never disagree with what the fixer
|
|
1287
|
+
* edits. Keying on the identifier NODE, rather than on ancestry walked from
|
|
1288
|
+
* a reference, keeps the answer available for entries the traversal has not
|
|
1289
|
+
* reached yet, which is precisely the later-hook entry that #2210 turns on.
|
|
1290
|
+
*/
|
|
1291
|
+
let dependencyEntryIdentifiers = null;
|
|
1292
|
+
function collectDependencyEntryIdentifiers() {
|
|
1293
|
+
if (dependencyEntryIdentifiers) {
|
|
1294
|
+
return dependencyEntryIdentifiers;
|
|
1295
|
+
}
|
|
1296
|
+
const entries = new Set();
|
|
1297
|
+
const visit = (node) => {
|
|
1298
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression && isHookCall(node)) {
|
|
1299
|
+
const deps = node.arguments[node.arguments.length - 1];
|
|
1300
|
+
if (deps && deps.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
1301
|
+
for (const entry of deps.elements) {
|
|
1302
|
+
if (entry)
|
|
1303
|
+
entries.add(unwrapExpression(entry));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
forEachChildNode(node, visit);
|
|
1308
|
+
};
|
|
1309
|
+
visit(sourceCode.ast);
|
|
1310
|
+
dependencyEntryIdentifiers = entries;
|
|
1311
|
+
return entries;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Whether removing `element`'s binding from every dependency array that
|
|
1315
|
+
* lists it would leave the binding with no reader in the file.
|
|
1316
|
+
*
|
|
1317
|
+
* why: a value declared and then read ONLY inside dependency arrays is by
|
|
1285
1318
|
* construction load-bearing — the declaration would be pointless otherwise
|
|
1286
|
-
* — so removing
|
|
1287
|
-
* strands the declaration. The consumer runs
|
|
1288
|
-
* and builds with `noUnusedLocals`, so the
|
|
1289
|
-
* on their machine while staying green here.
|
|
1290
|
-
* class was fixed by deleting the stranded
|
|
1291
|
-
* is unavailable here: the declaration is
|
|
1292
|
-
* changes the component's hook order.
|
|
1293
|
-
* remedy, so the report stands and the
|
|
1319
|
+
* — so removing any one entry discards a deliberate recompute trigger, and
|
|
1320
|
+
* removing all of them strands the declaration. The consumer runs
|
|
1321
|
+
* `no-unused-vars` as an error and builds with `noUnusedLocals`, so the
|
|
1322
|
+
* rewrite turns a green file red on their machine while staying green here.
|
|
1323
|
+
* Every sibling instance of this class was fixed by deleting the stranded
|
|
1324
|
+
* declaration too, but that remedy is unavailable here: the declaration is
|
|
1325
|
+
* a hook CALL, and dropping it changes the component's hook order.
|
|
1326
|
+
* Declining the edit is the only safe remedy, so the report stands and the
|
|
1327
|
+
* autofix steps aside.
|
|
1328
|
+
*
|
|
1329
|
+
* The verdict deliberately ignores which entry is under repair. A test
|
|
1330
|
+
* scoped to one report's own range calls a sibling array's entry a
|
|
1331
|
+
* survivor, and when two hooks list the same unread dependency both reports
|
|
1332
|
+
* reach that conclusion, both fixes apply in one pass, and the binding is
|
|
1333
|
+
* stranded after all (#2210). Discounting every entry instead gates on a
|
|
1334
|
+
* property this fixer owns: no other fixer can manufacture a
|
|
1335
|
+
* non-dependency read, so no sibling edit can move the answer. The
|
|
1336
|
+
* inference is safe in the conservative direction — if a sibling report is
|
|
1337
|
+
* suppressed the cost is a withheld fix, never a dangling reference.
|
|
1294
1338
|
*
|
|
1295
1339
|
* Parameters are deliberately exempt. An unread parameter is not an unused
|
|
1296
1340
|
* BINDING to either instrument — `no-unused-vars` runs with `args: 'none'`
|
|
@@ -1308,16 +1352,12 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
1308
1352
|
if (variable.defs.some((def) => def.type === 'Parameter')) {
|
|
1309
1353
|
return false;
|
|
1310
1354
|
}
|
|
1311
|
-
const
|
|
1355
|
+
const entries = collectDependencyEntryIdentifiers();
|
|
1312
1356
|
// why: a declarator's own initializer counts as a WRITE reference, so a
|
|
1313
1357
|
// survivor test that accepts any reference never fires for `const x = …`
|
|
1314
1358
|
// — the shape this check exists for (#1868 is the same trap).
|
|
1315
|
-
return !variable.references.some((reference) =>
|
|
1316
|
-
|
|
1317
|
-
return false;
|
|
1318
|
-
const [from, to] = reference.identifier.range;
|
|
1319
|
-
return from < start || to > end;
|
|
1320
|
-
});
|
|
1359
|
+
return !variable.references.some((reference) => reference.isRead() &&
|
|
1360
|
+
!entries.has(reference.identifier));
|
|
1321
1361
|
}
|
|
1322
1362
|
// why: scanning every comment once per file rather than once per hook call
|
|
1323
1363
|
// keeps the check off the hot path of files with many hooks.
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.192",
|
|
4
|
+
"date": "2026-08-30T01:20:08.891Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-typescript-markdown-code-blocks",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2213
|
|
11
|
+
],
|
|
12
|
+
"summary": "never write inside a declined block (closes #2213)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.191",
|
|
18
|
+
"date": "2026-08-29T19:38:08.123Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-entire-object-hook-deps",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2210
|
|
25
|
+
],
|
|
26
|
+
"summary": "discount every dependency entry from the strand check (closes #2210)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.190",
|
|
4
32
|
"date": "2026-08-29T13:41:20.482Z",
|