@sarj/eslint-plugin 16.5.3 → 16.7.0

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
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
52
52
  // package.json
53
53
  var package_default = {
54
54
  name: "@sarj/eslint-plugin",
55
- version: "16.5.3",
55
+ version: "16.7.0",
56
56
  packageManager: "npm@12.1.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -470,8 +470,9 @@ function commentLineEnd(sourceText, start) {
470
470
  }
471
471
 
472
472
  // src/rules/require-explicit-contract-implementation.ts
473
+ var ESCAPED_CLASSES = /* @__PURE__ */ new WeakMap();
473
474
  var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
474
- defaultLevel: "warning",
475
+ defaultLevel: "error",
475
476
  summary: "Require nominal inheritance when an injected abstract class is rejected by an instanceof guard.",
476
477
  rationale: "A structurally assignable fake can still fail a constructor's nominal runtime guard. Pure structural interfaces do not require an implements clause.",
477
478
  remediation: "Use an implementation inheriting the existing abstract class, or remove the nominal guard if the boundary is intentionally structural.",
@@ -479,8 +480,8 @@ var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
479
480
  autofix: "none",
480
481
  limitations: [
481
482
  "Requires direct construction of both owned consumer and collaborator classes, with a first-statement consumer constructor guard rejecting the exact parameter with if (!(parameter instanceof Contract)) throw.",
482
- "The contract must resolve to an abstract class without decorators, unresolved ancestors, or custom computed static members in its resolved hierarchy.",
483
- "Structural interfaces, non-rejecting branches, reassignment before the guard, casts, unresolved heritage, dynamic factories, explicit implementation constructors or initializers, modules referencing prototype-mutator members or assigning prototypes, and third-party classes are excluded."
483
+ "The contract must resolve to an abstract class without decorators, static blocks, unresolved ancestors, or custom computed static members in its resolved hierarchy. Escaped class constructors are checked across every owned module in the typed program.",
484
+ "Structural interfaces, non-rejecting branches, reassignment before the guard, casts, unresolved heritage, dynamic factories, implementation static blocks, explicit implementation constructors or initializers, involved modules referencing prototype-mutator members or assigning prototypes, escaped class constructors, and third-party classes are excluded."
484
485
  ],
485
486
  examples: [
486
487
  {
@@ -503,6 +504,115 @@ var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
503
504
  }
504
505
  ]
505
506
  };
507
+ function hasPrototypeMutation(source) {
508
+ function visit(node) {
509
+ const member = memberName(node);
510
+ if (member === "setPrototypeOf" || member === "__proto__" || member === "hasInstance") return true;
511
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && memberName(node.left) === "prototype") return true;
512
+ return ts.forEachChild(node, visit);
513
+ }
514
+ return visit(source) === true;
515
+ }
516
+ function memberName(node) {
517
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
518
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
519
+ return null;
520
+ }
521
+ function ownedSource(source) {
522
+ return !source.isDeclarationFile && !/(?:^|[/\\])(?:node_modules|vendor|vendored|generated|dist)(?:[/\\])/u.test(source.fileName) && !isGeneratedFile(source.fileName, source.text);
523
+ }
524
+ function escapedClasses(program, checker) {
525
+ const cached = ESCAPED_CLASSES.get(program);
526
+ if (cached !== void 0) return cached;
527
+ const escaped = /* @__PURE__ */ new Set();
528
+ const namespaceValues = /* @__PURE__ */ new Map();
529
+ function visit(node) {
530
+ if (ts.isTypeNode(node) || ts.isImportDeclaration(node)) return;
531
+ if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
532
+ recordEscapedReference(checker, node, escaped, namespaceValues);
533
+ }
534
+ ts.forEachChild(node, visit);
535
+ }
536
+ for (const source of program.getSourceFiles()) if (ownedSource(source)) visit(source);
537
+ ESCAPED_CLASSES.set(program, escaped);
538
+ return escaped;
539
+ }
540
+ function recordEscapedReference(checker, node, escaped, namespaceValues) {
541
+ const symbol = canonicalSymbol(checker, node);
542
+ if (symbol !== void 0) {
543
+ let values = namespaceValues.get(symbol);
544
+ if (values === void 0) {
545
+ values = ownedClassValues(checker, symbol);
546
+ namespaceValues.set(symbol, values);
547
+ }
548
+ if (!namespacePrefix(symbol, node) && !safeClassReference(node)) for (const value of values) escaped.add(value);
549
+ }
550
+ if (ts.isElementAccessExpression(node) && !safeClassReference(node)) {
551
+ const receiver = canonicalSymbol(checker, node.expression) ?? checker.getTypeAtLocation(node.expression).getSymbol();
552
+ if (receiver !== void 0) for (const value of ownedClassValues(checker, receiver)) escaped.add(value);
553
+ }
554
+ }
555
+ function ownedClassValues(checker, original, seen = /* @__PURE__ */ new Set()) {
556
+ const symbol = original.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(original) : original;
557
+ const values = /* @__PURE__ */ new Set();
558
+ if (seen.has(symbol)) return values;
559
+ seen.add(symbol);
560
+ if (symbol.declarations?.some((declaration) => ts.isClassDeclaration(declaration) && ownedSource(declaration.getSourceFile()))) values.add(symbol);
561
+ if (symbol.declarations?.some((declaration) => (ts.isSourceFile(declaration) || ts.isModuleDeclaration(declaration)) && ownedSource(declaration.getSourceFile()))) {
562
+ for (const exported of checker.getExportsOfModule(symbol)) {
563
+ for (const value of ownedClassValues(checker, exported, seen)) values.add(value);
564
+ }
565
+ }
566
+ return values;
567
+ }
568
+ function safeClassReference(node) {
569
+ let reference = ts.isPropertyAccessExpression(node.parent) && node.parent.name === node ? node.parent : node;
570
+ while (ts.isParenthesizedExpression(reference.parent)) reference = reference.parent;
571
+ const parent = reference.parent;
572
+ return ts.isClassDeclaration(parent) && parent.name === reference || ts.isNewExpression(parent) && parent.expression === reference || ts.isExpressionWithTypeArguments(parent) && parent.expression === reference || ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword && parent.right === reference;
573
+ }
574
+ function namespacePrefix(symbol, node) {
575
+ const parent = node.parent;
576
+ const memberCall = (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && ts.isCallExpression(parent.parent) && parent.parent.expression === parent;
577
+ return !symbol.declarations?.some(ts.isClassDeclaration) && (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && parent.expression === node && !memberCall;
578
+ }
579
+ function closedNominalBoundary(program, checker, call, consumer, missing) {
580
+ const { implementation, contractSymbol } = missing;
581
+ const implementationSymbol = implementation.name === void 0 ? void 0 : canonicalSymbol(checker, implementation.name);
582
+ const consumerSymbol = consumer.name === void 0 ? void 0 : canonicalSymbol(checker, consumer.name);
583
+ if (implementationSymbol === void 0 || consumerSymbol === void 0) return false;
584
+ const contract = contractSymbol.declarations?.find(ts.isClassDeclaration);
585
+ if (contract === void 0) return false;
586
+ const targets = /* @__PURE__ */ new Set([consumerSymbol, ...classHierarchySymbols(checker, contract), ...classHierarchySymbols(checker, implementation)]);
587
+ const sources = /* @__PURE__ */ new Set([call.getSourceFile(), consumer.getSourceFile(), implementation.getSourceFile(), ...contractSymbol.declarations?.map((item) => item.getSourceFile()) ?? []]);
588
+ return ![...sources].some(hasPrototypeMutation) && ![...targets].some((target) => escapedClasses(program, checker).has(target));
589
+ }
590
+ function classHierarchySymbols(checker, declaration, seen = /* @__PURE__ */ new Set()) {
591
+ const symbol = declaration.name === void 0 ? void 0 : canonicalSymbol(checker, declaration.name);
592
+ if (symbol === void 0 || seen.has(symbol)) return seen;
593
+ seen.add(symbol);
594
+ for (const clause of declaration.heritageClauses ?? []) {
595
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
596
+ for (const parent of clause.types) {
597
+ const ancestor = canonicalSymbol(checker, parent.expression)?.declarations?.find(ts.isClassDeclaration);
598
+ if (ancestor !== void 0) classHierarchySymbols(checker, ancestor, seen);
599
+ }
600
+ }
601
+ return seen;
602
+ }
603
+ function missingImplementation(checker, signature, parameter, call, argument) {
604
+ const parameterType = checker.getTypeOfSymbolAtLocation(parameter, call);
605
+ const contractType = behavioralContract(checker, parameterType);
606
+ if (contractType === null) return null;
607
+ const contractSymbol = contractType.getSymbol();
608
+ if (contractSymbol === void 0 || !hasNominalRejectionGuard(checker, signature, parameter, contractSymbol)) return null;
609
+ const actualType = checker.getTypeAtLocation(argument);
610
+ if (!checker.isTypeAssignableTo(actualType, contractType)) return null;
611
+ const implementation = ownedClass(checker, argument);
612
+ if (implementation === null) return null;
613
+ if (nominallyInherits(checker, implementation, contractSymbol, /* @__PURE__ */ new Set()) !== false) return null;
614
+ return { implementation, contractSymbol };
615
+ }
506
616
  function behavioralContract(checker, type) {
507
617
  if (type.isUnion()) {
508
618
  const substantive = type.types.filter((member) => (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0);
@@ -527,23 +637,9 @@ function ownedClass(checker, expression) {
527
637
  if (declaration === void 0 || declaration !== constructed || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
528
638
  return declaration;
529
639
  }
530
- function memberName(node) {
531
- if (ts.isPropertyAccessExpression(node)) return node.name.text;
532
- if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
533
- return null;
534
- }
535
- function hasPrototypeMutation(source) {
536
- function visit(node) {
537
- const member = memberName(node);
538
- if (member === "setPrototypeOf" || member === "__proto__" || member === "hasInstance") return true;
539
- if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && memberName(node.left) === "prototype") return true;
540
- return ts.forEachChild(node, visit);
541
- }
542
- return visit(source) === true;
543
- }
544
640
  function nominallyInherits(checker, declaration, target, seen) {
545
641
  if (ts.canHaveDecorators(declaration) && ts.getDecorators(declaration)?.length) return null;
546
- if (hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some((member) => ts.isConstructorDeclaration(member) || ts.isPropertyDeclaration(member) && member.initializer !== void 0)) return null;
642
+ if (hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some((member) => ts.isConstructorDeclaration(member) || ts.isClassStaticBlockDeclaration(member) || ts.isPropertyDeclaration(member) && member.initializer !== void 0)) return null;
547
643
  const symbol = declaration.name === void 0 ? void 0 : canonicalSymbol(checker, declaration.name);
548
644
  if (symbol !== void 0) {
549
645
  if (seen.has(symbol)) return null;
@@ -558,10 +654,6 @@ function nominallyInherits(checker, declaration, target, seen) {
558
654
  const inherited = parentSymbol.declarations?.find(ts.isClassDeclaration);
559
655
  return inherited === void 0 ? null : nominallyInherits(checker, inherited, target, new Set(seen));
560
656
  }
561
- function canonicalSymbol(checker, node) {
562
- const symbol = checker.getSymbolAtLocation(node);
563
- return symbol === void 0 ? void 0 : (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
564
- }
565
657
  function hasNominalRejectionGuard(checker, signature, parameter, contract) {
566
658
  const declaration = signature.getDeclaration();
567
659
  if (declaration === void 0 || !ts.isConstructorDeclaration(declaration)) return false;
@@ -586,7 +678,7 @@ function alwaysThrows(statement) {
586
678
  return ts.isThrowStatement(statement) || ts.isBlock(statement) && statement.statements.length === 1 && ts.isThrowStatement(statement.statements[0]);
587
679
  }
588
680
  function hasOrdinaryInstanceCheck(checker, declaration, seen) {
589
- if (seen.has(declaration) || ts.getDecorators(declaration)?.length || hasPrototypeMutation(declaration.getSourceFile())) return false;
681
+ if (seen.has(declaration) || ts.getDecorators(declaration)?.length || hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some(ts.isClassStaticBlockDeclaration)) return false;
590
682
  seen.add(declaration);
591
683
  if (declaration.members.some((member) => member.name !== void 0 && ts.isComputedPropertyName(member.name) && ts.canHaveModifiers(member) && ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) return false;
592
684
  for (const clause of declaration.heritageClauses ?? []) {
@@ -598,6 +690,10 @@ function hasOrdinaryInstanceCheck(checker, declaration, seen) {
598
690
  }
599
691
  return true;
600
692
  }
693
+ function canonicalSymbol(checker, node) {
694
+ const symbol = checker.getSymbolAtLocation(node);
695
+ return symbol === void 0 ? void 0 : (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
696
+ }
601
697
  var require_explicit_contract_implementation_default = createRule({
602
698
  name: "require-explicit-contract-implementation",
603
699
  documentation: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION,
@@ -627,22 +723,13 @@ var require_explicit_contract_implementation_default = createRule({
627
723
  if (argument.type !== import_utils2.AST_NODE_TYPES.NewExpression) continue;
628
724
  const parameter = parameters[index];
629
725
  if (parameter === void 0) continue;
630
- const parameterType = checker.getTypeOfSymbolAtLocation(parameter, tsNode);
631
- const contractType = behavioralContract(checker, parameterType);
632
- if (contractType === null) continue;
633
- const contractSymbol = contractType.getSymbol();
634
- if (contractSymbol === void 0 || !hasNominalRejectionGuard(checker, signature, parameter, contractSymbol)) continue;
635
726
  const tsArgument = services.esTreeNodeToTSNodeMap.get(argument);
636
- const actualType = checker.getTypeAtLocation(tsArgument);
637
- if (!checker.isTypeAssignableTo(actualType, contractType)) continue;
638
- const implementation = ownedClass(checker, tsArgument);
639
- if (implementation === null) continue;
640
- const relationship = nominallyInherits(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
641
- if (relationship !== false) continue;
727
+ const missing = missingImplementation(checker, signature, parameter, tsNode, tsArgument);
728
+ if (missing === null || !closedNominalBoundary(services.program, checker, tsNode, consumer, missing)) continue;
642
729
  context.report({
643
730
  node: argument,
644
731
  messageId: "declareActualContract",
645
- data: { implementation: implementation.name?.text ?? "class", contract: contractSymbol.getName() }
732
+ data: { implementation: missing.implementation.name?.text ?? "class", contract: missing.contractSymbol.getName() }
646
733
  });
647
734
  }
648
735
  }
@@ -7618,7 +7705,8 @@ var NO_IMPOSSIBLE_ZOD_LITERAL_BOUNDS_DOCUMENTATION = {
7618
7705
  remediation: "Choose compatible lower and upper bounds, or remove the constraint that does not express the intended domain.",
7619
7706
  category: "correctness",
7620
7707
  limitations: [
7621
- "Only finite numeric literals in a single number, string, or array schema chain are compared.",
7708
+ "Only finite numeric literals in a single number, string, or array schema chain are compared; explicit int() chains also require an admitted integer.",
7709
+ "Integer-domain comparisons abstain near the JavaScript safe-integer boundary.",
7622
7710
  "Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.",
7623
7711
  "Test and generated files are excluded."
7624
7712
  ],
@@ -7657,6 +7745,7 @@ var KINDS = /* @__PURE__ */ new Set(["array", "number", "string"]);
7657
7745
  var NUMBER_METHODS = /* @__PURE__ */ new Set([
7658
7746
  "gt",
7659
7747
  "gte",
7748
+ "int",
7660
7749
  "lt",
7661
7750
  "lte",
7662
7751
  "max",
@@ -7698,9 +7787,20 @@ function strongerUpper(current, candidate2) {
7698
7787
  }
7699
7788
  return current;
7700
7789
  }
7701
- function isEmpty(lower, upper) {
7790
+ function isEmpty(lower, upper, integer) {
7702
7791
  if (lower === null || upper === null) return false;
7703
- return lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7792
+ const realEmpty = lower.value > upper.value || lower.value === upper.value && (lower.exclusive || upper.exclusive);
7793
+ if (integer) {
7794
+ if (realEmpty) return false;
7795
+ const safeMargin = Number.MAX_SAFE_INTEGER - 1;
7796
+ if (Math.abs(lower.value) >= safeMargin || Math.abs(upper.value) >= safeMargin) {
7797
+ return false;
7798
+ }
7799
+ const minimum = lower.exclusive ? Math.floor(lower.value) + 1 : Math.ceil(lower.value);
7800
+ const maximum = upper.exclusive ? Math.ceil(upper.value) - 1 : Math.floor(upper.value);
7801
+ return minimum > maximum;
7802
+ }
7803
+ return realEmpty;
7704
7804
  }
7705
7805
  function isOutermostCall(node) {
7706
7806
  const parent = node.parent;
@@ -7794,6 +7894,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7794
7894
  const allowed = chain.kind === "number" ? NUMBER_METHODS : LENGTH_METHODS;
7795
7895
  let lower = null;
7796
7896
  let upper = null;
7897
+ let integer = false;
7797
7898
  function addBound(method, semantic, value) {
7798
7899
  const label = `${method}(${String(value)})`;
7799
7900
  if (semantic !== null) {
@@ -7829,6 +7930,11 @@ var no_impossible_zod_literal_bounds_default = createRule({
7829
7930
  }
7830
7931
  for (const { method, node } of chain.calls) {
7831
7932
  if (!allowed.has(method)) return null;
7933
+ if (method === "int") {
7934
+ if (node.arguments.length !== 0) return null;
7935
+ integer = true;
7936
+ continue;
7937
+ }
7832
7938
  const semantic = zeroBoundSemantic(method);
7833
7939
  const value = semantic?.value ?? finiteNumber(node.arguments[0]);
7834
7940
  if (value === null) return null;
@@ -7838,7 +7944,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7838
7944
  if (semantic !== null && node.arguments.length !== 0) return null;
7839
7945
  addBound(method, semantic, value);
7840
7946
  }
7841
- return isEmpty(lower, upper) && lower !== null && upper !== null ? { lower, upper } : null;
7947
+ return isEmpty(lower, upper, integer) && lower !== null && upper !== null ? { lower, upper } : null;
7842
7948
  }
7843
7949
  return {
7844
7950
  ImportDeclaration(node) {
@@ -17695,14 +17801,14 @@ var prefer_named_callback_domain_default = createRule({
17695
17801
  // src/rules/prefer-named-complex-return-type.ts
17696
17802
  var import_utils101 = require("@typescript-eslint/utils");
17697
17803
  var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
17698
- defaultLevel: "warning",
17804
+ defaultLevel: "error",
17699
17805
  summary: "Prefer a named contract for structurally complex function return types.",
17700
17806
  rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
17701
17807
  remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
17702
17808
  category: "maintainability",
17703
17809
  limitations: [
17704
17810
  "Only explicit object types with at least three members and unions with at least three object variants are reported.",
17705
- "Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters."
17811
+ "Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters with generic named contracts. Types depending on polymorphic this or a local value query are excluded."
17706
17812
  ],
17707
17813
  examples: [
17708
17814
  { id: "named-result", title: "Name a multi-state result", outcome: "no-match", files: [{ path: "src/queue.ts", source: "type ClaimResult = { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string }; export function claim(): ClaimResult { return { state: 'idle' }; }" }], focusPath: "src/queue.ts", expectedCount: 0, public: true },
@@ -17718,10 +17824,13 @@ function unwrap4(node) {
17718
17824
  }
17719
17825
  function report(context, node) {
17720
17826
  const annotation = node.returnType?.typeAnnotation;
17721
- if (annotation !== void 0 && isComplex(annotation)) {
17827
+ if (annotation !== void 0 && isComplex(annotation) && !hasValueDependency(annotation)) {
17722
17828
  context.report({ node: annotation, messageId: "nameComplexReturnType" });
17723
17829
  }
17724
17830
  }
17831
+ function hasValueDependency(node) {
17832
+ return node.type === import_utils101.AST_NODE_TYPES.TSThisType || node.type === import_utils101.AST_NODE_TYPES.TSTypeQuery || forEachOwnAstChild(node, hasValueDependency);
17833
+ }
17725
17834
  function isComplex(node) {
17726
17835
  const type = unwrap4(node);
17727
17836
  if (type.type === import_utils101.AST_NODE_TYPES.TSTypeLiteral) return type.members.length >= 3;
@@ -19369,20 +19478,20 @@ var prefer_shared_zod_enum_default = createRule({
19369
19478
  // src/rules/prefer-switch-for-repeated-equality.ts
19370
19479
  var import_utils110 = require("@typescript-eslint/utils");
19371
19480
  var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
19372
- defaultLevel: "warning",
19481
+ defaultLevel: "error",
19373
19482
  summary: "Prefer switch over long if/else-if chains that compare one value for strict equality.",
19374
19483
  rationale: "A switch makes finite dispatch cases visually uniform and easier to extend without duplicating the discriminant.",
19375
19484
  remediation: "Replace three or more strict-equality branches over the same discriminant with a switch; keep if statements for ranges, guards, and heterogeneous predicates.",
19376
19485
  category: "maintainability",
19377
19486
  limitations: [
19378
19487
  "Only direct if/else-if chains with at least three strict-equality tests are reported.",
19379
- "The discriminant must be a bare identifier; calls, getters, indexed reads and other repeatedly evaluated expressions are excluded. Review case-value effects, selector mutation, branch scoping, and break/continue targets when converting manually; this is not an equivalence proof.",
19380
- "Case values may be literals, enum-like member references, or upper-case named constants; dynamic expressions are excluded.",
19488
+ "The discriminant must be a resolved lexical variable or parameter; unresolved globals, calls, getters, and indexed reads are excluded. Preserve branch scopes and break/continue targets when converting manually; no autofix is offered.",
19489
+ "Case values must be primitive literals. Member references and named constants are excluded because evaluating them can observe getters or mutate the selector. Generated sources are excluded.",
19381
19490
  "The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
19382
19491
  ],
19383
19492
  examples: [
19384
19493
  { id: "switch-dispatch", title: "Make finite dispatch explicit", outcome: "no-match", files: [{ path: "src/render.ts", source: "switch (kind) { case 'a': return a(); case 'b': return b(); case 'c': return c(); default: return fallback(); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
19385
- { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c();" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
19494
+ { id: "repeated-equality", title: "Avoid repeating the discriminant", outcome: "match", files: [{ path: "src/render.ts", source: "function render(kind: string) { if (kind === 'a') return a(); else if (kind === 'b') return b(); else if (kind === 'c') return c(); }" }], focusPath: "src/render.ts", expectedCount: 1, public: true },
19386
19495
  { id: "escaped-middle", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}
19387
19496
  console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
19388
19497
  { id: "different-discriminants", scenarioId: "native-discriminant", title: "Use native identifier identity", outcome: "no-match", files: [{ path: "src/dispatch.ts", source: `function probe(kind){const other="other";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}
@@ -19398,10 +19507,7 @@ function discriminantName(test) {
19398
19507
  return discriminant.type === import_utils110.AST_NODE_TYPES.Identifier ? discriminant.name : null;
19399
19508
  }
19400
19509
  function isCaseValue(node) {
19401
- if (node.type === import_utils110.AST_NODE_TYPES.Literal) return true;
19402
- if (node.type === import_utils110.AST_NODE_TYPES.Identifier) return /^[A-Z][A-Z0-9_]*$/u.test(node.name);
19403
- if (node.type !== import_utils110.AST_NODE_TYPES.MemberExpression || node.computed) return false;
19404
- return node.property.type === import_utils110.AST_NODE_TYPES.Identifier && (node.object.type === import_utils110.AST_NODE_TYPES.Identifier || isCaseValue(node.object));
19510
+ return node.type === import_utils110.AST_NODE_TYPES.Literal && !("regex" in node);
19405
19511
  }
19406
19512
  var prefer_switch_for_repeated_equality_default = createRule({
19407
19513
  name: "prefer-switch-for-repeated-equality",
@@ -19416,11 +19522,16 @@ var prefer_switch_for_repeated_equality_default = createRule({
19416
19522
  },
19417
19523
  defaultOptions: [],
19418
19524
  create(context) {
19525
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) return {};
19419
19526
  return {
19420
19527
  IfStatement(node) {
19421
19528
  if (node.parent.type === import_utils110.AST_NODE_TYPES.IfStatement && node.parent.alternate === node) return;
19422
19529
  const first = discriminantName(node.test);
19423
19530
  if (first === null) return;
19531
+ const binding = import_utils110.ASTUtils.findVariable(context.sourceCode.getScope(node), first);
19532
+ if (binding === null || binding.defs.length !== 1 || binding.scope.type === "global") return;
19533
+ const [definition] = binding.defs;
19534
+ if (definition?.type !== "Parameter" && definition?.type !== "Variable") return;
19424
19535
  let count = 1;
19425
19536
  let current = node.alternate;
19426
19537
  while (current?.type === import_utils110.AST_NODE_TYPES.IfStatement) {
@@ -21427,17 +21538,17 @@ var UNRESOLVED_TYPE_NODES = /* @__PURE__ */ new Set([
21427
21538
  import_utils116.AST_NODE_TYPES.TSUnknownKeyword
21428
21539
  ]);
21429
21540
  var NAMED_CHILD_SCHEMA_TYPE_DOCUMENTATION = {
21430
- defaultLevel: "warning",
21541
+ defaultLevel: "error",
21431
21542
  summary: "Reference a named Zod record child's complete schema type instead of repeating its explicit annotation.",
21432
21543
  rationale: "Repeating a child schema's generic contract creates a second maintained declaration even though the record already uses that child at runtime.",
21433
- remediation: "Replace only the matching record type argument with `typeof ChildSchema`; retain the outer annotation required by isolatedDeclarations and every runtime schema operation.",
21544
+ remediation: "Replace only the matching record type argument with `typeof ChildSchema`, or share one explicit named schema type alias between child and record to retain an independently named public contract. Keep the outer annotation required by isolatedDeclarations and every runtime schema operation.",
21434
21545
  category: "maintainability",
21435
21546
  autofix: "none",
21436
21547
  limitations: [
21437
21548
  "Only direct module-level const z.record calls with two arguments, a ZodRecord annotation, and an earlier local const child with a complete explicit Zod type are inspected.",
21438
21549
  "The entire generic child annotation must match token for token; input/output, defaults, transforms, readonly modifiers and brands are never compared by output shape alone.",
21439
21550
  "Bare leaf types, broad ZodType constraints, type queries, imported or unqualified type references, annotation cycles, comments on the repeated argument, mutable bindings and generated files are excluded.",
21440
- "Import aliases and namespace imports resolve through scope bindings. Independent public contract pins require review; this warning does not remove annotations or change runtime validation."
21551
+ "Import aliases and namespace imports resolve through scope bindings. Independent public contract pins can use one shared named type alias or an exact reasoned suppression; runtime validation and outer annotations are preserved."
21441
21552
  ],
21442
21553
  examples: [
21443
21554
  { id: "named-child", title: "Reference the named child contract", outcome: "no-match", files: [{ path: "src/record.ts", source: 'import { z } from "zod"; const KeySchema: z.ZodEnum<{ a: "a"; b: "b" }> = z.enum(["a", "b"]); export const RecordSchema: z.ZodRecord<typeof KeySchema, z.ZodString> = z.record(KeySchema, z.string());' }], focusPath: "src/record.ts", expectedCount: 0, public: true },
@@ -21456,7 +21567,7 @@ var named_child_schema_type_default = createRule({
21456
21567
  type: "suggestion",
21457
21568
  docs: { description: NAMED_CHILD_SCHEMA_TYPE_DOCUMENTATION.summary },
21458
21569
  schema: [],
21459
- messages: { namedChildType: "Use `typeof {{name}}` for this exact child contract; keep the outer schema annotation and runtime operations." }
21570
+ messages: { namedChildType: "Use `typeof {{name}}` or one shared named type alias for this exact child contract; keep the outer schema annotation and runtime operations." }
21460
21571
  },
21461
21572
  defaultOptions: [],
21462
21573
  create(context) {
@@ -24856,7 +24967,6 @@ var LIBRARY_IMPORT_POLICY = ["error", {
24856
24967
  }];
24857
24968
  var ADVISORY_RULES = [
24858
24969
  "@sarj/excessive-commentary",
24859
- "@sarj/named-child-schema-type",
24860
24970
  "@sarj/no-bespoke-api-case-conversion",
24861
24971
  "@sarj/no-duplicate-test-case",
24862
24972
  "@sarj/no-json-stringify-object-equality",
@@ -24865,21 +24975,18 @@ var ADVISORY_RULES = [
24865
24975
  "@sarj/prefer-millisecond-control-duration-schema",
24866
24976
  "@sarj/prefer-module-level-refined-schema",
24867
24977
  "@sarj/prefer-multi-value-zod-literal",
24868
- "@sarj/prefer-named-complex-return-type",
24869
24978
  "@sarj/prefer-node-crypto-hash",
24870
24979
  "@sarj/prefer-node-fs-promises",
24871
24980
  "@sarj/prefer-nominal-id-types",
24872
24981
  "@sarj/prefer-shared-zod-enum",
24873
- "@sarj/prefer-switch-for-repeated-equality",
24874
24982
  "@sarj/prefer-whole-object-assertion",
24875
24983
  "@sarj/require-camelcase-properties",
24876
- "@sarj/require-explicit-contract-implementation",
24877
24984
  "@sarj/require-explicit-service-contract",
24878
24985
  "@sarj/require-interface-for-exported-class",
24879
24986
  "@sarj/require-sql-access-class"
24880
24987
  ];
24881
24988
  var RECOMMENDED_RULES = {
24882
- "@sarj/named-child-schema-type": "warn",
24989
+ "@sarj/named-child-schema-type": "error",
24883
24990
  "@sarj/no-conditional-empty-object-spread": "error",
24884
24991
  "@sarj/prefer-typed-reflection": "error",
24885
24992
  "@sarj/no-broad-return-type": "error",
@@ -24963,7 +25070,7 @@ var RECOMMENDED_RULES = {
24963
25070
  "@sarj/prefer-multi-value-zod-literal": ["warn", { zodMajorVersion: 4 }],
24964
25071
  "@sarj/prefer-named-callback-domain": "error",
24965
25072
  "@sarj/prefer-nominal-id-types": "warn",
24966
- "@sarj/prefer-named-complex-return-type": "warn",
25073
+ "@sarj/prefer-named-complex-return-type": "error",
24967
25074
  "@sarj/prefer-node-crypto-hash": "warn",
24968
25075
  "@sarj/prefer-node-fs-promises": "warn",
24969
25076
  "@sarj/prefer-non-nullable-collection": "error",
@@ -24971,7 +25078,7 @@ var RECOMMENDED_RULES = {
24971
25078
  "@sarj/prefer-await-in-async-return": "error",
24972
25079
  "@sarj/prefer-schema-for-api-payload": "error",
24973
25080
  "@sarj/prefer-shared-zod-enum": "warn",
24974
- "@sarj/prefer-switch-for-repeated-equality": "warn",
25081
+ "@sarj/prefer-switch-for-repeated-equality": "error",
24975
25082
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
24976
25083
  "@sarj/prefer-server-actions": "error",
24977
25084
  "@sarj/prefer-whole-object-assertion": "warn",
@@ -24980,7 +25087,7 @@ var RECOMMENDED_RULES = {
24980
25087
  "@sarj/prefer-zod-parse-output-type": "error",
24981
25088
  "@sarj/require-assert-never": "error",
24982
25089
  "@sarj/require-fetch-timeout": "error",
24983
- "@sarj/require-explicit-contract-implementation": "warn",
25090
+ "@sarj/require-explicit-contract-implementation": "error",
24984
25091
  "@sarj/require-explicit-service-contract": "warn",
24985
25092
  "@sarj/require-interface-for-exported-class": "warn",
24986
25093
  "@sarj/require-port-for-service": "error",
@@ -24997,7 +25104,7 @@ var RECOMMENDED_RULES = {
24997
25104
  "@sarj/require-pascal-case-zod-schema-name": "error"
24998
25105
  };
24999
25106
  var STRICT_RULES = {
25000
- "@sarj/named-child-schema-type": "warn",
25107
+ "@sarj/named-child-schema-type": "error",
25001
25108
  "@sarj/no-conditional-empty-object-spread": "error",
25002
25109
  "@sarj/prefer-typed-reflection": "error",
25003
25110
  "@sarj/no-broad-return-type": "error",
@@ -25085,7 +25192,7 @@ var STRICT_RULES = {
25085
25192
  "@sarj/prefer-multi-value-zod-literal": ["warn", { zodMajorVersion: 4 }],
25086
25193
  "@sarj/prefer-named-callback-domain": "error",
25087
25194
  "@sarj/prefer-nominal-id-types": "warn",
25088
- "@sarj/prefer-named-complex-return-type": "warn",
25195
+ "@sarj/prefer-named-complex-return-type": "error",
25089
25196
  "@sarj/prefer-node-crypto-hash": "warn",
25090
25197
  "@sarj/prefer-node-fs-promises": "warn",
25091
25198
  "@sarj/prefer-non-nullable-collection": "error",
@@ -25093,7 +25200,7 @@ var STRICT_RULES = {
25093
25200
  "@sarj/prefer-await-in-async-return": "error",
25094
25201
  "@sarj/prefer-schema-for-api-payload": "error",
25095
25202
  "@sarj/prefer-shared-zod-enum": "warn",
25096
- "@sarj/prefer-switch-for-repeated-equality": "warn",
25203
+ "@sarj/prefer-switch-for-repeated-equality": "error",
25097
25204
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
25098
25205
  "@sarj/prefer-server-actions": "error",
25099
25206
  "@sarj/prefer-whole-object-assertion": "warn",
@@ -25102,7 +25209,7 @@ var STRICT_RULES = {
25102
25209
  "@sarj/prefer-zod-parse-output-type": "error",
25103
25210
  "@sarj/require-assert-never": "error",
25104
25211
  "@sarj/require-fetch-timeout": "error",
25105
- "@sarj/require-explicit-contract-implementation": "warn",
25212
+ "@sarj/require-explicit-contract-implementation": "error",
25106
25213
  "@sarj/require-explicit-service-contract": "warn",
25107
25214
  "@sarj/require-interface-for-exported-class": "warn",
25108
25215
  "@sarj/require-port-for-service": "error",
package/dist/index.d.cts CHANGED
@@ -291,9 +291,9 @@ declare const RULES: {
291
291
  /** @deprecated All repositories use one policy; retained for import compatibility. */
292
292
  declare const APPLICATION_ONLY_RULES: readonly [];
293
293
  /** Rules staged as non-blocking warnings while corpus adoption evidence accumulates. */
294
- declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/named-child-schema-type", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-duplicate-test-case", "@sarj/no-json-stringify-object-equality", "@sarj/no-restated-comment", "@sarj/no-restated-jsdoc", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-named-complex-return-type", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nominal-id-types", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-switch-for-repeated-equality", "@sarj/prefer-whole-object-assertion", "@sarj/require-camelcase-properties", "@sarj/require-explicit-contract-implementation", "@sarj/require-explicit-service-contract", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class"];
294
+ declare const ADVISORY_RULES: readonly ["@sarj/excessive-commentary", "@sarj/no-bespoke-api-case-conversion", "@sarj/no-duplicate-test-case", "@sarj/no-json-stringify-object-equality", "@sarj/no-restated-comment", "@sarj/no-restated-jsdoc", "@sarj/prefer-millisecond-control-duration-schema", "@sarj/prefer-module-level-refined-schema", "@sarj/prefer-multi-value-zod-literal", "@sarj/prefer-node-crypto-hash", "@sarj/prefer-node-fs-promises", "@sarj/prefer-nominal-id-types", "@sarj/prefer-shared-zod-enum", "@sarj/prefer-whole-object-assertion", "@sarj/require-camelcase-properties", "@sarj/require-explicit-service-contract", "@sarj/require-interface-for-exported-class", "@sarj/require-sql-access-class"];
295
295
  declare const RECOMMENDED_RULES: {
296
- readonly "@sarj/named-child-schema-type": "warn";
296
+ readonly "@sarj/named-child-schema-type": "error";
297
297
  readonly "@sarj/no-conditional-empty-object-spread": "error";
298
298
  readonly "@sarj/prefer-typed-reflection": "error";
299
299
  readonly "@sarj/no-broad-return-type": "error";
@@ -570,7 +570,7 @@ declare const RECOMMENDED_RULES: {
570
570
  }];
571
571
  readonly "@sarj/prefer-named-callback-domain": "error";
572
572
  readonly "@sarj/prefer-nominal-id-types": "warn";
573
- readonly "@sarj/prefer-named-complex-return-type": "warn";
573
+ readonly "@sarj/prefer-named-complex-return-type": "error";
574
574
  readonly "@sarj/prefer-node-crypto-hash": "warn";
575
575
  readonly "@sarj/prefer-node-fs-promises": "warn";
576
576
  readonly "@sarj/prefer-non-nullable-collection": "error";
@@ -578,7 +578,7 @@ declare const RECOMMENDED_RULES: {
578
578
  readonly "@sarj/prefer-await-in-async-return": "error";
579
579
  readonly "@sarj/prefer-schema-for-api-payload": "error";
580
580
  readonly "@sarj/prefer-shared-zod-enum": "warn";
581
- readonly "@sarj/prefer-switch-for-repeated-equality": "warn";
581
+ readonly "@sarj/prefer-switch-for-repeated-equality": "error";
582
582
  readonly "@sarj/prefer-semantic-colors": readonly ["error", {
583
583
  readonly requireSemanticTokens: true;
584
584
  }];
@@ -589,7 +589,7 @@ declare const RECOMMENDED_RULES: {
589
589
  readonly "@sarj/prefer-zod-parse-output-type": "error";
590
590
  readonly "@sarj/require-assert-never": "error";
591
591
  readonly "@sarj/require-fetch-timeout": "error";
592
- readonly "@sarj/require-explicit-contract-implementation": "warn";
592
+ readonly "@sarj/require-explicit-contract-implementation": "error";
593
593
  readonly "@sarj/require-explicit-service-contract": "warn";
594
594
  readonly "@sarj/require-interface-for-exported-class": "warn";
595
595
  readonly "@sarj/require-port-for-service": "error";
@@ -606,7 +606,7 @@ declare const RECOMMENDED_RULES: {
606
606
  readonly "@sarj/require-pascal-case-zod-schema-name": "error";
607
607
  };
608
608
  declare const STRICT_RULES: {
609
- readonly "@sarj/named-child-schema-type": "warn";
609
+ readonly "@sarj/named-child-schema-type": "error";
610
610
  readonly "@sarj/no-conditional-empty-object-spread": "error";
611
611
  readonly "@sarj/prefer-typed-reflection": "error";
612
612
  readonly "@sarj/no-broad-return-type": "error";
@@ -887,7 +887,7 @@ declare const STRICT_RULES: {
887
887
  }];
888
888
  readonly "@sarj/prefer-named-callback-domain": "error";
889
889
  readonly "@sarj/prefer-nominal-id-types": "warn";
890
- readonly "@sarj/prefer-named-complex-return-type": "warn";
890
+ readonly "@sarj/prefer-named-complex-return-type": "error";
891
891
  readonly "@sarj/prefer-node-crypto-hash": "warn";
892
892
  readonly "@sarj/prefer-node-fs-promises": "warn";
893
893
  readonly "@sarj/prefer-non-nullable-collection": "error";
@@ -895,7 +895,7 @@ declare const STRICT_RULES: {
895
895
  readonly "@sarj/prefer-await-in-async-return": "error";
896
896
  readonly "@sarj/prefer-schema-for-api-payload": "error";
897
897
  readonly "@sarj/prefer-shared-zod-enum": "warn";
898
- readonly "@sarj/prefer-switch-for-repeated-equality": "warn";
898
+ readonly "@sarj/prefer-switch-for-repeated-equality": "error";
899
899
  readonly "@sarj/prefer-semantic-colors": readonly ["error", {
900
900
  readonly requireSemanticTokens: true;
901
901
  }];
@@ -906,7 +906,7 @@ declare const STRICT_RULES: {
906
906
  readonly "@sarj/prefer-zod-parse-output-type": "error";
907
907
  readonly "@sarj/require-assert-never": "error";
908
908
  readonly "@sarj/require-fetch-timeout": "error";
909
- readonly "@sarj/require-explicit-contract-implementation": "warn";
909
+ readonly "@sarj/require-explicit-contract-implementation": "error";
910
910
  readonly "@sarj/require-explicit-service-contract": "warn";
911
911
  readonly "@sarj/require-interface-for-exported-class": "warn";
912
912
  readonly "@sarj/require-port-for-service": "error";