@blumintinc/eslint-plugin-blumint 1.20.152 → 1.20.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -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.152',
226
+ version: '1.20.154',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -69,6 +69,111 @@ function memberNameOf(key) {
69
69
  }
70
70
  return undefined;
71
71
  }
72
+ const EQUALITY_OPERATORS = new Set([
73
+ '===',
74
+ '!==',
75
+ '==',
76
+ '!=',
77
+ ]);
78
+ /**
79
+ * The text of a literal string, written either way round: `'string'` and
80
+ * `` `string` `` assert the same thing about the operand beside them.
81
+ */
82
+ function stringLiteralValueOf(node) {
83
+ if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
84
+ return node.value;
85
+ }
86
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
87
+ node.expressions.length === 0 &&
88
+ node.quasis.length === 1) {
89
+ return node.quasis[0].value.cooked;
90
+ }
91
+ return undefined;
92
+ }
93
+ /**
94
+ * The operand an equality comparison holds opposite `operand`, so operand
95
+ * order carries no meaning: `typeof x === 'string'` and
96
+ * `'string' === typeof x` are the same assertion.
97
+ */
98
+ function comparedAgainst(comparison, operand) {
99
+ if (!EQUALITY_OPERATORS.has(comparison.operator))
100
+ return undefined;
101
+ if (comparison.left === operand)
102
+ return comparison.right;
103
+ if (comparison.right === operand)
104
+ return comparison.left;
105
+ return undefined;
106
+ }
107
+ /**
108
+ * The outermost node standing for the same value, so a contradiction written
109
+ * around `verdict!` or `verdict as string` is a contradiction about
110
+ * `verdict`.
111
+ */
112
+ function passthroughValueOf(node) {
113
+ let current = node;
114
+ while (current.parent) {
115
+ const { parent } = current;
116
+ const wraps = (parent.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
117
+ parent.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
118
+ parent.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
119
+ parent.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
120
+ parent.type === utils_1.AST_NODE_TYPES.ChainExpression) &&
121
+ parent.expression ===
122
+ current;
123
+ if (!wraps)
124
+ break;
125
+ current = parent;
126
+ }
127
+ return current;
128
+ }
129
+ /** `Error`, `TypeError` and any `…Error` class take a string message. */
130
+ function isErrorConstructor(callee) {
131
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
132
+ return callee.name.endsWith('Error');
133
+ }
134
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
135
+ const member = memberNameOf(callee.property);
136
+ return !!member && member.name.endsWith('Error');
137
+ }
138
+ return false;
139
+ }
140
+ /**
141
+ * Whether this reference uses the value in a way a boolean could not be
142
+ * used, which disproves a booleanness read off a name.
143
+ */
144
+ function referenceContradictsBoolean(reference) {
145
+ const value = passthroughValueOf(reference);
146
+ const { parent } = value;
147
+ if (!parent)
148
+ return false;
149
+ // `typeof verdict === 'string'`. A tag of `'boolean'` AFFIRMS the boolean
150
+ // reading whichever equality operator carries it — `!== 'boolean'` is how
151
+ // a boolean guard is spelled — so only some other tag contradicts.
152
+ if (parent.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
153
+ parent.operator === 'typeof' &&
154
+ parent.argument === value) {
155
+ const comparison = parent.parent;
156
+ if (comparison?.type !== utils_1.AST_NODE_TYPES.BinaryExpression)
157
+ return false;
158
+ const other = comparedAgainst(comparison, parent);
159
+ const tag = other ? stringLiteralValueOf(other) : undefined;
160
+ return tag !== undefined && tag !== 'boolean';
161
+ }
162
+ // `verdict === 'occupied'` — a value compared with a string is not a
163
+ // boolean, since no boolean is ever equal to one.
164
+ if (parent.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
165
+ const other = comparedAgainst(parent, value);
166
+ return !!other && stringLiteralValueOf(other) !== undefined;
167
+ }
168
+ // `throw new Error(verdict)` — the message parameter is a string, so the
169
+ // binding carries the failure reason rather than a verdict flag.
170
+ if (parent.type === utils_1.AST_NODE_TYPES.NewExpression &&
171
+ parent.arguments[0] === value &&
172
+ isErrorConstructor(parent.callee)) {
173
+ return true;
174
+ }
175
+ return false;
176
+ }
72
177
  exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
73
178
  name: 'enforce-boolean-naming-prefixes',
74
179
  meta: {
@@ -914,6 +1019,66 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
914
1019
  }
915
1020
  return calleeReturnEvaluation(calleeName) !== 'nonBoolean';
916
1021
  }
1022
+ /**
1023
+ * Whether the only evidence that a binding holds a boolean is a NAME.
1024
+ *
1025
+ * `calleeReturnEvaluation` answers "does this callee demonstrably return a
1026
+ * non-boolean?"; its 'indeterminate' verdict is the case where the callee's
1027
+ * body is out of reach (an import, a parameter, a value read off a builder
1028
+ * chain) and the callee's `is`/`has`/`can` prefix is all that is left. A
1029
+ * boolean-sounding property (`state.isValid`) is the same kind of evidence.
1030
+ *
1031
+ * Everything else — an explicit `: boolean` annotation, a boolean literal, a
1032
+ * comparison or negation, a `Boolean()` coercion, a resolvable declaration
1033
+ * whose return classifies as boolean — is evidence about the VALUE, which no
1034
+ * use site is allowed to outrank.
1035
+ */
1036
+ function booleanEvidenceIsNameOnly(declarator) {
1037
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
1038
+ hasBooleanTypeAnnotation(declarator.id) ||
1039
+ !declarator.init) {
1040
+ return false;
1041
+ }
1042
+ const restsOnName = (expression) => {
1043
+ const value = unwrapChainExpression(expression);
1044
+ // A property name is the whole of the evidence in
1045
+ // `isLikelyBooleanByMemberExpression`, the only path that reads one.
1046
+ if (value.type === utils_1.AST_NODE_TYPES.MemberExpression) {
1047
+ return true;
1048
+ }
1049
+ if (value.type === utils_1.AST_NODE_TYPES.CallExpression &&
1050
+ value.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
1051
+ return (!isGlobalBooleanCall(value) &&
1052
+ calleeReturnEvaluation(value.callee.name) === 'indeterminate');
1053
+ }
1054
+ // `isFoo(x) || fallback` reaches booleanness through its left operand.
1055
+ if (value.type === utils_1.AST_NODE_TYPES.LogicalExpression &&
1056
+ value.operator === '||') {
1057
+ return restsOnName(value.left);
1058
+ }
1059
+ return false;
1060
+ };
1061
+ return restsOnName(declarator.init);
1062
+ }
1063
+ /**
1064
+ * Whether any use of the binding contradicts booleanness.
1065
+ *
1066
+ * Validator families built on `ValidatorPipeline` return `true | string` —
1067
+ * `true` for a pass, the failure message for a fail — while
1068
+ * `enforce-is-prefix-validators` requires the validator itself to be
1069
+ * `is`-prefixed. Inferring the result's booleanness from that mandated
1070
+ * prefix makes the two rules unsatisfiable together, so a use site that
1071
+ * reads the value as a string settles it against the name.
1072
+ *
1073
+ * References come from the scope manager, never from matching the name as
1074
+ * text: a contradiction must belong to THIS binding, not to a shadowing
1075
+ * inner one, a sibling scope's binding, or an unrelated same-named value.
1076
+ */
1077
+ function useSiteContradictsBoolean(declarator) {
1078
+ return context
1079
+ .getDeclaredVariables(declarator)
1080
+ .some((variable) => variable.references.some((reference) => referenceContradictsBoolean(reference.identifier)));
1081
+ }
917
1082
  /**
918
1083
  * Check if a variable is used in a while loop condition and is likely a DOM element or tree node
919
1084
  * This helps identify variables like 'parent', 'element', 'node', etc. that are used
@@ -1233,6 +1398,14 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
1233
1398
  utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
1234
1399
  isBooleanVar = true;
1235
1400
  }
1401
+ // A booleanness read off a name loses to a use site that treats the value
1402
+ // as something else, which is what keeps this rule satisfiable alongside
1403
+ // `enforce-is-prefix-validators` for `true | string` validator verdicts.
1404
+ if (isBooleanVar &&
1405
+ booleanEvidenceIsNameOnly(node) &&
1406
+ useSiteContradictsBoolean(node)) {
1407
+ return;
1408
+ }
1236
1409
  if (isBooleanVar && !hasApprovedPrefix(variableName)) {
1237
1410
  context.report({
1238
1411
  node: node.id,
@@ -207,25 +207,41 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
207
207
  return undefined;
208
208
  }
209
209
  /**
210
- * `as const` turns an array literal into a readonly *tuple*, which TS4104
211
- * refuses to assign to a mutable array or tuple. Where the annotation says
212
- * the value must be mutable, appending `as const` breaks the build, and no
213
- * edit at the literal can satisfy the rule — honouring it would mean
214
- * rewriting the signature, a call the author has to make. So the rule stays
215
- * silent rather than reporting something the developer cannot act on
216
- * (#1526).
210
+ * `as const` turns an array literal into a fixed-length readonly *tuple*,
211
+ * strictly narrower than the mutable array the literal otherwise gets. Two
212
+ * separate breakages follow from that narrowing, and neither is visible at
213
+ * the literal:
214
+ *
215
+ * - Where the enclosing signature declares a mutable array or tuple, TS4104
216
+ * refuses the assignment, so appending `as const` breaks the build. No
217
+ * edit at the literal satisfies the rule — honouring it means rewriting
218
+ * the signature, a call the author has to make (#1526).
219
+ * - Where the signature is inferred, the frozen arity becomes part of the
220
+ * return type and every caller inherits it: `.length` narrows to a literal
221
+ * number (TS2367 against any other length), `.includes` narrows its
222
+ * parameter to the element union — `never` for `[]` — (TS2345), and the
223
+ * value stops satisfying a mutable `T[]` parameter. The break lands in a
224
+ * different function than the one edited, and the callers are beyond what
225
+ * the rule can see (#2015).
226
+ *
227
+ * So an array literal is left alone unless the enclosing signature states a
228
+ * type that accepts a readonly tuple. An annotation the rule cannot resolve
229
+ * still counts as accepting, per `acceptsReadonlyArray`: the annotation, not
230
+ * the literal, is what callers read, so the arity never escapes.
217
231
  *
218
232
  * Object literals are unaffected: `readonly` property modifiers do not
219
233
  * enter assignability, so `{ a: 1 } as const` still satisfies a mutable
220
- * `{ a: number }`.
234
+ * `{ a: number }`, and freezing one fixes no arity.
221
235
  */
222
- function conflictsWithDeclaredType(literal, ancestors) {
236
+ function freezingArrayIsUnsafe(literal, ancestors) {
223
237
  if (!isArrayLiteral(literal)) {
224
238
  return false;
225
239
  }
226
240
  const enclosingFunction = enclosingFunctionOf(ancestors);
227
- if (!enclosingFunction) {
228
- return false;
241
+ // With no declared return type in view, the inferred tuple is what the
242
+ // callers get.
243
+ if (!enclosingFunction || !declaredReturnTypeOf(enclosingFunction)) {
244
+ return true;
229
245
  }
230
246
  const returnedValueType = returnedValueTypeOf(enclosingFunction);
231
247
  return !!returnedValueType && !acceptsReadonlyArray(returnedValueType);
@@ -285,10 +301,10 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
285
301
  if (isInsideReactHook(ancestors) && isArrayLiteral(literal)) {
286
302
  return;
287
303
  }
288
- // Skip arrays the enclosing signature declares mutable: `as const`
289
- // cannot compile there and the developer cannot act on the report
290
- // (#1526)
291
- if (conflictsWithDeclaredType(literal, ancestors)) {
304
+ // Skip arrays whose enclosing signature does not accept the readonly
305
+ // tuple `as const` produces declared mutable (#1526) or inferred, in
306
+ // which case the frozen arity reaches every caller (#2015)
307
+ if (freezingArrayIsUnsafe(literal, ancestors)) {
292
308
  return;
293
309
  }
294
310
  // Report the issue and provide a fix
@@ -108,6 +108,139 @@ const isComponentFactoryCall = (node) => {
108
108
  // the same terms as `const Row = (props) => {...}` (Issue #1681).
109
109
  const isFunctionValue = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
110
110
  node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
111
+ // `as const` does more than pin literal types: it makes the value deeply
112
+ // `readonly`. A binding that is written through after its declaration therefore
113
+ // cannot carry the assertion at all — appending it turns compiling code into
114
+ // `TS2339: Property 'push' does not exist on type 'readonly []'` for an array
115
+ // and `TS2540: Cannot assign to 'a' because it is a read-only property` for an
116
+ // object (Issue #2013). These are the built-in methods that mutate their
117
+ // receiver rather than returning a fresh value, so a call to one of them is a
118
+ // write even though no assignment target names the binding.
119
+ const MUTATING_METHOD_NAMES = new Set([
120
+ 'push',
121
+ 'pop',
122
+ 'shift',
123
+ 'unshift',
124
+ 'splice',
125
+ 'sort',
126
+ 'reverse',
127
+ 'fill',
128
+ 'copyWithin',
129
+ ]);
130
+ /**
131
+ * Climbs out of the wrappers that denote the same value as `node` — type
132
+ * wrappers (`(X as any).push()`, `X!.push()`) and the `ChainExpression` an
133
+ * optional access hangs on the outside of the whole chain (`delete X?.a`). The
134
+ * role a node plays in its statement is decided by the outermost such wrapper,
135
+ * so a classifier that reads `node.parent` directly answers for the wrapper
136
+ * instead of the access.
137
+ */
138
+ const outermostValueOf = (node) => {
139
+ let current = node;
140
+ for (;;) {
141
+ const parent = current.parent;
142
+ if (parent &&
143
+ ((isValueWrapper(parent) && parent.expression === current) ||
144
+ (parent.type === utils_1.AST_NODE_TYPES.ChainExpression &&
145
+ parent.expression === current))) {
146
+ current = parent;
147
+ continue;
148
+ }
149
+ return current;
150
+ }
151
+ };
152
+ /**
153
+ * The outermost property-access path rooted at `identifier`: `X` in `X.a.b`
154
+ * yields the `X.a.b` member expression. Returns `null` when the identifier is
155
+ * not the base of any access, which is every reference that merely reads the
156
+ * binding as a value — `other.push(X)` passes it as an ARGUMENT, so the
157
+ * mutation happens to `other`, not to `X`.
158
+ *
159
+ * The climb stops at the first parent that is not a member access on the
160
+ * current node, so `X.map(f).push(1)` yields `X.map`: the mutated receiver
161
+ * there is the array `map` returned, not `X`.
162
+ */
163
+ const accessPathOf = (identifier) => {
164
+ let current = outermostValueOf(identifier);
165
+ let path = null;
166
+ for (;;) {
167
+ const parent = current.parent;
168
+ if (!parent ||
169
+ parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
170
+ parent.object !== current) {
171
+ return path;
172
+ }
173
+ path = parent;
174
+ current = outermostValueOf(parent);
175
+ }
176
+ };
177
+ /** The property name an access reads, for `X.push` and `X['push']` alike. */
178
+ const accessedPropertyName = (path) => {
179
+ if (!path.computed && path.property.type === utils_1.AST_NODE_TYPES.Identifier) {
180
+ return path.property.name;
181
+ }
182
+ if (path.computed &&
183
+ path.property.type === utils_1.AST_NODE_TYPES.Literal &&
184
+ typeof path.property.value === 'string') {
185
+ return path.property.value;
186
+ }
187
+ return null;
188
+ };
189
+ const isMutatingMethodCall = (path) => {
190
+ const propertyName = accessedPropertyName(path);
191
+ if (propertyName === null || !MUTATING_METHOD_NAMES.has(propertyName)) {
192
+ return false;
193
+ }
194
+ const callee = outermostValueOf(path);
195
+ return (callee.parent?.type === utils_1.AST_NODE_TYPES.CallExpression &&
196
+ callee.parent.callee === callee);
197
+ };
198
+ /**
199
+ * Whether `node` sits in a position that writes to it: the left of an
200
+ * assignment (plain or compound), the operand of `++`/`--` or `delete`, the
201
+ * loop variable of `for…in`/`for…of`, or a slot in a destructuring assignment
202
+ * target (`[X.a] = […]`, `({ p: X.a } = …)`).
203
+ */
204
+ const isWriteTarget = (node) => {
205
+ const value = outermostValueOf(node);
206
+ const parent = value.parent;
207
+ if (!parent) {
208
+ return false;
209
+ }
210
+ switch (parent.type) {
211
+ case utils_1.AST_NODE_TYPES.AssignmentExpression:
212
+ return parent.left === value;
213
+ case utils_1.AST_NODE_TYPES.UpdateExpression:
214
+ return parent.argument === value;
215
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
216
+ return parent.operator === 'delete' && parent.argument === value;
217
+ case utils_1.AST_NODE_TYPES.ForInStatement:
218
+ case utils_1.AST_NODE_TYPES.ForOfStatement:
219
+ return parent.left === value;
220
+ // Destructuring targets nest, so the answer belongs to the pattern's own
221
+ // position. The same node types appear in ObjectExpression/ArrayExpression
222
+ // VALUES, where the recursion reaches a non-assignment parent and stops.
223
+ case utils_1.AST_NODE_TYPES.ArrayPattern:
224
+ case utils_1.AST_NODE_TYPES.ObjectPattern:
225
+ case utils_1.AST_NODE_TYPES.Property:
226
+ case utils_1.AST_NODE_TYPES.RestElement:
227
+ case utils_1.AST_NODE_TYPES.AssignmentPattern:
228
+ return isWriteTarget(parent);
229
+ default:
230
+ return false;
231
+ }
232
+ };
233
+ /**
234
+ * Whether the binding is written through anywhere in the file. Answered from
235
+ * the scope manager's reference list rather than a textual search for the
236
+ * name, so a same-named binding in another scope (`const arr` shadowed inside a
237
+ * callback) contributes nothing, and a same-named method on an unrelated
238
+ * receiver (`other.push(1)`) is never even visited.
239
+ */
240
+ const isBindingMutated = (variable) => variable.references.some((reference) => {
241
+ const path = accessPathOf(reference.identifier);
242
+ return path !== null && (isMutatingMethodCall(path) || isWriteTarget(path));
243
+ });
111
244
  /**
112
245
  * Walks the scope chain upward from `scope` (inclusive) and reports whether
113
246
  * `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
@@ -366,9 +499,22 @@ exports.default = (0, createRule_1.createRule)({
366
499
  (target.value === null || typeof target.value === 'boolean')) {
367
500
  return false;
368
501
  }
369
- return (target.type === utils_1.AST_NODE_TYPES.Literal ||
370
- target.type === utils_1.AST_NODE_TYPES.ArrayExpression ||
371
- target.type === utils_1.AST_NODE_TYPES.ObjectExpression);
502
+ if (target.type !== utils_1.AST_NODE_TYPES.Literal &&
503
+ target.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
504
+ target.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
505
+ return false;
506
+ }
507
+ // A binding that is mutated later can never take the assertion:
508
+ // `as const` types the value `readonly`, so the appended text
509
+ // turns working code into TS2339/TS2540 (Issue #2013). The
510
+ // report is withheld rather than merely the fix, on the same
511
+ // terms as the `null`/boolean carve-out above — a violation no
512
+ // legal edit can clear is not a violation. The rename is a
513
+ // separate concern and still applies.
514
+ const declaredVariable = context
515
+ .getDeclaredVariables(declaration)
516
+ .find((variable) => variable.name === name);
517
+ return !declaredVariable || !isBindingMutated(declaredVariable);
372
518
  };
373
519
  if (shouldHaveAsConst(init)) {
374
520
  context.report({
@@ -713,6 +713,104 @@ function declaresVoidResult(returnType) {
713
713
  return (typeArguments?.length === 1 &&
714
714
  typeArguments[0].type === utils_1.AST_NODE_TYPES.TSVoidKeyword);
715
715
  }
716
+ // TypeScript's built-in decorator signatures. A factory annotated with one of
717
+ // these is the one shape where the annotation is WIDER than what inference
718
+ // produces rather than a restatement of it: `MethodDecorator` accepts three
719
+ // parameters, the returned closure typically declares none, and a decoration
720
+ // site requires the declared arity. Stripping the annotation therefore turns
721
+ // every `@Factory()` use into TS1329 (#2014).
722
+ const DECORATOR_TYPE_NAMES = new Set([
723
+ 'ClassDecorator',
724
+ 'MethodDecorator',
725
+ 'ParameterDecorator',
726
+ 'PropertyDecorator',
727
+ ]);
728
+ /**
729
+ * The identifier a type name resolves to. A qualified name (`ts.MethodDecorator`)
730
+ * denotes the type its right-most segment names, so that segment is what decides
731
+ * — a substring test over the printed annotation would equally match
732
+ * `MyMethodDecoratorConfig`, which is an unrelated user type.
733
+ */
734
+ function rightmostTypeName(typeName) {
735
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
736
+ return typeName.name;
737
+ }
738
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
739
+ return rightmostTypeName(typeName.right);
740
+ }
741
+ return undefined;
742
+ }
743
+ function namesDecoratorType(annotation) {
744
+ if (annotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
745
+ const name = rightmostTypeName(annotation.typeName);
746
+ return name !== undefined && DECORATOR_TYPE_NAMES.has(name);
747
+ }
748
+ // A factory usable in more than one position (`ClassDecorator &
749
+ // MethodDecorator`) still owes every decoration site the declared shape.
750
+ if (annotation.type === utils_1.AST_NODE_TYPES.TSUnionType ||
751
+ annotation.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
752
+ return annotation.types.some(namesDecoratorType);
753
+ }
754
+ return false;
755
+ }
756
+ /**
757
+ * The identifier a CALLED decorator invokes: `Log` for `@Log()` and `@Log()()`.
758
+ *
759
+ * Only a called decorator identifies a factory, and only a factory's return type
760
+ * is what the decoration site consumes. A bare `@Log` names the decorator
761
+ * itself, whose annotation restates the value it returns exactly as inference
762
+ * would — so it stays reportable rather than being silenced by proximity to a
763
+ * decorator.
764
+ *
765
+ * An owner-qualified decorator (`@registry.log()`) names a property rather than
766
+ * a binding, and matching it by property name alone would silence the rule on
767
+ * every unrelated method of the same name, so it yields nothing.
768
+ */
769
+ function decoratorFactoryIdentifier(expression) {
770
+ if (expression.type !== utils_1.AST_NODE_TYPES.CallExpression)
771
+ return undefined;
772
+ const callee = expression.callee;
773
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
774
+ return callee;
775
+ }
776
+ return decoratorFactoryIdentifier(callee);
777
+ }
778
+ /**
779
+ * The declarations invoked by a decorator in this file.
780
+ *
781
+ * This catches the factory whose annotation is a user-defined decorator type
782
+ * (`type Cached = (t: object, k: string, d: PropertyDescriptor) => void`), which
783
+ * no name test can recognise. Each identifier is resolved through the scope
784
+ * manager rather than compared by name, so a same-named binding elsewhere in the
785
+ * file cannot silence the rule on a function no decorator actually reaches.
786
+ */
787
+ function decoratorReferencedDeclarations(source, visitorKeys) {
788
+ const heads = new Set();
789
+ const stack = [source.ast];
790
+ while (stack.length > 0) {
791
+ const current = stack.pop();
792
+ if (current.type === utils_1.AST_NODE_TYPES.Decorator) {
793
+ const head = decoratorFactoryIdentifier(current.expression);
794
+ if (head) {
795
+ heads.add(head);
796
+ }
797
+ }
798
+ pushChildren(current, visitorKeys, stack);
799
+ }
800
+ const declarations = new Set();
801
+ if (heads.size === 0)
802
+ return declarations;
803
+ for (const scope of source.scopeManager?.scopes ?? []) {
804
+ for (const reference of scope.references) {
805
+ if (!heads.has(reference.identifier))
806
+ continue;
807
+ for (const definition of reference.resolved?.defs ?? []) {
808
+ declarations.add(definition.node);
809
+ }
810
+ }
811
+ }
812
+ return declarations;
813
+ }
716
814
  function containsRange(outer, inner) {
717
815
  return inner[0] >= outer[0] && inner[1] <= outer[1];
718
816
  }
@@ -967,6 +1065,40 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
967
1065
  // Edges are resolved lazily, and only for functions a direct
968
1066
  // self-reference has already failed to explain.
969
1067
  const participatesInReturnCycle = createReturnCycleResolver(visitorKeys);
1068
+ // Decorators are visited after the functions they name — a class body is
1069
+ // walked long after the top-level factory it decorates with — so the
1070
+ // answer is computed from the whole tree rather than accumulated during
1071
+ // the walk, and memoised because most files hold no decorator at all.
1072
+ let decoratedDeclarations;
1073
+ const declarationsNamedByDecorators = () => {
1074
+ decoratedDeclarations ??= decoratorReferencedDeclarations(sourceCode, visitorKeys);
1075
+ return decoratedDeclarations;
1076
+ };
1077
+ /**
1078
+ * True when the annotation is what makes the function usable in a
1079
+ * decorator position. TypeScript infers the concrete closure the factory
1080
+ * returns — `() => void` for `return () => {};` — which declares fewer
1081
+ * parameters than a decoration site passes, so removing the annotation
1082
+ * turns every `@Factory()` use into TS1329 (#2014).
1083
+ *
1084
+ * The question is answered syntactically. A `RuleTester` fixture carries
1085
+ * no `parserOptions.project`, so a type-based answer would be untestable
1086
+ * and would silently no-op wherever consumers lint without a program.
1087
+ */
1088
+ function isDecoratorFactory(node, returnType) {
1089
+ if (namesDecoratorType(returnType.typeAnnotation))
1090
+ return true;
1091
+ const declarations = declarationsNamedByDecorators();
1092
+ if (declarations.size === 0)
1093
+ return false;
1094
+ if (declarations.has(node))
1095
+ return true;
1096
+ // `const Log = (): Cached => ...` is bound by its declarator, which is
1097
+ // what a decorator's identifier resolves to.
1098
+ const parent = node.parent;
1099
+ return (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
1100
+ declarations.has(parent));
1101
+ }
970
1102
  /**
971
1103
  * True when TypeScript cannot infer the return type because the function
972
1104
  * is referenced from within its own return expression (TS7023). Removing
@@ -1100,6 +1232,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1100
1232
  if (isTypeGuardFunction(node) ||
1101
1233
  isReadonlyWideningReturnType(returnType) ||
1102
1234
  isAllowedVoidReturnType(returnType) ||
1235
+ isDecoratorFactory(node, returnType) ||
1103
1236
  (mergedOptions.allowRecursiveFunctions &&
1104
1237
  isRecursiveFunction(node)) ||
1105
1238
  isReturnTypeRequiredByRecursion(node)) {
@@ -1117,6 +1250,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1117
1250
  if (isTypeGuardFunction(node) ||
1118
1251
  isReadonlyWideningReturnType(returnType) ||
1119
1252
  isAllowedVoidReturnType(returnType) ||
1253
+ isDecoratorFactory(node, returnType) ||
1120
1254
  (mergedOptions.allowRecursiveFunctions &&
1121
1255
  isRecursiveFunction(node)) ||
1122
1256
  isReturnTypeRequiredByRecursion(node)) {
@@ -1131,6 +1265,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1131
1265
  if (isTypeGuardFunction(node) ||
1132
1266
  isReadonlyWideningReturnType(returnType) ||
1133
1267
  isAllowedVoidReturnType(returnType) ||
1268
+ isDecoratorFactory(node, returnType) ||
1134
1269
  isReturnTypeRequiredByRecursion(node)) {
1135
1270
  return;
1136
1271
  }
@@ -1156,6 +1291,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1156
1291
  if (isTypeGuardFunction(node.value) ||
1157
1292
  isReadonlyWideningReturnType(returnType) ||
1158
1293
  isAllowedVoidReturnType(returnType) ||
1294
+ isDecoratorFactory(node, returnType) ||
1159
1295
  (mergedOptions.allowAbstractMethodSignatures &&
1160
1296
  isInterfaceOrAbstractMethodSignature(node)) ||
1161
1297
  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. (#1924)
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
- visit(node);
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
- return awaitExpr
1451
- ? getInstancePathKeys(awaitExpr.argument)
1452
- : new Set();
1511
+ if (!awaitExpr) {
1512
+ return new Set();
1513
+ }
1514
+ const read = getInstancePathKeys(awaitExpr.argument);
1515
+ const calleeFunction = resolveCalleeFunction(awaitExpr);
1516
+ if (!calleeFunction) {
1517
+ return read;
1518
+ }
1519
+ return new Set([...read, ...getInstancePathKeys(calleeFunction)]);
1453
1520
  });
1454
1521
  for (let i = 1; i < awaitNodes.length; i++) {
1455
1522
  const currentIds = allIdentifiers[i];
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Shared machinery for reading the documented examples out of `docs/rules/*.md`
3
+ * and linting them.
4
+ *
5
+ * Extracted so that more than one guard can ask a question of the SAME parsed
6
+ * corpus. `docs-examples-conformance` asks whether a block satisfies its own
7
+ * rule; `docs-correct-block-regression` asks whether the blocks #1982 fixed
8
+ * still satisfy the OTHER rule that used to report on them. Hand-rolling the
9
+ * fence walker or the candidate-filename list a second time is how two guards
10
+ * come to disagree about which blocks exist — the failure `fixtureCorpus.ts`
11
+ * exists to prevent on the RuleTester side, and the reason four guards there
12
+ * inherited the same two silent losses (#1984).
13
+ *
14
+ * The filename list in particular is load-bearing and must not be duplicated:
15
+ * many rules key off the path, so judging a block under a path the rule was
16
+ * never meant to see manufactures a failure.
17
+ */
18
+ export declare const PREFIX = "@blumintinc/blumint/";
19
+ export declare const DOCS_DIR: string;
20
+ export declare const pageExists: (rule: string) => boolean;
21
+ export declare const readPage: (rule: string) => string | null;
22
+ /** Fence languages that hold lintable TypeScript. */
23
+ export declare const LINTABLE_LANGS: Set<string>;
24
+ /**
25
+ * Candidate filenames, tried in order. Many rules key off the path (cloud
26
+ * function entry points, test-file exemptions, component directories), so a
27
+ * single hard-coded filename would make correct examples report for reasons the
28
+ * doc never claimed.
29
+ */
30
+ export declare const TS_CANDIDATES: string[];
31
+ export declare const TSX_CANDIDATES: string[];
32
+ /**
33
+ * Rules that match on path segments need a rooted path — `functions/src/types/x.ts`
34
+ * relative does not satisfy the same check that `/repo/functions/src/types/x.ts`
35
+ * does, which would fail a doc example for a reason the doc never claimed.
36
+ */
37
+ export declare const ROOT = "/repo/";
38
+ export declare const anchor: (p: string) => string;
39
+ export type Block = {
40
+ /**
41
+ * `null` for a fence under no example heading. Such blocks are kept rather
42
+ * than dropped: a page whose fences all come back unlabelled is a detection
43
+ * failure, and dropping them made it indistinguishable from a page that
44
+ * documents no examples at all (#1499).
45
+ */
46
+ polarity: 'correct' | 'incorrect' | null;
47
+ lang: string;
48
+ code: string;
49
+ line: number;
50
+ };
51
+ /**
52
+ * Classify an example heading.
53
+ *
54
+ * Only H2+ headings count: the H1 title ends with the rule id, and rule names
55
+ * routinely contain `prefer`, `valid`, or `no`, which would otherwise classify
56
+ * every block in the intro prose. The rule-id parenthetical is stripped for the
57
+ * same reason.
58
+ *
59
+ * Order matters — "incorrect" contains "correct" and "invalid" contains "valid",
60
+ * so the negative spellings must be tested first.
61
+ */
62
+ export declare function headingPolarity(line: string): Block['polarity'] | null;
63
+ /**
64
+ * Pull every fenced code block, tagged with the polarity of the example heading
65
+ * it sits under (`null` when it sits under none).
66
+ *
67
+ * Polarity is inherited by DEEPER headings, because docs routinely split an
68
+ * example section into named cases (`#### Option 1: …` under `### Examples of
69
+ * correct code`). Treating such a sub-heading as the end of the section dropped
70
+ * every block beneath it, which is how three whole pages asserted nothing.
71
+ */
72
+ export declare function extractBlocks(md: string): Block[];
73
+ /**
74
+ * Docs declare the context a snippet assumes inside the snippet itself:
75
+ * `// File: functions/src/...` (or a bare path comment) for path-sensitive
76
+ * rules, and `// eslint-options: {...}` for an example that only holds under a
77
+ * non-default option. Honouring both is what lets every correct block be
78
+ * enforced without exempting the awkward ones.
79
+ */
80
+ export declare function filenameHint(code: string): string | null;
81
+ export declare function optionsHint(code: string): unknown | null;
82
+ export type LintResult = {
83
+ reports: string[];
84
+ /** 1-based lines of the same reports, for segment attribution (#1622). */
85
+ reportLines: number[];
86
+ skipped: boolean;
87
+ reason?: string;
88
+ };
89
+ export declare function lintBlock(ruleName: string, filename: string, code: string, options: unknown | null): LintResult;
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.lintBlock = exports.optionsHint = exports.filenameHint = exports.extractBlocks = exports.headingPolarity = exports.anchor = exports.ROOT = exports.TSX_CANDIDATES = exports.TS_CANDIDATES = exports.LINTABLE_LANGS = exports.readPage = exports.pageExists = exports.DOCS_DIR = exports.PREFIX = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const eslint_1 = require("eslint");
10
+ /* eslint-disable @typescript-eslint/no-var-requires */
11
+ const plugin = require('../index');
12
+ const tsParser = require('@typescript-eslint/parser');
13
+ /* eslint-enable @typescript-eslint/no-var-requires */
14
+ /**
15
+ * Shared machinery for reading the documented examples out of `docs/rules/*.md`
16
+ * and linting them.
17
+ *
18
+ * Extracted so that more than one guard can ask a question of the SAME parsed
19
+ * corpus. `docs-examples-conformance` asks whether a block satisfies its own
20
+ * rule; `docs-correct-block-regression` asks whether the blocks #1982 fixed
21
+ * still satisfy the OTHER rule that used to report on them. Hand-rolling the
22
+ * fence walker or the candidate-filename list a second time is how two guards
23
+ * come to disagree about which blocks exist — the failure `fixtureCorpus.ts`
24
+ * exists to prevent on the RuleTester side, and the reason four guards there
25
+ * inherited the same two silent losses (#1984).
26
+ *
27
+ * The filename list in particular is load-bearing and must not be duplicated:
28
+ * many rules key off the path, so judging a block under a path the rule was
29
+ * never meant to see manufactures a failure.
30
+ */
31
+ exports.PREFIX = '@blumintinc/blumint/';
32
+ exports.DOCS_DIR = path_1.default.join(__dirname, '../../docs/rules');
33
+ const pageExists = (rule) => fs_1.default.existsSync(path_1.default.join(exports.DOCS_DIR, `${rule}.md`));
34
+ exports.pageExists = pageExists;
35
+ const readPage = (rule) => (0, exports.pageExists)(rule)
36
+ ? fs_1.default.readFileSync(path_1.default.join(exports.DOCS_DIR, `${rule}.md`), 'utf8')
37
+ : null;
38
+ exports.readPage = readPage;
39
+ /** Fence languages that hold lintable TypeScript. */
40
+ exports.LINTABLE_LANGS = new Set([
41
+ 'ts',
42
+ 'tsx',
43
+ 'js',
44
+ 'jsx',
45
+ 'typescript',
46
+ 'javascript',
47
+ '',
48
+ ]);
49
+ /**
50
+ * Candidate filenames, tried in order. Many rules key off the path (cloud
51
+ * function entry points, test-file exemptions, component directories), so a
52
+ * single hard-coded filename would make correct examples report for reasons the
53
+ * doc never claimed.
54
+ */
55
+ exports.TS_CANDIDATES = [
56
+ 'src/util/helper.ts',
57
+ 'functions/src/callable/handler.f.ts',
58
+ 'functions/src/util/helper.ts',
59
+ 'src/util/helper.test.ts',
60
+ 'src/components/Widget.tsx',
61
+ ];
62
+ exports.TSX_CANDIDATES = [
63
+ 'src/components/Widget.tsx',
64
+ 'src/pages/index.tsx',
65
+ ];
66
+ /**
67
+ * Rules that match on path segments need a rooted path — `functions/src/types/x.ts`
68
+ * relative does not satisfy the same check that `/repo/functions/src/types/x.ts`
69
+ * does, which would fail a doc example for a reason the doc never claimed.
70
+ */
71
+ exports.ROOT = '/repo/';
72
+ const anchor = (p) => (p.startsWith('/') ? p : exports.ROOT + p);
73
+ exports.anchor = anchor;
74
+ /**
75
+ * Classify an example heading.
76
+ *
77
+ * Only H2+ headings count: the H1 title ends with the rule id, and rule names
78
+ * routinely contain `prefer`, `valid`, or `no`, which would otherwise classify
79
+ * every block in the intro prose. The rule-id parenthetical is stripped for the
80
+ * same reason.
81
+ *
82
+ * Order matters — "incorrect" contains "correct" and "invalid" contains "valid",
83
+ * so the negative spellings must be tested first.
84
+ */
85
+ function headingPolarity(line) {
86
+ if (!/^#{2,6}\s/.test(line))
87
+ return null;
88
+ const text = line
89
+ .replace(/^#{2,6}\s*/, '')
90
+ .replace(/\(`?@blumintinc\/blumint\/[^)]*`?\)/g, '')
91
+ .toLowerCase();
92
+ if (/❌|👎|\bincorrect\b|\binvalid\b|\bbad\b|\bwrong\b/.test(text))
93
+ return 'incorrect';
94
+ if (/✅|👍|\bcorrect\b|\bvalid\b|\bgood\b/.test(text))
95
+ return 'correct';
96
+ return null;
97
+ }
98
+ exports.headingPolarity = headingPolarity;
99
+ /**
100
+ * Pull every fenced code block, tagged with the polarity of the example heading
101
+ * it sits under (`null` when it sits under none).
102
+ *
103
+ * Polarity is inherited by DEEPER headings, because docs routinely split an
104
+ * example section into named cases (`#### Option 1: …` under `### Examples of
105
+ * correct code`). Treating such a sub-heading as the end of the section dropped
106
+ * every block beneath it, which is how three whole pages asserted nothing.
107
+ */
108
+ function extractBlocks(md) {
109
+ const lines = md.split('\n');
110
+ const blocks = [];
111
+ let polarity = null;
112
+ let polarityDepth = 0;
113
+ let fence = null;
114
+ let buf = [];
115
+ let lang = '';
116
+ let startLine = 0;
117
+ for (let i = 0; i < lines.length; i += 1) {
118
+ const line = lines[i];
119
+ const fenceMatch = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
120
+ if (fence) {
121
+ if (fenceMatch &&
122
+ fenceMatch[1][0] === fence[0] &&
123
+ fenceMatch[1].length >= fence.length) {
124
+ blocks.push({
125
+ polarity,
126
+ lang: lang.trim().toLowerCase(),
127
+ code: buf.join('\n'),
128
+ line: startLine,
129
+ });
130
+ fence = null;
131
+ buf = [];
132
+ }
133
+ else {
134
+ buf.push(line);
135
+ }
136
+ continue;
137
+ }
138
+ const heading = /^(#{1,6})\s/.exec(line);
139
+ if (heading) {
140
+ const depth = heading[1].length;
141
+ const own = headingPolarity(line);
142
+ if (own) {
143
+ polarity = own;
144
+ polarityDepth = depth;
145
+ }
146
+ else if (!(polarity && depth > polarityDepth)) {
147
+ // A sibling or shallower heading ends the example section; a deeper one
148
+ // is a named case inside it and keeps the section's polarity.
149
+ polarity = null;
150
+ polarityDepth = 0;
151
+ }
152
+ continue;
153
+ }
154
+ if (fenceMatch) {
155
+ fence = fenceMatch[1];
156
+ lang = fenceMatch[2] || '';
157
+ startLine = i + 1;
158
+ buf = [];
159
+ }
160
+ }
161
+ return blocks;
162
+ }
163
+ exports.extractBlocks = extractBlocks;
164
+ /**
165
+ * Docs declare the context a snippet assumes inside the snippet itself:
166
+ * `// File: functions/src/...` (or a bare path comment) for path-sensitive
167
+ * rules, and `// eslint-options: {...}` for an example that only holds under a
168
+ * non-default option. Honouring both is what lets every correct block be
169
+ * enforced without exempting the awkward ones.
170
+ */
171
+ function filenameHint(code) {
172
+ const explicit = /^\s*(?:\/\/|\/\*)\s*File:\s*([^\s*]+)/im.exec(code);
173
+ if (explicit)
174
+ return (0, exports.anchor)(explicit[1].replace(/^\.\//, ''));
175
+ const firstLine = code.split('\n').find((l) => l.trim().length > 0) || '';
176
+ const bare = /^\s*\/\/\s*((?:[\w.-]+\/)+[\w.-]+\.tsx?)\b/.exec(firstLine);
177
+ return bare ? (0, exports.anchor)(bare[1]) : null;
178
+ }
179
+ exports.filenameHint = filenameHint;
180
+ function optionsHint(code) {
181
+ const m = /^\s*\/\/\s*eslint-options:\s*(\{.*\})\s*$/im.exec(code);
182
+ if (!m)
183
+ return null;
184
+ try {
185
+ return JSON.parse(m[1]);
186
+ }
187
+ catch {
188
+ throw new Error(`malformed // eslint-options: ${m[1]}`);
189
+ }
190
+ }
191
+ exports.optionsHint = optionsHint;
192
+ const linter = new eslint_1.Linter();
193
+ for (const [name, rule] of Object.entries(plugin.rules)) {
194
+ linter.defineRule(exports.PREFIX + name, rule);
195
+ }
196
+ linter.defineParser('ts', tsParser);
197
+ function lintBlock(ruleName, filename, code, options) {
198
+ const config = {
199
+ parser: 'ts',
200
+ parserOptions: {
201
+ ecmaVersion: 2022,
202
+ sourceType: 'module',
203
+ ecmaFeatures: { jsx: filename.endsWith('.tsx') },
204
+ },
205
+ rules: {
206
+ [exports.PREFIX + ruleName]: options
207
+ ? ['error', options]
208
+ : 'error',
209
+ },
210
+ };
211
+ let messages;
212
+ try {
213
+ messages = linter.verify(code, config, { filename });
214
+ }
215
+ catch (error) {
216
+ // A rule needing type information throws without `parserOptions.project`,
217
+ // which the RuleTester cannot supply; such rules are out of scope here.
218
+ return {
219
+ reports: [],
220
+ reportLines: [],
221
+ skipped: true,
222
+ reason: `the rule threw: ${error.message}`,
223
+ };
224
+ }
225
+ // A block that does not parse never ran the rule. That is not a pass — see
226
+ // UNCHECKABLE_BLOCKS.
227
+ const fatal = messages.find((m) => m.fatal);
228
+ if (fatal) {
229
+ return {
230
+ reports: [],
231
+ reportLines: [],
232
+ skipped: true,
233
+ reason: `parse failure at block line ${fatal.line}: ${fatal.message}`,
234
+ };
235
+ }
236
+ const mine = messages.filter((m) => m.ruleId === exports.PREFIX + ruleName);
237
+ return {
238
+ reports: mine.map((m) => `line ${m.line}: ${m.message}`),
239
+ reportLines: mine.map((m) => m.line),
240
+ skipped: false,
241
+ };
242
+ }
243
+ exports.lintBlock = lintBlock;
244
+ //# sourceMappingURL=docsFixtures.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.152",
3
+ "version": "1.20.154",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,56 @@
1
1
  [
2
+ {
3
+ "version": "1.20.154",
4
+ "date": "2026-08-15T03:47:51.570Z",
5
+ "rules": [
6
+ {
7
+ "name": "parallelize-async-operations",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2017
11
+ ],
12
+ "summary": "order callback-deferred instance mutations against later reads (closes #2017)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.153",
18
+ "date": "2026-08-14T20:21:23.691Z",
19
+ "rules": [
20
+ {
21
+ "name": "enforce-boolean-naming-prefixes",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2016
25
+ ],
26
+ "summary": "decline when the use site contradicts the callee's name (closes #2016)"
27
+ },
28
+ {
29
+ "name": "enforce-object-literal-as-const",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 2015
33
+ ],
34
+ "summary": "keep an unannotated returned array unfrozen (closes #2015)"
35
+ },
36
+ {
37
+ "name": "global-const-style",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 2013
41
+ ],
42
+ "summary": "decline the as const when the binding is mutated later (closes #2013)"
43
+ },
44
+ {
45
+ "name": "no-explicit-return-type",
46
+ "changeType": "fix",
47
+ "issues": [
48
+ 2014
49
+ ],
50
+ "summary": "keep a decorator factory's annotation (closes #2014)"
51
+ }
52
+ ]
53
+ },
2
54
  {
3
55
  "version": "1.20.152",
4
56
  "date": "2026-08-14T10:28:08.765Z",