@sarj/eslint-plugin 16.6.0 → 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.6.0",
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
  }
@@ -17714,14 +17801,14 @@ var prefer_named_callback_domain_default = createRule({
17714
17801
  // src/rules/prefer-named-complex-return-type.ts
17715
17802
  var import_utils101 = require("@typescript-eslint/utils");
17716
17803
  var PREFER_NAMED_COMPLEX_RETURN_TYPE_DOCUMENTATION = {
17717
- defaultLevel: "warning",
17804
+ defaultLevel: "error",
17718
17805
  summary: "Prefer a named contract for structurally complex function return types.",
17719
17806
  rationale: "A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.",
17720
17807
  remediation: "Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.",
17721
17808
  category: "maintainability",
17722
17809
  limitations: [
17723
17810
  "Only explicit object types with at least three members and unions with at least three object variants are reported.",
17724
- "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."
17725
17812
  ],
17726
17813
  examples: [
17727
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 },
@@ -17737,10 +17824,13 @@ function unwrap4(node) {
17737
17824
  }
17738
17825
  function report(context, node) {
17739
17826
  const annotation = node.returnType?.typeAnnotation;
17740
- if (annotation !== void 0 && isComplex(annotation)) {
17827
+ if (annotation !== void 0 && isComplex(annotation) && !hasValueDependency(annotation)) {
17741
17828
  context.report({ node: annotation, messageId: "nameComplexReturnType" });
17742
17829
  }
17743
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
+ }
17744
17834
  function isComplex(node) {
17745
17835
  const type = unwrap4(node);
17746
17836
  if (type.type === import_utils101.AST_NODE_TYPES.TSTypeLiteral) return type.members.length >= 3;
@@ -19388,20 +19478,20 @@ var prefer_shared_zod_enum_default = createRule({
19388
19478
  // src/rules/prefer-switch-for-repeated-equality.ts
19389
19479
  var import_utils110 = require("@typescript-eslint/utils");
19390
19480
  var PREFER_SWITCH_FOR_REPEATED_EQUALITY_DOCUMENTATION = {
19391
- defaultLevel: "warning",
19481
+ defaultLevel: "error",
19392
19482
  summary: "Prefer switch over long if/else-if chains that compare one value for strict equality.",
19393
19483
  rationale: "A switch makes finite dispatch cases visually uniform and easier to extend without duplicating the discriminant.",
19394
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.",
19395
19485
  category: "maintainability",
19396
19486
  limitations: [
19397
19487
  "Only direct if/else-if chains with at least three strict-equality tests are reported.",
19398
- "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.",
19399
- "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.",
19400
19490
  "The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."
19401
19491
  ],
19402
19492
  examples: [
19403
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 },
19404
- { 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 },
19405
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;}
19406
19496
  console.log(JSON.stringify(["a","b","c","d"].map(probe)));` }], focusPath: "src/dispatch.ts", expectedCount: 1, public: true },
19407
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;}
@@ -19417,10 +19507,7 @@ function discriminantName(test) {
19417
19507
  return discriminant.type === import_utils110.AST_NODE_TYPES.Identifier ? discriminant.name : null;
19418
19508
  }
19419
19509
  function isCaseValue(node) {
19420
- if (node.type === import_utils110.AST_NODE_TYPES.Literal) return true;
19421
- if (node.type === import_utils110.AST_NODE_TYPES.Identifier) return /^[A-Z][A-Z0-9_]*$/u.test(node.name);
19422
- if (node.type !== import_utils110.AST_NODE_TYPES.MemberExpression || node.computed) return false;
19423
- 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);
19424
19511
  }
19425
19512
  var prefer_switch_for_repeated_equality_default = createRule({
19426
19513
  name: "prefer-switch-for-repeated-equality",
@@ -19435,11 +19522,16 @@ var prefer_switch_for_repeated_equality_default = createRule({
19435
19522
  },
19436
19523
  defaultOptions: [],
19437
19524
  create(context) {
19525
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) return {};
19438
19526
  return {
19439
19527
  IfStatement(node) {
19440
19528
  if (node.parent.type === import_utils110.AST_NODE_TYPES.IfStatement && node.parent.alternate === node) return;
19441
19529
  const first = discriminantName(node.test);
19442
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;
19443
19535
  let count = 1;
19444
19536
  let current = node.alternate;
19445
19537
  while (current?.type === import_utils110.AST_NODE_TYPES.IfStatement) {
@@ -21446,17 +21538,17 @@ var UNRESOLVED_TYPE_NODES = /* @__PURE__ */ new Set([
21446
21538
  import_utils116.AST_NODE_TYPES.TSUnknownKeyword
21447
21539
  ]);
21448
21540
  var NAMED_CHILD_SCHEMA_TYPE_DOCUMENTATION = {
21449
- defaultLevel: "warning",
21541
+ defaultLevel: "error",
21450
21542
  summary: "Reference a named Zod record child's complete schema type instead of repeating its explicit annotation.",
21451
21543
  rationale: "Repeating a child schema's generic contract creates a second maintained declaration even though the record already uses that child at runtime.",
21452
- 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.",
21453
21545
  category: "maintainability",
21454
21546
  autofix: "none",
21455
21547
  limitations: [
21456
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.",
21457
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.",
21458
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.",
21459
- "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."
21460
21552
  ],
21461
21553
  examples: [
21462
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 },
@@ -21475,7 +21567,7 @@ var named_child_schema_type_default = createRule({
21475
21567
  type: "suggestion",
21476
21568
  docs: { description: NAMED_CHILD_SCHEMA_TYPE_DOCUMENTATION.summary },
21477
21569
  schema: [],
21478
- 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." }
21479
21571
  },
21480
21572
  defaultOptions: [],
21481
21573
  create(context) {
@@ -24875,7 +24967,6 @@ var LIBRARY_IMPORT_POLICY = ["error", {
24875
24967
  }];
24876
24968
  var ADVISORY_RULES = [
24877
24969
  "@sarj/excessive-commentary",
24878
- "@sarj/named-child-schema-type",
24879
24970
  "@sarj/no-bespoke-api-case-conversion",
24880
24971
  "@sarj/no-duplicate-test-case",
24881
24972
  "@sarj/no-json-stringify-object-equality",
@@ -24884,21 +24975,18 @@ var ADVISORY_RULES = [
24884
24975
  "@sarj/prefer-millisecond-control-duration-schema",
24885
24976
  "@sarj/prefer-module-level-refined-schema",
24886
24977
  "@sarj/prefer-multi-value-zod-literal",
24887
- "@sarj/prefer-named-complex-return-type",
24888
24978
  "@sarj/prefer-node-crypto-hash",
24889
24979
  "@sarj/prefer-node-fs-promises",
24890
24980
  "@sarj/prefer-nominal-id-types",
24891
24981
  "@sarj/prefer-shared-zod-enum",
24892
- "@sarj/prefer-switch-for-repeated-equality",
24893
24982
  "@sarj/prefer-whole-object-assertion",
24894
24983
  "@sarj/require-camelcase-properties",
24895
- "@sarj/require-explicit-contract-implementation",
24896
24984
  "@sarj/require-explicit-service-contract",
24897
24985
  "@sarj/require-interface-for-exported-class",
24898
24986
  "@sarj/require-sql-access-class"
24899
24987
  ];
24900
24988
  var RECOMMENDED_RULES = {
24901
- "@sarj/named-child-schema-type": "warn",
24989
+ "@sarj/named-child-schema-type": "error",
24902
24990
  "@sarj/no-conditional-empty-object-spread": "error",
24903
24991
  "@sarj/prefer-typed-reflection": "error",
24904
24992
  "@sarj/no-broad-return-type": "error",
@@ -24982,7 +25070,7 @@ var RECOMMENDED_RULES = {
24982
25070
  "@sarj/prefer-multi-value-zod-literal": ["warn", { zodMajorVersion: 4 }],
24983
25071
  "@sarj/prefer-named-callback-domain": "error",
24984
25072
  "@sarj/prefer-nominal-id-types": "warn",
24985
- "@sarj/prefer-named-complex-return-type": "warn",
25073
+ "@sarj/prefer-named-complex-return-type": "error",
24986
25074
  "@sarj/prefer-node-crypto-hash": "warn",
24987
25075
  "@sarj/prefer-node-fs-promises": "warn",
24988
25076
  "@sarj/prefer-non-nullable-collection": "error",
@@ -24990,7 +25078,7 @@ var RECOMMENDED_RULES = {
24990
25078
  "@sarj/prefer-await-in-async-return": "error",
24991
25079
  "@sarj/prefer-schema-for-api-payload": "error",
24992
25080
  "@sarj/prefer-shared-zod-enum": "warn",
24993
- "@sarj/prefer-switch-for-repeated-equality": "warn",
25081
+ "@sarj/prefer-switch-for-repeated-equality": "error",
24994
25082
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
24995
25083
  "@sarj/prefer-server-actions": "error",
24996
25084
  "@sarj/prefer-whole-object-assertion": "warn",
@@ -24999,7 +25087,7 @@ var RECOMMENDED_RULES = {
24999
25087
  "@sarj/prefer-zod-parse-output-type": "error",
25000
25088
  "@sarj/require-assert-never": "error",
25001
25089
  "@sarj/require-fetch-timeout": "error",
25002
- "@sarj/require-explicit-contract-implementation": "warn",
25090
+ "@sarj/require-explicit-contract-implementation": "error",
25003
25091
  "@sarj/require-explicit-service-contract": "warn",
25004
25092
  "@sarj/require-interface-for-exported-class": "warn",
25005
25093
  "@sarj/require-port-for-service": "error",
@@ -25016,7 +25104,7 @@ var RECOMMENDED_RULES = {
25016
25104
  "@sarj/require-pascal-case-zod-schema-name": "error"
25017
25105
  };
25018
25106
  var STRICT_RULES = {
25019
- "@sarj/named-child-schema-type": "warn",
25107
+ "@sarj/named-child-schema-type": "error",
25020
25108
  "@sarj/no-conditional-empty-object-spread": "error",
25021
25109
  "@sarj/prefer-typed-reflection": "error",
25022
25110
  "@sarj/no-broad-return-type": "error",
@@ -25104,7 +25192,7 @@ var STRICT_RULES = {
25104
25192
  "@sarj/prefer-multi-value-zod-literal": ["warn", { zodMajorVersion: 4 }],
25105
25193
  "@sarj/prefer-named-callback-domain": "error",
25106
25194
  "@sarj/prefer-nominal-id-types": "warn",
25107
- "@sarj/prefer-named-complex-return-type": "warn",
25195
+ "@sarj/prefer-named-complex-return-type": "error",
25108
25196
  "@sarj/prefer-node-crypto-hash": "warn",
25109
25197
  "@sarj/prefer-node-fs-promises": "warn",
25110
25198
  "@sarj/prefer-non-nullable-collection": "error",
@@ -25112,7 +25200,7 @@ var STRICT_RULES = {
25112
25200
  "@sarj/prefer-await-in-async-return": "error",
25113
25201
  "@sarj/prefer-schema-for-api-payload": "error",
25114
25202
  "@sarj/prefer-shared-zod-enum": "warn",
25115
- "@sarj/prefer-switch-for-repeated-equality": "warn",
25203
+ "@sarj/prefer-switch-for-repeated-equality": "error",
25116
25204
  "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
25117
25205
  "@sarj/prefer-server-actions": "error",
25118
25206
  "@sarj/prefer-whole-object-assertion": "warn",
@@ -25121,7 +25209,7 @@ var STRICT_RULES = {
25121
25209
  "@sarj/prefer-zod-parse-output-type": "error",
25122
25210
  "@sarj/require-assert-never": "error",
25123
25211
  "@sarj/require-fetch-timeout": "error",
25124
- "@sarj/require-explicit-contract-implementation": "warn",
25212
+ "@sarj/require-explicit-contract-implementation": "error",
25125
25213
  "@sarj/require-explicit-service-contract": "warn",
25126
25214
  "@sarj/require-interface-for-exported-class": "warn",
25127
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";
package/dist/index.d.ts 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";