@blumintinc/eslint-plugin-blumint 1.20.94 → 1.20.96

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.
@@ -30,6 +30,28 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
30
30
  const evaluatedParentNodes = new Set();
31
31
  // Sentinel distinguishes "no const literal resolved" from valid literal values (including null/undefined)
32
32
  const NOT_FOUND = Symbol('literal-not-found');
33
+ const ASSERTION_EXPRESSION_TYPES = new Set([
34
+ utils_1.AST_NODE_TYPES.TSAsExpression,
35
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
36
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
37
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
38
+ ]);
39
+ /**
40
+ * Strips type/non-null assertions from an expression. An assertion only
41
+ * describes a value, so the value behind it is exactly as knowable as the
42
+ * value itself — `"a" as const` is *more* certain than `"a"`, never less.
43
+ * Every resolution site strips them, otherwise a wrapper silently retires
44
+ * detection: sibling fixers add `as const` to module-scope constants, so a
45
+ * resolver that stopped at the wrapper would go blind on each constant they
46
+ * touch.
47
+ */
48
+ function unwrapAssertions(node) {
49
+ let current = node;
50
+ while (ASSERTION_EXPRESSION_TYPES.has(current.type)) {
51
+ current = current.expression;
52
+ }
53
+ return current;
54
+ }
33
55
  /**
34
56
  * Walks the current scope chain to locate a variable by name so identifier
35
57
  * lookups can resolve const initializers even when declared in parent scopes.
@@ -71,11 +93,74 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
71
93
  return NOT_FOUND;
72
94
  }
73
95
  const initializer = definition.node.init;
74
- if (initializer?.type === utils_1.AST_NODE_TYPES.Literal) {
75
- return initializer.value;
96
+ if (!initializer) {
97
+ return NOT_FOUND;
98
+ }
99
+ const value = unwrapAssertions(initializer);
100
+ if (value.type === utils_1.AST_NODE_TYPES.Literal) {
101
+ return value.value;
76
102
  }
77
103
  return NOT_FOUND;
78
104
  }
105
+ /**
106
+ * A const binding freezes the binding, not the object it points at, so a
107
+ * resolved object literal only stays constant while every reference merely
108
+ * reads a property off it. Writing through a member expression, or handing
109
+ * the object to anything that can retain and mutate it, makes the property
110
+ * values unknowable from the declaration alone.
111
+ */
112
+ function isPropertyReadReference(reference) {
113
+ const { identifier } = reference;
114
+ const parent = identifier.parent;
115
+ if (!parent) {
116
+ return false;
117
+ }
118
+ // The declaration's own initialization write.
119
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
120
+ parent.id === identifier) {
121
+ return true;
122
+ }
123
+ if (parent.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
124
+ parent.object !== identifier) {
125
+ return false;
126
+ }
127
+ const access = parent.parent?.type === utils_1.AST_NODE_TYPES.ChainExpression
128
+ ? parent.parent.parent
129
+ : parent.parent;
130
+ if (!access) {
131
+ return true;
132
+ }
133
+ const writesTheProperty = (access.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
134
+ access.left === parent) ||
135
+ access.type === utils_1.AST_NODE_TYPES.UpdateExpression ||
136
+ (access.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
137
+ access.operator === 'delete');
138
+ return !writesTheProperty;
139
+ }
140
+ /**
141
+ * Resolves an identifier to the object literal it is initialized with when
142
+ * declared as a const nothing can mutate. Returns undefined whenever the
143
+ * binding is absent, is not a const, is initialized with anything other
144
+ * than an object literal, or escapes to code that could rewrite it.
145
+ */
146
+ function resolveIdentifierObject(identifier) {
147
+ const variable = findVariableInScopes(identifier.name);
148
+ if (!variable) {
149
+ return undefined;
150
+ }
151
+ const definition = findConstIdentifierDefinition(variable, identifier.name);
152
+ if (!definition?.node.init) {
153
+ return undefined;
154
+ }
155
+ const initializer = unwrapAssertions(definition.node.init);
156
+ if (initializer.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
157
+ return undefined;
158
+ }
159
+ if (!variable.references.every(isPropertyReadReference)) {
160
+ return undefined;
161
+ }
162
+ return initializer;
163
+ }
79
164
  /**
80
165
  * Checks if a literal value is always truthy or falsy
81
166
  */
@@ -95,6 +180,117 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
95
180
  return {};
96
181
  }
97
182
  }
183
+ /**
184
+ * Classifies an expression that a constant declaration binds, so a resolved
185
+ * property value is judged by what it is rather than by where it was found.
186
+ * Anything whose value depends on runtime state stays unresolved.
187
+ */
188
+ function checkConstantValue(node) {
189
+ const value = unwrapAssertions(node);
190
+ switch (value.type) {
191
+ case utils_1.AST_NODE_TYPES.Literal:
192
+ // A regex literal is an object, and its `value` is unusable when the
193
+ // linting runtime cannot construct the pattern.
194
+ return 'regex' in value
195
+ ? { isTruthy: true }
196
+ : checkLiteralValue(value);
197
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
198
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
199
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
200
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
201
+ case utils_1.AST_NODE_TYPES.ClassExpression:
202
+ return { isTruthy: true };
203
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
204
+ return value.expressions.length === 0
205
+ ? value.quasis[0].value.raw === ''
206
+ ? { isFalsy: true }
207
+ : { isTruthy: true }
208
+ : {};
209
+ default:
210
+ return {};
211
+ }
212
+ }
213
+ /**
214
+ * Reads the property name a member expression accesses. Returns undefined
215
+ * when the key is computed from something other than a literal, since the
216
+ * accessed property is then unknown at lint time.
217
+ */
218
+ function memberPropertyName(member) {
219
+ if (member.computed) {
220
+ const key = unwrapAssertions(member.property);
221
+ return key.type === utils_1.AST_NODE_TYPES.Literal &&
222
+ (typeof key.value === 'string' || typeof key.value === 'number')
223
+ ? String(key.value)
224
+ : undefined;
225
+ }
226
+ return member.property.type === utils_1.AST_NODE_TYPES.Identifier
227
+ ? member.property.name
228
+ : undefined;
229
+ }
230
+ /**
231
+ * Reads the key an object literal property declares, mirroring the member
232
+ * access side so both are compared as strings.
233
+ */
234
+ function propertyKeyName(property) {
235
+ const { key } = property;
236
+ if (key.type === utils_1.AST_NODE_TYPES.Literal &&
237
+ (typeof key.value === 'string' || typeof key.value === 'number')) {
238
+ return String(key.value);
239
+ }
240
+ return !property.computed && key.type === utils_1.AST_NODE_TYPES.Identifier
241
+ ? key.name
242
+ : undefined;
243
+ }
244
+ /**
245
+ * Evaluates a property read against a resolved object literal. The result
246
+ * stays empty whenever the literal's shape hides the answer — a spread, a
247
+ * key computed at runtime, an accessor — and whenever the property is
248
+ * absent, because a missing own property can still resolve through the
249
+ * prototype chain (`config.toString`) or through declaration merging.
250
+ */
251
+ function checkObjectPropertyAccess(objectExpression, member) {
252
+ const propertyName = memberPropertyName(member);
253
+ if (propertyName === undefined) {
254
+ return {};
255
+ }
256
+ let resolvedValue;
257
+ for (const property of objectExpression.properties) {
258
+ if (property.type !== utils_1.AST_NODE_TYPES.Property) {
259
+ // A spread can contribute or overwrite any key.
260
+ return {};
261
+ }
262
+ const keyName = propertyKeyName(property);
263
+ if (keyName === undefined) {
264
+ return {};
265
+ }
266
+ if (keyName === propertyName) {
267
+ if (property.kind !== 'init') {
268
+ // An accessor returns whatever it likes.
269
+ return {};
270
+ }
271
+ // A later duplicate key wins.
272
+ resolvedValue = property.value;
273
+ }
274
+ }
275
+ return resolvedValue ? checkConstantValue(resolvedValue) : {};
276
+ }
277
+ /**
278
+ * Evaluates a member access whose object the rule can pin to an object
279
+ * literal, whether the literal is written inline or reached through a const
280
+ * binding. Optional chaining off such an object behaves like plain access,
281
+ * because the object is known not to be nullish.
282
+ */
283
+ function checkMemberAccess(member) {
284
+ const object = unwrapAssertions(member.object);
285
+ if (object.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
286
+ return checkObjectPropertyAccess(object, member);
287
+ }
288
+ if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
289
+ const resolved = resolveIdentifierObject(object);
290
+ return resolved ? checkObjectPropertyAccess(resolved, member) : {};
291
+ }
292
+ return {};
293
+ }
98
294
  const MATH_FOLDABLE_METHODS = new Set(['max', 'min']);
99
295
  /**
100
296
  * Resolves a `Math.max`/`Math.min` call over numeric literals to the number
@@ -130,11 +326,12 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
130
326
  * Returns undefined when the operand depends on runtime values.
131
327
  */
132
328
  function resolveNumericOperand(node) {
133
- if (node.type === utils_1.AST_NODE_TYPES.Literal &&
134
- typeof node.value === 'number') {
135
- return node.value;
329
+ const operand = unwrapAssertions(node);
330
+ if (operand.type === utils_1.AST_NODE_TYPES.Literal &&
331
+ typeof operand.value === 'number') {
332
+ return operand.value;
136
333
  }
137
- return evaluateMathMinMaxCall(node);
334
+ return evaluateMathMinMaxCall(operand);
138
335
  }
139
336
  function compareNumericValues(leftValue, operator, rightValue) {
140
337
  switch (operator) {
@@ -172,13 +369,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
172
369
  * Checks if a binary expression with literals is always truthy or falsy
173
370
  */
174
371
  function checkBinaryExpression(node) {
372
+ // An assertion around an operand leaves the compared value untouched.
373
+ const left = unwrapAssertions(node.left);
374
+ const right = unwrapAssertions(node.right);
175
375
  // Check for bitwise operations
176
376
  if (node.operator === '&') {
177
- if (node.left.type === utils_1.AST_NODE_TYPES.Literal &&
178
- node.right.type === utils_1.AST_NODE_TYPES.Literal &&
179
- typeof node.left.value === 'number' &&
180
- typeof node.right.value === 'number') {
181
- const result = node.left.value & node.right.value;
377
+ if (left.type === utils_1.AST_NODE_TYPES.Literal &&
378
+ right.type === utils_1.AST_NODE_TYPES.Literal &&
379
+ typeof left.value === 'number' &&
380
+ typeof right.value === 'number') {
381
+ const result = left.value & right.value;
182
382
  if (result === 0) {
183
383
  return { isFalsy: true };
184
384
  }
@@ -190,17 +390,17 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
190
390
  // A non-literal operand can still be a compile-time constant, so resolve
191
391
  // it before abandoning the comparison. Bailing out unconditionally here
192
392
  // hid every folded-call comparison (Math.max(1, 2) === 0) from the rule.
193
- if (node.left.type !== utils_1.AST_NODE_TYPES.Literal ||
194
- node.right.type !== utils_1.AST_NODE_TYPES.Literal) {
195
- const resolvedLeft = resolveNumericOperand(node.left);
196
- const resolvedRight = resolveNumericOperand(node.right);
393
+ if (left.type !== utils_1.AST_NODE_TYPES.Literal ||
394
+ right.type !== utils_1.AST_NODE_TYPES.Literal) {
395
+ const resolvedLeft = resolveNumericOperand(left);
396
+ const resolvedRight = resolveNumericOperand(right);
197
397
  if (resolvedLeft === undefined || resolvedRight === undefined) {
198
398
  return {};
199
399
  }
200
400
  return compareNumericValues(resolvedLeft, node.operator, resolvedRight);
201
401
  }
202
- const leftValue = node.left.value;
203
- const rightValue = node.right.value;
402
+ const leftValue = left.value;
403
+ const rightValue = right.value;
204
404
  // Skip if either value is null or undefined
205
405
  if (leftValue === null ||
206
406
  leftValue === undefined ||
@@ -376,15 +576,6 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
376
576
  }
377
577
  return {};
378
578
  }
379
- /**
380
- * Check if a node is an "as const" expression
381
- */
382
- function isAsConstExpression(node) {
383
- return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
384
- node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
385
- node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
386
- node.typeAnnotation.typeName.name === 'const');
387
- }
388
579
  /**
389
580
  * Check if a logical expression is being used for default value assignment
390
581
  * This includes patterns like:
@@ -870,6 +1061,11 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
870
1061
  if (evaluatedParentNodes.has(node)) {
871
1062
  return {};
872
1063
  }
1064
+ // An assertion wrapper carries the same value as the expression inside it
1065
+ const unwrapped = unwrapAssertions(node);
1066
+ if (unwrapped !== node) {
1067
+ return evaluateConstantExpression(unwrapped);
1068
+ }
873
1069
  // Check special constants and identifiers first
874
1070
  const specialResult = checkSpecialValues(node);
875
1071
  if (specialResult.isTruthy || specialResult.isFalsy) {
@@ -950,11 +1146,6 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
950
1146
  return { isFalsy: true };
951
1147
  }
952
1148
  }
953
- // "as const" expressions
954
- if (isAsConstExpression(node) &&
955
- node.expression.type === utils_1.AST_NODE_TYPES.Literal) {
956
- return checkLiteralValue(node.expression);
957
- }
958
1149
  // Special checks for const variables in tests
959
1150
  if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
960
1151
  if (node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
@@ -1056,38 +1247,16 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1056
1247
  return evaluateConstantExpression(logicalNode.left);
1057
1248
  }
1058
1249
  }
1059
- // Handle optional chaining with literal objects
1250
+ // Optional chaining off an object the rule can pin to an object literal.
1251
+ // The guard the operator adds is dead there, and the property it reads is
1252
+ // as knowable as the literal that declares it, whatever the binding and
1253
+ // the property are named. Anything else — a binding initialized from a
1254
+ // call, a parameter, a value that may legitimately be undefined — stays
1255
+ // unresolved.
1060
1256
  if (node.type === utils_1.AST_NODE_TYPES.ChainExpression &&
1061
- node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
1062
- if (node.expression.optional) {
1063
- // Handle object?.prop
1064
- if (
1065
- // Only consider it always truthy if it's a literal object expression
1066
- node.expression.object.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
1067
- // If we have a property access and we know the object is a literal object
1068
- return { isTruthy: true };
1069
- }
1070
- // Special case for identifiers that are explicitly defined in the same scope
1071
- // This is to handle test cases like `const obj = { prop: "value" }; if (obj?.prop) {}`
1072
- if (node.expression.object.type === utils_1.AST_NODE_TYPES.Identifier &&
1073
- node.expression.object.name === 'obj' &&
1074
- node.expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
1075
- node.expression.property.name === 'prop') {
1076
- // This is specifically for the test case "obj?.prop"
1077
- return { isTruthy: true };
1078
- }
1079
- // Special case for array length checks with optional chaining
1080
- // This handles cases like `filtered?.length` where filtered could be undefined
1081
- if (node.expression.property.type === utils_1.AST_NODE_TYPES.Identifier &&
1082
- node.expression.property.name === 'length') {
1083
- // For array length checks with optional chaining, we can't determine
1084
- // if the condition is always truthy or falsy, so return empty result
1085
- return {};
1086
- }
1087
- // For other cases like arrays, identifiers, etc., we can't determine
1088
- // if the condition is always truthy or falsy, so return empty result
1089
- return {};
1090
- }
1257
+ node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
1258
+ node.expression.optional) {
1259
+ return checkMemberAccess(node.expression);
1091
1260
  }
1092
1261
  // Handle Object.keys().length
1093
1262
  if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noRedundantParamTypes = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const disableDirectives_1 = require("../utils/disableDirectives");
6
7
  const importRemoval_1 = require("../utils/importRemoval");
7
8
  /**
8
9
  * The annotation a parameter carries, or `undefined` when it has none. A
@@ -68,57 +69,88 @@ exports.noRedundantParamTypes = (0, createRule_1.createRule)({
68
69
  create(context) {
69
70
  const sourceCode = context.getSourceCode();
70
71
  /**
71
- * Reports a parameter annotation, taking with it any import it was the only
72
+ * An annotation is stripped together with any import it was the only
72
73
  * consumer of. The two are one fix: applying either half alone leaves the
73
74
  * file worse than applying neither — a stripped annotation with its import
74
75
  * left behind fails `no-unused-vars`, and since this rule's own report is
75
76
  * resolved by the fix, nothing re-reports the debt.
76
77
  *
77
- * Orphanhood is judged against this one annotation's own removal and the
78
- * file as it stands, never against what the rest of the `--fix` run might
79
- * also delete. A sibling annotation naming the same type may be
80
- * `eslint-disable`d — which a rule cannot see, since suppression is applied
81
- * to reports after they are emitted — so an edit that assumes its sibling
82
- * will also go deletes an import the surviving annotation still references,
83
- * trading an unused import for a dangling type. Judging one edit at a time
84
- * is suppression-safe by construction: a suppressed report's fix never
85
- * applies, so it can never have been depended on.
78
+ * Reporting is therefore deferred to `Program:exit`: an import is unbound
79
+ * only once no reference to it survives the fix, and a file where two
80
+ * annotations name the same imported type strips both in a single pass.
81
+ * Judging each removal alone sees the sibling annotation still standing,
82
+ * concludes the binding is alive, and leaves the import stranded with no
83
+ * later pass to notice.
84
+ */
85
+ const sites = [];
86
+ /**
87
+ * Suppression is applied to reports after a rule emits them, so a suppressed
88
+ * site keeps its annotation while losing its fix. Counting its removal
89
+ * toward orphanhood would unbind an import the surviving text still spells,
90
+ * trading an unused import for a dangling type.
91
+ */
92
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
93
+ /**
94
+ * The strips that actually ship. A site is excluded when its report will be
95
+ * suppressed, or when its own removal orphans something the helper cannot
96
+ * rewrite — a local alias, an interface, a type parameter. Deleting a
97
+ * declaration is a materially riskier edit than dropping an import
98
+ * specifier, and the author is better placed to decide whether the type
99
+ * should go or be used elsewhere.
86
100
  *
87
- * The cost is that a type shared by several strippable annotations is not
88
- * unbound in a single pass; each pass removes the annotations it can see,
89
- * and only a pass that leaves the binding with no reference at all removes
90
- * the import.
101
+ * Screening individually before batching keeps one unfixable site from
102
+ * vetoing the rest: orphanhood grows monotonically with the removed set, so
103
+ * a site that cannot be planned alone can only ever poison the batch.
91
104
  */
92
- function reportParam(param, typeAnnotation) {
93
- const removal = annotationRemovalRange(typeAnnotation, sourceCode);
94
- const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [removal]);
95
- context.report({
96
- node: param,
97
- messageId: 'redundantParamType',
98
- data: {
99
- paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
100
- },
101
- // No plan means no binding can be unbound safely, so the annotation
102
- // stays too: the report without a fixer is the lesser damage.
103
- ...(importRanges
104
- ? {
105
- fix: (fixer) => [
106
- fixer.removeRange([removal[0], removal[1]]),
107
- ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
108
- ],
109
- }
110
- : {}),
111
- });
105
+ function selectFixableSites() {
106
+ return sites.filter((site) => !isReportSuppressed(site.param) &&
107
+ (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.removal]) !== null);
112
108
  }
113
109
  return {
110
+ 'Program:exit'() {
111
+ if (sites.length === 0)
112
+ return;
113
+ const fixable = selectFixableSites();
114
+ const removals = fixable.map((site) => site.removal);
115
+ // One plan over every surviving strip: an import referenced solely by
116
+ // annotations that all go in this pass is orphaned by their union, even
117
+ // though no single one of them orphans it.
118
+ const importRanges = removals.length > 0
119
+ ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, removals)
120
+ : null;
121
+ // The whole batch ships as one fix, so no strip can land without the
122
+ // others that the import's orphanhood was judged against. The rest
123
+ // report without a fixer; the carrier's pass already resolves them.
124
+ //
125
+ // No plan at all means no binding can be unbound safely, so every
126
+ // annotation stays: reports without a fixer are the lesser damage.
127
+ const carrier = importRanges ? fixable[0] : undefined;
128
+ for (const site of sites) {
129
+ context.report({
130
+ node: site.param,
131
+ messageId: 'redundantParamType',
132
+ data: { paramText: site.paramText },
133
+ fix: site === carrier && importRanges
134
+ ? (fixer) => [
135
+ ...removals.map((range) => fixer.removeRange([range[0], range[1]])),
136
+ ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
137
+ ]
138
+ : null,
139
+ });
140
+ }
141
+ },
114
142
  ArrowFunctionExpression(node) {
115
143
  if (!hasRedundantTypeAnnotation(node))
116
144
  return;
117
145
  node.params.forEach((param) => {
118
146
  const typeAnnotation = annotationOf(param);
119
- if (typeAnnotation) {
120
- reportParam(param, typeAnnotation);
121
- }
147
+ if (!typeAnnotation)
148
+ return;
149
+ sites.push({
150
+ param,
151
+ removal: annotationRemovalRange(typeAnnotation, sourceCode),
152
+ paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
153
+ });
122
154
  });
123
155
  },
124
156
  };
@@ -439,11 +439,7 @@ function contractCoversName(typeName, memberName, index, visited) {
439
439
  }
440
440
  const alias = index.typeAliases.get(typeName);
441
441
  if (alias) {
442
- // An alias to anything but a type literal (an intersection, a mapped type,
443
- // a reference to an imported type) hides its member list from a syntactic
444
- // reader, so it is treated as unreadable.
445
- return (alias.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral ||
446
- membersDeclareName(alias.typeAnnotation.members, memberName));
442
+ return aliasCoversName(alias.typeAnnotation, memberName, index, visited);
447
443
  }
448
444
  const classDeclaration = index.classes.get(typeName);
449
445
  if (classDeclaration) {
@@ -454,6 +450,57 @@ function contractCoversName(typeName, memberName, index, visited) {
454
450
  // and its members are unreadable here.
455
451
  return true;
456
452
  }
453
+ /**
454
+ * Reports whether the type an alias names accounts for `memberName`.
455
+ *
456
+ * A type literal lists its members outright. An intersection is readable as far
457
+ * as its constituents are, which matters because `prefer-type-over-interface`
458
+ * ships in the same recommended config and rewrites
459
+ * `interface S extends Base { … }` to `type S = Base & { … }` by `--fix`:
460
+ * treating every non-literal alias as unreadable would let that sibling fix
461
+ * retire the member check for every contract written as an interface with a
462
+ * heritage clause (#1679). Anything else — a union, a mapped or conditional
463
+ * type, a reference whose target lives in another module — hides its member
464
+ * list from a syntactic reader and stays unreadable, so the member keeps its
465
+ * exemption.
466
+ */
467
+ function aliasCoversName(typeNode, memberName, index, visited) {
468
+ switch (typeNode.type) {
469
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
470
+ return membersDeclareName(typeNode.members, memberName);
471
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
472
+ return intersectionCoversName(typeNode, memberName, index, visited);
473
+ default:
474
+ return true;
475
+ }
476
+ }
477
+ /**
478
+ * Reports whether an intersection accounts for `memberName`. An intersection
479
+ * contributes every member of every constituent, so one constituent declaring
480
+ * the name settles the question — and a constituent whose members cannot be
481
+ * read settles it too, in the exempting direction: the name may well be that
482
+ * hidden part's, and this plugin prefers a false negative over a false positive
483
+ * (#1350). The member is only left to the class author when every constituent
484
+ * is readable and none of them declares it.
485
+ */
486
+ function intersectionCoversName(intersection, memberName, index, visited) {
487
+ return intersection.types.some((constituent) => constituentCoversName(constituent, memberName, index, visited));
488
+ }
489
+ /**
490
+ * Answers `intersectionCoversName` for one constituent. A reference is followed
491
+ * through the file's declarations by the same resolver a heritage clause uses,
492
+ * so a contract split across a local base and an inline literal reads as a
493
+ * whole; a reference the file does not declare, and a qualified or otherwise
494
+ * opaque constituent, count as unreadable.
495
+ */
496
+ function constituentCoversName(typeNode, memberName, index, visited) {
497
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
498
+ return typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
499
+ ? contractCoversName(typeNode.typeName.name, memberName, index, visited)
500
+ : true;
501
+ }
502
+ return aliasCoversName(typeNode, memberName, index, visited);
503
+ }
457
504
  function heritageCoversName(heritage, memberName, index, visited) {
458
505
  return heritage.some((clause) => {
459
506
  const typeName = heritageTypeName(clause.expression);
@@ -2,6 +2,7 @@ type Options = [
2
2
  {
3
3
  coordinatorPatterns?: string[];
4
4
  rateLimitedPatterns?: string[];
5
+ ignoreTestFiles?: boolean;
5
6
  }
6
7
  ];
7
8
  export declare const parallelizeLoopAwaits: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"parallelizeLoopAwaits", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;