@sarj/eslint-plugin 16.36.6 → 16.36.7

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/dist/index.cjs CHANGED
@@ -2727,6 +2727,7 @@ function directCalls(binding3) {
2727
2727
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2728
2728
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2729
2729
  var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2730
+ var OPTIONAL_EXAMPLE = EXAMPLE.replace("readonly breed: string", "readonly breed?: string");
2730
2731
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2731
2732
  defaultLevel: "warning",
2732
2733
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2738,12 +2739,14 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2738
2739
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2739
2740
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2740
2741
  "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2741
- "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2742
+ "Reports proper native assignability, where covered is assignable to covering and the reverse is false, or mutual native assignability with a strict smaller declared covering surface and only optional covered-only members. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any cannot replace explicit declared coverage or the native Optional requirement. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; equal-key and incomparable mutually assignable structures are excluded. The mutual case additionally requires inline contracts or locally bound interfaces/type aliases whose alias and heritage references can be traced through the parser-owned type scope. Merged interface fragments must all resolve to the reference's same native symbol; mixed exported/local recovery abstains. Imported, global and qualified-namespace references in that path abstain; proper one-way coverage retains its existing alias/import support.",
2742
2743
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2743
2744
  ],
2744
2745
  examples: [
2745
2746
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2746
2747
  { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2748
+ { id: "optional-covered-alternative", scenarioId: "optional-readonly-contract", title: "An unread optional breed retains the smaller Animal surface", outcome: "match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2749
+ { id: "optional-public-contract", scenarioId: "optional-readonly-contract", title: "A public optional union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2747
2750
  { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2748
2751
  { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2749
2752
  ]
@@ -2758,6 +2761,68 @@ function scalarFields(type, checker) {
2758
2761
  }
2759
2762
  return fields2.size ? fields2 : null;
2760
2763
  }
2764
+ function localContractBindings(roots, source, services) {
2765
+ const pending = [...roots];
2766
+ const visited = /* @__PURE__ */ new Set();
2767
+ for (let index = 0; index < pending.length; index++) {
2768
+ const node = pending[index];
2769
+ if (!node || visited.has(node)) continue;
2770
+ visited.add(node);
2771
+ const references = localContractReferences(node, source, services);
2772
+ if (references === null) return false;
2773
+ pending.push(...references);
2774
+ }
2775
+ return true;
2776
+ }
2777
+ function localContractReferences(node, source, services) {
2778
+ switch (node.type) {
2779
+ case import_utils24.AST_NODE_TYPES.TSTypeLiteral:
2780
+ return [];
2781
+ case import_utils24.AST_NODE_TYPES.TSIntersectionType:
2782
+ return node.types;
2783
+ case import_utils24.AST_NODE_TYPES.TSTypeReference:
2784
+ return !node.typeArguments && node.typeName.type === import_utils24.AST_NODE_TYPES.Identifier ? [node.typeName] : null;
2785
+ case import_utils24.AST_NODE_TYPES.Identifier:
2786
+ return localBindingReferences(node, source, services);
2787
+ default:
2788
+ return null;
2789
+ }
2790
+ }
2791
+ function localBindingReferences(identifier, source, services) {
2792
+ const definitions2 = typeBinding(source, identifier)?.defs.filter((definition) => definition.isTypeDefinition).map((definition) => definition.node);
2793
+ if (!definitions2?.length || !definitions2.every(moduleTypeDeclaration)) return null;
2794
+ const first = definitions2[0];
2795
+ if (definitions2.length === 1 && first?.type === import_utils24.AST_NODE_TYPES.TSTypeAliasDeclaration && !first.typeParameters) return [first.typeAnnotation];
2796
+ const interfaces = definitions2.filter((definition) => definition.type === import_utils24.AST_NODE_TYPES.TSInterfaceDeclaration);
2797
+ if (interfaces.length !== definitions2.length || interfaces.some((declaration) => declaration.typeParameters)) return null;
2798
+ if (interfaces.length > 1) {
2799
+ const checker = services.program.getTypeChecker();
2800
+ const symbol = checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(identifier));
2801
+ if (!symbol || interfaces.some((declaration) => checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(declaration.id)) !== symbol)) return null;
2802
+ }
2803
+ const bases = interfaces.flatMap((declaration) => declaration.extends.map((base) => base.expression));
2804
+ return bases.every((base) => base.type === import_utils24.AST_NODE_TYPES.Identifier) ? bases : null;
2805
+ }
2806
+ function moduleTypeDeclaration(node) {
2807
+ const parent = node.parent;
2808
+ if (parent?.type === import_utils24.AST_NODE_TYPES.Program) return true;
2809
+ if (parent?.type !== import_utils24.AST_NODE_TYPES.ExportNamedDeclaration && parent?.type !== import_utils24.AST_NODE_TYPES.ExportDefaultDeclaration) return false;
2810
+ return parent.parent.type === import_utils24.AST_NODE_TYPES.Program;
2811
+ }
2812
+ function coveringAlternative(leftNode, rightNode, checker, fieldsOf, reads) {
2813
+ const left = checker.getTypeFromTypeNode(leftNode);
2814
+ const right = checker.getTypeFromTypeNode(rightNode);
2815
+ const leftFields = fieldsOf(left);
2816
+ const rightFields = fieldsOf(right);
2817
+ if (!leftFields || !rightFields) return null;
2818
+ const forward = checker.isTypeAssignableTo(left, right);
2819
+ const reverse = checker.isTypeAssignableTo(right, left);
2820
+ const mutual = forward && reverse;
2821
+ if (!forward && !reverse || mutual && leftFields.size === rightFields.size) return null;
2822
+ const [covered2, covering, coveredFields, coveringFields] = forward && (!reverse || leftFields.size > rightFields.size) ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2823
+ if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || mutual && [...coveredFields].some(([key, required]) => required && !coveringFields.has(key)) || [...reads].some((key) => !coveringFields.has(key)) || ![...reads].some((key) => coveringFields.get(key))) return null;
2824
+ return { covered: covered2, covering, mutual };
2825
+ }
2761
2826
  function privateUnionFix(sourceCode, services, proof) {
2762
2827
  const covering = services.tsNodeToESTreeNodeMap.get(proof.covering);
2763
2828
  if (!validConsumerFixCalls(proof.candidate, proof.parameter, sourceCode, services)) return null;
@@ -2808,17 +2873,11 @@ var no_redundant_consumer_union_default = createRule({
2808
2873
  const leftNode = annotation.types[0];
2809
2874
  const rightNode = annotation.types[1];
2810
2875
  if (!leftNode || !rightNode) continue;
2811
- const left = checker.getTypeFromTypeNode(leftNode);
2812
- const right = checker.getTypeFromTypeNode(rightNode);
2813
- const leftFields = fieldsOf(left);
2814
- const rightFields = fieldsOf(right);
2815
- if (!leftFields || !rightFields) continue;
2816
- const forward = checker.isTypeAssignableTo(left, right);
2817
- const reverse = checker.isTypeAssignableTo(right, left);
2818
- if (forward === reverse) continue;
2819
- const [coveredNode, coveringNode, coveredFields, coveringFields] = forward ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2820
- if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || [...consumer.reads].some((key) => !coveringFields.has(key)) || ![...consumer.reads].some((key) => coveringFields.get(key))) continue;
2876
+ const coverage = coveringAlternative(leftNode, rightNode, checker, fieldsOf, consumer.reads);
2877
+ if (!coverage) continue;
2821
2878
  const reportNode = services.tsNodeToESTreeNodeMap.get(annotation);
2879
+ if (coverage.mutual && (reportNode.type !== import_utils24.AST_NODE_TYPES.TSUnionType || !localContractBindings(reportNode.types, context.sourceCode, services))) continue;
2880
+ const { covered: coveredNode, covering: coveringNode } = coverage;
2822
2881
  const fix = privateUnionFix(context.sourceCode, services, {
2823
2882
  candidate: candidate2,
2824
2883
  parameter: consumer.parameter,
@@ -4025,7 +4084,7 @@ function isSearchParamReceiver(call, services) {
4025
4084
  // package.json
4026
4085
  var package_default = {
4027
4086
  name: "@sarj/eslint-plugin",
4028
- version: "16.36.6",
4087
+ version: "16.36.7",
4029
4088
  packageManager: "npm@12.2.1",
4030
4089
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
4031
4090
  type: "module",
package/dist/index.js CHANGED
@@ -2676,6 +2676,7 @@ function directCalls(binding3) {
2676
2676
  var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
2677
2677
  var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
2678
2678
  var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
2679
+ var OPTIONAL_EXAMPLE = EXAMPLE.replace("readonly breed: string", "readonly breed?: string");
2679
2680
  var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2680
2681
  defaultLevel: "warning",
2681
2682
  summary: "Keep the covering object contract in a private scalar consumer union.",
@@ -2687,12 +2688,14 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
2687
2688
  "Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
2688
2689
  "Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
2689
2690
  "Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
2690
- "Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
2691
+ "Reports proper native assignability, where covered is assignable to covering and the reverse is false, or mutual native assignability with a strict smaller declared covering surface and only optional covered-only members. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any cannot replace explicit declared coverage or the native Optional requirement. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; equal-key and incomparable mutually assignable structures are excluded. The mutual case additionally requires inline contracts or locally bound interfaces/type aliases whose alias and heritage references can be traced through the parser-owned type scope. Merged interface fragments must all resolve to the reference's same native symbol; mixed exported/local recovery abstains. Imported, global and qualified-namespace references in that path abstain; proper one-way coverage retains its existing alias/import support.",
2691
2692
  "General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
2692
2693
  ],
2693
2694
  examples: [
2694
2695
  { id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
2695
2696
  { id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2697
+ { id: "optional-covered-alternative", scenarioId: "optional-readonly-contract", title: "An unread optional breed retains the smaller Animal surface", outcome: "match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2698
+ { id: "optional-public-contract", scenarioId: "optional-readonly-contract", title: "A public optional union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: OPTIONAL_EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
2696
2699
  { id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
2697
2700
  { id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
2698
2701
  ]
@@ -2707,6 +2710,68 @@ function scalarFields(type, checker) {
2707
2710
  }
2708
2711
  return fields2.size ? fields2 : null;
2709
2712
  }
2713
+ function localContractBindings(roots, source, services) {
2714
+ const pending = [...roots];
2715
+ const visited = /* @__PURE__ */ new Set();
2716
+ for (let index = 0; index < pending.length; index++) {
2717
+ const node = pending[index];
2718
+ if (!node || visited.has(node)) continue;
2719
+ visited.add(node);
2720
+ const references = localContractReferences(node, source, services);
2721
+ if (references === null) return false;
2722
+ pending.push(...references);
2723
+ }
2724
+ return true;
2725
+ }
2726
+ function localContractReferences(node, source, services) {
2727
+ switch (node.type) {
2728
+ case AST_NODE_TYPES22.TSTypeLiteral:
2729
+ return [];
2730
+ case AST_NODE_TYPES22.TSIntersectionType:
2731
+ return node.types;
2732
+ case AST_NODE_TYPES22.TSTypeReference:
2733
+ return !node.typeArguments && node.typeName.type === AST_NODE_TYPES22.Identifier ? [node.typeName] : null;
2734
+ case AST_NODE_TYPES22.Identifier:
2735
+ return localBindingReferences(node, source, services);
2736
+ default:
2737
+ return null;
2738
+ }
2739
+ }
2740
+ function localBindingReferences(identifier, source, services) {
2741
+ const definitions2 = typeBinding(source, identifier)?.defs.filter((definition) => definition.isTypeDefinition).map((definition) => definition.node);
2742
+ if (!definitions2?.length || !definitions2.every(moduleTypeDeclaration)) return null;
2743
+ const first = definitions2[0];
2744
+ if (definitions2.length === 1 && first?.type === AST_NODE_TYPES22.TSTypeAliasDeclaration && !first.typeParameters) return [first.typeAnnotation];
2745
+ const interfaces = definitions2.filter((definition) => definition.type === AST_NODE_TYPES22.TSInterfaceDeclaration);
2746
+ if (interfaces.length !== definitions2.length || interfaces.some((declaration) => declaration.typeParameters)) return null;
2747
+ if (interfaces.length > 1) {
2748
+ const checker = services.program.getTypeChecker();
2749
+ const symbol = checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(identifier));
2750
+ if (!symbol || interfaces.some((declaration) => checker.getSymbolAtLocation(services.esTreeNodeToTSNodeMap.get(declaration.id)) !== symbol)) return null;
2751
+ }
2752
+ const bases = interfaces.flatMap((declaration) => declaration.extends.map((base) => base.expression));
2753
+ return bases.every((base) => base.type === AST_NODE_TYPES22.Identifier) ? bases : null;
2754
+ }
2755
+ function moduleTypeDeclaration(node) {
2756
+ const parent = node.parent;
2757
+ if (parent?.type === AST_NODE_TYPES22.Program) return true;
2758
+ if (parent?.type !== AST_NODE_TYPES22.ExportNamedDeclaration && parent?.type !== AST_NODE_TYPES22.ExportDefaultDeclaration) return false;
2759
+ return parent.parent.type === AST_NODE_TYPES22.Program;
2760
+ }
2761
+ function coveringAlternative(leftNode, rightNode, checker, fieldsOf, reads) {
2762
+ const left = checker.getTypeFromTypeNode(leftNode);
2763
+ const right = checker.getTypeFromTypeNode(rightNode);
2764
+ const leftFields = fieldsOf(left);
2765
+ const rightFields = fieldsOf(right);
2766
+ if (!leftFields || !rightFields) return null;
2767
+ const forward = checker.isTypeAssignableTo(left, right);
2768
+ const reverse = checker.isTypeAssignableTo(right, left);
2769
+ const mutual = forward && reverse;
2770
+ if (!forward && !reverse || mutual && leftFields.size === rightFields.size) return null;
2771
+ const [covered2, covering, coveredFields, coveringFields] = forward && (!reverse || leftFields.size > rightFields.size) ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2772
+ if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || mutual && [...coveredFields].some(([key, required]) => required && !coveringFields.has(key)) || [...reads].some((key) => !coveringFields.has(key)) || ![...reads].some((key) => coveringFields.get(key))) return null;
2773
+ return { covered: covered2, covering, mutual };
2774
+ }
2710
2775
  function privateUnionFix(sourceCode, services, proof) {
2711
2776
  const covering = services.tsNodeToESTreeNodeMap.get(proof.covering);
2712
2777
  if (!validConsumerFixCalls(proof.candidate, proof.parameter, sourceCode, services)) return null;
@@ -2757,17 +2822,11 @@ var no_redundant_consumer_union_default = createRule({
2757
2822
  const leftNode = annotation.types[0];
2758
2823
  const rightNode = annotation.types[1];
2759
2824
  if (!leftNode || !rightNode) continue;
2760
- const left = checker.getTypeFromTypeNode(leftNode);
2761
- const right = checker.getTypeFromTypeNode(rightNode);
2762
- const leftFields = fieldsOf(left);
2763
- const rightFields = fieldsOf(right);
2764
- if (!leftFields || !rightFields) continue;
2765
- const forward = checker.isTypeAssignableTo(left, right);
2766
- const reverse = checker.isTypeAssignableTo(right, left);
2767
- if (forward === reverse) continue;
2768
- const [coveredNode, coveringNode, coveredFields, coveringFields] = forward ? [leftNode, rightNode, leftFields, rightFields] : [rightNode, leftNode, rightFields, leftFields];
2769
- if ([...coveringFields.keys()].some((key) => !coveredFields.has(key)) || [...consumer.reads].some((key) => !coveringFields.has(key)) || ![...consumer.reads].some((key) => coveringFields.get(key))) continue;
2825
+ const coverage = coveringAlternative(leftNode, rightNode, checker, fieldsOf, consumer.reads);
2826
+ if (!coverage) continue;
2770
2827
  const reportNode = services.tsNodeToESTreeNodeMap.get(annotation);
2828
+ if (coverage.mutual && (reportNode.type !== AST_NODE_TYPES22.TSUnionType || !localContractBindings(reportNode.types, context.sourceCode, services))) continue;
2829
+ const { covered: coveredNode, covering: coveringNode } = coverage;
2771
2830
  const fix = privateUnionFix(context.sourceCode, services, {
2772
2831
  candidate: candidate2,
2773
2832
  parameter: consumer.parameter,
@@ -3978,7 +4037,7 @@ function isSearchParamReceiver(call, services) {
3978
4037
  // package.json
3979
4038
  var package_default = {
3980
4039
  name: "@sarj/eslint-plugin",
3981
- version: "16.36.6",
4040
+ version: "16.36.7",
3982
4041
  packageManager: "npm@12.2.1",
3983
4042
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
3984
4043
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.36.6",
3
+ "version": "16.36.7",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",