@blumintinc/eslint-plugin-blumint 1.20.96 → 1.20.97
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-boolean-naming-prefixes.js +38 -3
- package/lib/rules/enforce-positive-naming.js +112 -24
- package/lib/utils/harvestRuleTesterCases.d.ts +48 -0
- package/lib/utils/harvestRuleTesterCases.js +168 -0
- package/package.json +1 -1
- package/release-manifest.json +23 -0
package/lib/index.js
CHANGED
|
@@ -113,12 +113,25 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
113
113
|
return true;
|
|
114
114
|
}
|
|
115
115
|
const nextChar = normalizedName.charAt(p.length);
|
|
116
|
-
// For SCREAMING_SNAKE_CASE or similar all-uppercase names
|
|
117
|
-
//
|
|
116
|
+
// For SCREAMING_SNAKE_CASE or similar all-uppercase names the prefix must
|
|
117
|
+
// end at a separator, since case can no longer mark the word boundary
|
|
118
|
+
// (ISVALID stays unprefixed). Digits fused onto the prefix belong to that
|
|
119
|
+
// first segment rather than to the next word — ARE2_VALID is the
|
|
120
|
+
// UPPER_SNAKE spelling of are2Valid, which the camelCase branch below
|
|
121
|
+
// accepts — so a trailing digit run is consumed before the separator is
|
|
122
|
+
// examined, and the same separators the camelCase branch honours (`_`,
|
|
123
|
+
// `$`, end of name) close the segment. Only a digit run directly after
|
|
124
|
+
// the prefix qualifies: ARENA2_MAP still fails because a letter follows
|
|
125
|
+
// the prefix, and H2AS_ITEMS never reaches here because the prefix does
|
|
126
|
+
// not match the segment's start.
|
|
118
127
|
const isAllUppercase = normalizedName === normalizedName.toUpperCase() &&
|
|
119
128
|
/[a-z]/i.test(normalizedName);
|
|
120
129
|
if (isAllUppercase) {
|
|
121
|
-
|
|
130
|
+
const afterPrefix = normalizedName
|
|
131
|
+
.slice(p.length)
|
|
132
|
+
.replace(/^\d+/, '');
|
|
133
|
+
const boundaryChar = afterPrefix.charAt(0);
|
|
134
|
+
return (boundaryChar === '' || boundaryChar === '_' || boundaryChar === '$');
|
|
122
135
|
}
|
|
123
136
|
// For camelCase, the next char must be uppercase, a digit, or $
|
|
124
137
|
return (nextChar === '_' ||
|
|
@@ -710,6 +723,28 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
710
723
|
expression.name === 'undefined') {
|
|
711
724
|
return 'nonBoolean';
|
|
712
725
|
}
|
|
726
|
+
// A returned binding is governed by this very rule: a boolean variable,
|
|
727
|
+
// parameter or function must carry an approved prefix. So an unprefixed
|
|
728
|
+
// `id` — or the result of calling an unprefixed `compute(x)` — is not a
|
|
729
|
+
// boolean under the regime the rule enforces, and the callee's own name
|
|
730
|
+
// must not override that. Deciding here keeps the exemption in the body
|
|
731
|
+
// rather than in a return annotation, which `no-explicit-return-type`
|
|
732
|
+
// deletes (issue #1691).
|
|
733
|
+
//
|
|
734
|
+
// Member accesses (`source.flag`, `source.read()`) are deliberately
|
|
735
|
+
// excluded: property signatures are only enforced under
|
|
736
|
+
// `enforceForPropertySignatures` and third-party method names are outside
|
|
737
|
+
// this rule's reach, so an unprefixed member may legitimately yield a
|
|
738
|
+
// boolean and the callee's name keeps its say.
|
|
739
|
+
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
740
|
+
return identifierIsBoolean(expression) ? 'boolean' : 'nonBoolean';
|
|
741
|
+
}
|
|
742
|
+
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
743
|
+
expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
744
|
+
return callExpressionLooksBoolean(expression) === 'boolean'
|
|
745
|
+
? 'boolean'
|
|
746
|
+
: 'nonBoolean';
|
|
747
|
+
}
|
|
713
748
|
if (expression.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
714
749
|
if (expression.operator === '!' || expression.operator === 'delete') {
|
|
715
750
|
return 'boolean';
|
|
@@ -973,6 +973,78 @@ function isDefinitelyNonBooleanExpression(node) {
|
|
|
973
973
|
return false;
|
|
974
974
|
}
|
|
975
975
|
}
|
|
976
|
+
// Operators whose result is always a boolean, regardless of operand types.
|
|
977
|
+
const BOOLEAN_BINARY_OPERATORS = new Set([
|
|
978
|
+
'==',
|
|
979
|
+
'!=',
|
|
980
|
+
'===',
|
|
981
|
+
'!==',
|
|
982
|
+
'<',
|
|
983
|
+
'<=',
|
|
984
|
+
'>',
|
|
985
|
+
'>=',
|
|
986
|
+
'in',
|
|
987
|
+
'instanceof',
|
|
988
|
+
]);
|
|
989
|
+
/**
|
|
990
|
+
* Detects an expression that is definitively a boolean — a boolean literal, a
|
|
991
|
+
* negation, a comparison, or a branch/`Boolean()` call built from those. Opaque
|
|
992
|
+
* expressions (calls, identifiers, member accesses) yield no verdict here: they
|
|
993
|
+
* are the shapes a validator's body takes once its return annotation is gone,
|
|
994
|
+
* and assuming boolean for them is what produced the false positive in #1692.
|
|
995
|
+
*/
|
|
996
|
+
function isDefinitelyBooleanExpression(node) {
|
|
997
|
+
switch (node.type) {
|
|
998
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
999
|
+
return typeof node.value === 'boolean';
|
|
1000
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
1001
|
+
return node.operator === '!' || node.operator === 'delete';
|
|
1002
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
1003
|
+
return BOOLEAN_BINARY_OPERATORS.has(node.operator);
|
|
1004
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
1005
|
+
return (isDefinitelyBooleanExpression(node.left) &&
|
|
1006
|
+
isDefinitelyBooleanExpression(node.right));
|
|
1007
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
1008
|
+
return (isDefinitelyBooleanExpression(node.consequent) &&
|
|
1009
|
+
isDefinitelyBooleanExpression(node.alternate));
|
|
1010
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
1011
|
+
// `Boolean(x)` is the one call whose result is boolean by construction.
|
|
1012
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
1013
|
+
node.callee.name === 'Boolean');
|
|
1014
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
1015
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
1016
|
+
// `x as boolean` asserts booleanness; `x as const` and other assertions
|
|
1017
|
+
// say nothing, so the asserted expression decides.
|
|
1018
|
+
return (isBooleanOnlyType(node.typeAnnotation) ||
|
|
1019
|
+
isDefinitelyBooleanExpression(node.expression));
|
|
1020
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
1021
|
+
return isDefinitelyBooleanExpression(node.expression);
|
|
1022
|
+
default:
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function classifyExpression(node) {
|
|
1027
|
+
// A definitively non-boolean shape wins over a boolean one: a validator's
|
|
1028
|
+
// `return 'Must not be blank'` proves the function is not a predicate even
|
|
1029
|
+
// though its success path returns `true`.
|
|
1030
|
+
if (isDefinitelyNonBooleanExpression(node))
|
|
1031
|
+
return 'nonBoolean';
|
|
1032
|
+
if (isDefinitelyBooleanExpression(node))
|
|
1033
|
+
return 'boolean';
|
|
1034
|
+
return 'indeterminate';
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Combines the verdicts of a body's returns under the same precedence:
|
|
1038
|
+
* non-boolean beats boolean, and boolean beats no verdict at all. An empty list
|
|
1039
|
+
* (a body with no returns) is `indeterminate`.
|
|
1040
|
+
*/
|
|
1041
|
+
function combineReturnKinds(kinds) {
|
|
1042
|
+
if (kinds.includes('nonBoolean'))
|
|
1043
|
+
return 'nonBoolean';
|
|
1044
|
+
if (kinds.includes('boolean'))
|
|
1045
|
+
return 'boolean';
|
|
1046
|
+
return 'indeterminate';
|
|
1047
|
+
}
|
|
976
1048
|
/**
|
|
977
1049
|
* Yields the immediate AST-node children of `node`, skipping the `parent`
|
|
978
1050
|
* back-reference so traversal only walks downward.
|
|
@@ -995,10 +1067,12 @@ function childNodesOf(node) {
|
|
|
995
1067
|
return children;
|
|
996
1068
|
}
|
|
997
1069
|
/**
|
|
998
|
-
*
|
|
999
|
-
* function's)
|
|
1070
|
+
* Classifies the `return` statements belonging to `fn`'s own body (not a nested
|
|
1071
|
+
* function's). A bare `return;` carries no verdict, so it neither exempts the
|
|
1072
|
+
* function nor keeps a sibling boolean return from deciding.
|
|
1000
1073
|
*/
|
|
1001
|
-
function
|
|
1074
|
+
function classifyBlockReturns(block) {
|
|
1075
|
+
const kinds = [];
|
|
1002
1076
|
const stack = [block];
|
|
1003
1077
|
while (stack.length > 0) {
|
|
1004
1078
|
const current = stack.pop();
|
|
@@ -1009,41 +1083,54 @@ function blockReturnsNonBoolean(block) {
|
|
|
1009
1083
|
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
1010
1084
|
continue;
|
|
1011
1085
|
}
|
|
1012
|
-
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
1013
|
-
current.argument
|
|
1014
|
-
isDefinitelyNonBooleanExpression(current.argument)) {
|
|
1015
|
-
return true;
|
|
1086
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement && current.argument) {
|
|
1087
|
+
kinds.push(classifyExpression(current.argument));
|
|
1016
1088
|
}
|
|
1017
1089
|
for (const child of childNodesOf(current)) {
|
|
1018
1090
|
stack.push(child);
|
|
1019
1091
|
}
|
|
1020
1092
|
}
|
|
1021
|
-
return
|
|
1093
|
+
return combineReturnKinds(kinds);
|
|
1022
1094
|
}
|
|
1023
1095
|
/**
|
|
1024
|
-
*
|
|
1025
|
-
* non-boolean value — e.g. a validator predicate returning `string | true`. An
|
|
1096
|
+
* The booleanness of a function backing an `is`/`has`-prefixed name. An
|
|
1026
1097
|
* explicit return-type annotation is authoritative; otherwise the body's own
|
|
1027
|
-
* `return` statements (or the concise-arrow expression)
|
|
1098
|
+
* `return` statements (or the concise-arrow expression) decide.
|
|
1028
1099
|
*/
|
|
1029
|
-
function
|
|
1100
|
+
function classifyFunctionReturn(fn) {
|
|
1030
1101
|
if (fn.returnType) {
|
|
1031
|
-
return
|
|
1102
|
+
return isBooleanOnlyType(fn.returnType.typeAnnotation)
|
|
1103
|
+
? 'boolean'
|
|
1104
|
+
: 'nonBoolean';
|
|
1032
1105
|
}
|
|
1033
1106
|
if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
1034
|
-
return
|
|
1107
|
+
return classifyExpression(fn.body);
|
|
1035
1108
|
}
|
|
1036
|
-
return
|
|
1109
|
+
return classifyBlockReturns(fn.body);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Whether a function backing an `is`/`has`-prefixed name must be exempt from
|
|
1113
|
+
* boolean negative-naming. Only a function proven to return a boolean is
|
|
1114
|
+
* flagged: a validator predicate returning `string | true` is exempt, and so is
|
|
1115
|
+
* one whose returns are syntactically opaque (`=> validate(value)`). That
|
|
1116
|
+
* matters because `no-explicit-return-type` deletes the very annotation that
|
|
1117
|
+
* spells the validator's non-boolean return, leaving nothing but the name to go
|
|
1118
|
+
* on — and guessing "boolean" from the name alone reports a rename that inverts
|
|
1119
|
+
* the predicate's meaning (#1692). Preferring a false negative here is the
|
|
1120
|
+
* repository's stated trade-off.
|
|
1121
|
+
*/
|
|
1122
|
+
function isExemptFromBooleanNaming(fn) {
|
|
1123
|
+
return classifyFunctionReturn(fn) !== 'boolean';
|
|
1037
1124
|
}
|
|
1038
1125
|
/**
|
|
1039
|
-
* When a declarator/property value is a function, whether that function is
|
|
1040
|
-
*
|
|
1126
|
+
* When a declarator/property value is a function, whether that function is
|
|
1127
|
+
* exempt from boolean negative-naming.
|
|
1041
1128
|
*/
|
|
1042
|
-
function
|
|
1129
|
+
function isExemptFunctionValue(node) {
|
|
1043
1130
|
return (!!node &&
|
|
1044
1131
|
(node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
1045
1132
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
1046
|
-
|
|
1133
|
+
isExemptFromBooleanNaming(node));
|
|
1047
1134
|
}
|
|
1048
1135
|
exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
1049
1136
|
name: 'enforce-positive-naming',
|
|
@@ -1280,7 +1367,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1280
1367
|
// with `is`/`has` but is not a boolean, so its domain-correct negation
|
|
1281
1368
|
// ("isNotBlank") must not be flagged. The name heuristic alone cannot
|
|
1282
1369
|
// tell them apart; the initializer's return shape can.
|
|
1283
|
-
if (
|
|
1370
|
+
if (isExemptFunctionValue(node.init))
|
|
1284
1371
|
return;
|
|
1285
1372
|
const variableName = node.id.name;
|
|
1286
1373
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(variableName);
|
|
@@ -1323,8 +1410,9 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1323
1410
|
if (!isBooleanLike(node.id || node))
|
|
1324
1411
|
return;
|
|
1325
1412
|
// Skip validator predicates that return a non-boolean value (e.g.
|
|
1326
|
-
// `string | true`), whose negation is the domain-correct term
|
|
1327
|
-
|
|
1413
|
+
// `string | true`), whose negation is the domain-correct term, and any
|
|
1414
|
+
// function whose returns give no syntactic verdict.
|
|
1415
|
+
if (isExemptFromBooleanNaming(node))
|
|
1328
1416
|
return;
|
|
1329
1417
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(functionName);
|
|
1330
1418
|
if (isNegative) {
|
|
@@ -1348,7 +1436,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1348
1436
|
if (!isBooleanLike(node.key))
|
|
1349
1437
|
return;
|
|
1350
1438
|
// Skip validator predicates returning a non-boolean value.
|
|
1351
|
-
if (
|
|
1439
|
+
if (isExemptFunctionValue(node.value))
|
|
1352
1440
|
return;
|
|
1353
1441
|
const methodName = node.key.name;
|
|
1354
1442
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(methodName);
|
|
@@ -1373,7 +1461,7 @@ exports.enforcePositiveNaming = (0, createRule_1.createRule)({
|
|
|
1373
1461
|
if (!isBooleanLike(node.key))
|
|
1374
1462
|
return;
|
|
1375
1463
|
// Skip validator predicates returning a non-boolean value.
|
|
1376
|
-
if (
|
|
1464
|
+
if (isExemptFunctionValue(node.value))
|
|
1377
1465
|
return;
|
|
1378
1466
|
const propertyName = node.key.name;
|
|
1379
1467
|
const { isNegative, alternatives } = hasBooleanNegativeNaming(propertyName);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects every `RuleTester` case the suite declares WITHOUT executing any of
|
|
3
|
+
* them, by shadowing `run` on the shared tester instances and then loading each
|
|
4
|
+
* suite for its declarations alone.
|
|
5
|
+
*
|
|
6
|
+
* A guard that wants to exercise fixtures rather than documented snippets has no
|
|
7
|
+
* other way in. `src/tests/*.test.ts` call `RuleTester.run` at module scope, so
|
|
8
|
+
* importing one normally re-executes it — measured at 2350 tests, 2 minutes and
|
|
9
|
+
* 48 cross-file side-effect failures, which is why
|
|
10
|
+
* `recommended-config-fix-closure.test.ts` reads docs fenced blocks instead.
|
|
11
|
+
* Shadowing `run` before the load turns each of those calls into a declaration
|
|
12
|
+
* capture, so the cases are collected at the cost of loading the module and
|
|
13
|
+
* nothing more.
|
|
14
|
+
*
|
|
15
|
+
* The fixtures are worth reaching precisely because they are not the docs: a
|
|
16
|
+
* rule's `valid` list is written to sit on its carve-out boundaries, which is
|
|
17
|
+
* where a sibling fixer destroys an exemption. Every finding of that class
|
|
18
|
+
* (#1595-#1599, #1603, #1677-#1682) came from this corpus; the docs corpus
|
|
19
|
+
* caught none of them.
|
|
20
|
+
*/
|
|
21
|
+
/** A single `ruleTester.run(name, rule, tests)` call, captured but not run. */
|
|
22
|
+
export type HarvestedSuite = {
|
|
23
|
+
/** The display name the suite passed to `run`. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Which shared tester export declared it, which fixes the parser. */
|
|
26
|
+
tester: string;
|
|
27
|
+
/** Basename of the declaring file, so a finding is reproducible by hand. */
|
|
28
|
+
file: string;
|
|
29
|
+
/**
|
|
30
|
+
* The rule object itself. Callers resolve a rule NAME from this by identity
|
|
31
|
+
* against the plugin's own map rather than from `name`: 100 of the suites
|
|
32
|
+
* pass a display name that is not a rule name (`requireMemo`,
|
|
33
|
+
* `prefer-next-dynamic (JSX scenarios)`, `no-hungarian-phone-number-test`),
|
|
34
|
+
* and name-keyed matching silently drops every one of them.
|
|
35
|
+
*/
|
|
36
|
+
rule: unknown;
|
|
37
|
+
valid: readonly unknown[];
|
|
38
|
+
invalid: readonly unknown[];
|
|
39
|
+
};
|
|
40
|
+
export type HarvestResult = {
|
|
41
|
+
suites: HarvestedSuite[];
|
|
42
|
+
/** Files that threw while loading, `basename: message`. */
|
|
43
|
+
failures: string[];
|
|
44
|
+
/** Non-vacuity accounting: a silent drop here would fake a clean sweep. */
|
|
45
|
+
filesLoaded: number;
|
|
46
|
+
filesSkipped: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function harvestRuleTesterCases(): HarvestResult;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.harvestRuleTesterCases = void 0;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const os = __importStar(require("os"));
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
const sharedTesters = __importStar(require("./ruleTester"));
|
|
31
|
+
const TESTS_DIR = path.join(__dirname, '..', 'tests');
|
|
32
|
+
/**
|
|
33
|
+
* Only suites that import the shared tester module can declare a case, since
|
|
34
|
+
* `src/tests/no-local-rule-tester.test.ts` forbids a locally-built tester. That
|
|
35
|
+
* makes the import a sound admission test rather than a heuristic, and it is
|
|
36
|
+
* what keeps this affordable: the ~28 files it excludes are the meta-suites
|
|
37
|
+
* (`fixer-type-safety`, `docs-examples-conformance`, `rule-crash-robustness`,
|
|
38
|
+
* this guard's own siblings) which run full corpus sweeps at module scope and
|
|
39
|
+
* cost more to load than every rule suite combined.
|
|
40
|
+
*
|
|
41
|
+
* Matching the import rather than a `ruleTesterTs.run(` call site is
|
|
42
|
+
* deliberate: `prefer-next-dynamic.test.ts` aliases the tester
|
|
43
|
+
* (`const jsx = ruleTesterJsx`) before calling `run`, so a call-site pattern
|
|
44
|
+
* drops it.
|
|
45
|
+
*/
|
|
46
|
+
const IMPORTS_SHARED_TESTER = /from\s+'\.\.\/utils\/ruleTester'/;
|
|
47
|
+
/**
|
|
48
|
+
* Jest registers a test for every `describe`/`it` a loaded module calls, so
|
|
49
|
+
* loading 271 suites inside a suite would graft their entire test list onto
|
|
50
|
+
* this one. Neutralizing the registrars for the duration of the load keeps the
|
|
51
|
+
* captured declarations and discards the registrations.
|
|
52
|
+
*
|
|
53
|
+
* `describe` bodies still execute — several suites call `run` inside one, and
|
|
54
|
+
* skipping the body would drop those cases — but everything that would register
|
|
55
|
+
* or assert becomes a no-op.
|
|
56
|
+
*/
|
|
57
|
+
const REGISTRAR_GLOBALS = [
|
|
58
|
+
'it',
|
|
59
|
+
'test',
|
|
60
|
+
'beforeEach',
|
|
61
|
+
'afterEach',
|
|
62
|
+
'beforeAll',
|
|
63
|
+
'afterAll',
|
|
64
|
+
];
|
|
65
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
66
|
+
const asAny = (value) => value;
|
|
67
|
+
const noop = () => undefined;
|
|
68
|
+
/** `it.each(rows)(name, fn)` is a call chain, so the stub needs the same shape. */
|
|
69
|
+
const withEach = (fn) => {
|
|
70
|
+
fn.each = () => () => undefined;
|
|
71
|
+
fn.only = fn;
|
|
72
|
+
fn.skip = fn;
|
|
73
|
+
fn.todo = noop;
|
|
74
|
+
fn.failing = fn;
|
|
75
|
+
fn.concurrent = fn;
|
|
76
|
+
return fn;
|
|
77
|
+
};
|
|
78
|
+
function harvestRuleTesterCases() {
|
|
79
|
+
const suites = [];
|
|
80
|
+
const failures = [];
|
|
81
|
+
let currentFile = '';
|
|
82
|
+
let filesLoaded = 0;
|
|
83
|
+
let filesSkipped = 0;
|
|
84
|
+
const testerEntries = Object.entries(sharedTesters).filter(([, value]) => typeof asAny(value)?.run === 'function');
|
|
85
|
+
const originalRun = new Map();
|
|
86
|
+
for (const [key, tester] of testerEntries) {
|
|
87
|
+
originalRun.set(key, tester.run);
|
|
88
|
+
tester.run = (name, rule, tests) => {
|
|
89
|
+
const bag = asAny(tests) || {};
|
|
90
|
+
suites.push({
|
|
91
|
+
name,
|
|
92
|
+
tester: key,
|
|
93
|
+
file: currentFile,
|
|
94
|
+
rule,
|
|
95
|
+
valid: bag.valid || [],
|
|
96
|
+
invalid: bag.invalid || [],
|
|
97
|
+
});
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const globalScope = global;
|
|
102
|
+
const savedGlobals = new Map();
|
|
103
|
+
const stub = (key, value) => {
|
|
104
|
+
savedGlobals.set(key, globalScope[key]);
|
|
105
|
+
globalScope[key] = value;
|
|
106
|
+
};
|
|
107
|
+
stub('describe', withEach((_name, body) => {
|
|
108
|
+
if (typeof body === 'function')
|
|
109
|
+
body();
|
|
110
|
+
}));
|
|
111
|
+
for (const key of REGISTRAR_GLOBALS)
|
|
112
|
+
stub(key, withEach(noop));
|
|
113
|
+
/**
|
|
114
|
+
* A handful of suites write fixture files at module scope, and
|
|
115
|
+
* `test-file-location-enforcement` writes them under
|
|
116
|
+
* `path.join(process.cwd(), '.cursor/tmp/...')` — a path it also `rmSync`s in
|
|
117
|
+
* an `afterAll` that the stubs above turn into a no-op. Loading it from the
|
|
118
|
+
* real working directory would therefore race that suite when it runs
|
|
119
|
+
* concurrently in another worker (its cleanup landing between this harvest's
|
|
120
|
+
* `mkdirSync` and `writeFileSync` throws ENOENT) and would strand fixtures in
|
|
121
|
+
* the repo when it does not. Pointing the working directory at a private
|
|
122
|
+
* scratch root for the duration of the load sends every cwd-derived write
|
|
123
|
+
* somewhere no other worker can see, and it is removed below.
|
|
124
|
+
*
|
|
125
|
+
* The paths the suites *record* are unaffected: they are made relative to the
|
|
126
|
+
* same cwd they were built from, so the harvested filenames come out
|
|
127
|
+
* identical either way.
|
|
128
|
+
*/
|
|
129
|
+
const scratchRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blumint-harvest-'));
|
|
130
|
+
const realCwd = process.cwd();
|
|
131
|
+
process.chdir(scratchRoot);
|
|
132
|
+
try {
|
|
133
|
+
const files = fs
|
|
134
|
+
.readdirSync(TESTS_DIR)
|
|
135
|
+
.filter((file) => file.endsWith('.test.ts'))
|
|
136
|
+
.sort();
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const fullPath = path.join(TESTS_DIR, file);
|
|
139
|
+
if (!IMPORTS_SHARED_TESTER.test(fs.readFileSync(fullPath, 'utf8'))) {
|
|
140
|
+
filesSkipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
currentFile = file;
|
|
144
|
+
try {
|
|
145
|
+
require(fullPath);
|
|
146
|
+
filesLoaded++;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
failures.push(`${file}: ${error?.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
process.chdir(realCwd);
|
|
155
|
+
fs.rmSync(scratchRoot, { recursive: true, force: true });
|
|
156
|
+
for (const [key, tester] of testerEntries) {
|
|
157
|
+
const original = originalRun.get(key);
|
|
158
|
+
if (original)
|
|
159
|
+
tester.run = original;
|
|
160
|
+
}
|
|
161
|
+
for (const [key, value] of savedGlobals)
|
|
162
|
+
globalScope[key] = value;
|
|
163
|
+
}
|
|
164
|
+
return { suites, failures, filesLoaded, filesSkipped };
|
|
165
|
+
}
|
|
166
|
+
exports.harvestRuleTesterCases = harvestRuleTesterCases;
|
|
167
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
168
|
+
//# sourceMappingURL=harvestRuleTesterCases.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.97",
|
|
4
|
+
"date": "2026-08-04T08:50:14.394Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1690,
|
|
11
|
+
1691
|
|
12
|
+
],
|
|
13
|
+
"summary": "infer a callee's return from its body when the annotation is absent (closes #1691); accept a digit or $ fused onto an UPPER_SNAKE prefix (closes #1690)"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "enforce-positive-naming",
|
|
17
|
+
"changeType": "fix",
|
|
18
|
+
"issues": [
|
|
19
|
+
1692
|
|
20
|
+
],
|
|
21
|
+
"summary": "decline when a function's returns yield no verdict (closes #1692)"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
2
25
|
{
|
|
3
26
|
"version": "1.20.96",
|
|
4
27
|
"date": "2026-08-04T07:23:58.552Z",
|