@sarj/eslint-plugin 15.6.1 → 15.6.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/dist/index.cjs CHANGED
@@ -5648,9 +5648,9 @@ var no_restated_jsdoc_default = createRule({
5648
5648
  continue;
5649
5649
  }
5650
5650
  if ((paramTags.length > 0 || returnTags.length > 0) && documentsTypedFunction(sourceCode, comment)) continue;
5651
- const nameTokens = tokensOf([declaration.name]);
5651
+ const nameTokens2 = tokensOf([declaration.name]);
5652
5652
  const paramTokens = tokensOf(declaration.params);
5653
- const known = /* @__PURE__ */ new Set([...nameTokens, ...paramTokens]);
5653
+ const known = /* @__PURE__ */ new Set([...nameTokens2, ...paramTokens]);
5654
5654
  let addsNothing = covered(describedText, known);
5655
5655
  for (const tag of paramTags) {
5656
5656
  const text = tag.text.replace(/^\{[^}]*\}\s*/, "");
@@ -5665,7 +5665,7 @@ var no_restated_jsdoc_default = createRule({
5665
5665
  addsNothing = false;
5666
5666
  break;
5667
5667
  }
5668
- const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens]);
5668
+ const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens2]);
5669
5669
  if (!covered(match[2] ?? "", own)) {
5670
5670
  addsNothing = false;
5671
5671
  break;
@@ -12254,6 +12254,14 @@ var OPTIONAL_MODIFIERS = /* @__PURE__ */ new Set([
12254
12254
  "nullish"
12255
12255
  ]);
12256
12256
  var NULLABLE_MODIFIERS = /* @__PURE__ */ new Set(["nullable", "nullish"]);
12257
+ var DOMAIN_PRESERVING_MODIFIERS = /* @__PURE__ */ new Set([
12258
+ ...SHAPE_PRESERVING_METHODS,
12259
+ ...OPTIONAL_MODIFIERS,
12260
+ ...NULLABLE_MODIFIERS,
12261
+ "catch",
12262
+ "default",
12263
+ "prefault"
12264
+ ]);
12257
12265
  var RESHAPING_MODIFIERS = /* @__PURE__ */ new Set([
12258
12266
  "transform",
12259
12267
  "pipe",
@@ -12314,6 +12322,107 @@ var LEAF_NODE_TYPES = {
12314
12322
  discriminatedUnion: [import_utils61.AST_NODE_TYPES.TSUnionType, import_utils61.AST_NODE_TYPES.TSTypeReference],
12315
12323
  intersection: [import_utils61.AST_NODE_TYPES.TSIntersectionType, import_utils61.AST_NODE_TYPES.TSTypeReference]
12316
12324
  };
12325
+ function primitiveLiteralKey(node) {
12326
+ if (node.type !== import_utils61.AST_NODE_TYPES.Literal) {
12327
+ return null;
12328
+ }
12329
+ if (node.value === null) {
12330
+ return "null";
12331
+ }
12332
+ switch (typeof node.value) {
12333
+ case "string":
12334
+ return `string:${node.value}`;
12335
+ case "number":
12336
+ return `number:${String(node.value)}`;
12337
+ case "boolean":
12338
+ return `boolean:${String(node.value)}`;
12339
+ case "bigint":
12340
+ return `bigint:${String(node.value)}`;
12341
+ default:
12342
+ return null;
12343
+ }
12344
+ }
12345
+ function exactDomain(keys) {
12346
+ if (keys.length === 0 || keys.some((key) => key === null)) {
12347
+ return null;
12348
+ }
12349
+ const domain = new Set(keys);
12350
+ return domain.size === keys.length ? domain : null;
12351
+ }
12352
+ function staticZodDomain(leaf, call) {
12353
+ if (leaf === null || call === null) {
12354
+ return void 0;
12355
+ }
12356
+ if (leaf === "literal") {
12357
+ const [argument] = call.arguments;
12358
+ if (argument === void 0 || argument.type === import_utils61.AST_NODE_TYPES.SpreadElement) {
12359
+ return null;
12360
+ }
12361
+ if (argument.type === import_utils61.AST_NODE_TYPES.ArrayExpression) {
12362
+ return exactDomain(
12363
+ argument.elements.map(
12364
+ (element) => element === null || element.type === import_utils61.AST_NODE_TYPES.SpreadElement ? null : primitiveLiteralKey(element)
12365
+ )
12366
+ );
12367
+ }
12368
+ return exactDomain([primitiveLiteralKey(argument)]);
12369
+ }
12370
+ if (leaf === "enum") {
12371
+ const [argument] = call.arguments;
12372
+ if (argument === void 0 || argument.type === import_utils61.AST_NODE_TYPES.SpreadElement) {
12373
+ return null;
12374
+ }
12375
+ if (argument.type === import_utils61.AST_NODE_TYPES.ArrayExpression) {
12376
+ return exactDomain(
12377
+ argument.elements.map((element) => {
12378
+ if (element === null || element.type === import_utils61.AST_NODE_TYPES.SpreadElement) {
12379
+ return null;
12380
+ }
12381
+ const key = primitiveLiteralKey(element);
12382
+ return key?.startsWith("string:") === true ? key : null;
12383
+ })
12384
+ );
12385
+ }
12386
+ if (argument.type === import_utils61.AST_NODE_TYPES.ObjectExpression) {
12387
+ return exactDomain(
12388
+ argument.properties.map((property) => {
12389
+ if (property.type !== import_utils61.AST_NODE_TYPES.Property || property.computed || property.kind !== "init" || property.method || property.shorthand) {
12390
+ return null;
12391
+ }
12392
+ const key = primitiveLiteralKey(property.value);
12393
+ return key?.startsWith("string:") === true ? key : null;
12394
+ })
12395
+ );
12396
+ }
12397
+ return null;
12398
+ }
12399
+ if (leaf === "nativeEnum" || leaf === "union" || leaf === "discriminatedUnion") {
12400
+ return null;
12401
+ }
12402
+ return void 0;
12403
+ }
12404
+ function sameDomain(left, right) {
12405
+ if (left.size !== right.size) {
12406
+ return false;
12407
+ }
12408
+ for (const value of left) {
12409
+ if (!right.has(value)) {
12410
+ return false;
12411
+ }
12412
+ }
12413
+ return true;
12414
+ }
12415
+ function isExportedDeclaration(node) {
12416
+ return node.parent?.type === import_utils61.AST_NODE_TYPES.ExportNamedDeclaration;
12417
+ }
12418
+ function isModuleLevelConst(node) {
12419
+ const declaration = node.parent;
12420
+ if (declaration.type !== import_utils61.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") {
12421
+ return false;
12422
+ }
12423
+ const container = declaration.parent;
12424
+ return container.type === import_utils61.AST_NODE_TYPES.Program || container.type === import_utils61.AST_NODE_TYPES.ExportNamedDeclaration && container.parent.type === import_utils61.AST_NODE_TYPES.Program;
12425
+ }
12317
12426
  function normalizeSchemaName(name) {
12318
12427
  return name.replace(/Schema$/i, "").replace(/^Z(?=[A-Z])/, "").toLowerCase();
12319
12428
  }
@@ -12344,7 +12453,18 @@ function unwrapNullish(annotation) {
12344
12453
  }
12345
12454
  return { core: rest.length === 0 ? null : annotation, nullable };
12346
12455
  }
12347
- function leafAgrees(leaf, annotation) {
12456
+ function leafAgrees(field, annotation) {
12457
+ if (field.domain === null) {
12458
+ return false;
12459
+ }
12460
+ if (field.domain !== void 0) {
12461
+ if (annotation === null) {
12462
+ return false;
12463
+ }
12464
+ const annotationDomain = typeLiteralDomain(annotation);
12465
+ return annotationDomain !== null && sameDomain(field.domain, annotationDomain);
12466
+ }
12467
+ const { leaf } = field;
12348
12468
  if (leaf === null || annotation === null) {
12349
12469
  return null;
12350
12470
  }
@@ -12361,6 +12481,47 @@ function leafAgrees(leaf, annotation) {
12361
12481
  }
12362
12482
  return expected.includes(core.type);
12363
12483
  }
12484
+ function typeLiteralDomain(annotation) {
12485
+ const members = annotation.type === import_utils61.AST_NODE_TYPES.TSUnionType ? annotation.types : [annotation];
12486
+ const keys = [];
12487
+ for (const member of members) {
12488
+ if (member.type === import_utils61.AST_NODE_TYPES.TSNullKeyword) {
12489
+ continue;
12490
+ }
12491
+ if (member.type !== import_utils61.AST_NODE_TYPES.TSLiteralType) {
12492
+ return null;
12493
+ }
12494
+ keys.push(primitiveLiteralKey(member.literal));
12495
+ }
12496
+ return exactDomain(keys);
12497
+ }
12498
+ function staticStringUnionDomain(node) {
12499
+ if (node.type !== import_utils61.AST_NODE_TYPES.TSUnionType) {
12500
+ return null;
12501
+ }
12502
+ const keys = node.types.map((member) => {
12503
+ if (member.type !== import_utils61.AST_NODE_TYPES.TSLiteralType) {
12504
+ return null;
12505
+ }
12506
+ const key = primitiveLiteralKey(member.literal);
12507
+ return key?.startsWith("string:") === true ? key : null;
12508
+ });
12509
+ const domain = exactDomain(keys);
12510
+ return domain !== null && domain.size >= 2 ? domain : null;
12511
+ }
12512
+ function nameTokens(name) {
12513
+ return name.replace(/([a-z\d])([A-Z])/gu, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2").split(/[^A-Za-z\d]+/u).filter((token) => token !== "").map((token) => token.toLowerCase());
12514
+ }
12515
+ function schemaNameTokens(name) {
12516
+ const tokens = nameTokens(name);
12517
+ const withoutPrefix = tokens[0] === "z" ? tokens.slice(1) : tokens;
12518
+ return withoutPrefix.at(-1) === "schema" ? withoutPrefix.slice(0, -1) : withoutPrefix;
12519
+ }
12520
+ function endsWithTokens(candidate, suffix) {
12521
+ return suffix.length >= 2 && candidate.length >= suffix.length && suffix.every(
12522
+ (segment, index) => candidate[candidate.length - suffix.length + index] === segment
12523
+ );
12524
+ }
12364
12525
  var prefer_zod_infer_default = createRule({
12365
12526
  name: "prefer-zod-infer",
12366
12527
  documentation: preferZodInferDocumentation,
@@ -12383,7 +12544,8 @@ var prefer_zod_infer_default = createRule({
12383
12544
  }
12384
12545
  ],
12385
12546
  messages: {
12386
- handWrittenTwin: "`{{typeName}}` restates the shape of the Zod schema `{{schemaName}}` declared in this module. Derive it instead \u2014 `type {{typeName}} = z.infer<typeof {{schemaName}}>` \u2014 so a field added to the schema cannot silently leave the type behind."
12547
+ handWrittenTwin: "`{{typeName}}` restates the shape of the Zod schema `{{schemaName}}` declared in this module. Derive it instead \u2014 `type {{typeName}} = z.infer<typeof {{schemaName}}>` \u2014 so a field added to the schema cannot silently leave the type behind.",
12548
+ repeatedEnumUnion: "`{{propertyName}}` repeats the literal domain already named by `{{typeName}}` and `{{schemaName}}` in multiple object types. Reuse `{{typeName}}` so the accepted values have one source of truth."
12387
12549
  }
12388
12550
  },
12389
12551
  defaultOptions: [{}],
@@ -12397,6 +12559,9 @@ var prefer_zod_infer_default = createRule({
12397
12559
  }
12398
12560
  const zodNamespaces = /* @__PURE__ */ new Set();
12399
12561
  const schemas = [];
12562
+ const enumSchemas = [];
12563
+ const inferredAliases = [];
12564
+ const literalUnionOccurrences = [];
12400
12565
  const typeDeclarations = [];
12401
12566
  const constrainedTypeNames = /* @__PURE__ */ new Set();
12402
12567
  const reshapedSchemaNames = /* @__PURE__ */ new Set();
@@ -12421,10 +12586,21 @@ var prefer_zod_infer_default = createRule({
12421
12586
  const callee = call.callee;
12422
12587
  return callee.type === import_utils61.AST_NODE_TYPES.MemberExpression && callee.property.type === import_utils61.AST_NODE_TYPES.Identifier ? callee.property.name : "";
12423
12588
  }
12589
+ function recordZodImport(node) {
12590
+ if (!isZodModule(node.source.value)) {
12591
+ return;
12592
+ }
12593
+ for (const specifier of node.specifiers) {
12594
+ if (specifier.type === import_utils61.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils61.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils61.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils61.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
12595
+ zodNamespaces.add(specifier.local.name);
12596
+ }
12597
+ }
12598
+ }
12424
12599
  function schemaField(node) {
12425
12600
  const modifiers = [];
12426
12601
  let current = node;
12427
12602
  let leaf = null;
12603
+ let leafCall = null;
12428
12604
  while (current.type === import_utils61.AST_NODE_TYPES.CallExpression) {
12429
12605
  const callee = current.callee;
12430
12606
  if (callee.type !== import_utils61.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils61.AST_NODE_TYPES.Identifier) {
@@ -12433,18 +12609,72 @@ var prefer_zod_infer_default = createRule({
12433
12609
  const receiver = callee.object;
12434
12610
  if (receiver.type === import_utils61.AST_NODE_TYPES.Identifier && zodNamespaces.has(receiver.name)) {
12435
12611
  leaf = callee.property.name;
12612
+ leafCall = current;
12436
12613
  break;
12437
12614
  }
12438
12615
  modifiers.push(callee.property.name);
12439
12616
  current = receiver;
12440
12617
  }
12441
12618
  return {
12619
+ domain: modifiers.every(
12620
+ (name) => DOMAIN_PRESERVING_MODIFIERS.has(name)
12621
+ ) ? staticZodDomain(leaf, leafCall) : null,
12442
12622
  leaf,
12443
12623
  optional: modifiers.some((name) => OPTIONAL_MODIFIERS.has(name)),
12444
12624
  nullable: modifiers.some((name) => NULLABLE_MODIFIERS.has(name)),
12445
12625
  reshaped: modifiers.some((name) => RESHAPING_MODIFIERS.has(name))
12446
12626
  };
12447
12627
  }
12628
+ function enumSchemaDomain(init) {
12629
+ const chain = zodCallChain(init);
12630
+ if (chain === null || chain.length !== 1) {
12631
+ return null;
12632
+ }
12633
+ const [call] = chain;
12634
+ if (call === void 0 || methodName2(call) !== "enum") {
12635
+ return null;
12636
+ }
12637
+ const domain = staticZodDomain("enum", call);
12638
+ return domain instanceof Set && domain.size >= 2 ? domain : null;
12639
+ }
12640
+ function inferredSchemaName(node) {
12641
+ if (node.type !== import_utils61.AST_NODE_TYPES.TSTypeReference || node.typeName.type !== import_utils61.AST_NODE_TYPES.TSQualifiedName || node.typeName.left.type !== import_utils61.AST_NODE_TYPES.Identifier || !zodNamespaces.has(node.typeName.left.name) || node.typeName.right.name !== "infer") {
12642
+ return null;
12643
+ }
12644
+ const arguments_ = node.typeArguments?.params ?? [];
12645
+ const [argument] = arguments_;
12646
+ return arguments_.length === 1 && argument?.type === import_utils61.AST_NODE_TYPES.TSTypeQuery && argument.exprName.type === import_utils61.AST_NODE_TYPES.Identifier ? argument.exprName.name : null;
12647
+ }
12648
+ function recordLiteralUnions(members, owner, ownerName, exported) {
12649
+ for (const member of members) {
12650
+ if (member.type !== import_utils61.AST_NODE_TYPES.TSPropertySignature || member.computed || member.optional || member.readonly || member.typeAnnotation === void 0) {
12651
+ continue;
12652
+ }
12653
+ const key = member.key;
12654
+ const propertyName3 = key.type === import_utils61.AST_NODE_TYPES.Identifier ? key.name : key.type === import_utils61.AST_NODE_TYPES.Literal && typeof key.value === "string" ? key.value : null;
12655
+ if (propertyName3 === null) {
12656
+ continue;
12657
+ }
12658
+ const propertyTokens = nameTokens(propertyName3);
12659
+ if (propertyTokens.length < 2) {
12660
+ continue;
12661
+ }
12662
+ const annotation = member.typeAnnotation.typeAnnotation;
12663
+ const domain = staticStringUnionDomain(annotation);
12664
+ if (domain === null || annotation.type !== import_utils61.AST_NODE_TYPES.TSUnionType) {
12665
+ continue;
12666
+ }
12667
+ literalUnionOccurrences.push({
12668
+ domain,
12669
+ exported,
12670
+ node: annotation,
12671
+ owner,
12672
+ ownerName,
12673
+ propertyName: propertyName3,
12674
+ propertyTokens
12675
+ });
12676
+ }
12677
+ }
12448
12678
  function schemaFields(init) {
12449
12679
  const chain = zodCallChain(init);
12450
12680
  if (chain === null || chain.length === 0) {
@@ -12545,7 +12775,7 @@ var prefer_zod_infer_default = createRule({
12545
12775
  if (member.readonly) {
12546
12776
  return false;
12547
12777
  }
12548
- const agrees = leafAgrees(field.leaf, member.annotation);
12778
+ const agrees = leafAgrees(field, member.annotation);
12549
12779
  if (agrees === false) {
12550
12780
  return false;
12551
12781
  }
@@ -12556,16 +12786,16 @@ var prefer_zod_infer_default = createRule({
12556
12786
  return agreements > 0;
12557
12787
  }
12558
12788
  return {
12559
- ImportDeclaration(node) {
12560
- if (!isZodModule(node.source.value)) {
12561
- return;
12562
- }
12563
- for (const specifier of node.specifiers) {
12564
- if (specifier.type === import_utils61.AST_NODE_TYPES.ImportNamespaceSpecifier || specifier.type === import_utils61.AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === import_utils61.AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === import_utils61.AST_NODE_TYPES.Identifier && specifier.imported.name === "z") {
12565
- zodNamespaces.add(specifier.local.name);
12789
+ Program(node) {
12790
+ for (const statement of node.body) {
12791
+ if (statement.type === import_utils61.AST_NODE_TYPES.ImportDeclaration) {
12792
+ recordZodImport(statement);
12566
12793
  }
12567
12794
  }
12568
12795
  },
12796
+ ImportDeclaration(node) {
12797
+ recordZodImport(node);
12798
+ },
12569
12799
  VariableDeclarator(node) {
12570
12800
  if (node.id.type !== import_utils61.AST_NODE_TYPES.Identifier || node.init == null) {
12571
12801
  return;
@@ -12574,6 +12804,13 @@ var prefer_zod_infer_default = createRule({
12574
12804
  if (fields !== null) {
12575
12805
  schemas.push({ name: node.id.name, fields });
12576
12806
  }
12807
+ if (isModuleLevelConst(node)) {
12808
+ const domain = enumSchemaDomain(node.init);
12809
+ const tokens = schemaNameTokens(node.id.name);
12810
+ if (domain !== null && tokens.length >= 2) {
12811
+ enumSchemas.push({ domain, name: node.id.name, tokens });
12812
+ }
12813
+ }
12577
12814
  },
12578
12815
  /** Records `XSchema.transform(...)` and equivalent module-level reshaping. */
12579
12816
  "MemberExpression[computed=false]"(node) {
@@ -12600,8 +12837,22 @@ var prefer_zod_infer_default = createRule({
12600
12837
  if (members !== null) {
12601
12838
  typeDeclarations.push({ name: node.id.name, node: node.id, members });
12602
12839
  }
12840
+ recordLiteralUnions(
12841
+ node.body.body,
12842
+ node,
12843
+ node.id.name,
12844
+ isExportedDeclaration(node)
12845
+ );
12603
12846
  },
12604
12847
  TSTypeAliasDeclaration(node) {
12848
+ const schemaName = inferredSchemaName(node.typeAnnotation);
12849
+ if (schemaName !== null) {
12850
+ inferredAliases.push({
12851
+ exported: isExportedDeclaration(node),
12852
+ schemaName,
12853
+ typeName: node.id.name
12854
+ });
12855
+ }
12605
12856
  if (node.typeParameters !== void 0 || node.typeAnnotation.type !== import_utils61.AST_NODE_TYPES.TSTypeLiteral) {
12606
12857
  return;
12607
12858
  }
@@ -12609,11 +12860,15 @@ var prefer_zod_infer_default = createRule({
12609
12860
  if (members !== null) {
12610
12861
  typeDeclarations.push({ name: node.id.name, node: node.id, members });
12611
12862
  }
12863
+ recordLiteralUnions(
12864
+ node.typeAnnotation.members,
12865
+ node,
12866
+ node.id.name,
12867
+ isExportedDeclaration(node)
12868
+ );
12612
12869
  },
12613
12870
  "Program:exit"() {
12614
- if (schemas.length === 0 || typeDeclarations.length === 0) {
12615
- return;
12616
- }
12871
+ const twinTypeNames = /* @__PURE__ */ new Set();
12617
12872
  const byName = /* @__PURE__ */ new Map();
12618
12873
  for (const schema of schemas) {
12619
12874
  const key = normalizeSchemaName(schema.name);
@@ -12635,12 +12890,62 @@ var prefer_zod_infer_default = createRule({
12635
12890
  if (!isTwin(schema.fields, declaration.members)) {
12636
12891
  continue;
12637
12892
  }
12893
+ twinTypeNames.add(declaration.name);
12638
12894
  context.report({
12639
12895
  node: declaration.node,
12640
12896
  messageId: "handWrittenTwin",
12641
12897
  data: { typeName: declaration.name, schemaName: schema.name }
12642
12898
  });
12643
12899
  }
12900
+ const aliasesBySchema = /* @__PURE__ */ new Map();
12901
+ for (const alias of inferredAliases) {
12902
+ const aliases = aliasesBySchema.get(alias.schemaName) ?? [];
12903
+ aliases.push(alias);
12904
+ aliasesBySchema.set(alias.schemaName, aliases);
12905
+ }
12906
+ const groups = /* @__PURE__ */ new Map();
12907
+ for (const occurrence of literalUnionOccurrences) {
12908
+ const { ownerName } = occurrence;
12909
+ if (twinTypeNames.has(ownerName ?? "") || ownerName !== null && ignorePatterns.some((pattern) => pattern.test(ownerName))) {
12910
+ continue;
12911
+ }
12912
+ const candidates = enumSchemas.filter(
12913
+ (schema2) => sameDomain(schema2.domain, occurrence.domain) && endsWithTokens(schema2.tokens, occurrence.propertyTokens) && (aliasesBySchema.get(schema2.name)?.length ?? 0) === 1
12914
+ );
12915
+ const [schema] = candidates;
12916
+ if (candidates.length !== 1 || schema === void 0) {
12917
+ continue;
12918
+ }
12919
+ const [alias] = aliasesBySchema.get(schema.name) ?? [];
12920
+ if (alias === void 0 || occurrence.exported && !alias.exported) {
12921
+ continue;
12922
+ }
12923
+ const key = `${schema.name}\0${occurrence.propertyName}`;
12924
+ const group = groups.get(key);
12925
+ if (group === void 0) {
12926
+ groups.set(key, { alias, occurrences: [occurrence], schema });
12927
+ } else {
12928
+ group.occurrences.push(occurrence);
12929
+ }
12930
+ }
12931
+ for (const { alias, occurrences, schema } of groups.values()) {
12932
+ if (new Set(occurrences.map(({ ownerName }) => ownerName)).size < 2 || new Set(occurrences.map(({ owner }) => owner)).size < 2) {
12933
+ continue;
12934
+ }
12935
+ const [first] = occurrences;
12936
+ if (first === void 0) {
12937
+ continue;
12938
+ }
12939
+ context.report({
12940
+ node: first.node,
12941
+ messageId: "repeatedEnumUnion",
12942
+ data: {
12943
+ propertyName: first.propertyName,
12944
+ schemaName: schema.name,
12945
+ typeName: alias.typeName
12946
+ }
12947
+ });
12948
+ }
12644
12949
  }
12645
12950
  };
12646
12951
  }
@@ -14581,7 +14886,7 @@ var rules = {
14581
14886
  };
14582
14887
  var meta = {
14583
14888
  name: "@sarj/eslint-plugin",
14584
- version: "15.6.1"
14889
+ version: "15.6.2"
14585
14890
  };
14586
14891
  var applicationOnlyRules = [
14587
14892
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -263,7 +263,7 @@ declare const rules: {
263
263
  readonly "prefer-zod-infer": DocumentedRule<readonly [{
264
264
  ignoreTypeNames?: readonly string[];
265
265
  requireIdenticalShape?: boolean;
266
- }?], "handWrittenTwin">;
266
+ }?], "handWrittenTwin" | "repeatedEnumUnion">;
267
267
  readonly "require-assert-never": DocumentedRule<readonly [], "missingAssertNever">;
268
268
  readonly "require-fetch-timeout": DocumentedRule<readonly [{
269
269
  allowIn?: readonly string[];
@@ -415,7 +415,7 @@ type FlatPreset = {
415
415
  declare const plugin: {
416
416
  readonly meta: {
417
417
  readonly name: "@sarj/eslint-plugin";
418
- readonly version: "15.6.1";
418
+ readonly version: "15.6.2";
419
419
  };
420
420
  readonly rules: {
421
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -493,7 +493,7 @@ declare const plugin: {
493
493
  readonly "prefer-zod-infer": DocumentedRule<readonly [{
494
494
  ignoreTypeNames?: readonly string[];
495
495
  requireIdenticalShape?: boolean;
496
- }?], "handWrittenTwin">;
496
+ }?], "handWrittenTwin" | "repeatedEnumUnion">;
497
497
  readonly "require-assert-never": DocumentedRule<readonly [], "missingAssertNever">;
498
498
  readonly "require-fetch-timeout": DocumentedRule<readonly [{
499
499
  allowIn?: readonly string[];
package/dist/index.d.ts CHANGED
@@ -263,7 +263,7 @@ declare const rules: {
263
263
  readonly "prefer-zod-infer": DocumentedRule<readonly [{
264
264
  ignoreTypeNames?: readonly string[];
265
265
  requireIdenticalShape?: boolean;
266
- }?], "handWrittenTwin">;
266
+ }?], "handWrittenTwin" | "repeatedEnumUnion">;
267
267
  readonly "require-assert-never": DocumentedRule<readonly [], "missingAssertNever">;
268
268
  readonly "require-fetch-timeout": DocumentedRule<readonly [{
269
269
  allowIn?: readonly string[];
@@ -415,7 +415,7 @@ type FlatPreset = {
415
415
  declare const plugin: {
416
416
  readonly meta: {
417
417
  readonly name: "@sarj/eslint-plugin";
418
- readonly version: "15.6.1";
418
+ readonly version: "15.6.2";
419
419
  };
420
420
  readonly rules: {
421
421
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
@@ -493,7 +493,7 @@ declare const plugin: {
493
493
  readonly "prefer-zod-infer": DocumentedRule<readonly [{
494
494
  ignoreTypeNames?: readonly string[];
495
495
  requireIdenticalShape?: boolean;
496
- }?], "handWrittenTwin">;
496
+ }?], "handWrittenTwin" | "repeatedEnumUnion">;
497
497
  readonly "require-assert-never": DocumentedRule<readonly [], "missingAssertNever">;
498
498
  readonly "require-fetch-timeout": DocumentedRule<readonly [{
499
499
  allowIn?: readonly string[];
package/dist/index.js CHANGED
@@ -5610,9 +5610,9 @@ var no_restated_jsdoc_default = createRule({
5610
5610
  continue;
5611
5611
  }
5612
5612
  if ((paramTags.length > 0 || returnTags.length > 0) && documentsTypedFunction(sourceCode, comment)) continue;
5613
- const nameTokens = tokensOf([declaration.name]);
5613
+ const nameTokens2 = tokensOf([declaration.name]);
5614
5614
  const paramTokens = tokensOf(declaration.params);
5615
- const known = /* @__PURE__ */ new Set([...nameTokens, ...paramTokens]);
5615
+ const known = /* @__PURE__ */ new Set([...nameTokens2, ...paramTokens]);
5616
5616
  let addsNothing = covered(describedText, known);
5617
5617
  for (const tag of paramTags) {
5618
5618
  const text = tag.text.replace(/^\{[^}]*\}\s*/, "");
@@ -5627,7 +5627,7 @@ var no_restated_jsdoc_default = createRule({
5627
5627
  addsNothing = false;
5628
5628
  break;
5629
5629
  }
5630
- const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens]);
5630
+ const own = /* @__PURE__ */ new Set([...splitIdentifier(path.split(".").pop() ?? ""), ...nameTokens2]);
5631
5631
  if (!covered(match[2] ?? "", own)) {
5632
5632
  addsNothing = false;
5633
5633
  break;
@@ -12223,6 +12223,14 @@ var OPTIONAL_MODIFIERS = /* @__PURE__ */ new Set([
12223
12223
  "nullish"
12224
12224
  ]);
12225
12225
  var NULLABLE_MODIFIERS = /* @__PURE__ */ new Set(["nullable", "nullish"]);
12226
+ var DOMAIN_PRESERVING_MODIFIERS = /* @__PURE__ */ new Set([
12227
+ ...SHAPE_PRESERVING_METHODS,
12228
+ ...OPTIONAL_MODIFIERS,
12229
+ ...NULLABLE_MODIFIERS,
12230
+ "catch",
12231
+ "default",
12232
+ "prefault"
12233
+ ]);
12226
12234
  var RESHAPING_MODIFIERS = /* @__PURE__ */ new Set([
12227
12235
  "transform",
12228
12236
  "pipe",
@@ -12283,6 +12291,107 @@ var LEAF_NODE_TYPES = {
12283
12291
  discriminatedUnion: [AST_NODE_TYPES48.TSUnionType, AST_NODE_TYPES48.TSTypeReference],
12284
12292
  intersection: [AST_NODE_TYPES48.TSIntersectionType, AST_NODE_TYPES48.TSTypeReference]
12285
12293
  };
12294
+ function primitiveLiteralKey(node) {
12295
+ if (node.type !== AST_NODE_TYPES48.Literal) {
12296
+ return null;
12297
+ }
12298
+ if (node.value === null) {
12299
+ return "null";
12300
+ }
12301
+ switch (typeof node.value) {
12302
+ case "string":
12303
+ return `string:${node.value}`;
12304
+ case "number":
12305
+ return `number:${String(node.value)}`;
12306
+ case "boolean":
12307
+ return `boolean:${String(node.value)}`;
12308
+ case "bigint":
12309
+ return `bigint:${String(node.value)}`;
12310
+ default:
12311
+ return null;
12312
+ }
12313
+ }
12314
+ function exactDomain(keys) {
12315
+ if (keys.length === 0 || keys.some((key) => key === null)) {
12316
+ return null;
12317
+ }
12318
+ const domain = new Set(keys);
12319
+ return domain.size === keys.length ? domain : null;
12320
+ }
12321
+ function staticZodDomain(leaf, call) {
12322
+ if (leaf === null || call === null) {
12323
+ return void 0;
12324
+ }
12325
+ if (leaf === "literal") {
12326
+ const [argument] = call.arguments;
12327
+ if (argument === void 0 || argument.type === AST_NODE_TYPES48.SpreadElement) {
12328
+ return null;
12329
+ }
12330
+ if (argument.type === AST_NODE_TYPES48.ArrayExpression) {
12331
+ return exactDomain(
12332
+ argument.elements.map(
12333
+ (element) => element === null || element.type === AST_NODE_TYPES48.SpreadElement ? null : primitiveLiteralKey(element)
12334
+ )
12335
+ );
12336
+ }
12337
+ return exactDomain([primitiveLiteralKey(argument)]);
12338
+ }
12339
+ if (leaf === "enum") {
12340
+ const [argument] = call.arguments;
12341
+ if (argument === void 0 || argument.type === AST_NODE_TYPES48.SpreadElement) {
12342
+ return null;
12343
+ }
12344
+ if (argument.type === AST_NODE_TYPES48.ArrayExpression) {
12345
+ return exactDomain(
12346
+ argument.elements.map((element) => {
12347
+ if (element === null || element.type === AST_NODE_TYPES48.SpreadElement) {
12348
+ return null;
12349
+ }
12350
+ const key = primitiveLiteralKey(element);
12351
+ return key?.startsWith("string:") === true ? key : null;
12352
+ })
12353
+ );
12354
+ }
12355
+ if (argument.type === AST_NODE_TYPES48.ObjectExpression) {
12356
+ return exactDomain(
12357
+ argument.properties.map((property) => {
12358
+ if (property.type !== AST_NODE_TYPES48.Property || property.computed || property.kind !== "init" || property.method || property.shorthand) {
12359
+ return null;
12360
+ }
12361
+ const key = primitiveLiteralKey(property.value);
12362
+ return key?.startsWith("string:") === true ? key : null;
12363
+ })
12364
+ );
12365
+ }
12366
+ return null;
12367
+ }
12368
+ if (leaf === "nativeEnum" || leaf === "union" || leaf === "discriminatedUnion") {
12369
+ return null;
12370
+ }
12371
+ return void 0;
12372
+ }
12373
+ function sameDomain(left, right) {
12374
+ if (left.size !== right.size) {
12375
+ return false;
12376
+ }
12377
+ for (const value of left) {
12378
+ if (!right.has(value)) {
12379
+ return false;
12380
+ }
12381
+ }
12382
+ return true;
12383
+ }
12384
+ function isExportedDeclaration(node) {
12385
+ return node.parent?.type === AST_NODE_TYPES48.ExportNamedDeclaration;
12386
+ }
12387
+ function isModuleLevelConst(node) {
12388
+ const declaration = node.parent;
12389
+ if (declaration.type !== AST_NODE_TYPES48.VariableDeclaration || declaration.kind !== "const") {
12390
+ return false;
12391
+ }
12392
+ const container = declaration.parent;
12393
+ return container.type === AST_NODE_TYPES48.Program || container.type === AST_NODE_TYPES48.ExportNamedDeclaration && container.parent.type === AST_NODE_TYPES48.Program;
12394
+ }
12286
12395
  function normalizeSchemaName(name) {
12287
12396
  return name.replace(/Schema$/i, "").replace(/^Z(?=[A-Z])/, "").toLowerCase();
12288
12397
  }
@@ -12313,7 +12422,18 @@ function unwrapNullish(annotation) {
12313
12422
  }
12314
12423
  return { core: rest.length === 0 ? null : annotation, nullable };
12315
12424
  }
12316
- function leafAgrees(leaf, annotation) {
12425
+ function leafAgrees(field, annotation) {
12426
+ if (field.domain === null) {
12427
+ return false;
12428
+ }
12429
+ if (field.domain !== void 0) {
12430
+ if (annotation === null) {
12431
+ return false;
12432
+ }
12433
+ const annotationDomain = typeLiteralDomain(annotation);
12434
+ return annotationDomain !== null && sameDomain(field.domain, annotationDomain);
12435
+ }
12436
+ const { leaf } = field;
12317
12437
  if (leaf === null || annotation === null) {
12318
12438
  return null;
12319
12439
  }
@@ -12330,6 +12450,47 @@ function leafAgrees(leaf, annotation) {
12330
12450
  }
12331
12451
  return expected.includes(core.type);
12332
12452
  }
12453
+ function typeLiteralDomain(annotation) {
12454
+ const members = annotation.type === AST_NODE_TYPES48.TSUnionType ? annotation.types : [annotation];
12455
+ const keys = [];
12456
+ for (const member of members) {
12457
+ if (member.type === AST_NODE_TYPES48.TSNullKeyword) {
12458
+ continue;
12459
+ }
12460
+ if (member.type !== AST_NODE_TYPES48.TSLiteralType) {
12461
+ return null;
12462
+ }
12463
+ keys.push(primitiveLiteralKey(member.literal));
12464
+ }
12465
+ return exactDomain(keys);
12466
+ }
12467
+ function staticStringUnionDomain(node) {
12468
+ if (node.type !== AST_NODE_TYPES48.TSUnionType) {
12469
+ return null;
12470
+ }
12471
+ const keys = node.types.map((member) => {
12472
+ if (member.type !== AST_NODE_TYPES48.TSLiteralType) {
12473
+ return null;
12474
+ }
12475
+ const key = primitiveLiteralKey(member.literal);
12476
+ return key?.startsWith("string:") === true ? key : null;
12477
+ });
12478
+ const domain = exactDomain(keys);
12479
+ return domain !== null && domain.size >= 2 ? domain : null;
12480
+ }
12481
+ function nameTokens(name) {
12482
+ return name.replace(/([a-z\d])([A-Z])/gu, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2").split(/[^A-Za-z\d]+/u).filter((token) => token !== "").map((token) => token.toLowerCase());
12483
+ }
12484
+ function schemaNameTokens(name) {
12485
+ const tokens = nameTokens(name);
12486
+ const withoutPrefix = tokens[0] === "z" ? tokens.slice(1) : tokens;
12487
+ return withoutPrefix.at(-1) === "schema" ? withoutPrefix.slice(0, -1) : withoutPrefix;
12488
+ }
12489
+ function endsWithTokens(candidate, suffix) {
12490
+ return suffix.length >= 2 && candidate.length >= suffix.length && suffix.every(
12491
+ (segment, index) => candidate[candidate.length - suffix.length + index] === segment
12492
+ );
12493
+ }
12333
12494
  var prefer_zod_infer_default = createRule({
12334
12495
  name: "prefer-zod-infer",
12335
12496
  documentation: preferZodInferDocumentation,
@@ -12352,7 +12513,8 @@ var prefer_zod_infer_default = createRule({
12352
12513
  }
12353
12514
  ],
12354
12515
  messages: {
12355
- handWrittenTwin: "`{{typeName}}` restates the shape of the Zod schema `{{schemaName}}` declared in this module. Derive it instead \u2014 `type {{typeName}} = z.infer<typeof {{schemaName}}>` \u2014 so a field added to the schema cannot silently leave the type behind."
12516
+ handWrittenTwin: "`{{typeName}}` restates the shape of the Zod schema `{{schemaName}}` declared in this module. Derive it instead \u2014 `type {{typeName}} = z.infer<typeof {{schemaName}}>` \u2014 so a field added to the schema cannot silently leave the type behind.",
12517
+ repeatedEnumUnion: "`{{propertyName}}` repeats the literal domain already named by `{{typeName}}` and `{{schemaName}}` in multiple object types. Reuse `{{typeName}}` so the accepted values have one source of truth."
12356
12518
  }
12357
12519
  },
12358
12520
  defaultOptions: [{}],
@@ -12366,6 +12528,9 @@ var prefer_zod_infer_default = createRule({
12366
12528
  }
12367
12529
  const zodNamespaces = /* @__PURE__ */ new Set();
12368
12530
  const schemas = [];
12531
+ const enumSchemas = [];
12532
+ const inferredAliases = [];
12533
+ const literalUnionOccurrences = [];
12369
12534
  const typeDeclarations = [];
12370
12535
  const constrainedTypeNames = /* @__PURE__ */ new Set();
12371
12536
  const reshapedSchemaNames = /* @__PURE__ */ new Set();
@@ -12390,10 +12555,21 @@ var prefer_zod_infer_default = createRule({
12390
12555
  const callee = call.callee;
12391
12556
  return callee.type === AST_NODE_TYPES48.MemberExpression && callee.property.type === AST_NODE_TYPES48.Identifier ? callee.property.name : "";
12392
12557
  }
12558
+ function recordZodImport(node) {
12559
+ if (!isZodModule(node.source.value)) {
12560
+ return;
12561
+ }
12562
+ for (const specifier of node.specifiers) {
12563
+ if (specifier.type === AST_NODE_TYPES48.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES48.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && specifier.imported.name === "z") {
12564
+ zodNamespaces.add(specifier.local.name);
12565
+ }
12566
+ }
12567
+ }
12393
12568
  function schemaField(node) {
12394
12569
  const modifiers = [];
12395
12570
  let current = node;
12396
12571
  let leaf = null;
12572
+ let leafCall = null;
12397
12573
  while (current.type === AST_NODE_TYPES48.CallExpression) {
12398
12574
  const callee = current.callee;
12399
12575
  if (callee.type !== AST_NODE_TYPES48.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES48.Identifier) {
@@ -12402,18 +12578,72 @@ var prefer_zod_infer_default = createRule({
12402
12578
  const receiver = callee.object;
12403
12579
  if (receiver.type === AST_NODE_TYPES48.Identifier && zodNamespaces.has(receiver.name)) {
12404
12580
  leaf = callee.property.name;
12581
+ leafCall = current;
12405
12582
  break;
12406
12583
  }
12407
12584
  modifiers.push(callee.property.name);
12408
12585
  current = receiver;
12409
12586
  }
12410
12587
  return {
12588
+ domain: modifiers.every(
12589
+ (name) => DOMAIN_PRESERVING_MODIFIERS.has(name)
12590
+ ) ? staticZodDomain(leaf, leafCall) : null,
12411
12591
  leaf,
12412
12592
  optional: modifiers.some((name) => OPTIONAL_MODIFIERS.has(name)),
12413
12593
  nullable: modifiers.some((name) => NULLABLE_MODIFIERS.has(name)),
12414
12594
  reshaped: modifiers.some((name) => RESHAPING_MODIFIERS.has(name))
12415
12595
  };
12416
12596
  }
12597
+ function enumSchemaDomain(init) {
12598
+ const chain = zodCallChain(init);
12599
+ if (chain === null || chain.length !== 1) {
12600
+ return null;
12601
+ }
12602
+ const [call] = chain;
12603
+ if (call === void 0 || methodName2(call) !== "enum") {
12604
+ return null;
12605
+ }
12606
+ const domain = staticZodDomain("enum", call);
12607
+ return domain instanceof Set && domain.size >= 2 ? domain : null;
12608
+ }
12609
+ function inferredSchemaName(node) {
12610
+ if (node.type !== AST_NODE_TYPES48.TSTypeReference || node.typeName.type !== AST_NODE_TYPES48.TSQualifiedName || node.typeName.left.type !== AST_NODE_TYPES48.Identifier || !zodNamespaces.has(node.typeName.left.name) || node.typeName.right.name !== "infer") {
12611
+ return null;
12612
+ }
12613
+ const arguments_ = node.typeArguments?.params ?? [];
12614
+ const [argument] = arguments_;
12615
+ return arguments_.length === 1 && argument?.type === AST_NODE_TYPES48.TSTypeQuery && argument.exprName.type === AST_NODE_TYPES48.Identifier ? argument.exprName.name : null;
12616
+ }
12617
+ function recordLiteralUnions(members, owner, ownerName, exported) {
12618
+ for (const member of members) {
12619
+ if (member.type !== AST_NODE_TYPES48.TSPropertySignature || member.computed || member.optional || member.readonly || member.typeAnnotation === void 0) {
12620
+ continue;
12621
+ }
12622
+ const key = member.key;
12623
+ const propertyName3 = key.type === AST_NODE_TYPES48.Identifier ? key.name : key.type === AST_NODE_TYPES48.Literal && typeof key.value === "string" ? key.value : null;
12624
+ if (propertyName3 === null) {
12625
+ continue;
12626
+ }
12627
+ const propertyTokens = nameTokens(propertyName3);
12628
+ if (propertyTokens.length < 2) {
12629
+ continue;
12630
+ }
12631
+ const annotation = member.typeAnnotation.typeAnnotation;
12632
+ const domain = staticStringUnionDomain(annotation);
12633
+ if (domain === null || annotation.type !== AST_NODE_TYPES48.TSUnionType) {
12634
+ continue;
12635
+ }
12636
+ literalUnionOccurrences.push({
12637
+ domain,
12638
+ exported,
12639
+ node: annotation,
12640
+ owner,
12641
+ ownerName,
12642
+ propertyName: propertyName3,
12643
+ propertyTokens
12644
+ });
12645
+ }
12646
+ }
12417
12647
  function schemaFields(init) {
12418
12648
  const chain = zodCallChain(init);
12419
12649
  if (chain === null || chain.length === 0) {
@@ -12514,7 +12744,7 @@ var prefer_zod_infer_default = createRule({
12514
12744
  if (member.readonly) {
12515
12745
  return false;
12516
12746
  }
12517
- const agrees = leafAgrees(field.leaf, member.annotation);
12747
+ const agrees = leafAgrees(field, member.annotation);
12518
12748
  if (agrees === false) {
12519
12749
  return false;
12520
12750
  }
@@ -12525,16 +12755,16 @@ var prefer_zod_infer_default = createRule({
12525
12755
  return agreements > 0;
12526
12756
  }
12527
12757
  return {
12528
- ImportDeclaration(node) {
12529
- if (!isZodModule(node.source.value)) {
12530
- return;
12531
- }
12532
- for (const specifier of node.specifiers) {
12533
- if (specifier.type === AST_NODE_TYPES48.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES48.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES48.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES48.Identifier && specifier.imported.name === "z") {
12534
- zodNamespaces.add(specifier.local.name);
12758
+ Program(node) {
12759
+ for (const statement of node.body) {
12760
+ if (statement.type === AST_NODE_TYPES48.ImportDeclaration) {
12761
+ recordZodImport(statement);
12535
12762
  }
12536
12763
  }
12537
12764
  },
12765
+ ImportDeclaration(node) {
12766
+ recordZodImport(node);
12767
+ },
12538
12768
  VariableDeclarator(node) {
12539
12769
  if (node.id.type !== AST_NODE_TYPES48.Identifier || node.init == null) {
12540
12770
  return;
@@ -12543,6 +12773,13 @@ var prefer_zod_infer_default = createRule({
12543
12773
  if (fields !== null) {
12544
12774
  schemas.push({ name: node.id.name, fields });
12545
12775
  }
12776
+ if (isModuleLevelConst(node)) {
12777
+ const domain = enumSchemaDomain(node.init);
12778
+ const tokens = schemaNameTokens(node.id.name);
12779
+ if (domain !== null && tokens.length >= 2) {
12780
+ enumSchemas.push({ domain, name: node.id.name, tokens });
12781
+ }
12782
+ }
12546
12783
  },
12547
12784
  /** Records `XSchema.transform(...)` and equivalent module-level reshaping. */
12548
12785
  "MemberExpression[computed=false]"(node) {
@@ -12569,8 +12806,22 @@ var prefer_zod_infer_default = createRule({
12569
12806
  if (members !== null) {
12570
12807
  typeDeclarations.push({ name: node.id.name, node: node.id, members });
12571
12808
  }
12809
+ recordLiteralUnions(
12810
+ node.body.body,
12811
+ node,
12812
+ node.id.name,
12813
+ isExportedDeclaration(node)
12814
+ );
12572
12815
  },
12573
12816
  TSTypeAliasDeclaration(node) {
12817
+ const schemaName = inferredSchemaName(node.typeAnnotation);
12818
+ if (schemaName !== null) {
12819
+ inferredAliases.push({
12820
+ exported: isExportedDeclaration(node),
12821
+ schemaName,
12822
+ typeName: node.id.name
12823
+ });
12824
+ }
12574
12825
  if (node.typeParameters !== void 0 || node.typeAnnotation.type !== AST_NODE_TYPES48.TSTypeLiteral) {
12575
12826
  return;
12576
12827
  }
@@ -12578,11 +12829,15 @@ var prefer_zod_infer_default = createRule({
12578
12829
  if (members !== null) {
12579
12830
  typeDeclarations.push({ name: node.id.name, node: node.id, members });
12580
12831
  }
12832
+ recordLiteralUnions(
12833
+ node.typeAnnotation.members,
12834
+ node,
12835
+ node.id.name,
12836
+ isExportedDeclaration(node)
12837
+ );
12581
12838
  },
12582
12839
  "Program:exit"() {
12583
- if (schemas.length === 0 || typeDeclarations.length === 0) {
12584
- return;
12585
- }
12840
+ const twinTypeNames = /* @__PURE__ */ new Set();
12586
12841
  const byName = /* @__PURE__ */ new Map();
12587
12842
  for (const schema of schemas) {
12588
12843
  const key = normalizeSchemaName(schema.name);
@@ -12604,12 +12859,62 @@ var prefer_zod_infer_default = createRule({
12604
12859
  if (!isTwin(schema.fields, declaration.members)) {
12605
12860
  continue;
12606
12861
  }
12862
+ twinTypeNames.add(declaration.name);
12607
12863
  context.report({
12608
12864
  node: declaration.node,
12609
12865
  messageId: "handWrittenTwin",
12610
12866
  data: { typeName: declaration.name, schemaName: schema.name }
12611
12867
  });
12612
12868
  }
12869
+ const aliasesBySchema = /* @__PURE__ */ new Map();
12870
+ for (const alias of inferredAliases) {
12871
+ const aliases = aliasesBySchema.get(alias.schemaName) ?? [];
12872
+ aliases.push(alias);
12873
+ aliasesBySchema.set(alias.schemaName, aliases);
12874
+ }
12875
+ const groups = /* @__PURE__ */ new Map();
12876
+ for (const occurrence of literalUnionOccurrences) {
12877
+ const { ownerName } = occurrence;
12878
+ if (twinTypeNames.has(ownerName ?? "") || ownerName !== null && ignorePatterns.some((pattern) => pattern.test(ownerName))) {
12879
+ continue;
12880
+ }
12881
+ const candidates = enumSchemas.filter(
12882
+ (schema2) => sameDomain(schema2.domain, occurrence.domain) && endsWithTokens(schema2.tokens, occurrence.propertyTokens) && (aliasesBySchema.get(schema2.name)?.length ?? 0) === 1
12883
+ );
12884
+ const [schema] = candidates;
12885
+ if (candidates.length !== 1 || schema === void 0) {
12886
+ continue;
12887
+ }
12888
+ const [alias] = aliasesBySchema.get(schema.name) ?? [];
12889
+ if (alias === void 0 || occurrence.exported && !alias.exported) {
12890
+ continue;
12891
+ }
12892
+ const key = `${schema.name}\0${occurrence.propertyName}`;
12893
+ const group = groups.get(key);
12894
+ if (group === void 0) {
12895
+ groups.set(key, { alias, occurrences: [occurrence], schema });
12896
+ } else {
12897
+ group.occurrences.push(occurrence);
12898
+ }
12899
+ }
12900
+ for (const { alias, occurrences, schema } of groups.values()) {
12901
+ if (new Set(occurrences.map(({ ownerName }) => ownerName)).size < 2 || new Set(occurrences.map(({ owner }) => owner)).size < 2) {
12902
+ continue;
12903
+ }
12904
+ const [first] = occurrences;
12905
+ if (first === void 0) {
12906
+ continue;
12907
+ }
12908
+ context.report({
12909
+ node: first.node,
12910
+ messageId: "repeatedEnumUnion",
12911
+ data: {
12912
+ propertyName: first.propertyName,
12913
+ schemaName: schema.name,
12914
+ typeName: alias.typeName
12915
+ }
12916
+ });
12917
+ }
12613
12918
  }
12614
12919
  };
12615
12920
  }
@@ -14559,7 +14864,7 @@ var rules = {
14559
14864
  };
14560
14865
  var meta = {
14561
14866
  name: "@sarj/eslint-plugin",
14562
- version: "15.6.1"
14867
+ version: "15.6.2"
14563
14868
  };
14564
14869
  var applicationOnlyRules = [
14565
14870
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.6.1",
3
+ "version": "15.6.2",
4
4
  "packageManager": "npm@11.19.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",