@blumintinc/eslint-plugin-blumint 1.20.153 → 1.20.155
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/no-explicit-return-type.js +157 -21
- package/lib/rules/parallelize-async-operations.js +75 -8
- package/lib/rules/prefer-union-from-const-array.js +43 -0
- package/lib/rules/require-image-optimized.js +79 -1
- package/lib/utils/ASTHelpers.d.ts +6 -0
- package/lib/utils/ASTHelpers.js +41 -7
- package/lib/utils/docsFixtures.d.ts +89 -0
- package/lib/utils/docsFixtures.js +244 -0
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -569,32 +569,164 @@ function isOverloadedFunction(node) {
|
|
|
569
569
|
}
|
|
570
570
|
return false;
|
|
571
571
|
}
|
|
572
|
-
|
|
572
|
+
/**
|
|
573
|
+
* The statement list that directly holds `node`, looking through `export`.
|
|
574
|
+
*
|
|
575
|
+
* Overload signatures and their implementation are siblings of one statement
|
|
576
|
+
* list — an overload set cannot span containers — so this is the only list worth
|
|
577
|
+
* reading. Walking outward instead would let a same-named function in an
|
|
578
|
+
* enclosing scope answer for one it cannot overload.
|
|
579
|
+
*/
|
|
580
|
+
function siblingStatementsOf(node) {
|
|
581
|
+
const parent = node.parent;
|
|
582
|
+
if (!parent)
|
|
583
|
+
return undefined;
|
|
584
|
+
const container = parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
585
|
+
parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
586
|
+
? parent.parent
|
|
587
|
+
: parent;
|
|
588
|
+
return container ? (0, lexicalScope_1.statementsOf)(container) : undefined;
|
|
589
|
+
}
|
|
590
|
+
const EMPTY_OVERLOAD_SET = { signatures: 0, implementations: 0 };
|
|
591
|
+
/**
|
|
592
|
+
* How many declaration-only signatures and how many implementations the
|
|
593
|
+
* container holding `node` declares under `node`'s own name, `node` included.
|
|
594
|
+
*
|
|
595
|
+
* Every statement container is read, not just `Program` and `TSModuleBlock`: a
|
|
596
|
+
* function body, a bare block and a `switch` case each bind a name just as
|
|
597
|
+
* effectively, so reading only the top level makes the DEPTH of an overload set
|
|
598
|
+
* decide whether it exists (the same defect as #1771).
|
|
599
|
+
*/
|
|
600
|
+
function overloadSetOf(node) {
|
|
573
601
|
const functionName = node.id?.name;
|
|
574
602
|
if (!functionName)
|
|
603
|
+
return EMPTY_OVERLOAD_SET;
|
|
604
|
+
const statements = siblingStatementsOf(node);
|
|
605
|
+
if (!statements)
|
|
606
|
+
return EMPTY_OVERLOAD_SET;
|
|
607
|
+
let signatures = 0;
|
|
608
|
+
let implementations = 0;
|
|
609
|
+
for (const statement of statements) {
|
|
610
|
+
// `export function f(...)` is the same declaration one AST node deeper, and
|
|
611
|
+
// an overload set may export some of its members and not others.
|
|
612
|
+
const declaration = (0, lexicalScope_1.declarationOf)(statement);
|
|
613
|
+
if (declaration.type !== utils_1.AST_NODE_TYPES.TSDeclareFunction &&
|
|
614
|
+
declaration.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (declaration.id?.name !== functionName)
|
|
618
|
+
continue;
|
|
619
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.TSDeclareFunction ||
|
|
620
|
+
!declaration.body) {
|
|
621
|
+
signatures += 1;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
implementations += 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { signatures, implementations };
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* True when `node` is the IMPLEMENTATION of an overload set — a function with a
|
|
631
|
+
* body whose container also declares the same name as one or more
|
|
632
|
+
* declaration-only signatures.
|
|
633
|
+
*
|
|
634
|
+
* Its annotation is not a restatement of what the body returns: TypeScript
|
|
635
|
+
* checks each overload signature against the IMPLEMENTATION SIGNATURE, so the
|
|
636
|
+
* annotation is what the overloads are measured against. Inference yields the
|
|
637
|
+
* body's own type, which need not accept them — stripping `: void | string`
|
|
638
|
+
* from `function get(param?: string): void | string {}` infers `void` and makes
|
|
639
|
+
* the `: string` overload above it TS2394 (#2019).
|
|
640
|
+
*
|
|
641
|
+
* This carve-out ignores `allowOverloadedFunctions`. That option governs the
|
|
642
|
+
* declaration-only signatures, whose annotations are mandatory but carry no
|
|
643
|
+
* fixer, so reporting them costs nothing but a message. Here the report ships a
|
|
644
|
+
* fix that does not compile, which no option may ask for.
|
|
645
|
+
*/
|
|
646
|
+
function isOverloadImplementation(node) {
|
|
647
|
+
if (!node.body)
|
|
575
648
|
return false;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
649
|
+
return overloadSetOf(node).signatures > 0;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* True when `node` is a declaration-only signature that belongs to an overload
|
|
653
|
+
* set: another signature declares the same name, or an implementation below it
|
|
654
|
+
* does. A lone `declare function f(): number;` overloads nothing, so it stays
|
|
655
|
+
* reportable.
|
|
656
|
+
*/
|
|
657
|
+
function isOverloadedTsDeclareFunction(node) {
|
|
658
|
+
const { signatures, implementations } = overloadSetOf(node);
|
|
659
|
+
return signatures > 1 || implementations > 0;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* A method's identity inside its class body. Overloads agree on all three
|
|
663
|
+
* components; `static f` and `f` merely spell the same name, and a computed key
|
|
664
|
+
* names nothing resolvable, so it yields nothing. The separator keeps a private
|
|
665
|
+
* `#log` from colliding with a string key `'#log'`.
|
|
666
|
+
*/
|
|
667
|
+
function methodIdentityOf(node) {
|
|
668
|
+
if (node.computed)
|
|
669
|
+
return undefined;
|
|
670
|
+
if (node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
671
|
+
return `${node.static}\u0000private\u0000${node.key.name}`;
|
|
672
|
+
}
|
|
673
|
+
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
674
|
+
node.key.type !== utils_1.AST_NODE_TYPES.Literal) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
const name = getNameFromIdentifierOrLiteral(node.key);
|
|
678
|
+
return name === undefined
|
|
679
|
+
? undefined
|
|
680
|
+
: `${node.static}\u0000public\u0000${name}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The overload set a class method belongs to, counted over the members of its
|
|
684
|
+
* own class body. A member without a body is an overload signature
|
|
685
|
+
* (`TSEmptyBodyFunctionExpression`); the one member with a body is the
|
|
686
|
+
* implementation.
|
|
687
|
+
*/
|
|
688
|
+
function classOverloadSetOf(node) {
|
|
689
|
+
const identity = methodIdentityOf(node);
|
|
690
|
+
const classBody = node.parent;
|
|
691
|
+
if (!identity || classBody?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
692
|
+
return EMPTY_OVERLOAD_SET;
|
|
693
|
+
}
|
|
694
|
+
let signatures = 0;
|
|
695
|
+
let implementations = 0;
|
|
696
|
+
for (const member of classBody.body) {
|
|
697
|
+
if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition)
|
|
698
|
+
continue;
|
|
699
|
+
if (methodIdentityOf(member) !== identity)
|
|
700
|
+
continue;
|
|
701
|
+
if (member.value.body) {
|
|
702
|
+
implementations += 1;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
signatures += 1;
|
|
594
706
|
}
|
|
595
|
-
container = container.parent;
|
|
596
707
|
}
|
|
597
|
-
return
|
|
708
|
+
return { signatures, implementations };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* True when the method is the implementation of an overloaded class method, for
|
|
712
|
+
* the reason {@link isOverloadImplementation} gives — a class overload set is
|
|
713
|
+
* checked exactly as a function one is (#2019).
|
|
714
|
+
*/
|
|
715
|
+
function isOverloadImplementationMethod(node) {
|
|
716
|
+
if (!node.value.body)
|
|
717
|
+
return false;
|
|
718
|
+
return classOverloadSetOf(node).signatures > 0;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* True when the method is a declaration-only overload signature. A body-less
|
|
722
|
+
* method is legal only as part of an overload set, so it is exempt whenever the
|
|
723
|
+
* set holds anything else at all.
|
|
724
|
+
*/
|
|
725
|
+
function isOverloadedClassMethodSignature(node) {
|
|
726
|
+
if (node.value.body)
|
|
727
|
+
return false;
|
|
728
|
+
const { signatures, implementations } = classOverloadSetOf(node);
|
|
729
|
+
return signatures > 1 || implementations > 0;
|
|
598
730
|
}
|
|
599
731
|
function isInterfaceOrAbstractMethodSignature(node) {
|
|
600
732
|
if (node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition)
|
|
@@ -1233,6 +1365,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1233
1365
|
isReadonlyWideningReturnType(returnType) ||
|
|
1234
1366
|
isAllowedVoidReturnType(returnType) ||
|
|
1235
1367
|
isDecoratorFactory(node, returnType) ||
|
|
1368
|
+
isOverloadImplementation(node) ||
|
|
1236
1369
|
(mergedOptions.allowRecursiveFunctions &&
|
|
1237
1370
|
isRecursiveFunction(node)) ||
|
|
1238
1371
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -1292,6 +1425,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1292
1425
|
isReadonlyWideningReturnType(returnType) ||
|
|
1293
1426
|
isAllowedVoidReturnType(returnType) ||
|
|
1294
1427
|
isDecoratorFactory(node, returnType) ||
|
|
1428
|
+
isOverloadImplementationMethod(node) ||
|
|
1429
|
+
(mergedOptions.allowOverloadedFunctions &&
|
|
1430
|
+
isOverloadedClassMethodSignature(node)) ||
|
|
1295
1431
|
(mergedOptions.allowAbstractMethodSignatures &&
|
|
1296
1432
|
isInterfaceOrAbstractMethodSignature(node)) ||
|
|
1297
1433
|
isReturnTypeRequiredByRecursion(node)) {
|
|
@@ -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];
|
|
@@ -34,6 +34,46 @@ function isStringLiteralType(member) {
|
|
|
34
34
|
const { literal } = member;
|
|
35
35
|
return (literal.type === utils_1.AST_NODE_TYPES.Literal && typeof literal.value === 'string');
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A declaration file is ambient in its entirety, so a declaration in one is
|
|
39
|
+
* subject to the ambient restriction even without an explicit `declare`.
|
|
40
|
+
*/
|
|
41
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/i;
|
|
42
|
+
/**
|
|
43
|
+
* An ambient context accepts only a string, numeric or literal-enum `const`
|
|
44
|
+
* initializer (TS1254), so the derived `as const` array cannot be emitted there
|
|
45
|
+
* at all — `as const` or not, an array literal is rejected. Declining is the
|
|
46
|
+
* remedy rather than a different rewrite, because no legal rewrite exists in
|
|
47
|
+
* that position: the rule asks for importable runtime values and an ambient
|
|
48
|
+
* declaration is precisely the promise that no runtime value is emitted.
|
|
49
|
+
*
|
|
50
|
+
* `declare` on the OUTERMOST module declaration makes every level nested below
|
|
51
|
+
* it ambient too, so the whole ancestor chain is walked rather than just the
|
|
52
|
+
* enclosing block. A module named by a string literal (`module 'x' {}`) and a
|
|
53
|
+
* `global {}` augmentation are ambient by construction, with or without the
|
|
54
|
+
* modifier.
|
|
55
|
+
*/
|
|
56
|
+
function isInAmbientContext(node, filename) {
|
|
57
|
+
if (DECLARATION_FILE.test(filename)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
|
|
61
|
+
node.declare === true) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
let current = node.parent;
|
|
65
|
+
while (current) {
|
|
66
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration) {
|
|
67
|
+
if (current.declare === true ||
|
|
68
|
+
current.global === true ||
|
|
69
|
+
current.id.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
current = current.parent;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
37
77
|
/**
|
|
38
78
|
* Walk the scope chain upward collecting declared variable names. Used to skip
|
|
39
79
|
* the autofix (report-only) when the derived `{TYPE}_VALUES` name is already
|
|
@@ -154,6 +194,9 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
154
194
|
if (!members.every(isStringLiteralType)) {
|
|
155
195
|
return;
|
|
156
196
|
}
|
|
197
|
+
if (isInAmbientContext(node, context.getFilename())) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
157
200
|
const typeName = node.id.name;
|
|
158
201
|
const constName = `${toUpperSnake(typeName)}_VALUES`;
|
|
159
202
|
// The type alias is exported when wrapped in `export type X = ...`
|
|
@@ -62,6 +62,49 @@ const isInsideComponentMock = (node, componentModule) => {
|
|
|
62
62
|
}
|
|
63
63
|
return false;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Name a declaration binds, for the forms a component is declared under:
|
|
67
|
+
* `const ImageOptimized = ...` (including `memo(...)`/`forwardRef(...)` around
|
|
68
|
+
* the body), `function ImageOptimized()` and `class ImageOptimized`. Anything
|
|
69
|
+
* else — an object property, a parameter — binds no declaration name the
|
|
70
|
+
* wrapper can be identified by, and treating it as one would exempt a mock
|
|
71
|
+
* factory keyed by the component name whatever module it stands in for.
|
|
72
|
+
*/
|
|
73
|
+
const declaredNameOf = (node) => {
|
|
74
|
+
switch (node.type) {
|
|
75
|
+
case utils_1.AST_NODE_TYPES.VariableDeclarator:
|
|
76
|
+
return node.id.type === utils_1.AST_NODE_TYPES.Identifier ? node.id.name : null;
|
|
77
|
+
case utils_1.AST_NODE_TYPES.FunctionDeclaration:
|
|
78
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
79
|
+
case utils_1.AST_NODE_TYPES.ClassDeclaration:
|
|
80
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
81
|
+
return node.id ? node.id.name : null;
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Names each local binding is exported under, so a wrapper declared as
|
|
88
|
+
* `Picture` and shipped as `export { Picture as ImageOptimized }` is still
|
|
89
|
+
* recognized as the component's own definition. A specifier carrying a `from`
|
|
90
|
+
* clause re-exports another module's binding and declares nothing here.
|
|
91
|
+
*/
|
|
92
|
+
const exportedNamesByLocal = (program) => {
|
|
93
|
+
const exported = new Map();
|
|
94
|
+
for (const statement of program.body) {
|
|
95
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
96
|
+
statement.source ||
|
|
97
|
+
statement.exportKind === 'type') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const specifier of statement.specifiers) {
|
|
101
|
+
const names = exported.get(specifier.local.name) ?? new Set();
|
|
102
|
+
names.add(specifier.exported.name);
|
|
103
|
+
exported.set(specifier.local.name, names);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return exported;
|
|
107
|
+
};
|
|
65
108
|
/**
|
|
66
109
|
* A type-only specifier binds no value: it renders nothing, so it neither
|
|
67
110
|
* bypasses the optimization pipeline nor can back a fix. The modifier lives
|
|
@@ -219,6 +262,40 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
219
262
|
* rule exists to centralize, not a violation of it.
|
|
220
263
|
*/
|
|
221
264
|
const isComponentImplementationFile = moduleNameOf(context.getFilename()) === componentModule;
|
|
265
|
+
/**
|
|
266
|
+
* The wrapper is identified by the name the fixer would emit and by its
|
|
267
|
+
* module's name, which a component module's export shares by convention.
|
|
268
|
+
* Matching is exact so a distinct component whose name merely starts with
|
|
269
|
+
* it (`ImageOptimizedGallery`) stays reportable.
|
|
270
|
+
*/
|
|
271
|
+
const isComponentName = (name) => name === COMPONENT_NAME || name === componentModule;
|
|
272
|
+
const exportedNames = exportedNamesByLocal(sourceCode.ast);
|
|
273
|
+
const definesComponent = (name) => {
|
|
274
|
+
if (isComponentName(name)) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
const aliases = exportedNames.get(name);
|
|
278
|
+
return !!aliases && [...aliases].some(isComponentName);
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Whether the element sits inside the declaration of the component the fix
|
|
282
|
+
* points at. That declaration renders the image primitive by definition, so
|
|
283
|
+
* swapping it for the component makes the wrapper render itself: unbounded
|
|
284
|
+
* recursion, and a type error too, since the wrapper forwards only the
|
|
285
|
+
* props it destructured. The whole ancestry is walked because a helper
|
|
286
|
+
* nested inside the declaration is part of that implementation as well.
|
|
287
|
+
*/
|
|
288
|
+
const isInsideComponentDefinition = (node) => {
|
|
289
|
+
let current = node.parent;
|
|
290
|
+
while (current) {
|
|
291
|
+
const declared = declaredNameOf(current);
|
|
292
|
+
if (declared && definesComponent(declared)) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
current = current.parent;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
};
|
|
222
299
|
return {
|
|
223
300
|
// Handle JSX img elements
|
|
224
301
|
JSXElement(node) {
|
|
@@ -228,7 +305,8 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
228
305
|
return;
|
|
229
306
|
}
|
|
230
307
|
if (isComponentImplementationFile ||
|
|
231
|
-
isInsideComponentMock(node, componentModule)
|
|
308
|
+
isInsideComponentMock(node, componentModule) ||
|
|
309
|
+
isInsideComponentDefinition(node)) {
|
|
232
310
|
return;
|
|
233
311
|
}
|
|
234
312
|
const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
|
|
@@ -85,6 +85,12 @@ export declare class ASTHelpers {
|
|
|
85
85
|
* expression reads some other object.
|
|
86
86
|
*/
|
|
87
87
|
private static classMemberNameReferencedBy;
|
|
88
|
+
/**
|
|
89
|
+
* The ECMA private names a class body declares, spelled as the graph spells
|
|
90
|
+
* them. A private name is scoped to the body that declares it, so this set
|
|
91
|
+
* is what an enclosing class must not mistake for its own members.
|
|
92
|
+
*/
|
|
93
|
+
private static privateNamesDeclaredBy;
|
|
88
94
|
private static walkChildNodes;
|
|
89
95
|
static isNode(value: unknown): value is TSESTree.Node;
|
|
90
96
|
static hasReturnStatement(node: TSESTree.Node): boolean;
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -423,7 +423,16 @@ class ASTHelpers {
|
|
|
423
423
|
case 'ClassExpression': {
|
|
424
424
|
// Traversal continues so `<ClassName>.<member>` statics stay visible,
|
|
425
425
|
// but `this` no longer denotes the graphed instance.
|
|
426
|
-
|
|
426
|
+
const nested = [];
|
|
427
|
+
this.walkChildNodes(node, className, false, nested);
|
|
428
|
+
// A nested class body SHADOWS the private names it declares: its `#q`
|
|
429
|
+
// is a member of that class, distinct from an enclosing class's `#q`,
|
|
430
|
+
// so reads of it constrain the nested layout rather than this one.
|
|
431
|
+
// Private names the nested class does not declare still resolve
|
|
432
|
+
// outward, so only the declared ones are dropped. Filtering as the
|
|
433
|
+
// recursion unwinds composes across any depth of nesting.
|
|
434
|
+
const shadowed = this.privateNamesDeclaredBy(node);
|
|
435
|
+
dependencies.push(...nested.filter((name) => !shadowed.has(name)));
|
|
427
436
|
return;
|
|
428
437
|
}
|
|
429
438
|
case 'MemberExpression': {
|
|
@@ -447,6 +456,18 @@ class ASTHelpers {
|
|
|
447
456
|
*/
|
|
448
457
|
static classMemberNameReferencedBy(node, className, isThisTheInstance) {
|
|
449
458
|
const { object, property, computed } = node;
|
|
459
|
+
// A `#name` resolves LEXICALLY: it is a syntax error unless a class body
|
|
460
|
+
// enclosing the reference declares it, so the receiver it is read through
|
|
461
|
+
// cannot change which member it names. `other.#helper` and `this.#helper`
|
|
462
|
+
// reach the same member, which is why this branch precedes the receiver
|
|
463
|
+
// test that the dotted spellings need. Requiring `this`/<ClassName> here
|
|
464
|
+
// dropped the read a static initializer makes through another value of
|
|
465
|
+
// the same class, and with it the constraint that keeps the field's
|
|
466
|
+
// declaration above its reader (#2022). The `#` is part of the name so
|
|
467
|
+
// `#helper` and `helper` stay distinct members.
|
|
468
|
+
if (!computed && property?.type === 'PrivateIdentifier') {
|
|
469
|
+
return `#${property.name}`;
|
|
470
|
+
}
|
|
450
471
|
const readsInstance = object?.type === 'ThisExpression' && isThisTheInstance;
|
|
451
472
|
// An anonymous class expression has an empty name, which no identifier
|
|
452
473
|
// can match.
|
|
@@ -457,12 +478,6 @@ class ASTHelpers {
|
|
|
457
478
|
if (!computed && property?.type === 'Identifier') {
|
|
458
479
|
return property.name;
|
|
459
480
|
}
|
|
460
|
-
// `this.#helper` names a member as precisely as `this.helper` does, and it
|
|
461
|
-
// is the only spelling available for an ECMA private member. The `#` is
|
|
462
|
-
// part of the name so `#helper` and `helper` stay distinct members.
|
|
463
|
-
if (!computed && property?.type === 'PrivateIdentifier') {
|
|
464
|
-
return `#${property.name}`;
|
|
465
|
-
}
|
|
466
481
|
// `this['helper']` names the member as precisely as `this.helper` does,
|
|
467
482
|
// whereas `this[key]` names one only at runtime.
|
|
468
483
|
if (computed &&
|
|
@@ -472,6 +487,25 @@ class ASTHelpers {
|
|
|
472
487
|
}
|
|
473
488
|
return null;
|
|
474
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* The ECMA private names a class body declares, spelled as the graph spells
|
|
492
|
+
* them. A private name is scoped to the body that declares it, so this set
|
|
493
|
+
* is what an enclosing class must not mistake for its own members.
|
|
494
|
+
*/
|
|
495
|
+
static privateNamesDeclaredBy(node) {
|
|
496
|
+
const names = new Set();
|
|
497
|
+
const members = node.body?.body;
|
|
498
|
+
if (!Array.isArray(members)) {
|
|
499
|
+
return names;
|
|
500
|
+
}
|
|
501
|
+
for (const member of members) {
|
|
502
|
+
const key = member?.key;
|
|
503
|
+
if (key?.type === 'PrivateIdentifier') {
|
|
504
|
+
names.add(`#${key.name}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return names;
|
|
508
|
+
}
|
|
475
509
|
static walkChildNodes(node, className, isThisTheInstance, dependencies) {
|
|
476
510
|
for (const [key, value] of Object.entries(node)) {
|
|
477
511
|
if (ASTHelpers.NON_TRAVERSABLE_NODE_KEYS.has(key)) {
|
|
@@ -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,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.155",
|
|
4
|
+
"date": "2026-08-15T14:21:55.491Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "class-methods-read-top-to-bottom",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2022
|
|
11
|
+
],
|
|
12
|
+
"summary": "pin a # field by every read of it, not just this.#x (closes #2022)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-explicit-return-type",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
2019
|
|
19
|
+
],
|
|
20
|
+
"summary": "spare the implementation signature of an overload set (closes #2019)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer-union-from-const-array",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
2020
|
|
27
|
+
],
|
|
28
|
+
"summary": "decline in an ambient context, where no const array is legal (closes #2020)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "require-image-optimized",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
2021
|
|
35
|
+
],
|
|
36
|
+
"summary": "exempt the img inside ImageOptimized's own definition (closes #2021)"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "1.20.154",
|
|
42
|
+
"date": "2026-08-15T03:47:51.570Z",
|
|
43
|
+
"rules": [
|
|
44
|
+
{
|
|
45
|
+
"name": "parallelize-async-operations",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
2017
|
|
49
|
+
],
|
|
50
|
+
"summary": "order callback-deferred instance mutations against later reads (closes #2017)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.153",
|
|
4
56
|
"date": "2026-08-14T20:21:23.691Z",
|