@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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.96',
226
+ version: '1.20.97',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- // we require an underscore boundary.
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
- return nextChar === '_';
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
- * Whether any `return` statement belonging to `fn`'s own body (not a nested
999
- * function's) yields a definitively non-boolean value.
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 blockReturnsNonBoolean(block) {
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 false;
1093
+ return combineReturnKinds(kinds);
1022
1094
  }
1023
1095
  /**
1024
- * Whether a function backing an `is`/`has`-prefixed name actually returns a
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) are inspected.
1098
+ * `return` statements (or the concise-arrow expression) decide.
1028
1099
  */
1029
- function functionReturnsNonBoolean(fn) {
1100
+ function classifyFunctionReturn(fn) {
1030
1101
  if (fn.returnType) {
1031
- return !isBooleanOnlyType(fn.returnType.typeAnnotation);
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 isDefinitelyNonBooleanExpression(fn.body);
1107
+ return classifyExpression(fn.body);
1035
1108
  }
1036
- return blockReturnsNonBoolean(fn.body);
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 a
1040
- * non-boolean predicate that must be exempt from boolean negative-naming.
1126
+ * When a declarator/property value is a function, whether that function is
1127
+ * exempt from boolean negative-naming.
1041
1128
  */
1042
- function isNonBooleanFunctionValue(node) {
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
- functionReturnsNonBoolean(node));
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 (isNonBooleanFunctionValue(node.init))
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
- if (functionReturnsNonBoolean(node))
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 (isNonBooleanFunctionValue(node.value))
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 (isNonBooleanFunctionValue(node.value))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.96",
3
+ "version": "1.20.97",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",