@blumintinc/eslint-plugin-blumint 1.20.153 → 1.20.154
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
|
@@ -1127,9 +1127,54 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1127
1127
|
return variable.defs.every((definition) => definition.name.range[0] >= root.range[0] &&
|
|
1128
1128
|
definition.name.range[1] <= root.range[1]);
|
|
1129
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Records the instance slot a method call MUTATES THROUGH ITS RECEIVER.
|
|
1132
|
+
* `this.accumulated.set(doc, 1)` publishes to `this.accumulated` exactly as
|
|
1133
|
+
* `this.accumulated = next` does, but it is a CallExpression rather than an
|
|
1134
|
+
* assignment, so the assignment-shaped visit below never sees it. A sweep
|
|
1135
|
+
* that fills an accumulator through the accumulator's own API then reads as
|
|
1136
|
+
* writing nothing, a later await that reads that slot is classified
|
|
1137
|
+
* independent, and the rewrite runs the read against the still-empty
|
|
1138
|
+
* accumulator. (#2017)
|
|
1139
|
+
*
|
|
1140
|
+
* ANY method invoked on the slot counts, rather than a list of known
|
|
1141
|
+
* mutators. The receiver is already the unit barrier 7 treats as ordered,
|
|
1142
|
+
* and a domain `append`/`record`/`write` mutates its receiver exactly as
|
|
1143
|
+
* `set` does, so naming a subset would leave the same silent reorder
|
|
1144
|
+
* reachable under a different spelling. Over-recording a pure
|
|
1145
|
+
* `this.cache.size()` costs only a missed parallelization, which is the
|
|
1146
|
+
* trade this rule takes everywhere.
|
|
1147
|
+
*
|
|
1148
|
+
* Only calls in DEFERRED position qualify -- those the traversal reaches by
|
|
1149
|
+
* crossing into a callback or a resolved callee body. A call spelled in the
|
|
1150
|
+
* operand's own text already carries a receiver key, so barriers 7 and 12
|
|
1151
|
+
* order it with carve-outs calibrated against exactly this: they return no
|
|
1152
|
+
* key for a call-produced receiver (`this.realtimeDb.ref(pathA).remove()`)
|
|
1153
|
+
* or a varying subscript (`this.handlers[0].read()`), which is what keeps
|
|
1154
|
+
* two argument-disambiguated operations on one handle parallelizable.
|
|
1155
|
+
* Recording those same calls here would mint a write on the shared prefix
|
|
1156
|
+
* and silently override that calibration. Behind a callback no receiver key
|
|
1157
|
+
* exists at the operand level at all, so nothing is overridden -- that is
|
|
1158
|
+
* the blind spot, and its whole extent.
|
|
1159
|
+
*
|
|
1160
|
+
* The BARE instance (`this.storeAll()`) is excluded for the same reason:
|
|
1161
|
+
* recording it would mint a wildcard write overlapping every slot, turning
|
|
1162
|
+
* the precise treatment those barriers give it into a blanket one.
|
|
1163
|
+
*/
|
|
1164
|
+
function collectMutatedReceiver(call, targets) {
|
|
1165
|
+
const callee = unwrapExpression(call.callee);
|
|
1166
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const receiverPath = getInstancePathKey(callee.object);
|
|
1170
|
+
if (receiverPath !== null && receiverPath !== INSTANCE_RECEIVER_KEY) {
|
|
1171
|
+
targets.instancePaths.push(receiverPath);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1130
1174
|
/**
|
|
1131
1175
|
* Collects the state an awaited expression WRITES: the identifier names it
|
|
1132
|
-
* assigns, and the instance paths (`this.mutator`) it assigns
|
|
1176
|
+
* assigns, and the instance paths (`this.mutator`) it assigns or mutates
|
|
1177
|
+
* through a method call. (#1924, #2017)
|
|
1133
1178
|
*
|
|
1134
1179
|
* The traversal deliberately crosses function boundaries, which is the
|
|
1135
1180
|
* opposite of what containsSuspendingAwait needs: the write that matters
|
|
@@ -1144,7 +1189,7 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1144
1189
|
*/
|
|
1145
1190
|
function getAssignedState(node) {
|
|
1146
1191
|
const targets = { identifiers: [], instancePaths: [] };
|
|
1147
|
-
const visit = (current) => {
|
|
1192
|
+
const visit = (current, deferred) => {
|
|
1148
1193
|
if (current.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
1149
1194
|
collectAssignmentTarget(current.left, targets);
|
|
1150
1195
|
}
|
|
@@ -1158,6 +1203,10 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1158
1203
|
// iteration; only the declaration form introduces a fresh local.
|
|
1159
1204
|
collectAssignmentTarget(current.left, targets);
|
|
1160
1205
|
}
|
|
1206
|
+
else if (deferred && current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
1207
|
+
collectMutatedReceiver(current, targets);
|
|
1208
|
+
}
|
|
1209
|
+
const childrenDeferred = deferred || FUNCTION_BOUNDARY_TYPES.has(current.type);
|
|
1161
1210
|
for (const key in current) {
|
|
1162
1211
|
if (key === 'parent' || key === 'range' || key === 'loc')
|
|
1163
1212
|
continue;
|
|
@@ -1167,16 +1216,19 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1167
1216
|
if (Array.isArray(child)) {
|
|
1168
1217
|
for (const item of child) {
|
|
1169
1218
|
if (item && typeof item === 'object' && 'type' in item) {
|
|
1170
|
-
visit(item);
|
|
1219
|
+
visit(item, childrenDeferred);
|
|
1171
1220
|
}
|
|
1172
1221
|
}
|
|
1173
1222
|
}
|
|
1174
1223
|
else if ('type' in child) {
|
|
1175
|
-
visit(child);
|
|
1224
|
+
visit(child, childrenDeferred);
|
|
1176
1225
|
}
|
|
1177
1226
|
}
|
|
1178
1227
|
};
|
|
1179
|
-
|
|
1228
|
+
// A resolved callee body is itself deferred relative to the run, and
|
|
1229
|
+
// entering it crosses its own function boundary, so the flag lifts here
|
|
1230
|
+
// exactly as it does for a callback. (#1989, #2017)
|
|
1231
|
+
visit(node, false);
|
|
1180
1232
|
const names = new Set();
|
|
1181
1233
|
for (const target of targets.identifiers) {
|
|
1182
1234
|
if (!isDeclaredWithin(target, node)) {
|
|
@@ -1445,11 +1497,26 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
1445
1497
|
]),
|
|
1446
1498
|
};
|
|
1447
1499
|
});
|
|
1500
|
+
//
|
|
1501
|
+
// The READ side resolves the callee body for the same reason the write
|
|
1502
|
+
// side does, and the omission was the other half of #2017: `await
|
|
1503
|
+
// this.storeAll()` spells only the slot `this.storeAll`, so a preceding
|
|
1504
|
+
// write to `this.accumulated` -- the slot `storeAll` actually reads --
|
|
1505
|
+
// compares as disjoint and the pair parallelizes. Reading the resolved
|
|
1506
|
+
// body restores the edge. An unresolvable callee (inherited, computed,
|
|
1507
|
+
// imported) yields null and leaves the operand keyed on its own text, as
|
|
1508
|
+
// before.
|
|
1448
1509
|
const readInstancePaths = awaitNodes.map((node) => {
|
|
1449
1510
|
const awaitExpr = getAwaitExpression(node);
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1511
|
+
if (!awaitExpr) {
|
|
1512
|
+
return new Set();
|
|
1513
|
+
}
|
|
1514
|
+
const read = getInstancePathKeys(awaitExpr.argument);
|
|
1515
|
+
const calleeFunction = resolveCalleeFunction(awaitExpr);
|
|
1516
|
+
if (!calleeFunction) {
|
|
1517
|
+
return read;
|
|
1518
|
+
}
|
|
1519
|
+
return new Set([...read, ...getInstancePathKeys(calleeFunction)]);
|
|
1453
1520
|
});
|
|
1454
1521
|
for (let i = 1; i < awaitNodes.length; i++) {
|
|
1455
1522
|
const currentIds = allIdentifiers[i];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared machinery for reading the documented examples out of `docs/rules/*.md`
|
|
3
|
+
* and linting them.
|
|
4
|
+
*
|
|
5
|
+
* Extracted so that more than one guard can ask a question of the SAME parsed
|
|
6
|
+
* corpus. `docs-examples-conformance` asks whether a block satisfies its own
|
|
7
|
+
* rule; `docs-correct-block-regression` asks whether the blocks #1982 fixed
|
|
8
|
+
* still satisfy the OTHER rule that used to report on them. Hand-rolling the
|
|
9
|
+
* fence walker or the candidate-filename list a second time is how two guards
|
|
10
|
+
* come to disagree about which blocks exist — the failure `fixtureCorpus.ts`
|
|
11
|
+
* exists to prevent on the RuleTester side, and the reason four guards there
|
|
12
|
+
* inherited the same two silent losses (#1984).
|
|
13
|
+
*
|
|
14
|
+
* The filename list in particular is load-bearing and must not be duplicated:
|
|
15
|
+
* many rules key off the path, so judging a block under a path the rule was
|
|
16
|
+
* never meant to see manufactures a failure.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PREFIX = "@blumintinc/blumint/";
|
|
19
|
+
export declare const DOCS_DIR: string;
|
|
20
|
+
export declare const pageExists: (rule: string) => boolean;
|
|
21
|
+
export declare const readPage: (rule: string) => string | null;
|
|
22
|
+
/** Fence languages that hold lintable TypeScript. */
|
|
23
|
+
export declare const LINTABLE_LANGS: Set<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Candidate filenames, tried in order. Many rules key off the path (cloud
|
|
26
|
+
* function entry points, test-file exemptions, component directories), so a
|
|
27
|
+
* single hard-coded filename would make correct examples report for reasons the
|
|
28
|
+
* doc never claimed.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TS_CANDIDATES: string[];
|
|
31
|
+
export declare const TSX_CANDIDATES: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Rules that match on path segments need a rooted path — `functions/src/types/x.ts`
|
|
34
|
+
* relative does not satisfy the same check that `/repo/functions/src/types/x.ts`
|
|
35
|
+
* does, which would fail a doc example for a reason the doc never claimed.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ROOT = "/repo/";
|
|
38
|
+
export declare const anchor: (p: string) => string;
|
|
39
|
+
export type Block = {
|
|
40
|
+
/**
|
|
41
|
+
* `null` for a fence under no example heading. Such blocks are kept rather
|
|
42
|
+
* than dropped: a page whose fences all come back unlabelled is a detection
|
|
43
|
+
* failure, and dropping them made it indistinguishable from a page that
|
|
44
|
+
* documents no examples at all (#1499).
|
|
45
|
+
*/
|
|
46
|
+
polarity: 'correct' | 'incorrect' | null;
|
|
47
|
+
lang: string;
|
|
48
|
+
code: string;
|
|
49
|
+
line: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Classify an example heading.
|
|
53
|
+
*
|
|
54
|
+
* Only H2+ headings count: the H1 title ends with the rule id, and rule names
|
|
55
|
+
* routinely contain `prefer`, `valid`, or `no`, which would otherwise classify
|
|
56
|
+
* every block in the intro prose. The rule-id parenthetical is stripped for the
|
|
57
|
+
* same reason.
|
|
58
|
+
*
|
|
59
|
+
* Order matters — "incorrect" contains "correct" and "invalid" contains "valid",
|
|
60
|
+
* so the negative spellings must be tested first.
|
|
61
|
+
*/
|
|
62
|
+
export declare function headingPolarity(line: string): Block['polarity'] | null;
|
|
63
|
+
/**
|
|
64
|
+
* Pull every fenced code block, tagged with the polarity of the example heading
|
|
65
|
+
* it sits under (`null` when it sits under none).
|
|
66
|
+
*
|
|
67
|
+
* Polarity is inherited by DEEPER headings, because docs routinely split an
|
|
68
|
+
* example section into named cases (`#### Option 1: …` under `### Examples of
|
|
69
|
+
* correct code`). Treating such a sub-heading as the end of the section dropped
|
|
70
|
+
* every block beneath it, which is how three whole pages asserted nothing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractBlocks(md: string): Block[];
|
|
73
|
+
/**
|
|
74
|
+
* Docs declare the context a snippet assumes inside the snippet itself:
|
|
75
|
+
* `// File: functions/src/...` (or a bare path comment) for path-sensitive
|
|
76
|
+
* rules, and `// eslint-options: {...}` for an example that only holds under a
|
|
77
|
+
* non-default option. Honouring both is what lets every correct block be
|
|
78
|
+
* enforced without exempting the awkward ones.
|
|
79
|
+
*/
|
|
80
|
+
export declare function filenameHint(code: string): string | null;
|
|
81
|
+
export declare function optionsHint(code: string): unknown | null;
|
|
82
|
+
export type LintResult = {
|
|
83
|
+
reports: string[];
|
|
84
|
+
/** 1-based lines of the same reports, for segment attribution (#1622). */
|
|
85
|
+
reportLines: number[];
|
|
86
|
+
skipped: boolean;
|
|
87
|
+
reason?: string;
|
|
88
|
+
};
|
|
89
|
+
export declare function lintBlock(ruleName: string, filename: string, code: string, options: unknown | null): LintResult;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.lintBlock = exports.optionsHint = exports.filenameHint = exports.extractBlocks = exports.headingPolarity = exports.anchor = exports.ROOT = exports.TSX_CANDIDATES = exports.TS_CANDIDATES = exports.LINTABLE_LANGS = exports.readPage = exports.pageExists = exports.DOCS_DIR = exports.PREFIX = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const eslint_1 = require("eslint");
|
|
10
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
11
|
+
const plugin = require('../index');
|
|
12
|
+
const tsParser = require('@typescript-eslint/parser');
|
|
13
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
14
|
+
/**
|
|
15
|
+
* Shared machinery for reading the documented examples out of `docs/rules/*.md`
|
|
16
|
+
* and linting them.
|
|
17
|
+
*
|
|
18
|
+
* Extracted so that more than one guard can ask a question of the SAME parsed
|
|
19
|
+
* corpus. `docs-examples-conformance` asks whether a block satisfies its own
|
|
20
|
+
* rule; `docs-correct-block-regression` asks whether the blocks #1982 fixed
|
|
21
|
+
* still satisfy the OTHER rule that used to report on them. Hand-rolling the
|
|
22
|
+
* fence walker or the candidate-filename list a second time is how two guards
|
|
23
|
+
* come to disagree about which blocks exist — the failure `fixtureCorpus.ts`
|
|
24
|
+
* exists to prevent on the RuleTester side, and the reason four guards there
|
|
25
|
+
* inherited the same two silent losses (#1984).
|
|
26
|
+
*
|
|
27
|
+
* The filename list in particular is load-bearing and must not be duplicated:
|
|
28
|
+
* many rules key off the path, so judging a block under a path the rule was
|
|
29
|
+
* never meant to see manufactures a failure.
|
|
30
|
+
*/
|
|
31
|
+
exports.PREFIX = '@blumintinc/blumint/';
|
|
32
|
+
exports.DOCS_DIR = path_1.default.join(__dirname, '../../docs/rules');
|
|
33
|
+
const pageExists = (rule) => fs_1.default.existsSync(path_1.default.join(exports.DOCS_DIR, `${rule}.md`));
|
|
34
|
+
exports.pageExists = pageExists;
|
|
35
|
+
const readPage = (rule) => (0, exports.pageExists)(rule)
|
|
36
|
+
? fs_1.default.readFileSync(path_1.default.join(exports.DOCS_DIR, `${rule}.md`), 'utf8')
|
|
37
|
+
: null;
|
|
38
|
+
exports.readPage = readPage;
|
|
39
|
+
/** Fence languages that hold lintable TypeScript. */
|
|
40
|
+
exports.LINTABLE_LANGS = new Set([
|
|
41
|
+
'ts',
|
|
42
|
+
'tsx',
|
|
43
|
+
'js',
|
|
44
|
+
'jsx',
|
|
45
|
+
'typescript',
|
|
46
|
+
'javascript',
|
|
47
|
+
'',
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Candidate filenames, tried in order. Many rules key off the path (cloud
|
|
51
|
+
* function entry points, test-file exemptions, component directories), so a
|
|
52
|
+
* single hard-coded filename would make correct examples report for reasons the
|
|
53
|
+
* doc never claimed.
|
|
54
|
+
*/
|
|
55
|
+
exports.TS_CANDIDATES = [
|
|
56
|
+
'src/util/helper.ts',
|
|
57
|
+
'functions/src/callable/handler.f.ts',
|
|
58
|
+
'functions/src/util/helper.ts',
|
|
59
|
+
'src/util/helper.test.ts',
|
|
60
|
+
'src/components/Widget.tsx',
|
|
61
|
+
];
|
|
62
|
+
exports.TSX_CANDIDATES = [
|
|
63
|
+
'src/components/Widget.tsx',
|
|
64
|
+
'src/pages/index.tsx',
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Rules that match on path segments need a rooted path — `functions/src/types/x.ts`
|
|
68
|
+
* relative does not satisfy the same check that `/repo/functions/src/types/x.ts`
|
|
69
|
+
* does, which would fail a doc example for a reason the doc never claimed.
|
|
70
|
+
*/
|
|
71
|
+
exports.ROOT = '/repo/';
|
|
72
|
+
const anchor = (p) => (p.startsWith('/') ? p : exports.ROOT + p);
|
|
73
|
+
exports.anchor = anchor;
|
|
74
|
+
/**
|
|
75
|
+
* Classify an example heading.
|
|
76
|
+
*
|
|
77
|
+
* Only H2+ headings count: the H1 title ends with the rule id, and rule names
|
|
78
|
+
* routinely contain `prefer`, `valid`, or `no`, which would otherwise classify
|
|
79
|
+
* every block in the intro prose. The rule-id parenthetical is stripped for the
|
|
80
|
+
* same reason.
|
|
81
|
+
*
|
|
82
|
+
* Order matters — "incorrect" contains "correct" and "invalid" contains "valid",
|
|
83
|
+
* so the negative spellings must be tested first.
|
|
84
|
+
*/
|
|
85
|
+
function headingPolarity(line) {
|
|
86
|
+
if (!/^#{2,6}\s/.test(line))
|
|
87
|
+
return null;
|
|
88
|
+
const text = line
|
|
89
|
+
.replace(/^#{2,6}\s*/, '')
|
|
90
|
+
.replace(/\(`?@blumintinc\/blumint\/[^)]*`?\)/g, '')
|
|
91
|
+
.toLowerCase();
|
|
92
|
+
if (/❌|👎|\bincorrect\b|\binvalid\b|\bbad\b|\bwrong\b/.test(text))
|
|
93
|
+
return 'incorrect';
|
|
94
|
+
if (/✅|👍|\bcorrect\b|\bvalid\b|\bgood\b/.test(text))
|
|
95
|
+
return 'correct';
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
exports.headingPolarity = headingPolarity;
|
|
99
|
+
/**
|
|
100
|
+
* Pull every fenced code block, tagged with the polarity of the example heading
|
|
101
|
+
* it sits under (`null` when it sits under none).
|
|
102
|
+
*
|
|
103
|
+
* Polarity is inherited by DEEPER headings, because docs routinely split an
|
|
104
|
+
* example section into named cases (`#### Option 1: …` under `### Examples of
|
|
105
|
+
* correct code`). Treating such a sub-heading as the end of the section dropped
|
|
106
|
+
* every block beneath it, which is how three whole pages asserted nothing.
|
|
107
|
+
*/
|
|
108
|
+
function extractBlocks(md) {
|
|
109
|
+
const lines = md.split('\n');
|
|
110
|
+
const blocks = [];
|
|
111
|
+
let polarity = null;
|
|
112
|
+
let polarityDepth = 0;
|
|
113
|
+
let fence = null;
|
|
114
|
+
let buf = [];
|
|
115
|
+
let lang = '';
|
|
116
|
+
let startLine = 0;
|
|
117
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
const fenceMatch = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
|
|
120
|
+
if (fence) {
|
|
121
|
+
if (fenceMatch &&
|
|
122
|
+
fenceMatch[1][0] === fence[0] &&
|
|
123
|
+
fenceMatch[1].length >= fence.length) {
|
|
124
|
+
blocks.push({
|
|
125
|
+
polarity,
|
|
126
|
+
lang: lang.trim().toLowerCase(),
|
|
127
|
+
code: buf.join('\n'),
|
|
128
|
+
line: startLine,
|
|
129
|
+
});
|
|
130
|
+
fence = null;
|
|
131
|
+
buf = [];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
buf.push(line);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const heading = /^(#{1,6})\s/.exec(line);
|
|
139
|
+
if (heading) {
|
|
140
|
+
const depth = heading[1].length;
|
|
141
|
+
const own = headingPolarity(line);
|
|
142
|
+
if (own) {
|
|
143
|
+
polarity = own;
|
|
144
|
+
polarityDepth = depth;
|
|
145
|
+
}
|
|
146
|
+
else if (!(polarity && depth > polarityDepth)) {
|
|
147
|
+
// A sibling or shallower heading ends the example section; a deeper one
|
|
148
|
+
// is a named case inside it and keeps the section's polarity.
|
|
149
|
+
polarity = null;
|
|
150
|
+
polarityDepth = 0;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (fenceMatch) {
|
|
155
|
+
fence = fenceMatch[1];
|
|
156
|
+
lang = fenceMatch[2] || '';
|
|
157
|
+
startLine = i + 1;
|
|
158
|
+
buf = [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return blocks;
|
|
162
|
+
}
|
|
163
|
+
exports.extractBlocks = extractBlocks;
|
|
164
|
+
/**
|
|
165
|
+
* Docs declare the context a snippet assumes inside the snippet itself:
|
|
166
|
+
* `// File: functions/src/...` (or a bare path comment) for path-sensitive
|
|
167
|
+
* rules, and `// eslint-options: {...}` for an example that only holds under a
|
|
168
|
+
* non-default option. Honouring both is what lets every correct block be
|
|
169
|
+
* enforced without exempting the awkward ones.
|
|
170
|
+
*/
|
|
171
|
+
function filenameHint(code) {
|
|
172
|
+
const explicit = /^\s*(?:\/\/|\/\*)\s*File:\s*([^\s*]+)/im.exec(code);
|
|
173
|
+
if (explicit)
|
|
174
|
+
return (0, exports.anchor)(explicit[1].replace(/^\.\//, ''));
|
|
175
|
+
const firstLine = code.split('\n').find((l) => l.trim().length > 0) || '';
|
|
176
|
+
const bare = /^\s*\/\/\s*((?:[\w.-]+\/)+[\w.-]+\.tsx?)\b/.exec(firstLine);
|
|
177
|
+
return bare ? (0, exports.anchor)(bare[1]) : null;
|
|
178
|
+
}
|
|
179
|
+
exports.filenameHint = filenameHint;
|
|
180
|
+
function optionsHint(code) {
|
|
181
|
+
const m = /^\s*\/\/\s*eslint-options:\s*(\{.*\})\s*$/im.exec(code);
|
|
182
|
+
if (!m)
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(m[1]);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error(`malformed // eslint-options: ${m[1]}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.optionsHint = optionsHint;
|
|
192
|
+
const linter = new eslint_1.Linter();
|
|
193
|
+
for (const [name, rule] of Object.entries(plugin.rules)) {
|
|
194
|
+
linter.defineRule(exports.PREFIX + name, rule);
|
|
195
|
+
}
|
|
196
|
+
linter.defineParser('ts', tsParser);
|
|
197
|
+
function lintBlock(ruleName, filename, code, options) {
|
|
198
|
+
const config = {
|
|
199
|
+
parser: 'ts',
|
|
200
|
+
parserOptions: {
|
|
201
|
+
ecmaVersion: 2022,
|
|
202
|
+
sourceType: 'module',
|
|
203
|
+
ecmaFeatures: { jsx: filename.endsWith('.tsx') },
|
|
204
|
+
},
|
|
205
|
+
rules: {
|
|
206
|
+
[exports.PREFIX + ruleName]: options
|
|
207
|
+
? ['error', options]
|
|
208
|
+
: 'error',
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
let messages;
|
|
212
|
+
try {
|
|
213
|
+
messages = linter.verify(code, config, { filename });
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// A rule needing type information throws without `parserOptions.project`,
|
|
217
|
+
// which the RuleTester cannot supply; such rules are out of scope here.
|
|
218
|
+
return {
|
|
219
|
+
reports: [],
|
|
220
|
+
reportLines: [],
|
|
221
|
+
skipped: true,
|
|
222
|
+
reason: `the rule threw: ${error.message}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// A block that does not parse never ran the rule. That is not a pass — see
|
|
226
|
+
// UNCHECKABLE_BLOCKS.
|
|
227
|
+
const fatal = messages.find((m) => m.fatal);
|
|
228
|
+
if (fatal) {
|
|
229
|
+
return {
|
|
230
|
+
reports: [],
|
|
231
|
+
reportLines: [],
|
|
232
|
+
skipped: true,
|
|
233
|
+
reason: `parse failure at block line ${fatal.line}: ${fatal.message}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const mine = messages.filter((m) => m.ruleId === exports.PREFIX + ruleName);
|
|
237
|
+
return {
|
|
238
|
+
reports: mine.map((m) => `line ${m.line}: ${m.message}`),
|
|
239
|
+
reportLines: mine.map((m) => m.line),
|
|
240
|
+
skipped: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
exports.lintBlock = lintBlock;
|
|
244
|
+
//# sourceMappingURL=docsFixtures.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.154",
|
|
4
|
+
"date": "2026-08-15T03:47:51.570Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "parallelize-async-operations",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2017
|
|
11
|
+
],
|
|
12
|
+
"summary": "order callback-deferred instance mutations against later reads (closes #2017)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.153",
|
|
4
18
|
"date": "2026-08-14T20:21:23.691Z",
|