@sarj/eslint-plugin 16.2.5 → 16.3.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
@@ -349,69 +349,34 @@ function commentLineEnd(sourceText, start) {
349
349
  // src/rules/require-explicit-contract-implementation.ts
350
350
  var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
351
351
  defaultLevel: "warning",
352
- summary: "Require owned classes supplied as typed collaborators to explicitly implement their contract.",
353
- rationale: "Structural assignment checks a fake only at the use site; an implements clause keeps its obligations visible at the class declaration.",
354
- remediation: "Add an implements clause for the actual injected interface, or inherit an abstract class that declares it.",
352
+ summary: "Require nominal inheritance when an injected abstract class is rejected by an instanceof guard.",
353
+ rationale: "A structurally assignable fake can still fail a constructor's nominal runtime guard. Pure structural interfaces do not require an implements clause.",
354
+ remediation: "Use an implementation inheriting the existing abstract class, or remove the nominal guard if the boundary is intentionally structural.",
355
355
  category: "architecture",
356
356
  autofix: "none",
357
357
  limitations: [
358
- "Only class instances whose concrete class and selected constructor parameter resolve through the TypeScript checker are checked.",
359
- "Object literals, casts, any, callback-valued data properties, unresolved heritage, dynamic factories, and third-party classes are excluded."
358
+ "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.",
359
+ "The contract must resolve to an abstract class without decorators, unresolved ancestors, or custom computed static members in its resolved hierarchy.",
360
+ "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."
360
361
  ],
361
362
  examples: [
362
363
  {
363
- id: "structural-fake-at-injection",
364
- title: "A fake reaches a typed collaborator without implementing its interface",
364
+ id: "runtime-nominal-guard-with-structural-fake",
365
+ title: "A structurally compatible fake fails the constructor's runtime guard",
365
366
  outcome: "match",
366
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher { publish() {} } new Consumer(new FakePublisher());" }],
367
+ files: [{ path: "src/service.ts", source: "abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher { publish() {} } new Consumer(new FakePublisher());" }],
367
368
  focusPath: "src/service.ts",
368
369
  expectedCount: 1,
369
370
  public: true
370
371
  },
371
372
  {
372
- id: "explicit-fake-at-injection",
373
- title: "The fake names its actual interface",
373
+ id: "runtime-nominal-guard-with-inherited-fake",
374
+ title: "The fake satisfies the existing nominal runtime contract",
374
375
  outcome: "no-match",
375
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher implements Publisher { publish() {} } new Consumer(new FakePublisher());" }],
376
+ files: [{ path: "src/service.ts", source: "abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher extends Publisher { publish() {} } new Consumer(new FakePublisher());" }],
376
377
  focusPath: "src/service.ts",
377
378
  expectedCount: 0,
378
379
  public: true
379
- },
380
- {
381
- id: "inherited-implementation",
382
- title: "The fake inherits an implementation of the port",
383
- outcome: "no-match",
384
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class BasePublisher implements Publisher { publish() {} } class FakePublisher extends BasePublisher {} new Consumer(new FakePublisher());" }],
385
- focusPath: "src/service.ts",
386
- expectedCount: 0,
387
- public: false
388
- },
389
- {
390
- id: "non-contract-data-argument",
391
- title: "A data object is not a service port",
392
- outcome: "no-match",
393
- files: [{ path: "src/service.ts", source: "interface Options { name: string } class Consumer { constructor(readonly options: Options) {} } class FakeOptions { name = 'test' } new Consumer(new FakeOptions());" }],
394
- focusPath: "src/service.ts",
395
- expectedCount: 0,
396
- public: false
397
- },
398
- {
399
- id: "explicit-cast-origin-unknown",
400
- title: "A cast hides the concrete source",
401
- outcome: "no-match",
402
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher { publish() {} } new Consumer(new FakePublisher() as Publisher);" }],
403
- focusPath: "src/service.ts",
404
- expectedCount: 0,
405
- public: false
406
- },
407
- {
408
- id: "unrelated-interface",
409
- title: "Implementing another interface does not declare the supplied port",
410
- outcome: "match",
411
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } interface Other { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher implements Other { publish() {} } new Consumer(new FakePublisher());" }],
412
- focusPath: "src/service.ts",
413
- expectedCount: 1,
414
- public: false
415
380
  }
416
381
  ]
417
382
  };
@@ -422,7 +387,7 @@ function behavioralContract(checker, type) {
422
387
  return behavioralContract(checker, substantive[0]);
423
388
  }
424
389
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
425
- const declaration = type.getSymbol()?.declarations?.find((item) => ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item) || ts.isClassDeclaration(item) && item.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword));
390
+ const declaration = type.getSymbol()?.declarations?.find((item) => ts.isClassDeclaration(item) && item.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword));
426
391
  if (declaration === void 0) return null;
427
392
  return checker.getPropertiesOfType(type).some((property) => {
428
393
  if (!property.declarations?.some((member) => ts.isMethodSignature(member) || ts.isMethodDeclaration(member))) return false;
@@ -430,43 +395,86 @@ function behavioralContract(checker, type) {
430
395
  return propertyType.getCallSignatures().length > 0;
431
396
  }) ? type : null;
432
397
  }
433
- function ownedClass(type) {
398
+ function ownedClass(checker, expression) {
399
+ if (!ts.isNewExpression(expression)) return null;
400
+ const type = checker.getTypeAtLocation(expression);
434
401
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
435
402
  const declaration = type.getSymbol()?.declarations?.find(ts.isClassDeclaration);
436
- if (declaration === void 0 || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
403
+ const constructed = canonicalSymbol(checker, expression.expression)?.declarations?.find(ts.isClassDeclaration);
404
+ if (declaration === void 0 || declaration !== constructed || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
437
405
  return declaration;
438
406
  }
439
- function explicitlyImplements(checker, declaration, target, seen) {
407
+ function memberName(node) {
408
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
409
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
410
+ return null;
411
+ }
412
+ function hasPrototypeMutation(source) {
413
+ function visit(node) {
414
+ const member = memberName(node);
415
+ if (member === "setPrototypeOf" || member === "__proto__" || member === "hasInstance") return true;
416
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && memberName(node.left) === "prototype") return true;
417
+ return ts.forEachChild(node, visit);
418
+ }
419
+ return visit(source) === true;
420
+ }
421
+ function nominallyInherits(checker, declaration, target, seen) {
422
+ if (ts.canHaveDecorators(declaration) && ts.getDecorators(declaration)?.length) return null;
423
+ if (hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some((member) => ts.isConstructorDeclaration(member) || ts.isPropertyDeclaration(member) && member.initializer !== void 0)) return null;
440
424
  const symbol = declaration.name === void 0 ? void 0 : canonicalSymbol(checker, declaration.name);
441
425
  if (symbol !== void 0) {
442
426
  if (seen.has(symbol)) return null;
443
427
  seen.add(symbol);
444
428
  }
445
- let unknown = false;
446
- for (const clause of declaration.heritageClauses ?? []) {
447
- for (const parent of clause.types) {
448
- const parentSymbol = canonicalSymbol(checker, parent.expression);
449
- if (parentSymbol === void 0) {
450
- unknown = true;
451
- continue;
452
- }
453
- if (parentSymbol === target) return true;
454
- const inherited = parentSymbol.declarations?.find((item) => ts.isClassDeclaration(item) || ts.isInterfaceDeclaration(item));
455
- if (inherited === void 0) {
456
- unknown = true;
457
- continue;
458
- }
459
- const relationship = explicitlyImplements(checker, inherited, target, new Set(seen));
460
- if (relationship === true) return true;
461
- unknown ||= relationship === null;
462
- }
463
- }
464
- return unknown ? null : false;
429
+ const parents = declaration.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types;
430
+ if (parents === void 0) return false;
431
+ if (parents.length !== 1) return null;
432
+ const parentSymbol = canonicalSymbol(checker, parents[0].expression);
433
+ if (parentSymbol === void 0) return null;
434
+ if (parentSymbol === target) return true;
435
+ const inherited = parentSymbol.declarations?.find(ts.isClassDeclaration);
436
+ return inherited === void 0 ? null : nominallyInherits(checker, inherited, target, new Set(seen));
465
437
  }
466
438
  function canonicalSymbol(checker, node) {
467
439
  const symbol = checker.getSymbolAtLocation(node);
468
440
  return symbol === void 0 ? void 0 : (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
469
441
  }
442
+ function hasNominalRejectionGuard(checker, signature, parameter, contract) {
443
+ const declaration = signature.getDeclaration();
444
+ if (declaration === void 0 || !ts.isConstructorDeclaration(declaration)) return false;
445
+ if (ts.canHaveDecorators(declaration.parent) && ts.getDecorators(declaration.parent)?.length) return false;
446
+ if (hasPrototypeMutation(declaration.getSourceFile())) return false;
447
+ const first = declaration.body?.statements[0];
448
+ if (first === void 0 || !ts.isIfStatement(first) || !alwaysThrows(first.thenStatement)) return false;
449
+ const condition = unparenthesized(first.expression);
450
+ if (!ts.isPrefixUnaryExpression(condition) || condition.operator !== ts.SyntaxKind.ExclamationToken) return false;
451
+ const comparison = unparenthesized(condition.operand);
452
+ if (!ts.isBinaryExpression(comparison) || comparison.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return false;
453
+ if (canonicalSymbol(checker, comparison.left) !== parameter || canonicalSymbol(checker, comparison.right) !== contract) return false;
454
+ const target = contract.declarations?.find(ts.isClassDeclaration);
455
+ return target !== void 0 && hasOrdinaryInstanceCheck(checker, target, /* @__PURE__ */ new Set());
456
+ }
457
+ function unparenthesized(expression) {
458
+ let current = expression;
459
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
460
+ return current;
461
+ }
462
+ function alwaysThrows(statement) {
463
+ return ts.isThrowStatement(statement) || ts.isBlock(statement) && statement.statements.length === 1 && ts.isThrowStatement(statement.statements[0]);
464
+ }
465
+ function hasOrdinaryInstanceCheck(checker, declaration, seen) {
466
+ if (seen.has(declaration) || ts.getDecorators(declaration)?.length || hasPrototypeMutation(declaration.getSourceFile())) return false;
467
+ seen.add(declaration);
468
+ 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;
469
+ for (const clause of declaration.heritageClauses ?? []) {
470
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
471
+ for (const parent of clause.types) {
472
+ const ancestor = canonicalSymbol(checker, parent.expression)?.declarations?.find(ts.isClassDeclaration);
473
+ if (ancestor === void 0 || !hasOrdinaryInstanceCheck(checker, ancestor, seen)) return false;
474
+ }
475
+ }
476
+ return true;
477
+ }
470
478
  var require_explicit_contract_implementation_default = createRule({
471
479
  name: "require-explicit-contract-implementation",
472
480
  documentation: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION,
@@ -475,7 +483,7 @@ var require_explicit_contract_implementation_default = createRule({
475
483
  docs: { description: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION.summary },
476
484
  schema: [],
477
485
  messages: {
478
- declareActualContract: "`{{implementation}}` is supplied as `{{contract}}` without explicitly implementing that contract."
486
+ declareActualContract: "`{{implementation}}` structurally matches `{{contract}}` but fails the rejecting instanceof guard; use nominal inheritance or make the boundary structural."
479
487
  }
480
488
  },
481
489
  defaultOptions: [],
@@ -486,25 +494,27 @@ var require_explicit_contract_implementation_default = createRule({
486
494
  const checker = services.program.getTypeChecker();
487
495
  function inspect(node) {
488
496
  const tsNode = services.esTreeNodeToTSNodeMap.get(node);
489
- if (!ts.isCallExpression(tsNode) && !ts.isNewExpression(tsNode)) return;
497
+ if (!ts.isNewExpression(tsNode)) return;
498
+ const consumer = canonicalSymbol(checker, tsNode.expression)?.declarations?.find(ts.isClassDeclaration);
499
+ if (consumer === void 0) return;
490
500
  const signature = checker.getResolvedSignature(tsNode);
491
- if (signature === void 0) return;
501
+ if (signature === void 0 || signature.getDeclaration()?.parent !== consumer) return;
492
502
  const parameters = signature.getParameters();
493
503
  for (const [index, argument] of node.arguments.entries()) {
494
- if (argument.type === import_utils2.AST_NODE_TYPES.SpreadElement || argument.type === import_utils2.AST_NODE_TYPES.TSAsExpression || argument.type === import_utils2.AST_NODE_TYPES.TSTypeAssertion || argument.type === import_utils2.AST_NODE_TYPES.TSSatisfiesExpression) continue;
504
+ if (argument.type !== import_utils2.AST_NODE_TYPES.NewExpression) continue;
495
505
  const parameter = parameters[index];
496
506
  if (parameter === void 0) continue;
497
507
  const parameterType = checker.getTypeOfSymbolAtLocation(parameter, tsNode);
498
508
  const contractType = behavioralContract(checker, parameterType);
499
509
  if (contractType === null) continue;
500
510
  const contractSymbol = contractType.getSymbol();
501
- if (contractSymbol === void 0) continue;
511
+ if (contractSymbol === void 0 || !hasNominalRejectionGuard(checker, signature, parameter, contractSymbol)) continue;
502
512
  const tsArgument = services.esTreeNodeToTSNodeMap.get(argument);
503
513
  const actualType = checker.getTypeAtLocation(tsArgument);
504
514
  if (!checker.isTypeAssignableTo(actualType, contractType)) continue;
505
- const implementation = ownedClass(actualType);
515
+ const implementation = ownedClass(checker, tsArgument);
506
516
  if (implementation === null) continue;
507
- const relationship = explicitlyImplements(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
517
+ const relationship = nominallyInherits(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
508
518
  if (relationship !== false) continue;
509
519
  context.report({
510
520
  node: argument,
@@ -514,7 +524,6 @@ var require_explicit_contract_implementation_default = createRule({
514
524
  }
515
525
  }
516
526
  return {
517
- CallExpression: inspect,
518
527
  NewExpression: inspect
519
528
  };
520
529
  }
@@ -632,7 +641,7 @@ var propertySignatureTypes = (members) => {
632
641
  };
633
642
  var fileTypeIndex = (program) => {
634
643
  const objects = /* @__PURE__ */ new Map();
635
- const functionAliases = /* @__PURE__ */ new Set();
644
+ const functionAliases = localFunctionAliases(program);
636
645
  function collectDeclaredType(statement) {
637
646
  const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
638
647
  if (declaration?.type === import_utils3.AST_NODE_TYPES.TSInterfaceDeclaration) {
@@ -641,10 +650,6 @@ var fileTypeIndex = (program) => {
641
650
  }
642
651
  if (declaration?.type !== import_utils3.AST_NODE_TYPES.TSTypeAliasDeclaration) return;
643
652
  const aliased = declaration.typeAnnotation;
644
- if (aliased.type === import_utils3.AST_NODE_TYPES.TSFunctionType || aliased.type === import_utils3.AST_NODE_TYPES.TSConstructorType) {
645
- functionAliases.add(declaration.id.name);
646
- return;
647
- }
648
653
  const literals = aliased.type === import_utils3.AST_NODE_TYPES.TSTypeLiteral ? [aliased] : aliased.type === import_utils3.AST_NODE_TYPES.TSIntersectionType ? aliased.types.filter((part) => part.type === import_utils3.AST_NODE_TYPES.TSTypeLiteral) : [];
649
654
  if (literals.length === 0) return;
650
655
  const merged = /* @__PURE__ */ new Map();
@@ -660,6 +665,28 @@ var fileTypeIndex = (program) => {
660
665
  }
661
666
  return { objects, functionAliases };
662
667
  };
668
+ function localFunctionAliases(program) {
669
+ const names = /* @__PURE__ */ new Set();
670
+ const parents = /* @__PURE__ */ new Map();
671
+ for (const statement of program.body) {
672
+ const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
673
+ if (declaration?.type !== import_utils3.AST_NODE_TYPES.TSTypeAliasDeclaration) continue;
674
+ const annotation = declaration.typeAnnotation;
675
+ if (annotation.type === import_utils3.AST_NODE_TYPES.TSFunctionType || annotation.type === import_utils3.AST_NODE_TYPES.TSConstructorType) names.add(declaration.id.name);
676
+ if (annotation.type === import_utils3.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils3.AST_NODE_TYPES.Identifier) parents.set(declaration.id.name, annotation.typeName.name);
677
+ }
678
+ for (let pass = 0; pass < parents.size; pass += 1) {
679
+ let changed = false;
680
+ for (const [name, parent] of parents) {
681
+ if (names.has(parent) && !names.has(name)) {
682
+ names.add(name);
683
+ changed = true;
684
+ }
685
+ }
686
+ if (!changed) break;
687
+ }
688
+ return names;
689
+ }
663
690
  var typeParameterNames = (...declarations) => {
664
691
  const names = /* @__PURE__ */ new Set();
665
692
  for (const declaration of declarations) {
@@ -671,12 +698,15 @@ var readConstructor = (ctor, declared, typeParameters) => {
671
698
  const body2 = ctor.value.body;
672
699
  const storedFieldsFrom = /* @__PURE__ */ new Map();
673
700
  const storedMemberFieldsFrom = /* @__PURE__ */ new Map();
701
+ const constructedFieldNames = /* @__PURE__ */ new Set();
702
+ const localBindings2 = /* @__PURE__ */ new Set();
674
703
  let constructedFields = 0;
675
704
  if (body2 !== null && body2 !== void 0) scanStoredFields(body2);
676
705
  function scanStoredFields(body3) {
677
706
  const pending = [...body3.body];
678
707
  function recordStoredField(current) {
679
708
  if (current.type === import_utils3.AST_NODE_TYPES.ArrowFunctionExpression || current.type === import_utils3.AST_NODE_TYPES.FunctionExpression || current.type === import_utils3.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils3.AST_NODE_TYPES.ClassExpression || current.type === import_utils3.AST_NODE_TYPES.ClassDeclaration) return;
709
+ if (current.type === import_utils3.AST_NODE_TYPES.VariableDeclarator && current.id.type === import_utils3.AST_NODE_TYPES.Identifier) localBindings2.add(current.id.name);
680
710
  const expression = current.type === import_utils3.AST_NODE_TYPES.ExpressionStatement ? current.expression : null;
681
711
  const storedField = expression?.type === import_utils3.AST_NODE_TYPES.AssignmentExpression && expression.left.type === import_utils3.AST_NODE_TYPES.MemberExpression && expression.left.object.type === import_utils3.AST_NODE_TYPES.ThisExpression ? staticMemberName(expression.left) : null;
682
712
  if (expression?.type !== import_utils3.AST_NODE_TYPES.AssignmentExpression || !STORAGE_ASSIGNMENT_OPERATORS.has(expression.operator) || expression.left.type !== import_utils3.AST_NODE_TYPES.MemberExpression || expression.left.object.type !== import_utils3.AST_NODE_TYPES.ThisExpression || storedField === null) {
@@ -687,6 +717,7 @@ var readConstructor = (ctor, declared, typeParameters) => {
687
717
  while (source.type === import_utils3.AST_NODE_TYPES.TSNonNullExpression || source.type === import_utils3.AST_NODE_TYPES.TSAsExpression || source.type === import_utils3.AST_NODE_TYPES.TSSatisfiesExpression || source.type === import_utils3.AST_NODE_TYPES.TSTypeAssertion) source = source.expression;
688
718
  if (source.type === import_utils3.AST_NODE_TYPES.NewExpression) {
689
719
  constructedFields += 1;
720
+ constructedFieldNames.add(storedField);
690
721
  } else if (source.type === import_utils3.AST_NODE_TYPES.Identifier) {
691
722
  const fields = storedFieldsFrom.get(source.name) ?? /* @__PURE__ */ new Set();
692
723
  fields.add(storedField);
@@ -719,15 +750,19 @@ var readConstructor = (ctor, declared, typeParameters) => {
719
750
  const collaborators = [];
720
751
  function collectCollaborators(parameter) {
721
752
  for (const reference of parameterCollaborators(parameter, declared, storedMemberFieldsFrom)) {
722
- const fields = parameter.type === import_utils3.AST_NODE_TYPES.TSParameterProperty ? [reference.name] : reference.fields.length > 0 ? reference.fields : [...storedFieldsFrom.get(reference.name) ?? []];
723
- if (fields.length === 0) continue;
753
+ if (localBindings2.has(reference.name)) continue;
754
+ let fields = reference.fields;
755
+ if (parameter.type === import_utils3.AST_NODE_TYPES.TSParameterProperty) fields = [reference.name];
756
+ else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
757
+ const injectedFields = fields.filter((field) => !constructedFieldNames.has(field));
758
+ if (injectedFields.length === 0) continue;
724
759
  if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
725
760
  if (CONFIGISH_NAME_RE.test(reference.name)) continue;
726
761
  if (typeParameters.has(reference.typeName)) continue;
727
762
  if (BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName)) continue;
728
763
  if (VALUE_TYPE_RE.test(reference.typeName)) continue;
729
764
  if (declared().functionAliases.has(reference.typeName)) continue;
730
- collaborators.push({ ...reference, fields });
765
+ collaborators.push({ ...reference, fields: injectedFields });
731
766
  }
732
767
  }
733
768
  for (const parameter of ctor.value.params) {
@@ -946,11 +981,7 @@ function declaredMemberName(member) {
946
981
  function localInterfaceSurfaces(program) {
947
982
  const interfaces = /* @__PURE__ */ new Map();
948
983
  const parents = /* @__PURE__ */ new Map();
949
- const functionAliases = /* @__PURE__ */ new Set();
950
- for (const statement of program.body) {
951
- const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
952
- if (declaration?.type === import_utils3.AST_NODE_TYPES.TSTypeAliasDeclaration && (declaration.typeAnnotation.type === import_utils3.AST_NODE_TYPES.TSFunctionType || declaration.typeAnnotation.type === import_utils3.AST_NODE_TYPES.TSConstructorType)) functionAliases.add(declaration.id.name);
953
- }
984
+ const functionAliases = localFunctionAliases(program);
954
985
  function collectInterfaceSurface(statement) {
955
986
  const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
956
987
  if (declaration?.type === import_utils3.AST_NODE_TYPES.TSTypeAliasDeclaration) {
@@ -962,6 +993,10 @@ function localInterfaceSurfaces(program) {
962
993
  inherited.push(part.typeName.name);
963
994
  continue;
964
995
  }
996
+ if (part.type === import_utils3.AST_NODE_TYPES.TSTypeReference) {
997
+ inherited.push("*");
998
+ continue;
999
+ }
965
1000
  if (part.type !== import_utils3.AST_NODE_TYPES.TSTypeLiteral) continue;
966
1001
  addCallableMembers(part.members, callables2);
967
1002
  }
@@ -1125,30 +1160,18 @@ function createServiceOperationResolver(program) {
1125
1160
  if (retained.size === 0) return [];
1126
1161
  const callers = /* @__PURE__ */ new Map();
1127
1162
  const active = /* @__PURE__ */ new Set();
1128
- for (const member of node.body.body) {
1129
- if (member.type !== import_utils3.AST_NODE_TYPES.MethodDefinition || member.kind !== "method" || member.value.body === null) continue;
1130
- const name = declaredMemberName(member);
1131
- if (name === null) continue;
1132
- const visit = (current) => {
1133
- if (current.type === import_utils3.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils3.AST_NODE_TYPES.FunctionExpression || current.type === import_utils3.AST_NODE_TYPES.ClassDeclaration || current.type === import_utils3.AST_NODE_TYPES.ClassExpression) return;
1134
- if (current.type === import_utils3.AST_NODE_TYPES.IfStatement && current.test.type === import_utils3.AST_NODE_TYPES.Literal && typeof current.test.value === "boolean") {
1135
- visit(current.test.value ? current.consequent : current.alternate ?? current.test);
1136
- return;
1137
- }
1138
- if (current.type === import_utils3.AST_NODE_TYPES.CallExpression) {
1139
- if (retained.has(invokedInstanceField(current) ?? "")) active.add(name);
1140
- if (current.callee.type === import_utils3.AST_NODE_TYPES.MemberExpression && current.callee.object.type === import_utils3.AST_NODE_TYPES.ThisExpression) {
1141
- const callee = staticMemberName(current.callee);
1142
- if (callee !== null) {
1143
- const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
1144
- linked.add(name);
1145
- callers.set(callee, linked);
1146
- }
1163
+ for (const [name, body2] of serviceOperationBodies(node)) {
1164
+ forEachServiceCall(body2, (call) => {
1165
+ if (retained.has(invokedInstanceField(call) ?? "")) active.add(name);
1166
+ if (call.callee.type === import_utils3.AST_NODE_TYPES.MemberExpression && call.callee.object.type === import_utils3.AST_NODE_TYPES.ThisExpression) {
1167
+ const callee = staticMemberName(call.callee);
1168
+ if (callee !== null) {
1169
+ const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
1170
+ linked.add(name);
1171
+ callers.set(callee, linked);
1147
1172
  }
1148
1173
  }
1149
- forEachOwnAstChild(current, visit);
1150
- };
1151
- for (const statement of member.value.body.body) visit(statement);
1174
+ });
1152
1175
  }
1153
1176
  const pending = [...active];
1154
1177
  while (pending.length > 0) {
@@ -1163,38 +1186,127 @@ function createServiceOperationResolver(program) {
1163
1186
  return publicMethodNames(node.body, declared().functionAliases).filter((name) => active.has(name));
1164
1187
  };
1165
1188
  }
1189
+ function serviceOperationBodies(node) {
1190
+ const bodies = /* @__PURE__ */ new Map();
1191
+ for (const member of node.body.body) {
1192
+ if (member.type !== import_utils3.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils3.AST_NODE_TYPES.PropertyDefinition) continue;
1193
+ if (member.static || member.decorators.length > 0) continue;
1194
+ if (member.type === import_utils3.AST_NODE_TYPES.MethodDefinition && member.kind !== "method") continue;
1195
+ if (member.value?.type !== import_utils3.AST_NODE_TYPES.FunctionExpression && member.value?.type !== import_utils3.AST_NODE_TYPES.ArrowFunctionExpression) continue;
1196
+ const name = declaredMemberName(member);
1197
+ if (name !== null && member.value.body !== null) bodies.set(name, member.value.body);
1198
+ }
1199
+ return bodies;
1200
+ }
1201
+ function forEachServiceCall(node, onCall) {
1202
+ if (node.type === import_utils3.AST_NODE_TYPES.FunctionDeclaration || node.type === import_utils3.AST_NODE_TYPES.FunctionExpression || node.type === import_utils3.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils3.AST_NODE_TYPES.ClassDeclaration || node.type === import_utils3.AST_NODE_TYPES.ClassExpression) return;
1203
+ if (node.type === import_utils3.AST_NODE_TYPES.BlockStatement) {
1204
+ for (const statement of node.body) {
1205
+ forEachServiceCall(statement, onCall);
1206
+ if (terminatesOperation(statement)) break;
1207
+ }
1208
+ return;
1209
+ }
1210
+ const branch = literalExecutionBranch(node);
1211
+ if (branch !== null) {
1212
+ forEachServiceCall(branch, onCall);
1213
+ return;
1214
+ }
1215
+ if (node.type === import_utils3.AST_NODE_TYPES.CallExpression) onCall(node);
1216
+ forEachOwnAstChild(node, (child) => {
1217
+ forEachServiceCall(child, onCall);
1218
+ });
1219
+ }
1220
+ function literalExecutionBranch(node) {
1221
+ if (node.type === import_utils3.AST_NODE_TYPES.WhileStatement && node.test.type === import_utils3.AST_NODE_TYPES.Literal && node.test.value === false) return node.test;
1222
+ if ((node.type === import_utils3.AST_NODE_TYPES.IfStatement || node.type === import_utils3.AST_NODE_TYPES.ConditionalExpression) && node.test.type === import_utils3.AST_NODE_TYPES.Literal && typeof node.test.value === "boolean") {
1223
+ return node.test.value ? node.consequent : node.alternate ?? node.test;
1224
+ }
1225
+ if (node.type !== import_utils3.AST_NODE_TYPES.LogicalExpression || node.left.type !== import_utils3.AST_NODE_TYPES.Literal || typeof node.left.value !== "boolean") return null;
1226
+ if (node.operator === "&&") return node.left.value ? node.right : node.left;
1227
+ if (node.operator === "||") return node.left.value ? node.left : node.right;
1228
+ return node.left;
1229
+ }
1230
+ function terminatesOperation(node) {
1231
+ if (node.type === import_utils3.AST_NODE_TYPES.ReturnStatement || node.type === import_utils3.AST_NODE_TYPES.ThrowStatement) return true;
1232
+ if (node.type === import_utils3.AST_NODE_TYPES.BlockStatement) return node.body.some(terminatesOperation);
1233
+ if (node.type !== import_utils3.AST_NODE_TYPES.IfStatement) return false;
1234
+ const branch = literalExecutionBranch(node);
1235
+ if (branch !== null) return terminatesOperation(branch);
1236
+ return node.alternate !== null && terminatesOperation(node.consequent) && terminatesOperation(node.alternate);
1237
+ }
1166
1238
  function hasDeclaredServiceContract(node, operations, program) {
1167
- const classes = localClassAbstractness(program);
1168
1239
  const interfaces = localInterfaceSurfaces(program);
1169
- if (node.implements.length > 0) return implementedSurfaceCovers(node, operations, classes, interfaces);
1170
- if (node.superClass === null) return false;
1171
- if (node.superClass.type !== import_utils3.AST_NODE_TYPES.Identifier) return null;
1172
- const baseName = node.superClass.name;
1173
- const isAbstract = classes.get(baseName);
1174
- if (isAbstract === void 0) return null;
1175
- if (!isAbstract) return false;
1176
- const base = program.body.flatMap((statement) => {
1177
- const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
1178
- return declaration?.type === import_utils3.AST_NODE_TYPES.ClassDeclaration && declaration.id?.name === baseName ? [declaration] : [];
1179
- })[0];
1180
- if (base === void 0) return null;
1181
- const abstractOperations = new Set(base.body.body.flatMap(
1182
- (member) => member.type === import_utils3.AST_NODE_TYPES.TSAbstractMethodDefinition && member.accessibility !== "private" && member.accessibility !== "protected" && !member.static ? [declaredMemberName(member)] : []
1183
- ));
1184
- return operations.every((name) => abstractOperations.has(name) || publicMethodNames(base.body, /* @__PURE__ */ new Set()).includes(name));
1240
+ const classes = localClassDeclarations(program);
1241
+ const functionAliases = fileTypeIndex(program).functionAliases;
1242
+ const covered2 = /* @__PURE__ */ new Set();
1243
+ const pending = [node];
1244
+ const seen = /* @__PURE__ */ new Set();
1245
+ function addImplementedContracts(current) {
1246
+ for (const implementation of current.implements) {
1247
+ if (implementation.expression.type !== import_utils3.AST_NODE_TYPES.Identifier) return false;
1248
+ const name = implementation.expression.name;
1249
+ const surface = interfaces.get(name);
1250
+ const base = classes.get(name);
1251
+ if (surface === void 0 && base === void 0) return false;
1252
+ if (surface?.has("*")) return false;
1253
+ for (const method of surface ?? []) covered2.add(method);
1254
+ if (base?.abstract) pending.push(base);
1255
+ }
1256
+ return true;
1257
+ }
1258
+ while (pending.length > 0) {
1259
+ const current = pending.pop();
1260
+ if (current === void 0 || seen.has(current)) continue;
1261
+ seen.add(current);
1262
+ if (current.decorators.length > 0) return null;
1263
+ if (current.abstract) {
1264
+ for (const method of abstractContractOperations(current, functionAliases)) covered2.add(method);
1265
+ }
1266
+ if (!addImplementedContracts(current)) return null;
1267
+ if (current.superClass === null) continue;
1268
+ if (current.superClass.type !== import_utils3.AST_NODE_TYPES.Identifier) return null;
1269
+ const base = classes.get(current.superClass.name);
1270
+ if (base === void 0) return null;
1271
+ pending.push(base);
1272
+ }
1273
+ return operations.every((name) => covered2.has(name));
1274
+ }
1275
+ function localClassDeclarations(program) {
1276
+ const classes = /* @__PURE__ */ new Map();
1277
+ for (const statement of program.body) {
1278
+ const declaration = statement.type === import_utils3.AST_NODE_TYPES.ExportNamedDeclaration || statement.type === import_utils3.AST_NODE_TYPES.ExportDefaultDeclaration ? statement.declaration : statement;
1279
+ if (declaration?.type === import_utils3.AST_NODE_TYPES.ClassDeclaration && declaration.id !== null) classes.set(declaration.id.name, declaration);
1280
+ }
1281
+ return classes;
1282
+ }
1283
+ function abstractContractOperations(node, functionAliases) {
1284
+ const methods = new Set(publicMethodNames(node.body, functionAliases));
1285
+ for (const member of node.body.body) {
1286
+ if (member.type !== import_utils3.AST_NODE_TYPES.TSAbstractMethodDefinition && member.type !== import_utils3.AST_NODE_TYPES.TSAbstractPropertyDefinition) continue;
1287
+ if (member.static || member.accessibility === "private" || member.accessibility === "protected") continue;
1288
+ if (member.type === import_utils3.AST_NODE_TYPES.TSAbstractPropertyDefinition) {
1289
+ const annotation = member.typeAnnotation?.typeAnnotation;
1290
+ if (annotation?.type !== import_utils3.AST_NODE_TYPES.TSFunctionType && !(annotation?.type === import_utils3.AST_NODE_TYPES.TSTypeReference && annotation.typeName.type === import_utils3.AST_NODE_TYPES.Identifier && functionAliases.has(annotation.typeName.name))) continue;
1291
+ }
1292
+ const name = declaredMemberName(member);
1293
+ if (name !== null) methods.add(name);
1294
+ }
1295
+ return methods;
1185
1296
  }
1186
1297
 
1187
1298
  // src/rules/require-explicit-service-contract.ts
1188
1299
  var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1189
1300
  defaultLevel: "warning",
1190
1301
  summary: "Require explicit contracts for classes that invoke retained collaborators.",
1191
- rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that its callers and fakes must implement.",
1192
- remediation: "Export a focused interface or abstract class and explicitly implement or extend it.",
1302
+ rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that callers consume and implementations and fakes satisfy.",
1303
+ remediation: "Explicitly implement a focused interface or extend an abstract class covering the service operations. Export the contract only when its consumers need it.",
1193
1304
  category: "architecture",
1194
1305
  autofix: "none",
1195
1306
  limitations: [
1196
1307
  "The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.",
1197
- "Imported or dynamic base classes with unavailable declarations are unresolved and do not produce a finding.",
1308
+ "Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.",
1309
+ "Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.",
1198
1310
  "Generated files and JavaScript without implements syntax are excluded."
1199
1311
  ],
1200
1312
  examples: [
@@ -1216,7 +1328,7 @@ var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1216
1328
  outcome: "no-match",
1217
1329
  files: [{
1218
1330
  path: "src/service.ts",
1219
- source: "export interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"
1331
+ source: "interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"
1220
1332
  }],
1221
1333
  focusPath: "src/service.ts",
1222
1334
  expectedCount: 0,
@@ -1232,7 +1344,7 @@ var require_explicit_service_contract_default = createRule({
1232
1344
  docs: { description: REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION.summary },
1233
1345
  schema: [],
1234
1346
  messages: {
1235
- requireExplicitServiceContract: "`{{name}}` invokes a retained collaborator from {{operations}} without explicitly implementing a public contract for those operations."
1347
+ requireExplicitServiceContract: "`{{name}}` invokes a retained collaborator from {{operations}} without a declared interface or abstract-class contract covering those operations. Implement a focused contract; it can remain local."
1236
1348
  }
1237
1349
  },
1238
1350
  defaultOptions: [],
@@ -1243,7 +1355,7 @@ var require_explicit_service_contract_default = createRule({
1243
1355
  const isExportedServiceClass = createExportedServiceClassResolver(program);
1244
1356
  return {
1245
1357
  ClassDeclaration(node) {
1246
- if (node.id === null || node.abstract || !isServiceRole(node.id.name, context.filename)) return;
1358
+ if (node.id === null || node.abstract || node.decorators.length > 0 || !isServiceRole(node.id.name, context.filename)) return;
1247
1359
  if (isExportedServiceClass(node) && !isTestFile(context.filename) && !isStoryFile(context.filename) && !isScriptFile(context.filename)) return;
1248
1360
  const operations = operationsFor(node);
1249
1361
  if (operations.length === 0 || hasDeclaredServiceContract(node, operations, program) !== false) return;
@@ -7058,7 +7170,7 @@ var RESHAPING_METHODS = /* @__PURE__ */ new Set([
7058
7170
  function importedName2(specifier) {
7059
7171
  return specifier.imported.type === import_utils40.AST_NODE_TYPES.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
7060
7172
  }
7061
- function memberName(node) {
7173
+ function memberName2(node) {
7062
7174
  if (!node.computed && node.property.type === import_utils40.AST_NODE_TYPES.Identifier) {
7063
7175
  return node.property.name;
7064
7176
  }
@@ -7143,7 +7255,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7143
7255
  if (callee.type !== import_utils40.AST_NODE_TYPES.MemberExpression || callee.object.type !== import_utils40.AST_NODE_TYPES.CallExpression) {
7144
7256
  return null;
7145
7257
  }
7146
- const method = memberName(callee);
7258
+ const method = memberName2(callee);
7147
7259
  if (method === null) return null;
7148
7260
  calls.push({ method, node: current });
7149
7261
  current = callee.object;
@@ -7157,7 +7269,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7157
7269
  if (callee.type !== import_utils40.AST_NODE_TYPES.MemberExpression || callee.object.type !== import_utils40.AST_NODE_TYPES.Identifier || !namespaces.has(callee.object.name) || !resolvesToTrackedImport(callee.object)) {
7158
7270
  return null;
7159
7271
  }
7160
- const name = memberName(callee);
7272
+ const name = memberName2(callee);
7161
7273
  return name !== null && KINDS.has(name) ? name : null;
7162
7274
  }
7163
7275
  function isInsideReshapingCall(node) {
@@ -7166,7 +7278,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7166
7278
  while (parent !== void 0 && parent.type !== import_utils40.AST_NODE_TYPES.Program) {
7167
7279
  if (parent.type === import_utils40.AST_NODE_TYPES.CallExpression) {
7168
7280
  const callee = parent.callee;
7169
- if (callee.type === import_utils40.AST_NODE_TYPES.MemberExpression && RESHAPING_METHODS.has(memberName(callee) ?? "") && (callee.object.type === import_utils40.AST_NODE_TYPES.CallExpression || callee.object.type === import_utils40.AST_NODE_TYPES.Identifier && namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object))) {
7281
+ if (callee.type === import_utils40.AST_NODE_TYPES.MemberExpression && RESHAPING_METHODS.has(memberName2(callee) ?? "") && (callee.object.type === import_utils40.AST_NODE_TYPES.CallExpression || callee.object.type === import_utils40.AST_NODE_TYPES.Identifier && namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object))) {
7170
7282
  return true;
7171
7283
  }
7172
7284
  if (callee.type === import_utils40.AST_NODE_TYPES.Identifier && preprocessors.has(callee.name) && resolvesToTrackedImport(callee)) {
@@ -7428,7 +7540,7 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7428
7540
  function importedName3(specifier) {
7429
7541
  return specifier.imported.type === import_utils42.AST_NODE_TYPES.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
7430
7542
  }
7431
- function memberName2(node) {
7543
+ function memberName3(node) {
7432
7544
  if (!node.computed && node.property.type === import_utils42.AST_NODE_TYPES.Identifier) {
7433
7545
  return node.property.name;
7434
7546
  }
@@ -7480,7 +7592,7 @@ var no_redundant_optional_array_default_default = createRule({
7480
7592
  return arrayConstructors.has(callee.name) && resolvesToTrackedImport(callee);
7481
7593
  }
7482
7594
  if (callee.type !== import_utils42.AST_NODE_TYPES.MemberExpression) return false;
7483
- const method = memberName2(callee);
7595
+ const method = memberName3(callee);
7484
7596
  if (method === "array" && (callee.object.type === import_utils42.AST_NODE_TYPES.Identifier ? namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object) : isZodSchemaExpression(callee.object))) {
7485
7597
  return true;
7486
7598
  }
@@ -7521,12 +7633,12 @@ var no_redundant_optional_array_default_default = createRule({
7521
7633
  },
7522
7634
  CallExpression(node) {
7523
7635
  const defaultCallee = node.callee;
7524
- if (defaultCallee.type !== import_utils42.AST_NODE_TYPES.MemberExpression || memberName2(defaultCallee) !== "default" || defaultCallee.object.type !== import_utils42.AST_NODE_TYPES.CallExpression) {
7636
+ if (defaultCallee.type !== import_utils42.AST_NODE_TYPES.MemberExpression || memberName3(defaultCallee) !== "default" || defaultCallee.object.type !== import_utils42.AST_NODE_TYPES.CallExpression) {
7525
7637
  return;
7526
7638
  }
7527
7639
  const optionalCall = defaultCallee.object;
7528
7640
  const optionalCallee = optionalCall.callee;
7529
- if (optionalCallee.type !== import_utils42.AST_NODE_TYPES.MemberExpression || memberName2(optionalCallee) !== "optional" || optionalCall.arguments.length !== 0 || !isArraySchemaExpression(optionalCallee.object) || sourceCode.getCommentsInside(optionalCall).length > 0) {
7641
+ if (optionalCallee.type !== import_utils42.AST_NODE_TYPES.MemberExpression || memberName3(optionalCallee) !== "optional" || optionalCall.arguments.length !== 0 || !isArraySchemaExpression(optionalCallee.object) || sourceCode.getCommentsInside(optionalCall).length > 0) {
7530
7642
  return;
7531
7643
  }
7532
7644
  context.report({
@@ -7545,12 +7657,12 @@ var ts8 = __toESM(require("typescript"), 1);
7545
7657
 
7546
7658
  // src/rules/_class-private.ts
7547
7659
  var import_utils43 = require("@typescript-eslint/utils");
7548
- function memberName3(member) {
7660
+ function memberName4(member) {
7549
7661
  return !member.computed && member.key.type === import_utils43.AST_NODE_TYPES.Identifier ? member.key.name : null;
7550
7662
  }
7551
7663
  function convertibleMemberName(member) {
7552
7664
  if (member.type !== import_utils43.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils43.AST_NODE_TYPES.PropertyDefinition && member.type !== import_utils43.AST_NODE_TYPES.AccessorProperty) return null;
7553
- return memberName3(member);
7665
+ return memberName4(member);
7554
7666
  }
7555
7667
 
7556
7668
  // src/rules/interface-contract-members-private.ts
@@ -8152,7 +8264,7 @@ var LOWER_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
8152
8264
  function propertyName2(node) {
8153
8265
  return node.type === import_utils48.AST_NODE_TYPES.Identifier ? node.name : null;
8154
8266
  }
8155
- function memberName4(node) {
8267
+ function memberName5(node) {
8156
8268
  let current = node;
8157
8269
  while (current.type === import_utils48.AST_NODE_TYPES.TSAsExpression || current.type === import_utils48.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils48.AST_NODE_TYPES.TSTypeAssertion) {
8158
8270
  current = current.expression;
@@ -8216,7 +8328,7 @@ var no_bespoke_api_case_conversion_default = createRule({
8216
8328
  Property(node) {
8217
8329
  if (node.computed || node.method || node.shorthand) return;
8218
8330
  const key = propertyName2(node.key);
8219
- const value = memberName4(node.value);
8331
+ const value = memberName5(node.value);
8220
8332
  if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
8221
8333
  if (!hasApiReceiver(node.value)) return;
8222
8334
  const wireName = SNAKE_CASE_RE2.test(key) ? key : value;
@@ -8989,9 +9101,9 @@ var no_positional_tuple_return_default = createRule({
8989
9101
  TSMethodSignature(node) {
8990
9102
  const owner = owningInterface(node);
8991
9103
  const alias = owningTypeAlias(node);
8992
- const memberName8 = staticMemberName4(node.key);
8993
- if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 || memberName8 === null) return;
8994
- report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName8}`);
9104
+ const memberName9 = staticMemberName4(node.key);
9105
+ if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 || memberName9 === null) return;
9106
+ report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
8995
9107
  },
8996
9108
  TSTypeAliasDeclaration(node) {
8997
9109
  if (!typeExports.has(node.id.name)) return;
@@ -9002,11 +9114,11 @@ var no_positional_tuple_return_default = createRule({
9002
9114
  const owner = owningInterface(node);
9003
9115
  const alias = owningTypeAlias(node);
9004
9116
  const annotation = node.typeAnnotation?.typeAnnotation;
9005
- const memberName8 = staticMemberName4(node.key);
9006
- if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || memberName8 === null || annotation === void 0) return;
9117
+ const memberName9 = staticMemberName4(node.key);
9118
+ if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || memberName9 === null || annotation === void 0) return;
9007
9119
  const returnType = callableReturnType(annotation, aliases);
9008
9120
  if (returnType !== null) {
9009
- report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName8}`);
9121
+ report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
9010
9122
  }
9011
9123
  },
9012
9124
  PropertyDefinition(node) {
@@ -14728,7 +14840,8 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
14728
14840
  remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
14729
14841
  category: "correctness",
14730
14842
  limitations: [
14731
- "Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven."
14843
+ "Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven.",
14844
+ "Local type aliases are resolved before recognizing standard readonly types; imported or otherwise unknown type declarations are not inferred from their names. Readonly types have no runtime enforcement."
14732
14845
  ],
14733
14846
  examples: [
14734
14847
  {
@@ -14748,6 +14861,26 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
14748
14861
  focusPath: "src/constants.ts",
14749
14862
  expectedCount: 1,
14750
14863
  public: true
14864
+ },
14865
+ {
14866
+ id: "shadowed-readonly-alias",
14867
+ scenarioId: "type-binding",
14868
+ title: "A type named Readonly still exposes mutable properties",
14869
+ outcome: "match",
14870
+ files: [{ path: "src/constants.ts", source: "type Readonly = { voice: boolean };\nexport const TOOL_SUPPORT: Readonly = { voice: true };" }],
14871
+ focusPath: "src/constants.ts",
14872
+ expectedCount: 1,
14873
+ public: true
14874
+ },
14875
+ {
14876
+ id: "resolved-readonly-alias",
14877
+ scenarioId: "type-binding",
14878
+ title: "A local alias resolves to readonly properties",
14879
+ outcome: "no-match",
14880
+ files: [{ path: "src/constants.ts", source: "type ToolSupport = Readonly<{ voice: boolean }>;\nexport const TOOL_SUPPORT: ToolSupport = { voice: true };" }],
14881
+ focusPath: "src/constants.ts",
14882
+ expectedCount: 0,
14883
+ public: true
14751
14884
  }
14752
14885
  ]
14753
14886
  };
@@ -14802,39 +14935,84 @@ function collectionKind(node, isUnshadowedGlobal3) {
14802
14935
  }
14803
14936
  return null;
14804
14937
  }
14805
- function declaredReadonlyType(node, kind, aliases) {
14938
+ function declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) {
14806
14939
  const annotation = node.id.type === import_utils89.AST_NODE_TYPES.Identifier ? node.id.typeAnnotation : void 0;
14807
- if (annotation !== void 0 && isReadonlyTypeResolved(annotation.typeAnnotation, kind, aliases)) {
14940
+ if (annotation !== void 0 && readonlyTypeState(annotation.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly") {
14808
14941
  return true;
14809
14942
  }
14810
- return node.init?.type === import_utils89.AST_NODE_TYPES.TSAsExpression && isReadonlyTypeResolved(node.init.typeAnnotation, kind, aliases);
14943
+ return node.init?.type === import_utils89.AST_NODE_TYPES.TSAsExpression && readonlyTypeState(node.init.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly";
14811
14944
  }
14812
- function isReadonlyTypeResolved(node, kind, aliases, seen = /* @__PURE__ */ new Set()) {
14813
- if (isReadonlyType(node, kind)) return true;
14814
- if (node.type !== import_utils89.AST_NODE_TYPES.TSTypeReference || node.typeName.type !== import_utils89.AST_NODE_TYPES.Identifier) return false;
14815
- const name = node.typeName.name;
14816
- const target = aliases.get(name);
14817
- if (target === void 0 || seen.has(name)) return false;
14818
- return isReadonlyTypeResolved(target, kind, aliases, /* @__PURE__ */ new Set([...seen, name]));
14945
+ function readonlyTypeState(node, kind, resolveLocalType, isUnshadowedType, arguments_ = /* @__PURE__ */ new Map(), seen = /* @__PURE__ */ new Set()) {
14946
+ if (seen.has(node)) return "unknown";
14947
+ if (node.type !== import_utils89.AST_NODE_TYPES.TSTypeReference || node.typeName.type !== import_utils89.AST_NODE_TYPES.Identifier) {
14948
+ return directReadonlyState(node, kind, isUnshadowedType);
14949
+ }
14950
+ const declaration = resolveLocalType(node.typeName);
14951
+ if (declaration === void 0) return directReadonlyState(node, kind, isUnshadowedType);
14952
+ if (declaration.type === import_utils89.AST_NODE_TYPES.TSTypeParameter) {
14953
+ const argument = arguments_.get(declaration);
14954
+ return argument === void 0 ? "unknown" : readonlyTypeState(
14955
+ argument.node,
14956
+ kind,
14957
+ resolveLocalType,
14958
+ isUnshadowedType,
14959
+ argument.arguments,
14960
+ argument.seen
14961
+ );
14962
+ }
14963
+ const expanded = /* @__PURE__ */ new Set([...seen, node]);
14964
+ return readonlyTypeState(
14965
+ declaration.typeAnnotation,
14966
+ kind,
14967
+ resolveLocalType,
14968
+ isUnshadowedType,
14969
+ bindTypeArguments(declaration, node, arguments_, expanded),
14970
+ expanded
14971
+ );
14972
+ }
14973
+ function bindTypeArguments(declaration, reference, arguments_, seen) {
14974
+ const bindings = new Map(arguments_);
14975
+ for (const [index, parameter] of (declaration.typeParameters?.params ?? []).entries()) {
14976
+ const argument = reference.typeArguments?.params[index];
14977
+ const node = argument ?? parameter.default;
14978
+ if (node !== void 0) {
14979
+ bindings.set(parameter, { node, arguments: argument === void 0 ? new Map(bindings) : arguments_, seen });
14980
+ }
14981
+ }
14982
+ return bindings;
14819
14983
  }
14820
- function isReadonlyType(node, kind) {
14984
+ function directReadonlyState(node, kind, isUnshadowedType) {
14821
14985
  if (node.type === import_utils89.AST_NODE_TYPES.TSTypeOperator && node.operator === "readonly") {
14822
- return true;
14986
+ return "readonly";
14823
14987
  }
14824
- if (node.type !== import_utils89.AST_NODE_TYPES.TSTypeReference || node.typeName.type !== import_utils89.AST_NODE_TYPES.Identifier) {
14825
- return false;
14988
+ if (node.type === import_utils89.AST_NODE_TYPES.TSTypeLiteral) {
14989
+ if (node.members.length === 0) return "unknown";
14990
+ const readonly2 = kind === "literal" && node.members.length > 0 && node.members.every(
14991
+ (member) => (member.type === import_utils89.AST_NODE_TYPES.TSPropertySignature || member.type === import_utils89.AST_NODE_TYPES.TSIndexSignature) && member.readonly
14992
+ );
14993
+ return readonly2 ? "readonly" : "mutable";
14994
+ }
14995
+ if (node.type === import_utils89.AST_NODE_TYPES.TSArrayType || node.type === import_utils89.AST_NODE_TYPES.TSTupleType) return "mutable";
14996
+ if (node.type !== import_utils89.AST_NODE_TYPES.TSTypeReference || node.typeName.type !== import_utils89.AST_NODE_TYPES.Identifier || !isUnshadowedType(node.typeName)) {
14997
+ return "unknown";
14826
14998
  }
14827
14999
  if (node.typeName.name === "Readonly") {
14828
- return kind === "literal";
15000
+ return kind === "literal" ? "readonly" : "mutable";
14829
15001
  }
14830
- return kind === "literal" ? node.typeName.name === "ReadonlyArray" : node.typeName.name === `Readonly${kind}`;
15002
+ if (["Array", "Map", "Set"].includes(node.typeName.name)) return "mutable";
15003
+ if (!["ReadonlyArray", "ReadonlyMap", "ReadonlySet"].includes(node.typeName.name)) return "unknown";
15004
+ const readonly = kind === "literal" ? node.typeName.name === "ReadonlyArray" : node.typeName.name === `Readonly${kind}`;
15005
+ return readonly ? "readonly" : "mutable";
14831
15006
  }
14832
- function hasUnknownExplicitType(node, aliases) {
14833
- const annotation = node.id.type === import_utils89.AST_NODE_TYPES.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0;
15007
+ function hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType) {
15008
+ const annotation = (node.id.type === import_utils89.AST_NODE_TYPES.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0) ?? (node.init?.type === import_utils89.AST_NODE_TYPES.TSAsExpression ? node.init.typeAnnotation : void 0);
14834
15009
  if (annotation === void 0) return false;
14835
15010
  if (annotation.type === import_utils89.AST_NODE_TYPES.TSArrayType || annotation.type === import_utils89.AST_NODE_TYPES.TSTypeOperator) return false;
14836
15011
  if (annotation.type !== import_utils89.AST_NODE_TYPES.TSTypeReference || annotation.typeName.type !== import_utils89.AST_NODE_TYPES.Identifier) return true;
14837
- return !aliases.has(annotation.typeName.name) && !["Array", "Map", "Readonly", "ReadonlyArray", "ReadonlyMap", "ReadonlySet", "Set"].includes(annotation.typeName.name);
15012
+ if (resolveLocalType(annotation.typeName) !== void 0) {
15013
+ return readonlyTypeState(annotation, kind, resolveLocalType, isUnshadowedType) === "unknown";
15014
+ }
15015
+ return resolveLocalType(annotation.typeName) === void 0 && (!isUnshadowedType(annotation.typeName) || !["Array", "Map", "Readonly", "ReadonlyArray", "ReadonlyMap", "ReadonlySet", "Set"].includes(annotation.typeName.name));
14838
15016
  }
14839
15017
  function referenceMutates(identifier, isUnshadowedGlobal3) {
14840
15018
  let member = identifier.parent;
@@ -14877,11 +15055,20 @@ var prefer_immutable_module_constant_default = createRule({
14877
15055
  const variable = import_utils89.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
14878
15056
  return variable === null || variable.defs.length === 0;
14879
15057
  };
15058
+ const isUnshadowedType = (identifier) => {
15059
+ const variable = import_utils89.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
15060
+ return variable === null || !variable.defs.some((definition) => definition.isTypeDefinition);
15061
+ };
15062
+ const resolveLocalType = (identifier) => {
15063
+ const variable = import_utils89.ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
15064
+ const definitions = variable?.defs.filter((definition) => definition.isTypeDefinition) ?? [];
15065
+ const declaration = definitions.length === 1 ? definitions[0]?.node : void 0;
15066
+ return declaration?.type === import_utils89.AST_NODE_TYPES.TSTypeAliasDeclaration || declaration?.type === import_utils89.AST_NODE_TYPES.TSTypeParameter ? declaration : void 0;
15067
+ };
14880
15068
  if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
14881
15069
  return {};
14882
15070
  }
14883
15071
  const exportedNames2 = /* @__PURE__ */ new Set();
14884
- const typeAliases2 = /* @__PURE__ */ new Map();
14885
15072
  const mutatesThroughAlias = (root) => {
14886
15073
  const pending = [root];
14887
15074
  const seen = /* @__PURE__ */ new Set();
@@ -14906,10 +15093,6 @@ var prefer_immutable_module_constant_default = createRule({
14906
15093
  return {
14907
15094
  Program(node) {
14908
15095
  function collectModuleBindings(statement) {
14909
- const declaration = statement.type === import_utils89.AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
14910
- if (declaration?.type === import_utils89.AST_NODE_TYPES.TSTypeAliasDeclaration) {
14911
- typeAliases2.set(declaration.id.name, declaration.typeAnnotation);
14912
- }
14913
15096
  if (statement.type === import_utils89.AST_NODE_TYPES.ExportNamedDeclaration) {
14914
15097
  if (statement.source !== null || statement.exportKind === "type") return;
14915
15098
  for (const specifier of statement.specifiers) {
@@ -14942,7 +15125,7 @@ var prefer_immutable_module_constant_default = createRule({
14942
15125
  return;
14943
15126
  }
14944
15127
  const kind = collectionKind(node.init, isUnshadowedGlobal3);
14945
- if (kind === null || declaredReadonlyType(node, kind, typeAliases2) || hasUnknownExplicitType(node, typeAliases2)) {
15128
+ if (kind === null || declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) || hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType)) {
14946
15129
  return;
14947
15130
  }
14948
15131
  const variable = sourceCode.getDeclaredVariables(node)[0];
@@ -16974,7 +17157,7 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
16974
17157
  { id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
16975
17158
  ]
16976
17159
  };
16977
- function memberName5(node) {
17160
+ function memberName6(node) {
16978
17161
  if (!node.computed && node.property.type === import_utils99.AST_NODE_TYPES.Identifier) return node.property.name;
16979
17162
  if (node.computed && node.property.type === import_utils99.AST_NODE_TYPES.Literal && typeof node.property.value === "string") {
16980
17163
  return node.property.value;
@@ -16990,7 +17173,7 @@ function isCryptoLoader(node, resolve2) {
16990
17173
  if (node.callee.type === import_utils99.AST_NODE_TYPES.Identifier) {
16991
17174
  return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
16992
17175
  }
16993
- return node.callee.type === import_utils99.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils99.AST_NODE_TYPES.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName5(node.callee) === "getBuiltinModule";
17176
+ return node.callee.type === import_utils99.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils99.AST_NODE_TYPES.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName6(node.callee) === "getBuiltinModule";
16994
17177
  }
16995
17178
  function isCryptoSpecifier(node) {
16996
17179
  return node.type === import_utils99.AST_NODE_TYPES.Literal && (node.value === "crypto" || node.value === "node:crypto");
@@ -17072,14 +17255,14 @@ function importedName6(node) {
17072
17255
  return node.type === import_utils99.AST_NODE_TYPES.Identifier ? node.name : node.value;
17073
17256
  }
17074
17257
  function isMemberCall(node, name) {
17075
- return node.callee.type === import_utils99.AST_NODE_TYPES.MemberExpression && memberName5(node.callee) === name;
17258
+ return node.callee.type === import_utils99.AST_NODE_TYPES.MemberExpression && memberName6(node.callee) === name;
17076
17259
  }
17077
17260
  function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
17078
17261
  if (node.callee.type === import_utils99.AST_NODE_TYPES.Identifier) {
17079
17262
  const variable2 = resolve2(node.callee);
17080
17263
  return variable2 !== null && directBindings.has(variable2);
17081
17264
  }
17082
- if (node.callee.type !== import_utils99.AST_NODE_TYPES.MemberExpression || memberName5(node.callee) !== "createHash") {
17265
+ if (node.callee.type !== import_utils99.AST_NODE_TYPES.MemberExpression || memberName6(node.callee) !== "createHash") {
17083
17266
  return false;
17084
17267
  }
17085
17268
  if (isCryptoLoader(node.callee.object, resolve2)) return true;
@@ -17107,7 +17290,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
17107
17290
  { id: "sync-read", title: "Do not block on a synchronous read", outcome: "match", files: [{ path: "src/store.ts", source: "import { readFileSync } from 'node:fs'; export function load(path: string) { return readFileSync(path, 'utf8'); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
17108
17291
  ]
17109
17292
  };
17110
- function memberName6(node) {
17293
+ function memberName7(node) {
17111
17294
  if (!node.computed && node.property.type === import_utils100.AST_NODE_TYPES.Identifier) return node.property.name;
17112
17295
  if (node.computed && node.property.type === import_utils100.AST_NODE_TYPES.Literal && typeof node.property.value === "string") {
17113
17296
  return node.property.value;
@@ -17124,7 +17307,7 @@ function isFsLoader(node, isGlobal) {
17124
17307
  const [argument] = expression.arguments;
17125
17308
  if (argument === void 0 || argument.type === import_utils100.AST_NODE_TYPES.SpreadElement || !isFsSpecifier(argument)) return false;
17126
17309
  if (expression.callee.type === import_utils100.AST_NODE_TYPES.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
17127
- return expression.callee.type === import_utils100.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils100.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName6(expression.callee) === "getBuiltinModule";
17310
+ return expression.callee.type === import_utils100.AST_NODE_TYPES.MemberExpression && expression.callee.object.type === import_utils100.AST_NODE_TYPES.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName7(expression.callee) === "getBuiltinModule";
17128
17311
  }
17129
17312
  function isFsSpecifier(node) {
17130
17313
  return node.type === import_utils100.AST_NODE_TYPES.Literal && (node.value === "node:fs" || node.value === "fs");
@@ -17205,7 +17388,7 @@ var prefer_node_fs_promises_default = createRule({
17205
17388
  }
17206
17389
  },
17207
17390
  MemberExpression(node) {
17208
- const name = memberName6(node);
17391
+ const name = memberName7(node);
17209
17392
  if (name?.endsWith("Sync") !== true) return;
17210
17393
  const object = unwrapAwait2(node.object);
17211
17394
  if (object.type === import_utils100.AST_NODE_TYPES.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
@@ -21390,7 +21573,7 @@ var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
21390
21573
  }
21391
21574
  ]
21392
21575
  };
21393
- function memberName7(node) {
21576
+ function memberName8(node) {
21394
21577
  if (!node.computed && node.property.type === import_utils116.AST_NODE_TYPES.Identifier)
21395
21578
  return node.property.name;
21396
21579
  if (node.computed && node.property.type === import_utils116.AST_NODE_TYPES.Literal && typeof node.property.value === "string")
@@ -21408,7 +21591,7 @@ function databaseReceiver(node) {
21408
21591
  if (node.type === import_utils116.AST_NODE_TYPES.Identifier)
21409
21592
  return DATABASE_NAMES.test(node.name);
21410
21593
  if (node.type !== import_utils116.AST_NODE_TYPES.MemberExpression) return false;
21411
- const name = memberName7(node);
21594
+ const name = memberName8(node);
21412
21595
  return name === "DB" || name !== null && DATABASE_NAMES.test(name);
21413
21596
  }
21414
21597
  function databaseRootMember(node) {
@@ -21429,7 +21612,7 @@ function databaseRootMember(node) {
21429
21612
  }
21430
21613
  if (current.type === import_utils116.AST_NODE_TYPES.MemberExpression) {
21431
21614
  if (current.object.type === import_utils116.AST_NODE_TYPES.ThisExpression)
21432
- return memberName7(current);
21615
+ return memberName8(current);
21433
21616
  current = current.object;
21434
21617
  continue;
21435
21618
  }
@@ -21453,7 +21636,7 @@ function expressionHasDatabaseMarker(node) {
21453
21636
  continue;
21454
21637
  }
21455
21638
  if (current.type === import_utils116.AST_NODE_TYPES.MemberExpression) {
21456
- const name = memberName7(current);
21639
+ const name = memberName8(current);
21457
21640
  if (name === "DB" || name !== null && DATABASE_NAMES.test(name))
21458
21641
  return true;
21459
21642
  current = current.object;
@@ -21475,7 +21658,7 @@ function expressionCallsBuilder(node) {
21475
21658
  }
21476
21659
  if (current.type === import_utils116.AST_NODE_TYPES.CallExpression) {
21477
21660
  if (current.callee.type !== import_utils116.AST_NODE_TYPES.MemberExpression) return false;
21478
- const name = memberName7(current.callee);
21661
+ const name = memberName8(current.callee);
21479
21662
  if (name !== null && BUILDER_METHODS.has(name)) return true;
21480
21663
  current = current.callee.object;
21481
21664
  continue;
@@ -21526,7 +21709,7 @@ function thisRootMember(node) {
21526
21709
  let current = node;
21527
21710
  let root = null;
21528
21711
  while (current.type === import_utils116.AST_NODE_TYPES.MemberExpression) {
21529
- const name = memberName7(current);
21712
+ const name = memberName8(current);
21530
21713
  if (name === null) return null;
21531
21714
  root = name;
21532
21715
  current = current.object;
@@ -21580,7 +21763,7 @@ var require_sql_access_class_default = createRule({
21580
21763
  CallExpression(node) {
21581
21764
  if (node.callee.type !== import_utils116.AST_NODE_TYPES.MemberExpression)
21582
21765
  return;
21583
- const method = memberName7(node.callee);
21766
+ const method = memberName8(node.callee);
21584
21767
  if (knownNonDatabase(node.callee.object)) return;
21585
21768
  if (method === null || !isDatabaseOperation(method, node.callee.object))
21586
21769
  return;
@@ -23210,10 +23393,11 @@ var import_node_fs2 = require("fs");
23210
23393
  var import_node_path2 = require("path");
23211
23394
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
23212
23395
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
23396
+ var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
23213
23397
  var NEXT_ROUTE = /^(?:src\/)?app\/(?:.*\/)?route\.[jt]s$/u;
23214
23398
  function isFrameworkEntrypoint(filename, exportKey) {
23215
23399
  const normalized = filename.replaceAll("\\", "/");
23216
- if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy)\.[jt]s$/u.test(normalized)) return false;
23400
+ if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
23217
23401
  const owner = packageOwner(normalized);
23218
23402
  if (owner === null) return false;
23219
23403
  const path = (0, import_node_path2.relative)(owner.root, normalized).replaceAll("\\", "/");
@@ -23222,9 +23406,12 @@ function isFrameworkEntrypoint(filename, exportKey) {
23222
23406
  }
23223
23407
  function isNextEntrypoint(path, exportKey) {
23224
23408
  const privateAppFolder = /^(?:src\/)?app\/(?:.*\/)?_[^/]+\//u.test(path);
23225
- if (!privateAppFolder && NEXT_COMPONENT.test(path) && exportKey === "default") return true;
23409
+ if (!privateAppFolder && (NEXT_COMPONENT.test(path) || NEXT_METADATA.test(path)) && exportKey === "default") return true;
23226
23410
  if (!privateAppFolder && NEXT_ROUTE.test(path) && HTTP_METHODS.has(exportKey)) return true;
23227
23411
  if (/^(?:src\/)?pages\/.*\.[jt]sx?$/u.test(path) && exportKey === "default") return true;
23412
+ if (/^(?:src\/)?instrumentation\.[jt]s$/u.test(path)) {
23413
+ return exportKey === "register" || exportKey === "onRequestError";
23414
+ }
23228
23415
  const middleware = /^(?:src\/)?(middleware|proxy)\.[jt]s$/u.exec(path);
23229
23416
  return middleware !== null && (exportKey === "default" || exportKey === middleware[1]);
23230
23417
  }
@@ -23267,7 +23454,7 @@ var SOLE_EXPORT_MATCHES_FILENAME_DOCUMENTATION = {
23267
23454
  "Framework entrypoints, generic stems covered by no-generic-single-export-module, tests, generated files, anonymous defaults, CommonJS, unresolved re-exports, and emission-dependent exports are excluded.",
23268
23455
  "The rule compares the primary filename stem and preserves a single private underscore prefix and conventional suffixes such as .server or .worker. Supporting types do not exempt a sole runtime export; public runtime aliases count as distinct exports.",
23269
23456
  "Exported destructuring patterns are excluded rather than undercounted as public exports.",
23270
- "Next.js and Astro routing exemptions require the nearest package.json to declare the framework, a conventional entrypoint path, and its expected runtime export. Custom routing roots and pageExtensions are not inferred."
23457
+ "Next.js metadata routes, instrumentation, and other Next.js and Astro entrypoint exemptions require the nearest package.json to declare the framework, a conventional entrypoint path, and its expected runtime export. Custom routing roots and pageExtensions are not inferred."
23271
23458
  ],
23272
23459
  examples: [
23273
23460
  { id: "matching-class", title: "Match a class and module", outcome: "no-match", files: [{ path: "src/artifact-store.ts", source: "export class ArtifactStore {}" }], focusPath: "src/artifact-store.ts", expectedCount: 0, public: true },
@@ -23833,7 +24020,7 @@ var RULES = {
23833
24020
  };
23834
24021
  var meta = {
23835
24022
  name: "@sarj/eslint-plugin",
23836
- version: "16.2.5"
24023
+ version: "16.3.0"
23837
24024
  };
23838
24025
  var APPLICATION_ONLY_RULES = [];
23839
24026
  var LIBRARY_IMPORT_POLICY = ["error", {