@blumintinc/eslint-plugin-blumint 1.20.154 → 1.20.155

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.154',
226
+ version: '1.20.155',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -569,32 +569,164 @@ function isOverloadedFunction(node) {
569
569
  }
570
570
  return false;
571
571
  }
572
- function isOverloadedTsDeclareFunction(node) {
572
+ /**
573
+ * The statement list that directly holds `node`, looking through `export`.
574
+ *
575
+ * Overload signatures and their implementation are siblings of one statement
576
+ * list — an overload set cannot span containers — so this is the only list worth
577
+ * reading. Walking outward instead would let a same-named function in an
578
+ * enclosing scope answer for one it cannot overload.
579
+ */
580
+ function siblingStatementsOf(node) {
581
+ const parent = node.parent;
582
+ if (!parent)
583
+ return undefined;
584
+ const container = parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
585
+ parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration
586
+ ? parent.parent
587
+ : parent;
588
+ return container ? (0, lexicalScope_1.statementsOf)(container) : undefined;
589
+ }
590
+ const EMPTY_OVERLOAD_SET = { signatures: 0, implementations: 0 };
591
+ /**
592
+ * How many declaration-only signatures and how many implementations the
593
+ * container holding `node` declares under `node`'s own name, `node` included.
594
+ *
595
+ * Every statement container is read, not just `Program` and `TSModuleBlock`: a
596
+ * function body, a bare block and a `switch` case each bind a name just as
597
+ * effectively, so reading only the top level makes the DEPTH of an overload set
598
+ * decide whether it exists (the same defect as #1771).
599
+ */
600
+ function overloadSetOf(node) {
573
601
  const functionName = node.id?.name;
574
602
  if (!functionName)
603
+ return EMPTY_OVERLOAD_SET;
604
+ const statements = siblingStatementsOf(node);
605
+ if (!statements)
606
+ return EMPTY_OVERLOAD_SET;
607
+ let signatures = 0;
608
+ let implementations = 0;
609
+ for (const statement of statements) {
610
+ // `export function f(...)` is the same declaration one AST node deeper, and
611
+ // an overload set may export some of its members and not others.
612
+ const declaration = (0, lexicalScope_1.declarationOf)(statement);
613
+ if (declaration.type !== utils_1.AST_NODE_TYPES.TSDeclareFunction &&
614
+ declaration.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
615
+ continue;
616
+ }
617
+ if (declaration.id?.name !== functionName)
618
+ continue;
619
+ if (declaration.type === utils_1.AST_NODE_TYPES.TSDeclareFunction ||
620
+ !declaration.body) {
621
+ signatures += 1;
622
+ }
623
+ else {
624
+ implementations += 1;
625
+ }
626
+ }
627
+ return { signatures, implementations };
628
+ }
629
+ /**
630
+ * True when `node` is the IMPLEMENTATION of an overload set — a function with a
631
+ * body whose container also declares the same name as one or more
632
+ * declaration-only signatures.
633
+ *
634
+ * Its annotation is not a restatement of what the body returns: TypeScript
635
+ * checks each overload signature against the IMPLEMENTATION SIGNATURE, so the
636
+ * annotation is what the overloads are measured against. Inference yields the
637
+ * body's own type, which need not accept them — stripping `: void | string`
638
+ * from `function get(param?: string): void | string {}` infers `void` and makes
639
+ * the `: string` overload above it TS2394 (#2019).
640
+ *
641
+ * This carve-out ignores `allowOverloadedFunctions`. That option governs the
642
+ * declaration-only signatures, whose annotations are mandatory but carry no
643
+ * fixer, so reporting them costs nothing but a message. Here the report ships a
644
+ * fix that does not compile, which no option may ask for.
645
+ */
646
+ function isOverloadImplementation(node) {
647
+ if (!node.body)
575
648
  return false;
576
- let container = node.parent;
577
- while (container) {
578
- if (container.type === utils_1.AST_NODE_TYPES.Program ||
579
- container.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
580
- const declarations = container.body
581
- .map((statement) => {
582
- if (statement.type === utils_1.AST_NODE_TYPES.TSDeclareFunction) {
583
- return statement;
584
- }
585
- if (statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
586
- statement.declaration?.type === utils_1.AST_NODE_TYPES.TSDeclareFunction) {
587
- return statement.declaration;
588
- }
589
- return undefined;
590
- })
591
- .filter((value) => Boolean(value?.id?.name))
592
- .filter((decl) => decl.id.name === functionName);
593
- return declarations.length > 1;
649
+ return overloadSetOf(node).signatures > 0;
650
+ }
651
+ /**
652
+ * True when `node` is a declaration-only signature that belongs to an overload
653
+ * set: another signature declares the same name, or an implementation below it
654
+ * does. A lone `declare function f(): number;` overloads nothing, so it stays
655
+ * reportable.
656
+ */
657
+ function isOverloadedTsDeclareFunction(node) {
658
+ const { signatures, implementations } = overloadSetOf(node);
659
+ return signatures > 1 || implementations > 0;
660
+ }
661
+ /**
662
+ * A method's identity inside its class body. Overloads agree on all three
663
+ * components; `static f` and `f` merely spell the same name, and a computed key
664
+ * names nothing resolvable, so it yields nothing. The separator keeps a private
665
+ * `#log` from colliding with a string key `'#log'`.
666
+ */
667
+ function methodIdentityOf(node) {
668
+ if (node.computed)
669
+ return undefined;
670
+ if (node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
671
+ return `${node.static}\u0000private\u0000${node.key.name}`;
672
+ }
673
+ if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier &&
674
+ node.key.type !== utils_1.AST_NODE_TYPES.Literal) {
675
+ return undefined;
676
+ }
677
+ const name = getNameFromIdentifierOrLiteral(node.key);
678
+ return name === undefined
679
+ ? undefined
680
+ : `${node.static}\u0000public\u0000${name}`;
681
+ }
682
+ /**
683
+ * The overload set a class method belongs to, counted over the members of its
684
+ * own class body. A member without a body is an overload signature
685
+ * (`TSEmptyBodyFunctionExpression`); the one member with a body is the
686
+ * implementation.
687
+ */
688
+ function classOverloadSetOf(node) {
689
+ const identity = methodIdentityOf(node);
690
+ const classBody = node.parent;
691
+ if (!identity || classBody?.type !== utils_1.AST_NODE_TYPES.ClassBody) {
692
+ return EMPTY_OVERLOAD_SET;
693
+ }
694
+ let signatures = 0;
695
+ let implementations = 0;
696
+ for (const member of classBody.body) {
697
+ if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition)
698
+ continue;
699
+ if (methodIdentityOf(member) !== identity)
700
+ continue;
701
+ if (member.value.body) {
702
+ implementations += 1;
703
+ }
704
+ else {
705
+ signatures += 1;
594
706
  }
595
- container = container.parent;
596
707
  }
597
- return false;
708
+ return { signatures, implementations };
709
+ }
710
+ /**
711
+ * True when the method is the implementation of an overloaded class method, for
712
+ * the reason {@link isOverloadImplementation} gives — a class overload set is
713
+ * checked exactly as a function one is (#2019).
714
+ */
715
+ function isOverloadImplementationMethod(node) {
716
+ if (!node.value.body)
717
+ return false;
718
+ return classOverloadSetOf(node).signatures > 0;
719
+ }
720
+ /**
721
+ * True when the method is a declaration-only overload signature. A body-less
722
+ * method is legal only as part of an overload set, so it is exempt whenever the
723
+ * set holds anything else at all.
724
+ */
725
+ function isOverloadedClassMethodSignature(node) {
726
+ if (node.value.body)
727
+ return false;
728
+ const { signatures, implementations } = classOverloadSetOf(node);
729
+ return signatures > 1 || implementations > 0;
598
730
  }
599
731
  function isInterfaceOrAbstractMethodSignature(node) {
600
732
  if (node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition)
@@ -1233,6 +1365,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1233
1365
  isReadonlyWideningReturnType(returnType) ||
1234
1366
  isAllowedVoidReturnType(returnType) ||
1235
1367
  isDecoratorFactory(node, returnType) ||
1368
+ isOverloadImplementation(node) ||
1236
1369
  (mergedOptions.allowRecursiveFunctions &&
1237
1370
  isRecursiveFunction(node)) ||
1238
1371
  isReturnTypeRequiredByRecursion(node)) {
@@ -1292,6 +1425,9 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
1292
1425
  isReadonlyWideningReturnType(returnType) ||
1293
1426
  isAllowedVoidReturnType(returnType) ||
1294
1427
  isDecoratorFactory(node, returnType) ||
1428
+ isOverloadImplementationMethod(node) ||
1429
+ (mergedOptions.allowOverloadedFunctions &&
1430
+ isOverloadedClassMethodSignature(node)) ||
1295
1431
  (mergedOptions.allowAbstractMethodSignatures &&
1296
1432
  isInterfaceOrAbstractMethodSignature(node)) ||
1297
1433
  isReturnTypeRequiredByRecursion(node)) {
@@ -34,6 +34,46 @@ function isStringLiteralType(member) {
34
34
  const { literal } = member;
35
35
  return (literal.type === utils_1.AST_NODE_TYPES.Literal && typeof literal.value === 'string');
36
36
  }
37
+ /**
38
+ * A declaration file is ambient in its entirety, so a declaration in one is
39
+ * subject to the ambient restriction even without an explicit `declare`.
40
+ */
41
+ const DECLARATION_FILE = /\.d\.[cm]?ts$/i;
42
+ /**
43
+ * An ambient context accepts only a string, numeric or literal-enum `const`
44
+ * initializer (TS1254), so the derived `as const` array cannot be emitted there
45
+ * at all — `as const` or not, an array literal is rejected. Declining is the
46
+ * remedy rather than a different rewrite, because no legal rewrite exists in
47
+ * that position: the rule asks for importable runtime values and an ambient
48
+ * declaration is precisely the promise that no runtime value is emitted.
49
+ *
50
+ * `declare` on the OUTERMOST module declaration makes every level nested below
51
+ * it ambient too, so the whole ancestor chain is walked rather than just the
52
+ * enclosing block. A module named by a string literal (`module 'x' {}`) and a
53
+ * `global {}` augmentation are ambient by construction, with or without the
54
+ * modifier.
55
+ */
56
+ function isInAmbientContext(node, filename) {
57
+ if (DECLARATION_FILE.test(filename)) {
58
+ return true;
59
+ }
60
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
61
+ node.declare === true) {
62
+ return true;
63
+ }
64
+ let current = node.parent;
65
+ while (current) {
66
+ if (current.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration) {
67
+ if (current.declare === true ||
68
+ current.global === true ||
69
+ current.id.type === utils_1.AST_NODE_TYPES.Literal) {
70
+ return true;
71
+ }
72
+ }
73
+ current = current.parent;
74
+ }
75
+ return false;
76
+ }
37
77
  /**
38
78
  * Walk the scope chain upward collecting declared variable names. Used to skip
39
79
  * the autofix (report-only) when the derived `{TYPE}_VALUES` name is already
@@ -154,6 +194,9 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
154
194
  if (!members.every(isStringLiteralType)) {
155
195
  return;
156
196
  }
197
+ if (isInAmbientContext(node, context.getFilename())) {
198
+ return;
199
+ }
157
200
  const typeName = node.id.name;
158
201
  const constName = `${toUpperSnake(typeName)}_VALUES`;
159
202
  // The type alias is exported when wrapped in `export type X = ...`
@@ -62,6 +62,49 @@ const isInsideComponentMock = (node, componentModule) => {
62
62
  }
63
63
  return false;
64
64
  };
65
+ /**
66
+ * Name a declaration binds, for the forms a component is declared under:
67
+ * `const ImageOptimized = ...` (including `memo(...)`/`forwardRef(...)` around
68
+ * the body), `function ImageOptimized()` and `class ImageOptimized`. Anything
69
+ * else — an object property, a parameter — binds no declaration name the
70
+ * wrapper can be identified by, and treating it as one would exempt a mock
71
+ * factory keyed by the component name whatever module it stands in for.
72
+ */
73
+ const declaredNameOf = (node) => {
74
+ switch (node.type) {
75
+ case utils_1.AST_NODE_TYPES.VariableDeclarator:
76
+ return node.id.type === utils_1.AST_NODE_TYPES.Identifier ? node.id.name : null;
77
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
78
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
79
+ case utils_1.AST_NODE_TYPES.ClassDeclaration:
80
+ case utils_1.AST_NODE_TYPES.ClassExpression:
81
+ return node.id ? node.id.name : null;
82
+ default:
83
+ return null;
84
+ }
85
+ };
86
+ /**
87
+ * Names each local binding is exported under, so a wrapper declared as
88
+ * `Picture` and shipped as `export { Picture as ImageOptimized }` is still
89
+ * recognized as the component's own definition. A specifier carrying a `from`
90
+ * clause re-exports another module's binding and declares nothing here.
91
+ */
92
+ const exportedNamesByLocal = (program) => {
93
+ const exported = new Map();
94
+ for (const statement of program.body) {
95
+ if (statement.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
96
+ statement.source ||
97
+ statement.exportKind === 'type') {
98
+ continue;
99
+ }
100
+ for (const specifier of statement.specifiers) {
101
+ const names = exported.get(specifier.local.name) ?? new Set();
102
+ names.add(specifier.exported.name);
103
+ exported.set(specifier.local.name, names);
104
+ }
105
+ }
106
+ return exported;
107
+ };
65
108
  /**
66
109
  * A type-only specifier binds no value: it renders nothing, so it neither
67
110
  * bypasses the optimization pipeline nor can back a fix. The modifier lives
@@ -219,6 +262,40 @@ module.exports = (0, createRule_1.createRule)({
219
262
  * rule exists to centralize, not a violation of it.
220
263
  */
221
264
  const isComponentImplementationFile = moduleNameOf(context.getFilename()) === componentModule;
265
+ /**
266
+ * The wrapper is identified by the name the fixer would emit and by its
267
+ * module's name, which a component module's export shares by convention.
268
+ * Matching is exact so a distinct component whose name merely starts with
269
+ * it (`ImageOptimizedGallery`) stays reportable.
270
+ */
271
+ const isComponentName = (name) => name === COMPONENT_NAME || name === componentModule;
272
+ const exportedNames = exportedNamesByLocal(sourceCode.ast);
273
+ const definesComponent = (name) => {
274
+ if (isComponentName(name)) {
275
+ return true;
276
+ }
277
+ const aliases = exportedNames.get(name);
278
+ return !!aliases && [...aliases].some(isComponentName);
279
+ };
280
+ /**
281
+ * Whether the element sits inside the declaration of the component the fix
282
+ * points at. That declaration renders the image primitive by definition, so
283
+ * swapping it for the component makes the wrapper render itself: unbounded
284
+ * recursion, and a type error too, since the wrapper forwards only the
285
+ * props it destructured. The whole ancestry is walked because a helper
286
+ * nested inside the declaration is part of that implementation as well.
287
+ */
288
+ const isInsideComponentDefinition = (node) => {
289
+ let current = node.parent;
290
+ while (current) {
291
+ const declared = declaredNameOf(current);
292
+ if (declared && definesComponent(declared)) {
293
+ return true;
294
+ }
295
+ current = current.parent;
296
+ }
297
+ return false;
298
+ };
222
299
  return {
223
300
  // Handle JSX img elements
224
301
  JSXElement(node) {
@@ -228,7 +305,8 @@ module.exports = (0, createRule_1.createRule)({
228
305
  return;
229
306
  }
230
307
  if (isComponentImplementationFile ||
231
- isInsideComponentMock(node, componentModule)) {
308
+ isInsideComponentMock(node, componentModule) ||
309
+ isInsideComponentDefinition(node)) {
232
310
  return;
233
311
  }
234
312
  const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
@@ -85,6 +85,12 @@ export declare class ASTHelpers {
85
85
  * expression reads some other object.
86
86
  */
87
87
  private static classMemberNameReferencedBy;
88
+ /**
89
+ * The ECMA private names a class body declares, spelled as the graph spells
90
+ * them. A private name is scoped to the body that declares it, so this set
91
+ * is what an enclosing class must not mistake for its own members.
92
+ */
93
+ private static privateNamesDeclaredBy;
88
94
  private static walkChildNodes;
89
95
  static isNode(value: unknown): value is TSESTree.Node;
90
96
  static hasReturnStatement(node: TSESTree.Node): boolean;
@@ -423,7 +423,16 @@ class ASTHelpers {
423
423
  case 'ClassExpression': {
424
424
  // Traversal continues so `<ClassName>.<member>` statics stay visible,
425
425
  // but `this` no longer denotes the graphed instance.
426
- this.walkChildNodes(node, className, false, dependencies);
426
+ const nested = [];
427
+ this.walkChildNodes(node, className, false, nested);
428
+ // A nested class body SHADOWS the private names it declares: its `#q`
429
+ // is a member of that class, distinct from an enclosing class's `#q`,
430
+ // so reads of it constrain the nested layout rather than this one.
431
+ // Private names the nested class does not declare still resolve
432
+ // outward, so only the declared ones are dropped. Filtering as the
433
+ // recursion unwinds composes across any depth of nesting.
434
+ const shadowed = this.privateNamesDeclaredBy(node);
435
+ dependencies.push(...nested.filter((name) => !shadowed.has(name)));
427
436
  return;
428
437
  }
429
438
  case 'MemberExpression': {
@@ -447,6 +456,18 @@ class ASTHelpers {
447
456
  */
448
457
  static classMemberNameReferencedBy(node, className, isThisTheInstance) {
449
458
  const { object, property, computed } = node;
459
+ // A `#name` resolves LEXICALLY: it is a syntax error unless a class body
460
+ // enclosing the reference declares it, so the receiver it is read through
461
+ // cannot change which member it names. `other.#helper` and `this.#helper`
462
+ // reach the same member, which is why this branch precedes the receiver
463
+ // test that the dotted spellings need. Requiring `this`/<ClassName> here
464
+ // dropped the read a static initializer makes through another value of
465
+ // the same class, and with it the constraint that keeps the field's
466
+ // declaration above its reader (#2022). The `#` is part of the name so
467
+ // `#helper` and `helper` stay distinct members.
468
+ if (!computed && property?.type === 'PrivateIdentifier') {
469
+ return `#${property.name}`;
470
+ }
450
471
  const readsInstance = object?.type === 'ThisExpression' && isThisTheInstance;
451
472
  // An anonymous class expression has an empty name, which no identifier
452
473
  // can match.
@@ -457,12 +478,6 @@ class ASTHelpers {
457
478
  if (!computed && property?.type === 'Identifier') {
458
479
  return property.name;
459
480
  }
460
- // `this.#helper` names a member as precisely as `this.helper` does, and it
461
- // is the only spelling available for an ECMA private member. The `#` is
462
- // part of the name so `#helper` and `helper` stay distinct members.
463
- if (!computed && property?.type === 'PrivateIdentifier') {
464
- return `#${property.name}`;
465
- }
466
481
  // `this['helper']` names the member as precisely as `this.helper` does,
467
482
  // whereas `this[key]` names one only at runtime.
468
483
  if (computed &&
@@ -472,6 +487,25 @@ class ASTHelpers {
472
487
  }
473
488
  return null;
474
489
  }
490
+ /**
491
+ * The ECMA private names a class body declares, spelled as the graph spells
492
+ * them. A private name is scoped to the body that declares it, so this set
493
+ * is what an enclosing class must not mistake for its own members.
494
+ */
495
+ static privateNamesDeclaredBy(node) {
496
+ const names = new Set();
497
+ const members = node.body?.body;
498
+ if (!Array.isArray(members)) {
499
+ return names;
500
+ }
501
+ for (const member of members) {
502
+ const key = member?.key;
503
+ if (key?.type === 'PrivateIdentifier') {
504
+ names.add(`#${key.name}`);
505
+ }
506
+ }
507
+ return names;
508
+ }
475
509
  static walkChildNodes(node, className, isThisTheInstance, dependencies) {
476
510
  for (const [key, value] of Object.entries(node)) {
477
511
  if (ASTHelpers.NON_TRAVERSABLE_NODE_KEYS.has(key)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.154",
3
+ "version": "1.20.155",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,42 @@
1
1
  [
2
+ {
3
+ "version": "1.20.155",
4
+ "date": "2026-08-15T14:21:55.491Z",
5
+ "rules": [
6
+ {
7
+ "name": "class-methods-read-top-to-bottom",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2022
11
+ ],
12
+ "summary": "pin a # field by every read of it, not just this.#x (closes #2022)"
13
+ },
14
+ {
15
+ "name": "no-explicit-return-type",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 2019
19
+ ],
20
+ "summary": "spare the implementation signature of an overload set (closes #2019)"
21
+ },
22
+ {
23
+ "name": "prefer-union-from-const-array",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 2020
27
+ ],
28
+ "summary": "decline in an ambient context, where no const array is legal (closes #2020)"
29
+ },
30
+ {
31
+ "name": "require-image-optimized",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 2021
35
+ ],
36
+ "summary": "exempt the img inside ImageOptimized's own definition (closes #2021)"
37
+ }
38
+ ]
39
+ },
2
40
  {
3
41
  "version": "1.20.154",
4
42
  "date": "2026-08-15T03:47:51.570Z",