@blumintinc/eslint-plugin-blumint 1.20.1 → 1.20.2

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.1',
226
+ version: '1.20.2',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -206,6 +206,183 @@ function isNameBoundInSubtree(root, targetName) {
206
206
  }
207
207
  return false;
208
208
  }
209
+ /**
210
+ * Annotations that impose no excess-property check. A value annotated `any` or
211
+ * `unknown` may carry members its annotation never declares, so the annotation
212
+ * proves nothing about where a member name came from (#1350).
213
+ */
214
+ const UNCHECKED_ANNOTATION_TYPES = new Set([
215
+ utils_1.AST_NODE_TYPES.TSAnyKeyword,
216
+ utils_1.AST_NODE_TYPES.TSUnknownKeyword,
217
+ ]);
218
+ /**
219
+ * The declared type of an annotation-bearing node (`const x: T`, `field: T`),
220
+ * or null when the node carries no annotation.
221
+ */
222
+ function declaredTypeNode(node) {
223
+ const { typeAnnotation } = node;
224
+ return typeAnnotation?.typeAnnotation ?? null;
225
+ }
226
+ function checksExcessProperties(typeNode) {
227
+ return typeNode !== null && !UNCHECKED_ANNOTATION_TYPES.has(typeNode.type);
228
+ }
229
+ /**
230
+ * Reports whether `node` sits inside a value whose shape TypeScript checks
231
+ * against a declared type — a type-annotated variable or class field, or a
232
+ * `satisfies` clause. Excess-property checking makes such a literal unable to
233
+ * carry a member the target type does not declare, so a member name there is
234
+ * dictated by that type rather than chosen by the author, and renaming it would
235
+ * break conformance (#1350). No member resolution is needed: the signal alone
236
+ * is proof, because code carrying an undeclared member does not compile.
237
+ *
238
+ * The walk climbs object/array containers so an outer signal covers nested
239
+ * members, and stops at anything else — notably `as` assertions, which do not
240
+ * reject undeclared members the same way.
241
+ */
242
+ function hasConformanceSignal(node) {
243
+ let current = node;
244
+ for (;;) {
245
+ const parent = current.parent;
246
+ if (!parent) {
247
+ return false;
248
+ }
249
+ switch (parent.type) {
250
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
251
+ return (parent.expression === current &&
252
+ checksExcessProperties(parent.typeAnnotation));
253
+ case utils_1.AST_NODE_TYPES.VariableDeclarator:
254
+ return (parent.init === current &&
255
+ checksExcessProperties(declaredTypeNode(parent.id)));
256
+ case utils_1.AST_NODE_TYPES.PropertyDefinition:
257
+ return (parent.value === current &&
258
+ checksExcessProperties(declaredTypeNode(parent)));
259
+ case utils_1.AST_NODE_TYPES.Property:
260
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
261
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
262
+ current = parent;
263
+ break;
264
+ default:
265
+ return false;
266
+ }
267
+ }
268
+ }
269
+ function buildDeclarationIndex(sourceCode) {
270
+ const index = {
271
+ interfaces: new Map(),
272
+ typeAliases: new Map(),
273
+ classes: new Map(),
274
+ };
275
+ // Declarations are collected from the whole file rather than from
276
+ // `Program.body` alone so contracts declared inside modules, blocks or
277
+ // functions resolve as well as top-level ones.
278
+ const stack = [sourceCode.ast];
279
+ while (stack.length > 0) {
280
+ const current = stack.pop();
281
+ switch (current.type) {
282
+ case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration: {
283
+ const merged = index.interfaces.get(current.id.name) ?? [];
284
+ merged.push(current);
285
+ index.interfaces.set(current.id.name, merged);
286
+ break;
287
+ }
288
+ case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
289
+ index.typeAliases.set(current.id.name, current);
290
+ break;
291
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
292
+ case utils_1.AST_NODE_TYPES.ClassExpression:
293
+ if (current.id) {
294
+ index.classes.set(current.id.name, current);
295
+ }
296
+ break;
297
+ default:
298
+ break;
299
+ }
300
+ for (const key of sourceCode.visitorKeys[current.type] ?? []) {
301
+ const value = current[key];
302
+ const children = Array.isArray(value) ? value : [value];
303
+ for (const child of children) {
304
+ if (child && typeof child === 'object' && 'type' in child) {
305
+ stack.push(child);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ return index;
311
+ }
312
+ function heritageTypeName(expression) {
313
+ return expression.type === utils_1.AST_NODE_TYPES.Identifier ? expression.name : null;
314
+ }
315
+ function membersDeclareName(members, memberName) {
316
+ return members.some((member) => (member.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
317
+ member.type === utils_1.AST_NODE_TYPES.TSPropertySignature) &&
318
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
319
+ member.key.name === memberName);
320
+ }
321
+ function classBodyDeclaresName(members, memberName) {
322
+ return members.some((member) => (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
323
+ member.type === utils_1.AST_NODE_TYPES.PropertyDefinition) &&
324
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
325
+ member.key.name === memberName);
326
+ }
327
+ /**
328
+ * Reports whether the contract named `typeName` accounts for `memberName`,
329
+ * either by declaring it or by being unreadable from this file. A class may add
330
+ * members its contract never declares, so presence of a heritage clause alone
331
+ * cannot exempt a name — but an imported or otherwise unresolvable contract
332
+ * hides its members from a purely syntactic rule, and this plugin prefers a
333
+ * false negative over a false positive (#1350).
334
+ */
335
+ function contractCoversName(typeName, memberName, index, visited) {
336
+ // A name already inspected on this path adds nothing and would loop on a
337
+ // circular heritage chain.
338
+ if (visited.has(typeName)) {
339
+ return false;
340
+ }
341
+ visited.add(typeName);
342
+ const interfaces = index.interfaces.get(typeName);
343
+ if (interfaces) {
344
+ return interfaces.some((declaration) => membersDeclareName(declaration.body.body, memberName) ||
345
+ heritageCoversName(declaration.extends ?? [], memberName, index, visited));
346
+ }
347
+ const alias = index.typeAliases.get(typeName);
348
+ if (alias) {
349
+ // An alias to anything but a type literal (an intersection, a mapped type,
350
+ // a reference to an imported type) hides its member list from a syntactic
351
+ // reader, so it is treated as unreadable.
352
+ return (alias.typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral ||
353
+ membersDeclareName(alias.typeAnnotation.members, memberName));
354
+ }
355
+ const classDeclaration = index.classes.get(typeName);
356
+ if (classDeclaration) {
357
+ return (classBodyDeclaresName(classDeclaration.body.body, memberName) ||
358
+ classContractCoversName(classDeclaration, memberName, index, visited));
359
+ }
360
+ // Nothing under this name in the file: the contract lives in another module
361
+ // and its members are unreadable here.
362
+ return true;
363
+ }
364
+ function heritageCoversName(heritage, memberName, index, visited) {
365
+ return heritage.some((clause) => {
366
+ const typeName = heritageTypeName(clause.expression);
367
+ // A namespaced or computed heritage expression cannot be followed
368
+ // syntactically, so it counts as an unreadable contract.
369
+ return (typeName === null ||
370
+ contractCoversName(typeName, memberName, index, visited));
371
+ });
372
+ }
373
+ function classContractCoversName(classNode, memberName, index, visited) {
374
+ if (heritageCoversName(classNode.implements ?? [], memberName, index, visited)) {
375
+ return true;
376
+ }
377
+ const { superClass } = classNode;
378
+ if (!superClass) {
379
+ return false;
380
+ }
381
+ const superName = heritageTypeName(superClass);
382
+ // A computed superclass (a mixin call) is unreadable, like an imported one.
383
+ return (superName === null ||
384
+ contractCoversName(superName, memberName, index, visited));
385
+ }
209
386
  exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
210
387
  name: 'no-unnecessary-verb-suffix',
211
388
  meta: {
@@ -222,6 +399,37 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
222
399
  },
223
400
  defaultOptions: [],
224
401
  create(context) {
402
+ // Built on first heritage question only, since most files never ask one.
403
+ let declarationIndex = null;
404
+ function getDeclarationIndex() {
405
+ if (declarationIndex === null) {
406
+ declarationIndex = buildDeclarationIndex(context.sourceCode);
407
+ }
408
+ return declarationIndex;
409
+ }
410
+ /**
411
+ * Reports whether a class member's name comes from a contract the class
412
+ * declares conformance to, rather than from its author (#1350).
413
+ */
414
+ function isDictatedByHeritage(node, memberName) {
415
+ const classBody = node.parent;
416
+ if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
417
+ return false;
418
+ }
419
+ const classNode = classBody.parent;
420
+ if (!classNode ||
421
+ (classNode.type !== utils_1.AST_NODE_TYPES.ClassDeclaration &&
422
+ classNode.type !== utils_1.AST_NODE_TYPES.ClassExpression)) {
423
+ return false;
424
+ }
425
+ // Indexing the file's declarations is only worth its cost once a class
426
+ // declares conformance to something.
427
+ if ((classNode.implements ?? []).length === 0 &&
428
+ classNode.superClass === null) {
429
+ return false;
430
+ }
431
+ return classContractCoversName(classNode, memberName, getDeclarationIndex(), new Set());
432
+ }
225
433
  /**
226
434
  * Returns true when renaming the symbol to `suggestion` would collide with
227
435
  * an existing binding in any scope the rename touches, making the autofix
@@ -387,6 +595,11 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
387
595
  },
388
596
  MethodDefinition(node) {
389
597
  if (node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
598
+ // A member implementing a contract the class declares conformance to
599
+ // is named by that contract, so renaming it would break conformance.
600
+ if (isDictatedByHeritage(node, node.key.name)) {
601
+ return;
602
+ }
390
603
  // Class methods are called via member expressions (`this.method()`,
391
604
  // `instance.method()`) that the scope manager does not track as
392
605
  // references. A syntactic single-file fixer therefore cannot find and
@@ -408,6 +621,12 @@ exports.noUnnecessaryVerbSuffix = (0, createRule_1.createRule)({
408
621
  if (node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
409
622
  (node.value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
410
623
  node.value.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
624
+ // A literal checked against a declared type can only hold members
625
+ // that type declares, so its member names are not the author's to
626
+ // rename.
627
+ if (hasConformanceSignal(node)) {
628
+ return;
629
+ }
411
630
  // Object-literal method properties are accessed via member expressions
412
631
  // (`obj.method()`) the scope manager does not track. As with class
413
632
  // methods, the fix is suppressed to avoid orphaning call sites.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.1",
3
+ "version": "1.20.2",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.2",
4
+ "date": "2026-07-25T02:42:18.863Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-unnecessary-verb-suffix",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1350
11
+ ],
12
+ "summary": "exempt member names dictated by a declared contract (closes #1350)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.1",
4
18
  "date": "2026-07-24T22:02:37.444Z",