@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.js CHANGED
@@ -298,69 +298,34 @@ function commentLineEnd(sourceText, start) {
298
298
  // src/rules/require-explicit-contract-implementation.ts
299
299
  var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
300
300
  defaultLevel: "warning",
301
- summary: "Require owned classes supplied as typed collaborators to explicitly implement their contract.",
302
- rationale: "Structural assignment checks a fake only at the use site; an implements clause keeps its obligations visible at the class declaration.",
303
- remediation: "Add an implements clause for the actual injected interface, or inherit an abstract class that declares it.",
301
+ summary: "Require nominal inheritance when an injected abstract class is rejected by an instanceof guard.",
302
+ rationale: "A structurally assignable fake can still fail a constructor's nominal runtime guard. Pure structural interfaces do not require an implements clause.",
303
+ remediation: "Use an implementation inheriting the existing abstract class, or remove the nominal guard if the boundary is intentionally structural.",
304
304
  category: "architecture",
305
305
  autofix: "none",
306
306
  limitations: [
307
- "Only class instances whose concrete class and selected constructor parameter resolve through the TypeScript checker are checked.",
308
- "Object literals, casts, any, callback-valued data properties, unresolved heritage, dynamic factories, and third-party classes are excluded."
307
+ "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.",
308
+ "The contract must resolve to an abstract class without decorators, unresolved ancestors, or custom computed static members in its resolved hierarchy.",
309
+ "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."
309
310
  ],
310
311
  examples: [
311
312
  {
312
- id: "structural-fake-at-injection",
313
- title: "A fake reaches a typed collaborator without implementing its interface",
313
+ id: "runtime-nominal-guard-with-structural-fake",
314
+ title: "A structurally compatible fake fails the constructor's runtime guard",
314
315
  outcome: "match",
315
- files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher { publish() {} } new Consumer(new FakePublisher());" }],
316
+ 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());" }],
316
317
  focusPath: "src/service.ts",
317
318
  expectedCount: 1,
318
319
  public: true
319
320
  },
320
321
  {
321
- id: "explicit-fake-at-injection",
322
- title: "The fake names its actual interface",
322
+ id: "runtime-nominal-guard-with-inherited-fake",
323
+ title: "The fake satisfies the existing nominal runtime contract",
323
324
  outcome: "no-match",
324
- 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());" }],
325
+ 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());" }],
325
326
  focusPath: "src/service.ts",
326
327
  expectedCount: 0,
327
328
  public: true
328
- },
329
- {
330
- id: "inherited-implementation",
331
- title: "The fake inherits an implementation of the port",
332
- outcome: "no-match",
333
- 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());" }],
334
- focusPath: "src/service.ts",
335
- expectedCount: 0,
336
- public: false
337
- },
338
- {
339
- id: "non-contract-data-argument",
340
- title: "A data object is not a service port",
341
- outcome: "no-match",
342
- files: [{ path: "src/service.ts", source: "interface Options { name: string } class Consumer { constructor(readonly options: Options) {} } class FakeOptions { name = 'test' } new Consumer(new FakeOptions());" }],
343
- focusPath: "src/service.ts",
344
- expectedCount: 0,
345
- public: false
346
- },
347
- {
348
- id: "explicit-cast-origin-unknown",
349
- title: "A cast hides the concrete source",
350
- outcome: "no-match",
351
- 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);" }],
352
- focusPath: "src/service.ts",
353
- expectedCount: 0,
354
- public: false
355
- },
356
- {
357
- id: "unrelated-interface",
358
- title: "Implementing another interface does not declare the supplied port",
359
- outcome: "match",
360
- 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());" }],
361
- focusPath: "src/service.ts",
362
- expectedCount: 1,
363
- public: false
364
329
  }
365
330
  ]
366
331
  };
@@ -371,7 +336,7 @@ function behavioralContract(checker, type) {
371
336
  return behavioralContract(checker, substantive[0]);
372
337
  }
373
338
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
374
- 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));
339
+ const declaration = type.getSymbol()?.declarations?.find((item) => ts.isClassDeclaration(item) && item.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword));
375
340
  if (declaration === void 0) return null;
376
341
  return checker.getPropertiesOfType(type).some((property) => {
377
342
  if (!property.declarations?.some((member) => ts.isMethodSignature(member) || ts.isMethodDeclaration(member))) return false;
@@ -379,43 +344,86 @@ function behavioralContract(checker, type) {
379
344
  return propertyType.getCallSignatures().length > 0;
380
345
  }) ? type : null;
381
346
  }
382
- function ownedClass(type) {
347
+ function ownedClass(checker, expression) {
348
+ if (!ts.isNewExpression(expression)) return null;
349
+ const type = checker.getTypeAtLocation(expression);
383
350
  if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
384
351
  const declaration = type.getSymbol()?.declarations?.find(ts.isClassDeclaration);
385
- if (declaration === void 0 || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
352
+ const constructed = canonicalSymbol(checker, expression.expression)?.declarations?.find(ts.isClassDeclaration);
353
+ if (declaration === void 0 || declaration !== constructed || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
386
354
  return declaration;
387
355
  }
388
- function explicitlyImplements(checker, declaration, target, seen) {
356
+ function memberName(node) {
357
+ if (ts.isPropertyAccessExpression(node)) return node.name.text;
358
+ if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
359
+ return null;
360
+ }
361
+ function hasPrototypeMutation(source) {
362
+ function visit(node) {
363
+ const member = memberName(node);
364
+ if (member === "setPrototypeOf" || member === "__proto__" || member === "hasInstance") return true;
365
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && memberName(node.left) === "prototype") return true;
366
+ return ts.forEachChild(node, visit);
367
+ }
368
+ return visit(source) === true;
369
+ }
370
+ function nominallyInherits(checker, declaration, target, seen) {
371
+ if (ts.canHaveDecorators(declaration) && ts.getDecorators(declaration)?.length) return null;
372
+ if (hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some((member) => ts.isConstructorDeclaration(member) || ts.isPropertyDeclaration(member) && member.initializer !== void 0)) return null;
389
373
  const symbol = declaration.name === void 0 ? void 0 : canonicalSymbol(checker, declaration.name);
390
374
  if (symbol !== void 0) {
391
375
  if (seen.has(symbol)) return null;
392
376
  seen.add(symbol);
393
377
  }
394
- let unknown = false;
395
- for (const clause of declaration.heritageClauses ?? []) {
396
- for (const parent of clause.types) {
397
- const parentSymbol = canonicalSymbol(checker, parent.expression);
398
- if (parentSymbol === void 0) {
399
- unknown = true;
400
- continue;
401
- }
402
- if (parentSymbol === target) return true;
403
- const inherited = parentSymbol.declarations?.find((item) => ts.isClassDeclaration(item) || ts.isInterfaceDeclaration(item));
404
- if (inherited === void 0) {
405
- unknown = true;
406
- continue;
407
- }
408
- const relationship = explicitlyImplements(checker, inherited, target, new Set(seen));
409
- if (relationship === true) return true;
410
- unknown ||= relationship === null;
411
- }
412
- }
413
- return unknown ? null : false;
378
+ const parents = declaration.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types;
379
+ if (parents === void 0) return false;
380
+ if (parents.length !== 1) return null;
381
+ const parentSymbol = canonicalSymbol(checker, parents[0].expression);
382
+ if (parentSymbol === void 0) return null;
383
+ if (parentSymbol === target) return true;
384
+ const inherited = parentSymbol.declarations?.find(ts.isClassDeclaration);
385
+ return inherited === void 0 ? null : nominallyInherits(checker, inherited, target, new Set(seen));
414
386
  }
415
387
  function canonicalSymbol(checker, node) {
416
388
  const symbol = checker.getSymbolAtLocation(node);
417
389
  return symbol === void 0 ? void 0 : (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
418
390
  }
391
+ function hasNominalRejectionGuard(checker, signature, parameter, contract) {
392
+ const declaration = signature.getDeclaration();
393
+ if (declaration === void 0 || !ts.isConstructorDeclaration(declaration)) return false;
394
+ if (ts.canHaveDecorators(declaration.parent) && ts.getDecorators(declaration.parent)?.length) return false;
395
+ if (hasPrototypeMutation(declaration.getSourceFile())) return false;
396
+ const first = declaration.body?.statements[0];
397
+ if (first === void 0 || !ts.isIfStatement(first) || !alwaysThrows(first.thenStatement)) return false;
398
+ const condition = unparenthesized(first.expression);
399
+ if (!ts.isPrefixUnaryExpression(condition) || condition.operator !== ts.SyntaxKind.ExclamationToken) return false;
400
+ const comparison = unparenthesized(condition.operand);
401
+ if (!ts.isBinaryExpression(comparison) || comparison.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return false;
402
+ if (canonicalSymbol(checker, comparison.left) !== parameter || canonicalSymbol(checker, comparison.right) !== contract) return false;
403
+ const target = contract.declarations?.find(ts.isClassDeclaration);
404
+ return target !== void 0 && hasOrdinaryInstanceCheck(checker, target, /* @__PURE__ */ new Set());
405
+ }
406
+ function unparenthesized(expression) {
407
+ let current = expression;
408
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
409
+ return current;
410
+ }
411
+ function alwaysThrows(statement) {
412
+ return ts.isThrowStatement(statement) || ts.isBlock(statement) && statement.statements.length === 1 && ts.isThrowStatement(statement.statements[0]);
413
+ }
414
+ function hasOrdinaryInstanceCheck(checker, declaration, seen) {
415
+ if (seen.has(declaration) || ts.getDecorators(declaration)?.length || hasPrototypeMutation(declaration.getSourceFile())) return false;
416
+ seen.add(declaration);
417
+ 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;
418
+ for (const clause of declaration.heritageClauses ?? []) {
419
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
420
+ for (const parent of clause.types) {
421
+ const ancestor = canonicalSymbol(checker, parent.expression)?.declarations?.find(ts.isClassDeclaration);
422
+ if (ancestor === void 0 || !hasOrdinaryInstanceCheck(checker, ancestor, seen)) return false;
423
+ }
424
+ }
425
+ return true;
426
+ }
419
427
  var require_explicit_contract_implementation_default = createRule({
420
428
  name: "require-explicit-contract-implementation",
421
429
  documentation: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION,
@@ -424,7 +432,7 @@ var require_explicit_contract_implementation_default = createRule({
424
432
  docs: { description: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION.summary },
425
433
  schema: [],
426
434
  messages: {
427
- declareActualContract: "`{{implementation}}` is supplied as `{{contract}}` without explicitly implementing that contract."
435
+ declareActualContract: "`{{implementation}}` structurally matches `{{contract}}` but fails the rejecting instanceof guard; use nominal inheritance or make the boundary structural."
428
436
  }
429
437
  },
430
438
  defaultOptions: [],
@@ -435,25 +443,27 @@ var require_explicit_contract_implementation_default = createRule({
435
443
  const checker = services.program.getTypeChecker();
436
444
  function inspect(node) {
437
445
  const tsNode = services.esTreeNodeToTSNodeMap.get(node);
438
- if (!ts.isCallExpression(tsNode) && !ts.isNewExpression(tsNode)) return;
446
+ if (!ts.isNewExpression(tsNode)) return;
447
+ const consumer = canonicalSymbol(checker, tsNode.expression)?.declarations?.find(ts.isClassDeclaration);
448
+ if (consumer === void 0) return;
439
449
  const signature = checker.getResolvedSignature(tsNode);
440
- if (signature === void 0) return;
450
+ if (signature === void 0 || signature.getDeclaration()?.parent !== consumer) return;
441
451
  const parameters = signature.getParameters();
442
452
  for (const [index, argument] of node.arguments.entries()) {
443
- if (argument.type === AST_NODE_TYPES.SpreadElement || argument.type === AST_NODE_TYPES.TSAsExpression || argument.type === AST_NODE_TYPES.TSTypeAssertion || argument.type === AST_NODE_TYPES.TSSatisfiesExpression) continue;
453
+ if (argument.type !== AST_NODE_TYPES.NewExpression) continue;
444
454
  const parameter = parameters[index];
445
455
  if (parameter === void 0) continue;
446
456
  const parameterType = checker.getTypeOfSymbolAtLocation(parameter, tsNode);
447
457
  const contractType = behavioralContract(checker, parameterType);
448
458
  if (contractType === null) continue;
449
459
  const contractSymbol = contractType.getSymbol();
450
- if (contractSymbol === void 0) continue;
460
+ if (contractSymbol === void 0 || !hasNominalRejectionGuard(checker, signature, parameter, contractSymbol)) continue;
451
461
  const tsArgument = services.esTreeNodeToTSNodeMap.get(argument);
452
462
  const actualType = checker.getTypeAtLocation(tsArgument);
453
463
  if (!checker.isTypeAssignableTo(actualType, contractType)) continue;
454
- const implementation = ownedClass(actualType);
464
+ const implementation = ownedClass(checker, tsArgument);
455
465
  if (implementation === null) continue;
456
- const relationship = explicitlyImplements(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
466
+ const relationship = nominallyInherits(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
457
467
  if (relationship !== false) continue;
458
468
  context.report({
459
469
  node: argument,
@@ -463,7 +473,6 @@ var require_explicit_contract_implementation_default = createRule({
463
473
  }
464
474
  }
465
475
  return {
466
- CallExpression: inspect,
467
476
  NewExpression: inspect
468
477
  };
469
478
  }
@@ -581,7 +590,7 @@ var propertySignatureTypes = (members) => {
581
590
  };
582
591
  var fileTypeIndex = (program) => {
583
592
  const objects = /* @__PURE__ */ new Map();
584
- const functionAliases = /* @__PURE__ */ new Set();
593
+ const functionAliases = localFunctionAliases(program);
585
594
  function collectDeclaredType(statement) {
586
595
  const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
587
596
  if (declaration?.type === AST_NODE_TYPES2.TSInterfaceDeclaration) {
@@ -590,10 +599,6 @@ var fileTypeIndex = (program) => {
590
599
  }
591
600
  if (declaration?.type !== AST_NODE_TYPES2.TSTypeAliasDeclaration) return;
592
601
  const aliased = declaration.typeAnnotation;
593
- if (aliased.type === AST_NODE_TYPES2.TSFunctionType || aliased.type === AST_NODE_TYPES2.TSConstructorType) {
594
- functionAliases.add(declaration.id.name);
595
- return;
596
- }
597
602
  const literals = aliased.type === AST_NODE_TYPES2.TSTypeLiteral ? [aliased] : aliased.type === AST_NODE_TYPES2.TSIntersectionType ? aliased.types.filter((part) => part.type === AST_NODE_TYPES2.TSTypeLiteral) : [];
598
603
  if (literals.length === 0) return;
599
604
  const merged = /* @__PURE__ */ new Map();
@@ -609,6 +614,28 @@ var fileTypeIndex = (program) => {
609
614
  }
610
615
  return { objects, functionAliases };
611
616
  };
617
+ function localFunctionAliases(program) {
618
+ const names = /* @__PURE__ */ new Set();
619
+ const parents = /* @__PURE__ */ new Map();
620
+ for (const statement of program.body) {
621
+ const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
622
+ if (declaration?.type !== AST_NODE_TYPES2.TSTypeAliasDeclaration) continue;
623
+ const annotation = declaration.typeAnnotation;
624
+ if (annotation.type === AST_NODE_TYPES2.TSFunctionType || annotation.type === AST_NODE_TYPES2.TSConstructorType) names.add(declaration.id.name);
625
+ if (annotation.type === AST_NODE_TYPES2.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES2.Identifier) parents.set(declaration.id.name, annotation.typeName.name);
626
+ }
627
+ for (let pass = 0; pass < parents.size; pass += 1) {
628
+ let changed = false;
629
+ for (const [name, parent] of parents) {
630
+ if (names.has(parent) && !names.has(name)) {
631
+ names.add(name);
632
+ changed = true;
633
+ }
634
+ }
635
+ if (!changed) break;
636
+ }
637
+ return names;
638
+ }
612
639
  var typeParameterNames = (...declarations) => {
613
640
  const names = /* @__PURE__ */ new Set();
614
641
  for (const declaration of declarations) {
@@ -620,12 +647,15 @@ var readConstructor = (ctor, declared, typeParameters) => {
620
647
  const body2 = ctor.value.body;
621
648
  const storedFieldsFrom = /* @__PURE__ */ new Map();
622
649
  const storedMemberFieldsFrom = /* @__PURE__ */ new Map();
650
+ const constructedFieldNames = /* @__PURE__ */ new Set();
651
+ const localBindings2 = /* @__PURE__ */ new Set();
623
652
  let constructedFields = 0;
624
653
  if (body2 !== null && body2 !== void 0) scanStoredFields(body2);
625
654
  function scanStoredFields(body3) {
626
655
  const pending = [...body3.body];
627
656
  function recordStoredField(current) {
628
657
  if (current.type === AST_NODE_TYPES2.ArrowFunctionExpression || current.type === AST_NODE_TYPES2.FunctionExpression || current.type === AST_NODE_TYPES2.FunctionDeclaration || current.type === AST_NODE_TYPES2.ClassExpression || current.type === AST_NODE_TYPES2.ClassDeclaration) return;
658
+ if (current.type === AST_NODE_TYPES2.VariableDeclarator && current.id.type === AST_NODE_TYPES2.Identifier) localBindings2.add(current.id.name);
629
659
  const expression = current.type === AST_NODE_TYPES2.ExpressionStatement ? current.expression : null;
630
660
  const storedField = expression?.type === AST_NODE_TYPES2.AssignmentExpression && expression.left.type === AST_NODE_TYPES2.MemberExpression && expression.left.object.type === AST_NODE_TYPES2.ThisExpression ? staticMemberName(expression.left) : null;
631
661
  if (expression?.type !== AST_NODE_TYPES2.AssignmentExpression || !STORAGE_ASSIGNMENT_OPERATORS.has(expression.operator) || expression.left.type !== AST_NODE_TYPES2.MemberExpression || expression.left.object.type !== AST_NODE_TYPES2.ThisExpression || storedField === null) {
@@ -636,6 +666,7 @@ var readConstructor = (ctor, declared, typeParameters) => {
636
666
  while (source.type === AST_NODE_TYPES2.TSNonNullExpression || source.type === AST_NODE_TYPES2.TSAsExpression || source.type === AST_NODE_TYPES2.TSSatisfiesExpression || source.type === AST_NODE_TYPES2.TSTypeAssertion) source = source.expression;
637
667
  if (source.type === AST_NODE_TYPES2.NewExpression) {
638
668
  constructedFields += 1;
669
+ constructedFieldNames.add(storedField);
639
670
  } else if (source.type === AST_NODE_TYPES2.Identifier) {
640
671
  const fields = storedFieldsFrom.get(source.name) ?? /* @__PURE__ */ new Set();
641
672
  fields.add(storedField);
@@ -668,15 +699,19 @@ var readConstructor = (ctor, declared, typeParameters) => {
668
699
  const collaborators = [];
669
700
  function collectCollaborators(parameter) {
670
701
  for (const reference of parameterCollaborators(parameter, declared, storedMemberFieldsFrom)) {
671
- const fields = parameter.type === AST_NODE_TYPES2.TSParameterProperty ? [reference.name] : reference.fields.length > 0 ? reference.fields : [...storedFieldsFrom.get(reference.name) ?? []];
672
- if (fields.length === 0) continue;
702
+ if (localBindings2.has(reference.name)) continue;
703
+ let fields = reference.fields;
704
+ if (parameter.type === AST_NODE_TYPES2.TSParameterProperty) fields = [reference.name];
705
+ else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
706
+ const injectedFields = fields.filter((field) => !constructedFieldNames.has(field));
707
+ if (injectedFields.length === 0) continue;
673
708
  if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
674
709
  if (CONFIGISH_NAME_RE.test(reference.name)) continue;
675
710
  if (typeParameters.has(reference.typeName)) continue;
676
711
  if (BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName)) continue;
677
712
  if (VALUE_TYPE_RE.test(reference.typeName)) continue;
678
713
  if (declared().functionAliases.has(reference.typeName)) continue;
679
- collaborators.push({ ...reference, fields });
714
+ collaborators.push({ ...reference, fields: injectedFields });
680
715
  }
681
716
  }
682
717
  for (const parameter of ctor.value.params) {
@@ -895,11 +930,7 @@ function declaredMemberName(member) {
895
930
  function localInterfaceSurfaces(program) {
896
931
  const interfaces = /* @__PURE__ */ new Map();
897
932
  const parents = /* @__PURE__ */ new Map();
898
- const functionAliases = /* @__PURE__ */ new Set();
899
- for (const statement of program.body) {
900
- const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
901
- if (declaration?.type === AST_NODE_TYPES2.TSTypeAliasDeclaration && (declaration.typeAnnotation.type === AST_NODE_TYPES2.TSFunctionType || declaration.typeAnnotation.type === AST_NODE_TYPES2.TSConstructorType)) functionAliases.add(declaration.id.name);
902
- }
933
+ const functionAliases = localFunctionAliases(program);
903
934
  function collectInterfaceSurface(statement) {
904
935
  const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
905
936
  if (declaration?.type === AST_NODE_TYPES2.TSTypeAliasDeclaration) {
@@ -911,6 +942,10 @@ function localInterfaceSurfaces(program) {
911
942
  inherited.push(part.typeName.name);
912
943
  continue;
913
944
  }
945
+ if (part.type === AST_NODE_TYPES2.TSTypeReference) {
946
+ inherited.push("*");
947
+ continue;
948
+ }
914
949
  if (part.type !== AST_NODE_TYPES2.TSTypeLiteral) continue;
915
950
  addCallableMembers(part.members, callables2);
916
951
  }
@@ -1074,30 +1109,18 @@ function createServiceOperationResolver(program) {
1074
1109
  if (retained.size === 0) return [];
1075
1110
  const callers = /* @__PURE__ */ new Map();
1076
1111
  const active = /* @__PURE__ */ new Set();
1077
- for (const member of node.body.body) {
1078
- if (member.type !== AST_NODE_TYPES2.MethodDefinition || member.kind !== "method" || member.value.body === null) continue;
1079
- const name = declaredMemberName(member);
1080
- if (name === null) continue;
1081
- const visit = (current) => {
1082
- if (current.type === AST_NODE_TYPES2.FunctionDeclaration || current.type === AST_NODE_TYPES2.FunctionExpression || current.type === AST_NODE_TYPES2.ClassDeclaration || current.type === AST_NODE_TYPES2.ClassExpression) return;
1083
- if (current.type === AST_NODE_TYPES2.IfStatement && current.test.type === AST_NODE_TYPES2.Literal && typeof current.test.value === "boolean") {
1084
- visit(current.test.value ? current.consequent : current.alternate ?? current.test);
1085
- return;
1086
- }
1087
- if (current.type === AST_NODE_TYPES2.CallExpression) {
1088
- if (retained.has(invokedInstanceField(current) ?? "")) active.add(name);
1089
- if (current.callee.type === AST_NODE_TYPES2.MemberExpression && current.callee.object.type === AST_NODE_TYPES2.ThisExpression) {
1090
- const callee = staticMemberName(current.callee);
1091
- if (callee !== null) {
1092
- const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
1093
- linked.add(name);
1094
- callers.set(callee, linked);
1095
- }
1112
+ for (const [name, body2] of serviceOperationBodies(node)) {
1113
+ forEachServiceCall(body2, (call) => {
1114
+ if (retained.has(invokedInstanceField(call) ?? "")) active.add(name);
1115
+ if (call.callee.type === AST_NODE_TYPES2.MemberExpression && call.callee.object.type === AST_NODE_TYPES2.ThisExpression) {
1116
+ const callee = staticMemberName(call.callee);
1117
+ if (callee !== null) {
1118
+ const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
1119
+ linked.add(name);
1120
+ callers.set(callee, linked);
1096
1121
  }
1097
1122
  }
1098
- forEachOwnAstChild(current, visit);
1099
- };
1100
- for (const statement of member.value.body.body) visit(statement);
1123
+ });
1101
1124
  }
1102
1125
  const pending = [...active];
1103
1126
  while (pending.length > 0) {
@@ -1112,38 +1135,127 @@ function createServiceOperationResolver(program) {
1112
1135
  return publicMethodNames(node.body, declared().functionAliases).filter((name) => active.has(name));
1113
1136
  };
1114
1137
  }
1138
+ function serviceOperationBodies(node) {
1139
+ const bodies = /* @__PURE__ */ new Map();
1140
+ for (const member of node.body.body) {
1141
+ if (member.type !== AST_NODE_TYPES2.MethodDefinition && member.type !== AST_NODE_TYPES2.PropertyDefinition) continue;
1142
+ if (member.static || member.decorators.length > 0) continue;
1143
+ if (member.type === AST_NODE_TYPES2.MethodDefinition && member.kind !== "method") continue;
1144
+ if (member.value?.type !== AST_NODE_TYPES2.FunctionExpression && member.value?.type !== AST_NODE_TYPES2.ArrowFunctionExpression) continue;
1145
+ const name = declaredMemberName(member);
1146
+ if (name !== null && member.value.body !== null) bodies.set(name, member.value.body);
1147
+ }
1148
+ return bodies;
1149
+ }
1150
+ function forEachServiceCall(node, onCall) {
1151
+ if (node.type === AST_NODE_TYPES2.FunctionDeclaration || node.type === AST_NODE_TYPES2.FunctionExpression || node.type === AST_NODE_TYPES2.ArrowFunctionExpression || node.type === AST_NODE_TYPES2.ClassDeclaration || node.type === AST_NODE_TYPES2.ClassExpression) return;
1152
+ if (node.type === AST_NODE_TYPES2.BlockStatement) {
1153
+ for (const statement of node.body) {
1154
+ forEachServiceCall(statement, onCall);
1155
+ if (terminatesOperation(statement)) break;
1156
+ }
1157
+ return;
1158
+ }
1159
+ const branch = literalExecutionBranch(node);
1160
+ if (branch !== null) {
1161
+ forEachServiceCall(branch, onCall);
1162
+ return;
1163
+ }
1164
+ if (node.type === AST_NODE_TYPES2.CallExpression) onCall(node);
1165
+ forEachOwnAstChild(node, (child) => {
1166
+ forEachServiceCall(child, onCall);
1167
+ });
1168
+ }
1169
+ function literalExecutionBranch(node) {
1170
+ if (node.type === AST_NODE_TYPES2.WhileStatement && node.test.type === AST_NODE_TYPES2.Literal && node.test.value === false) return node.test;
1171
+ if ((node.type === AST_NODE_TYPES2.IfStatement || node.type === AST_NODE_TYPES2.ConditionalExpression) && node.test.type === AST_NODE_TYPES2.Literal && typeof node.test.value === "boolean") {
1172
+ return node.test.value ? node.consequent : node.alternate ?? node.test;
1173
+ }
1174
+ if (node.type !== AST_NODE_TYPES2.LogicalExpression || node.left.type !== AST_NODE_TYPES2.Literal || typeof node.left.value !== "boolean") return null;
1175
+ if (node.operator === "&&") return node.left.value ? node.right : node.left;
1176
+ if (node.operator === "||") return node.left.value ? node.left : node.right;
1177
+ return node.left;
1178
+ }
1179
+ function terminatesOperation(node) {
1180
+ if (node.type === AST_NODE_TYPES2.ReturnStatement || node.type === AST_NODE_TYPES2.ThrowStatement) return true;
1181
+ if (node.type === AST_NODE_TYPES2.BlockStatement) return node.body.some(terminatesOperation);
1182
+ if (node.type !== AST_NODE_TYPES2.IfStatement) return false;
1183
+ const branch = literalExecutionBranch(node);
1184
+ if (branch !== null) return terminatesOperation(branch);
1185
+ return node.alternate !== null && terminatesOperation(node.consequent) && terminatesOperation(node.alternate);
1186
+ }
1115
1187
  function hasDeclaredServiceContract(node, operations, program) {
1116
- const classes = localClassAbstractness(program);
1117
1188
  const interfaces = localInterfaceSurfaces(program);
1118
- if (node.implements.length > 0) return implementedSurfaceCovers(node, operations, classes, interfaces);
1119
- if (node.superClass === null) return false;
1120
- if (node.superClass.type !== AST_NODE_TYPES2.Identifier) return null;
1121
- const baseName = node.superClass.name;
1122
- const isAbstract = classes.get(baseName);
1123
- if (isAbstract === void 0) return null;
1124
- if (!isAbstract) return false;
1125
- const base = program.body.flatMap((statement) => {
1126
- const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
1127
- return declaration?.type === AST_NODE_TYPES2.ClassDeclaration && declaration.id?.name === baseName ? [declaration] : [];
1128
- })[0];
1129
- if (base === void 0) return null;
1130
- const abstractOperations = new Set(base.body.body.flatMap(
1131
- (member) => member.type === AST_NODE_TYPES2.TSAbstractMethodDefinition && member.accessibility !== "private" && member.accessibility !== "protected" && !member.static ? [declaredMemberName(member)] : []
1132
- ));
1133
- return operations.every((name) => abstractOperations.has(name) || publicMethodNames(base.body, /* @__PURE__ */ new Set()).includes(name));
1189
+ const classes = localClassDeclarations(program);
1190
+ const functionAliases = fileTypeIndex(program).functionAliases;
1191
+ const covered2 = /* @__PURE__ */ new Set();
1192
+ const pending = [node];
1193
+ const seen = /* @__PURE__ */ new Set();
1194
+ function addImplementedContracts(current) {
1195
+ for (const implementation of current.implements) {
1196
+ if (implementation.expression.type !== AST_NODE_TYPES2.Identifier) return false;
1197
+ const name = implementation.expression.name;
1198
+ const surface = interfaces.get(name);
1199
+ const base = classes.get(name);
1200
+ if (surface === void 0 && base === void 0) return false;
1201
+ if (surface?.has("*")) return false;
1202
+ for (const method of surface ?? []) covered2.add(method);
1203
+ if (base?.abstract) pending.push(base);
1204
+ }
1205
+ return true;
1206
+ }
1207
+ while (pending.length > 0) {
1208
+ const current = pending.pop();
1209
+ if (current === void 0 || seen.has(current)) continue;
1210
+ seen.add(current);
1211
+ if (current.decorators.length > 0) return null;
1212
+ if (current.abstract) {
1213
+ for (const method of abstractContractOperations(current, functionAliases)) covered2.add(method);
1214
+ }
1215
+ if (!addImplementedContracts(current)) return null;
1216
+ if (current.superClass === null) continue;
1217
+ if (current.superClass.type !== AST_NODE_TYPES2.Identifier) return null;
1218
+ const base = classes.get(current.superClass.name);
1219
+ if (base === void 0) return null;
1220
+ pending.push(base);
1221
+ }
1222
+ return operations.every((name) => covered2.has(name));
1223
+ }
1224
+ function localClassDeclarations(program) {
1225
+ const classes = /* @__PURE__ */ new Map();
1226
+ for (const statement of program.body) {
1227
+ const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration || statement.type === AST_NODE_TYPES2.ExportDefaultDeclaration ? statement.declaration : statement;
1228
+ if (declaration?.type === AST_NODE_TYPES2.ClassDeclaration && declaration.id !== null) classes.set(declaration.id.name, declaration);
1229
+ }
1230
+ return classes;
1231
+ }
1232
+ function abstractContractOperations(node, functionAliases) {
1233
+ const methods = new Set(publicMethodNames(node.body, functionAliases));
1234
+ for (const member of node.body.body) {
1235
+ if (member.type !== AST_NODE_TYPES2.TSAbstractMethodDefinition && member.type !== AST_NODE_TYPES2.TSAbstractPropertyDefinition) continue;
1236
+ if (member.static || member.accessibility === "private" || member.accessibility === "protected") continue;
1237
+ if (member.type === AST_NODE_TYPES2.TSAbstractPropertyDefinition) {
1238
+ const annotation = member.typeAnnotation?.typeAnnotation;
1239
+ if (annotation?.type !== AST_NODE_TYPES2.TSFunctionType && !(annotation?.type === AST_NODE_TYPES2.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES2.Identifier && functionAliases.has(annotation.typeName.name))) continue;
1240
+ }
1241
+ const name = declaredMemberName(member);
1242
+ if (name !== null) methods.add(name);
1243
+ }
1244
+ return methods;
1134
1245
  }
1135
1246
 
1136
1247
  // src/rules/require-explicit-service-contract.ts
1137
1248
  var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1138
1249
  defaultLevel: "warning",
1139
1250
  summary: "Require explicit contracts for classes that invoke retained collaborators.",
1140
- rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that its callers and fakes must implement.",
1141
- remediation: "Export a focused interface or abstract class and explicitly implement or extend it.",
1251
+ rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that callers consume and implementations and fakes satisfy.",
1252
+ remediation: "Explicitly implement a focused interface or extend an abstract class covering the service operations. Export the contract only when its consumers need it.",
1142
1253
  category: "architecture",
1143
1254
  autofix: "none",
1144
1255
  limitations: [
1145
1256
  "The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.",
1146
- "Imported or dynamic base classes with unavailable declarations are unresolved and do not produce a finding.",
1257
+ "Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.",
1258
+ "Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.",
1147
1259
  "Generated files and JavaScript without implements syntax are excluded."
1148
1260
  ],
1149
1261
  examples: [
@@ -1165,7 +1277,7 @@ var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1165
1277
  outcome: "no-match",
1166
1278
  files: [{
1167
1279
  path: "src/service.ts",
1168
- source: "export interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"
1280
+ source: "interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"
1169
1281
  }],
1170
1282
  focusPath: "src/service.ts",
1171
1283
  expectedCount: 0,
@@ -1181,7 +1293,7 @@ var require_explicit_service_contract_default = createRule({
1181
1293
  docs: { description: REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION.summary },
1182
1294
  schema: [],
1183
1295
  messages: {
1184
- requireExplicitServiceContract: "`{{name}}` invokes a retained collaborator from {{operations}} without explicitly implementing a public contract for those operations."
1296
+ 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."
1185
1297
  }
1186
1298
  },
1187
1299
  defaultOptions: [],
@@ -1192,7 +1304,7 @@ var require_explicit_service_contract_default = createRule({
1192
1304
  const isExportedServiceClass = createExportedServiceClassResolver(program);
1193
1305
  return {
1194
1306
  ClassDeclaration(node) {
1195
- if (node.id === null || node.abstract || !isServiceRole(node.id.name, context.filename)) return;
1307
+ if (node.id === null || node.abstract || node.decorators.length > 0 || !isServiceRole(node.id.name, context.filename)) return;
1196
1308
  if (isExportedServiceClass(node) && !isTestFile(context.filename) && !isStoryFile(context.filename) && !isScriptFile(context.filename)) return;
1197
1309
  const operations = operationsFor(node);
1198
1310
  if (operations.length === 0 || hasDeclaredServiceContract(node, operations, program) !== false) return;
@@ -7031,7 +7143,7 @@ var RESHAPING_METHODS = /* @__PURE__ */ new Set([
7031
7143
  function importedName2(specifier) {
7032
7144
  return specifier.imported.type === AST_NODE_TYPES34.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
7033
7145
  }
7034
- function memberName(node) {
7146
+ function memberName2(node) {
7035
7147
  if (!node.computed && node.property.type === AST_NODE_TYPES34.Identifier) {
7036
7148
  return node.property.name;
7037
7149
  }
@@ -7116,7 +7228,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7116
7228
  if (callee.type !== AST_NODE_TYPES34.MemberExpression || callee.object.type !== AST_NODE_TYPES34.CallExpression) {
7117
7229
  return null;
7118
7230
  }
7119
- const method = memberName(callee);
7231
+ const method = memberName2(callee);
7120
7232
  if (method === null) return null;
7121
7233
  calls.push({ method, node: current });
7122
7234
  current = callee.object;
@@ -7130,7 +7242,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7130
7242
  if (callee.type !== AST_NODE_TYPES34.MemberExpression || callee.object.type !== AST_NODE_TYPES34.Identifier || !namespaces.has(callee.object.name) || !resolvesToTrackedImport(callee.object)) {
7131
7243
  return null;
7132
7244
  }
7133
- const name = memberName(callee);
7245
+ const name = memberName2(callee);
7134
7246
  return name !== null && KINDS.has(name) ? name : null;
7135
7247
  }
7136
7248
  function isInsideReshapingCall(node) {
@@ -7139,7 +7251,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
7139
7251
  while (parent !== void 0 && parent.type !== AST_NODE_TYPES34.Program) {
7140
7252
  if (parent.type === AST_NODE_TYPES34.CallExpression) {
7141
7253
  const callee = parent.callee;
7142
- if (callee.type === AST_NODE_TYPES34.MemberExpression && RESHAPING_METHODS.has(memberName(callee) ?? "") && (callee.object.type === AST_NODE_TYPES34.CallExpression || callee.object.type === AST_NODE_TYPES34.Identifier && namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object))) {
7254
+ if (callee.type === AST_NODE_TYPES34.MemberExpression && RESHAPING_METHODS.has(memberName2(callee) ?? "") && (callee.object.type === AST_NODE_TYPES34.CallExpression || callee.object.type === AST_NODE_TYPES34.Identifier && namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object))) {
7143
7255
  return true;
7144
7256
  }
7145
7257
  if (callee.type === AST_NODE_TYPES34.Identifier && preprocessors.has(callee.name) && resolvesToTrackedImport(callee)) {
@@ -7405,7 +7517,7 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
7405
7517
  function importedName3(specifier) {
7406
7518
  return specifier.imported.type === AST_NODE_TYPES35.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
7407
7519
  }
7408
- function memberName2(node) {
7520
+ function memberName3(node) {
7409
7521
  if (!node.computed && node.property.type === AST_NODE_TYPES35.Identifier) {
7410
7522
  return node.property.name;
7411
7523
  }
@@ -7457,7 +7569,7 @@ var no_redundant_optional_array_default_default = createRule({
7457
7569
  return arrayConstructors.has(callee.name) && resolvesToTrackedImport(callee);
7458
7570
  }
7459
7571
  if (callee.type !== AST_NODE_TYPES35.MemberExpression) return false;
7460
- const method = memberName2(callee);
7572
+ const method = memberName3(callee);
7461
7573
  if (method === "array" && (callee.object.type === AST_NODE_TYPES35.Identifier ? namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object) : isZodSchemaExpression(callee.object))) {
7462
7574
  return true;
7463
7575
  }
@@ -7498,12 +7610,12 @@ var no_redundant_optional_array_default_default = createRule({
7498
7610
  },
7499
7611
  CallExpression(node) {
7500
7612
  const defaultCallee = node.callee;
7501
- if (defaultCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName2(defaultCallee) !== "default" || defaultCallee.object.type !== AST_NODE_TYPES35.CallExpression) {
7613
+ if (defaultCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName3(defaultCallee) !== "default" || defaultCallee.object.type !== AST_NODE_TYPES35.CallExpression) {
7502
7614
  return;
7503
7615
  }
7504
7616
  const optionalCall = defaultCallee.object;
7505
7617
  const optionalCallee = optionalCall.callee;
7506
- if (optionalCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName2(optionalCallee) !== "optional" || optionalCall.arguments.length !== 0 || !isArraySchemaExpression(optionalCallee.object) || sourceCode.getCommentsInside(optionalCall).length > 0) {
7618
+ if (optionalCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName3(optionalCallee) !== "optional" || optionalCall.arguments.length !== 0 || !isArraySchemaExpression(optionalCallee.object) || sourceCode.getCommentsInside(optionalCall).length > 0) {
7507
7619
  return;
7508
7620
  }
7509
7621
  context.report({
@@ -7527,12 +7639,12 @@ import * as ts8 from "typescript";
7527
7639
  import {
7528
7640
  AST_NODE_TYPES as AST_NODE_TYPES36
7529
7641
  } from "@typescript-eslint/utils";
7530
- function memberName3(member) {
7642
+ function memberName4(member) {
7531
7643
  return !member.computed && member.key.type === AST_NODE_TYPES36.Identifier ? member.key.name : null;
7532
7644
  }
7533
7645
  function convertibleMemberName(member) {
7534
7646
  if (member.type !== AST_NODE_TYPES36.MethodDefinition && member.type !== AST_NODE_TYPES36.PropertyDefinition && member.type !== AST_NODE_TYPES36.AccessorProperty) return null;
7535
- return memberName3(member);
7647
+ return memberName4(member);
7536
7648
  }
7537
7649
 
7538
7650
  // src/rules/interface-contract-members-private.ts
@@ -8134,7 +8246,7 @@ var LOWER_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
8134
8246
  function propertyName2(node) {
8135
8247
  return node.type === AST_NODE_TYPES40.Identifier ? node.name : null;
8136
8248
  }
8137
- function memberName4(node) {
8249
+ function memberName5(node) {
8138
8250
  let current = node;
8139
8251
  while (current.type === AST_NODE_TYPES40.TSAsExpression || current.type === AST_NODE_TYPES40.TSNonNullExpression || current.type === AST_NODE_TYPES40.TSTypeAssertion) {
8140
8252
  current = current.expression;
@@ -8198,7 +8310,7 @@ var no_bespoke_api_case_conversion_default = createRule({
8198
8310
  Property(node) {
8199
8311
  if (node.computed || node.method || node.shorthand) return;
8200
8312
  const key = propertyName2(node.key);
8201
- const value = memberName4(node.value);
8313
+ const value = memberName5(node.value);
8202
8314
  if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
8203
8315
  if (!hasApiReceiver(node.value)) return;
8204
8316
  const wireName = SNAKE_CASE_RE2.test(key) ? key : value;
@@ -8971,9 +9083,9 @@ var no_positional_tuple_return_default = createRule({
8971
9083
  TSMethodSignature(node) {
8972
9084
  const owner = owningInterface(node);
8973
9085
  const alias = owningTypeAlias(node);
8974
- const memberName8 = staticMemberName4(node.key);
8975
- if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 || memberName8 === null) return;
8976
- report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName8}`);
9086
+ const memberName9 = staticMemberName4(node.key);
9087
+ if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 || memberName9 === null) return;
9088
+ report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
8977
9089
  },
8978
9090
  TSTypeAliasDeclaration(node) {
8979
9091
  if (!typeExports.has(node.id.name)) return;
@@ -8984,11 +9096,11 @@ var no_positional_tuple_return_default = createRule({
8984
9096
  const owner = owningInterface(node);
8985
9097
  const alias = owningTypeAlias(node);
8986
9098
  const annotation = node.typeAnnotation?.typeAnnotation;
8987
- const memberName8 = staticMemberName4(node.key);
8988
- if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || memberName8 === null || annotation === void 0) return;
9099
+ const memberName9 = staticMemberName4(node.key);
9100
+ if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || memberName9 === null || annotation === void 0) return;
8989
9101
  const returnType = callableReturnType(annotation, aliases);
8990
9102
  if (returnType !== null) {
8991
- report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName8}`);
9103
+ report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
8992
9104
  }
8993
9105
  },
8994
9106
  PropertyDefinition(node) {
@@ -14723,7 +14835,8 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
14723
14835
  remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
14724
14836
  category: "correctness",
14725
14837
  limitations: [
14726
- "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."
14838
+ "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.",
14839
+ "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."
14727
14840
  ],
14728
14841
  examples: [
14729
14842
  {
@@ -14743,6 +14856,26 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
14743
14856
  focusPath: "src/constants.ts",
14744
14857
  expectedCount: 1,
14745
14858
  public: true
14859
+ },
14860
+ {
14861
+ id: "shadowed-readonly-alias",
14862
+ scenarioId: "type-binding",
14863
+ title: "A type named Readonly still exposes mutable properties",
14864
+ outcome: "match",
14865
+ files: [{ path: "src/constants.ts", source: "type Readonly = { voice: boolean };\nexport const TOOL_SUPPORT: Readonly = { voice: true };" }],
14866
+ focusPath: "src/constants.ts",
14867
+ expectedCount: 1,
14868
+ public: true
14869
+ },
14870
+ {
14871
+ id: "resolved-readonly-alias",
14872
+ scenarioId: "type-binding",
14873
+ title: "A local alias resolves to readonly properties",
14874
+ outcome: "no-match",
14875
+ files: [{ path: "src/constants.ts", source: "type ToolSupport = Readonly<{ voice: boolean }>;\nexport const TOOL_SUPPORT: ToolSupport = { voice: true };" }],
14876
+ focusPath: "src/constants.ts",
14877
+ expectedCount: 0,
14878
+ public: true
14746
14879
  }
14747
14880
  ]
14748
14881
  };
@@ -14797,39 +14930,84 @@ function collectionKind(node, isUnshadowedGlobal3) {
14797
14930
  }
14798
14931
  return null;
14799
14932
  }
14800
- function declaredReadonlyType(node, kind, aliases) {
14933
+ function declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) {
14801
14934
  const annotation = node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation : void 0;
14802
- if (annotation !== void 0 && isReadonlyTypeResolved(annotation.typeAnnotation, kind, aliases)) {
14935
+ if (annotation !== void 0 && readonlyTypeState(annotation.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly") {
14803
14936
  return true;
14804
14937
  }
14805
- return node.init?.type === AST_NODE_TYPES72.TSAsExpression && isReadonlyTypeResolved(node.init.typeAnnotation, kind, aliases);
14938
+ return node.init?.type === AST_NODE_TYPES72.TSAsExpression && readonlyTypeState(node.init.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly";
14806
14939
  }
14807
- function isReadonlyTypeResolved(node, kind, aliases, seen = /* @__PURE__ */ new Set()) {
14808
- if (isReadonlyType(node, kind)) return true;
14809
- if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier) return false;
14810
- const name = node.typeName.name;
14811
- const target = aliases.get(name);
14812
- if (target === void 0 || seen.has(name)) return false;
14813
- return isReadonlyTypeResolved(target, kind, aliases, /* @__PURE__ */ new Set([...seen, name]));
14940
+ function readonlyTypeState(node, kind, resolveLocalType, isUnshadowedType, arguments_ = /* @__PURE__ */ new Map(), seen = /* @__PURE__ */ new Set()) {
14941
+ if (seen.has(node)) return "unknown";
14942
+ if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier) {
14943
+ return directReadonlyState(node, kind, isUnshadowedType);
14944
+ }
14945
+ const declaration = resolveLocalType(node.typeName);
14946
+ if (declaration === void 0) return directReadonlyState(node, kind, isUnshadowedType);
14947
+ if (declaration.type === AST_NODE_TYPES72.TSTypeParameter) {
14948
+ const argument = arguments_.get(declaration);
14949
+ return argument === void 0 ? "unknown" : readonlyTypeState(
14950
+ argument.node,
14951
+ kind,
14952
+ resolveLocalType,
14953
+ isUnshadowedType,
14954
+ argument.arguments,
14955
+ argument.seen
14956
+ );
14957
+ }
14958
+ const expanded = /* @__PURE__ */ new Set([...seen, node]);
14959
+ return readonlyTypeState(
14960
+ declaration.typeAnnotation,
14961
+ kind,
14962
+ resolveLocalType,
14963
+ isUnshadowedType,
14964
+ bindTypeArguments(declaration, node, arguments_, expanded),
14965
+ expanded
14966
+ );
14967
+ }
14968
+ function bindTypeArguments(declaration, reference, arguments_, seen) {
14969
+ const bindings = new Map(arguments_);
14970
+ for (const [index, parameter] of (declaration.typeParameters?.params ?? []).entries()) {
14971
+ const argument = reference.typeArguments?.params[index];
14972
+ const node = argument ?? parameter.default;
14973
+ if (node !== void 0) {
14974
+ bindings.set(parameter, { node, arguments: argument === void 0 ? new Map(bindings) : arguments_, seen });
14975
+ }
14976
+ }
14977
+ return bindings;
14814
14978
  }
14815
- function isReadonlyType(node, kind) {
14979
+ function directReadonlyState(node, kind, isUnshadowedType) {
14816
14980
  if (node.type === AST_NODE_TYPES72.TSTypeOperator && node.operator === "readonly") {
14817
- return true;
14981
+ return "readonly";
14818
14982
  }
14819
- if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier) {
14820
- return false;
14983
+ if (node.type === AST_NODE_TYPES72.TSTypeLiteral) {
14984
+ if (node.members.length === 0) return "unknown";
14985
+ const readonly2 = kind === "literal" && node.members.length > 0 && node.members.every(
14986
+ (member) => (member.type === AST_NODE_TYPES72.TSPropertySignature || member.type === AST_NODE_TYPES72.TSIndexSignature) && member.readonly
14987
+ );
14988
+ return readonly2 ? "readonly" : "mutable";
14989
+ }
14990
+ if (node.type === AST_NODE_TYPES72.TSArrayType || node.type === AST_NODE_TYPES72.TSTupleType) return "mutable";
14991
+ if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier || !isUnshadowedType(node.typeName)) {
14992
+ return "unknown";
14821
14993
  }
14822
14994
  if (node.typeName.name === "Readonly") {
14823
- return kind === "literal";
14995
+ return kind === "literal" ? "readonly" : "mutable";
14824
14996
  }
14825
- return kind === "literal" ? node.typeName.name === "ReadonlyArray" : node.typeName.name === `Readonly${kind}`;
14997
+ if (["Array", "Map", "Set"].includes(node.typeName.name)) return "mutable";
14998
+ if (!["ReadonlyArray", "ReadonlyMap", "ReadonlySet"].includes(node.typeName.name)) return "unknown";
14999
+ const readonly = kind === "literal" ? node.typeName.name === "ReadonlyArray" : node.typeName.name === `Readonly${kind}`;
15000
+ return readonly ? "readonly" : "mutable";
14826
15001
  }
14827
- function hasUnknownExplicitType(node, aliases) {
14828
- const annotation = node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0;
15002
+ function hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType) {
15003
+ const annotation = (node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0) ?? (node.init?.type === AST_NODE_TYPES72.TSAsExpression ? node.init.typeAnnotation : void 0);
14829
15004
  if (annotation === void 0) return false;
14830
15005
  if (annotation.type === AST_NODE_TYPES72.TSArrayType || annotation.type === AST_NODE_TYPES72.TSTypeOperator) return false;
14831
15006
  if (annotation.type !== AST_NODE_TYPES72.TSTypeReference || annotation.typeName.type !== AST_NODE_TYPES72.Identifier) return true;
14832
- return !aliases.has(annotation.typeName.name) && !["Array", "Map", "Readonly", "ReadonlyArray", "ReadonlyMap", "ReadonlySet", "Set"].includes(annotation.typeName.name);
15007
+ if (resolveLocalType(annotation.typeName) !== void 0) {
15008
+ return readonlyTypeState(annotation, kind, resolveLocalType, isUnshadowedType) === "unknown";
15009
+ }
15010
+ return resolveLocalType(annotation.typeName) === void 0 && (!isUnshadowedType(annotation.typeName) || !["Array", "Map", "Readonly", "ReadonlyArray", "ReadonlyMap", "ReadonlySet", "Set"].includes(annotation.typeName.name));
14833
15011
  }
14834
15012
  function referenceMutates(identifier, isUnshadowedGlobal3) {
14835
15013
  let member = identifier.parent;
@@ -14872,11 +15050,20 @@ var prefer_immutable_module_constant_default = createRule({
14872
15050
  const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
14873
15051
  return variable === null || variable.defs.length === 0;
14874
15052
  };
15053
+ const isUnshadowedType = (identifier) => {
15054
+ const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
15055
+ return variable === null || !variable.defs.some((definition) => definition.isTypeDefinition);
15056
+ };
15057
+ const resolveLocalType = (identifier) => {
15058
+ const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
15059
+ const definitions = variable?.defs.filter((definition) => definition.isTypeDefinition) ?? [];
15060
+ const declaration = definitions.length === 1 ? definitions[0]?.node : void 0;
15061
+ return declaration?.type === AST_NODE_TYPES72.TSTypeAliasDeclaration || declaration?.type === AST_NODE_TYPES72.TSTypeParameter ? declaration : void 0;
15062
+ };
14875
15063
  if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
14876
15064
  return {};
14877
15065
  }
14878
15066
  const exportedNames2 = /* @__PURE__ */ new Set();
14879
- const typeAliases2 = /* @__PURE__ */ new Map();
14880
15067
  const mutatesThroughAlias = (root) => {
14881
15068
  const pending = [root];
14882
15069
  const seen = /* @__PURE__ */ new Set();
@@ -14901,10 +15088,6 @@ var prefer_immutable_module_constant_default = createRule({
14901
15088
  return {
14902
15089
  Program(node) {
14903
15090
  function collectModuleBindings(statement) {
14904
- const declaration = statement.type === AST_NODE_TYPES72.ExportNamedDeclaration ? statement.declaration : statement;
14905
- if (declaration?.type === AST_NODE_TYPES72.TSTypeAliasDeclaration) {
14906
- typeAliases2.set(declaration.id.name, declaration.typeAnnotation);
14907
- }
14908
15091
  if (statement.type === AST_NODE_TYPES72.ExportNamedDeclaration) {
14909
15092
  if (statement.source !== null || statement.exportKind === "type") return;
14910
15093
  for (const specifier of statement.specifiers) {
@@ -14937,7 +15120,7 @@ var prefer_immutable_module_constant_default = createRule({
14937
15120
  return;
14938
15121
  }
14939
15122
  const kind = collectionKind(node.init, isUnshadowedGlobal3);
14940
- if (kind === null || declaredReadonlyType(node, kind, typeAliases2) || hasUnknownExplicitType(node, typeAliases2)) {
15123
+ if (kind === null || declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) || hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType)) {
14941
15124
  return;
14942
15125
  }
14943
15126
  const variable = sourceCode.getDeclaredVariables(node)[0];
@@ -16975,7 +17158,7 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
16975
17158
  { 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 }
16976
17159
  ]
16977
17160
  };
16978
- function memberName5(node) {
17161
+ function memberName6(node) {
16979
17162
  if (!node.computed && node.property.type === AST_NODE_TYPES82.Identifier) return node.property.name;
16980
17163
  if (node.computed && node.property.type === AST_NODE_TYPES82.Literal && typeof node.property.value === "string") {
16981
17164
  return node.property.value;
@@ -16991,7 +17174,7 @@ function isCryptoLoader(node, resolve2) {
16991
17174
  if (node.callee.type === AST_NODE_TYPES82.Identifier) {
16992
17175
  return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
16993
17176
  }
16994
- return node.callee.type === AST_NODE_TYPES82.MemberExpression && node.callee.object.type === AST_NODE_TYPES82.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName5(node.callee) === "getBuiltinModule";
17177
+ return node.callee.type === AST_NODE_TYPES82.MemberExpression && node.callee.object.type === AST_NODE_TYPES82.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName6(node.callee) === "getBuiltinModule";
16995
17178
  }
16996
17179
  function isCryptoSpecifier(node) {
16997
17180
  return node.type === AST_NODE_TYPES82.Literal && (node.value === "crypto" || node.value === "node:crypto");
@@ -17073,14 +17256,14 @@ function importedName6(node) {
17073
17256
  return node.type === AST_NODE_TYPES82.Identifier ? node.name : node.value;
17074
17257
  }
17075
17258
  function isMemberCall(node, name) {
17076
- return node.callee.type === AST_NODE_TYPES82.MemberExpression && memberName5(node.callee) === name;
17259
+ return node.callee.type === AST_NODE_TYPES82.MemberExpression && memberName6(node.callee) === name;
17077
17260
  }
17078
17261
  function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
17079
17262
  if (node.callee.type === AST_NODE_TYPES82.Identifier) {
17080
17263
  const variable2 = resolve2(node.callee);
17081
17264
  return variable2 !== null && directBindings.has(variable2);
17082
17265
  }
17083
- if (node.callee.type !== AST_NODE_TYPES82.MemberExpression || memberName5(node.callee) !== "createHash") {
17266
+ if (node.callee.type !== AST_NODE_TYPES82.MemberExpression || memberName6(node.callee) !== "createHash") {
17084
17267
  return false;
17085
17268
  }
17086
17269
  if (isCryptoLoader(node.callee.object, resolve2)) return true;
@@ -17108,7 +17291,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
17108
17291
  { 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 }
17109
17292
  ]
17110
17293
  };
17111
- function memberName6(node) {
17294
+ function memberName7(node) {
17112
17295
  if (!node.computed && node.property.type === AST_NODE_TYPES83.Identifier) return node.property.name;
17113
17296
  if (node.computed && node.property.type === AST_NODE_TYPES83.Literal && typeof node.property.value === "string") {
17114
17297
  return node.property.value;
@@ -17125,7 +17308,7 @@ function isFsLoader(node, isGlobal) {
17125
17308
  const [argument] = expression.arguments;
17126
17309
  if (argument === void 0 || argument.type === AST_NODE_TYPES83.SpreadElement || !isFsSpecifier(argument)) return false;
17127
17310
  if (expression.callee.type === AST_NODE_TYPES83.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
17128
- return expression.callee.type === AST_NODE_TYPES83.MemberExpression && expression.callee.object.type === AST_NODE_TYPES83.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName6(expression.callee) === "getBuiltinModule";
17311
+ return expression.callee.type === AST_NODE_TYPES83.MemberExpression && expression.callee.object.type === AST_NODE_TYPES83.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName7(expression.callee) === "getBuiltinModule";
17129
17312
  }
17130
17313
  function isFsSpecifier(node) {
17131
17314
  return node.type === AST_NODE_TYPES83.Literal && (node.value === "node:fs" || node.value === "fs");
@@ -17206,7 +17389,7 @@ var prefer_node_fs_promises_default = createRule({
17206
17389
  }
17207
17390
  },
17208
17391
  MemberExpression(node) {
17209
- const name = memberName6(node);
17392
+ const name = memberName7(node);
17210
17393
  if (name?.endsWith("Sync") !== true) return;
17211
17394
  const object = unwrapAwait2(node.object);
17212
17395
  if (object.type === AST_NODE_TYPES83.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
@@ -21405,7 +21588,7 @@ var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
21405
21588
  }
21406
21589
  ]
21407
21590
  };
21408
- function memberName7(node) {
21591
+ function memberName8(node) {
21409
21592
  if (!node.computed && node.property.type === AST_NODE_TYPES98.Identifier)
21410
21593
  return node.property.name;
21411
21594
  if (node.computed && node.property.type === AST_NODE_TYPES98.Literal && typeof node.property.value === "string")
@@ -21423,7 +21606,7 @@ function databaseReceiver(node) {
21423
21606
  if (node.type === AST_NODE_TYPES98.Identifier)
21424
21607
  return DATABASE_NAMES.test(node.name);
21425
21608
  if (node.type !== AST_NODE_TYPES98.MemberExpression) return false;
21426
- const name = memberName7(node);
21609
+ const name = memberName8(node);
21427
21610
  return name === "DB" || name !== null && DATABASE_NAMES.test(name);
21428
21611
  }
21429
21612
  function databaseRootMember(node) {
@@ -21444,7 +21627,7 @@ function databaseRootMember(node) {
21444
21627
  }
21445
21628
  if (current.type === AST_NODE_TYPES98.MemberExpression) {
21446
21629
  if (current.object.type === AST_NODE_TYPES98.ThisExpression)
21447
- return memberName7(current);
21630
+ return memberName8(current);
21448
21631
  current = current.object;
21449
21632
  continue;
21450
21633
  }
@@ -21468,7 +21651,7 @@ function expressionHasDatabaseMarker(node) {
21468
21651
  continue;
21469
21652
  }
21470
21653
  if (current.type === AST_NODE_TYPES98.MemberExpression) {
21471
- const name = memberName7(current);
21654
+ const name = memberName8(current);
21472
21655
  if (name === "DB" || name !== null && DATABASE_NAMES.test(name))
21473
21656
  return true;
21474
21657
  current = current.object;
@@ -21490,7 +21673,7 @@ function expressionCallsBuilder(node) {
21490
21673
  }
21491
21674
  if (current.type === AST_NODE_TYPES98.CallExpression) {
21492
21675
  if (current.callee.type !== AST_NODE_TYPES98.MemberExpression) return false;
21493
- const name = memberName7(current.callee);
21676
+ const name = memberName8(current.callee);
21494
21677
  if (name !== null && BUILDER_METHODS.has(name)) return true;
21495
21678
  current = current.callee.object;
21496
21679
  continue;
@@ -21541,7 +21724,7 @@ function thisRootMember(node) {
21541
21724
  let current = node;
21542
21725
  let root = null;
21543
21726
  while (current.type === AST_NODE_TYPES98.MemberExpression) {
21544
- const name = memberName7(current);
21727
+ const name = memberName8(current);
21545
21728
  if (name === null) return null;
21546
21729
  root = name;
21547
21730
  current = current.object;
@@ -21595,7 +21778,7 @@ var require_sql_access_class_default = createRule({
21595
21778
  CallExpression(node) {
21596
21779
  if (node.callee.type !== AST_NODE_TYPES98.MemberExpression)
21597
21780
  return;
21598
- const method = memberName7(node.callee);
21781
+ const method = memberName8(node.callee);
21599
21782
  if (knownNonDatabase(node.callee.object)) return;
21600
21783
  if (method === null || !isDatabaseOperation(method, node.callee.object))
21601
21784
  return;
@@ -23228,10 +23411,11 @@ import { readFileSync as readFileSync3 } from "fs";
23228
23411
  import { dirname as dirname3, join as join3, relative as relative2 } from "path";
23229
23412
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
23230
23413
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
23414
+ var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
23231
23415
  var NEXT_ROUTE = /^(?:src\/)?app\/(?:.*\/)?route\.[jt]s$/u;
23232
23416
  function isFrameworkEntrypoint(filename, exportKey) {
23233
23417
  const normalized = filename.replaceAll("\\", "/");
23234
- if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy)\.[jt]s$/u.test(normalized)) return false;
23418
+ if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
23235
23419
  const owner = packageOwner(normalized);
23236
23420
  if (owner === null) return false;
23237
23421
  const path = relative2(owner.root, normalized).replaceAll("\\", "/");
@@ -23240,9 +23424,12 @@ function isFrameworkEntrypoint(filename, exportKey) {
23240
23424
  }
23241
23425
  function isNextEntrypoint(path, exportKey) {
23242
23426
  const privateAppFolder = /^(?:src\/)?app\/(?:.*\/)?_[^/]+\//u.test(path);
23243
- if (!privateAppFolder && NEXT_COMPONENT.test(path) && exportKey === "default") return true;
23427
+ if (!privateAppFolder && (NEXT_COMPONENT.test(path) || NEXT_METADATA.test(path)) && exportKey === "default") return true;
23244
23428
  if (!privateAppFolder && NEXT_ROUTE.test(path) && HTTP_METHODS.has(exportKey)) return true;
23245
23429
  if (/^(?:src\/)?pages\/.*\.[jt]sx?$/u.test(path) && exportKey === "default") return true;
23430
+ if (/^(?:src\/)?instrumentation\.[jt]s$/u.test(path)) {
23431
+ return exportKey === "register" || exportKey === "onRequestError";
23432
+ }
23246
23433
  const middleware = /^(?:src\/)?(middleware|proxy)\.[jt]s$/u.exec(path);
23247
23434
  return middleware !== null && (exportKey === "default" || exportKey === middleware[1]);
23248
23435
  }
@@ -23285,7 +23472,7 @@ var SOLE_EXPORT_MATCHES_FILENAME_DOCUMENTATION = {
23285
23472
  "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.",
23286
23473
  "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.",
23287
23474
  "Exported destructuring patterns are excluded rather than undercounted as public exports.",
23288
- "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."
23475
+ "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."
23289
23476
  ],
23290
23477
  examples: [
23291
23478
  { 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 },
@@ -23854,7 +24041,7 @@ var RULES = {
23854
24041
  };
23855
24042
  var meta = {
23856
24043
  name: "@sarj/eslint-plugin",
23857
- version: "16.2.5"
24044
+ version: "16.3.0"
23858
24045
  };
23859
24046
  var APPLICATION_ONLY_RULES = [];
23860
24047
  var LIBRARY_IMPORT_POLICY = ["error", {