@sarj/eslint-plugin 16.2.5 → 16.2.6

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) {
@@ -16975,7 +17087,7 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
16975
17087
  { 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
17088
  ]
16977
17089
  };
16978
- function memberName5(node) {
17090
+ function memberName6(node) {
16979
17091
  if (!node.computed && node.property.type === AST_NODE_TYPES82.Identifier) return node.property.name;
16980
17092
  if (node.computed && node.property.type === AST_NODE_TYPES82.Literal && typeof node.property.value === "string") {
16981
17093
  return node.property.value;
@@ -16991,7 +17103,7 @@ function isCryptoLoader(node, resolve2) {
16991
17103
  if (node.callee.type === AST_NODE_TYPES82.Identifier) {
16992
17104
  return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
16993
17105
  }
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";
17106
+ 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
17107
  }
16996
17108
  function isCryptoSpecifier(node) {
16997
17109
  return node.type === AST_NODE_TYPES82.Literal && (node.value === "crypto" || node.value === "node:crypto");
@@ -17073,14 +17185,14 @@ function importedName6(node) {
17073
17185
  return node.type === AST_NODE_TYPES82.Identifier ? node.name : node.value;
17074
17186
  }
17075
17187
  function isMemberCall(node, name) {
17076
- return node.callee.type === AST_NODE_TYPES82.MemberExpression && memberName5(node.callee) === name;
17188
+ return node.callee.type === AST_NODE_TYPES82.MemberExpression && memberName6(node.callee) === name;
17077
17189
  }
17078
17190
  function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
17079
17191
  if (node.callee.type === AST_NODE_TYPES82.Identifier) {
17080
17192
  const variable2 = resolve2(node.callee);
17081
17193
  return variable2 !== null && directBindings.has(variable2);
17082
17194
  }
17083
- if (node.callee.type !== AST_NODE_TYPES82.MemberExpression || memberName5(node.callee) !== "createHash") {
17195
+ if (node.callee.type !== AST_NODE_TYPES82.MemberExpression || memberName6(node.callee) !== "createHash") {
17084
17196
  return false;
17085
17197
  }
17086
17198
  if (isCryptoLoader(node.callee.object, resolve2)) return true;
@@ -17108,7 +17220,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
17108
17220
  { 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
17221
  ]
17110
17222
  };
17111
- function memberName6(node) {
17223
+ function memberName7(node) {
17112
17224
  if (!node.computed && node.property.type === AST_NODE_TYPES83.Identifier) return node.property.name;
17113
17225
  if (node.computed && node.property.type === AST_NODE_TYPES83.Literal && typeof node.property.value === "string") {
17114
17226
  return node.property.value;
@@ -17125,7 +17237,7 @@ function isFsLoader(node, isGlobal) {
17125
17237
  const [argument] = expression.arguments;
17126
17238
  if (argument === void 0 || argument.type === AST_NODE_TYPES83.SpreadElement || !isFsSpecifier(argument)) return false;
17127
17239
  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";
17240
+ 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
17241
  }
17130
17242
  function isFsSpecifier(node) {
17131
17243
  return node.type === AST_NODE_TYPES83.Literal && (node.value === "node:fs" || node.value === "fs");
@@ -17206,7 +17318,7 @@ var prefer_node_fs_promises_default = createRule({
17206
17318
  }
17207
17319
  },
17208
17320
  MemberExpression(node) {
17209
- const name = memberName6(node);
17321
+ const name = memberName7(node);
17210
17322
  if (name?.endsWith("Sync") !== true) return;
17211
17323
  const object = unwrapAwait2(node.object);
17212
17324
  if (object.type === AST_NODE_TYPES83.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
@@ -21405,7 +21517,7 @@ var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
21405
21517
  }
21406
21518
  ]
21407
21519
  };
21408
- function memberName7(node) {
21520
+ function memberName8(node) {
21409
21521
  if (!node.computed && node.property.type === AST_NODE_TYPES98.Identifier)
21410
21522
  return node.property.name;
21411
21523
  if (node.computed && node.property.type === AST_NODE_TYPES98.Literal && typeof node.property.value === "string")
@@ -21423,7 +21535,7 @@ function databaseReceiver(node) {
21423
21535
  if (node.type === AST_NODE_TYPES98.Identifier)
21424
21536
  return DATABASE_NAMES.test(node.name);
21425
21537
  if (node.type !== AST_NODE_TYPES98.MemberExpression) return false;
21426
- const name = memberName7(node);
21538
+ const name = memberName8(node);
21427
21539
  return name === "DB" || name !== null && DATABASE_NAMES.test(name);
21428
21540
  }
21429
21541
  function databaseRootMember(node) {
@@ -21444,7 +21556,7 @@ function databaseRootMember(node) {
21444
21556
  }
21445
21557
  if (current.type === AST_NODE_TYPES98.MemberExpression) {
21446
21558
  if (current.object.type === AST_NODE_TYPES98.ThisExpression)
21447
- return memberName7(current);
21559
+ return memberName8(current);
21448
21560
  current = current.object;
21449
21561
  continue;
21450
21562
  }
@@ -21468,7 +21580,7 @@ function expressionHasDatabaseMarker(node) {
21468
21580
  continue;
21469
21581
  }
21470
21582
  if (current.type === AST_NODE_TYPES98.MemberExpression) {
21471
- const name = memberName7(current);
21583
+ const name = memberName8(current);
21472
21584
  if (name === "DB" || name !== null && DATABASE_NAMES.test(name))
21473
21585
  return true;
21474
21586
  current = current.object;
@@ -21490,7 +21602,7 @@ function expressionCallsBuilder(node) {
21490
21602
  }
21491
21603
  if (current.type === AST_NODE_TYPES98.CallExpression) {
21492
21604
  if (current.callee.type !== AST_NODE_TYPES98.MemberExpression) return false;
21493
- const name = memberName7(current.callee);
21605
+ const name = memberName8(current.callee);
21494
21606
  if (name !== null && BUILDER_METHODS.has(name)) return true;
21495
21607
  current = current.callee.object;
21496
21608
  continue;
@@ -21541,7 +21653,7 @@ function thisRootMember(node) {
21541
21653
  let current = node;
21542
21654
  let root = null;
21543
21655
  while (current.type === AST_NODE_TYPES98.MemberExpression) {
21544
- const name = memberName7(current);
21656
+ const name = memberName8(current);
21545
21657
  if (name === null) return null;
21546
21658
  root = name;
21547
21659
  current = current.object;
@@ -21595,7 +21707,7 @@ var require_sql_access_class_default = createRule({
21595
21707
  CallExpression(node) {
21596
21708
  if (node.callee.type !== AST_NODE_TYPES98.MemberExpression)
21597
21709
  return;
21598
- const method = memberName7(node.callee);
21710
+ const method = memberName8(node.callee);
21599
21711
  if (knownNonDatabase(node.callee.object)) return;
21600
21712
  if (method === null || !isDatabaseOperation(method, node.callee.object))
21601
21713
  return;
@@ -23228,10 +23340,11 @@ import { readFileSync as readFileSync3 } from "fs";
23228
23340
  import { dirname as dirname3, join as join3, relative as relative2 } from "path";
23229
23341
  var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
23230
23342
  var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
23343
+ var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
23231
23344
  var NEXT_ROUTE = /^(?:src\/)?app\/(?:.*\/)?route\.[jt]s$/u;
23232
23345
  function isFrameworkEntrypoint(filename, exportKey) {
23233
23346
  const normalized = filename.replaceAll("\\", "/");
23234
- if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy)\.[jt]s$/u.test(normalized)) return false;
23347
+ if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
23235
23348
  const owner = packageOwner(normalized);
23236
23349
  if (owner === null) return false;
23237
23350
  const path = relative2(owner.root, normalized).replaceAll("\\", "/");
@@ -23240,9 +23353,12 @@ function isFrameworkEntrypoint(filename, exportKey) {
23240
23353
  }
23241
23354
  function isNextEntrypoint(path, exportKey) {
23242
23355
  const privateAppFolder = /^(?:src\/)?app\/(?:.*\/)?_[^/]+\//u.test(path);
23243
- if (!privateAppFolder && NEXT_COMPONENT.test(path) && exportKey === "default") return true;
23356
+ if (!privateAppFolder && (NEXT_COMPONENT.test(path) || NEXT_METADATA.test(path)) && exportKey === "default") return true;
23244
23357
  if (!privateAppFolder && NEXT_ROUTE.test(path) && HTTP_METHODS.has(exportKey)) return true;
23245
23358
  if (/^(?:src\/)?pages\/.*\.[jt]sx?$/u.test(path) && exportKey === "default") return true;
23359
+ if (/^(?:src\/)?instrumentation\.[jt]s$/u.test(path)) {
23360
+ return exportKey === "register" || exportKey === "onRequestError";
23361
+ }
23246
23362
  const middleware = /^(?:src\/)?(middleware|proxy)\.[jt]s$/u.exec(path);
23247
23363
  return middleware !== null && (exportKey === "default" || exportKey === middleware[1]);
23248
23364
  }
@@ -23285,7 +23401,7 @@ var SOLE_EXPORT_MATCHES_FILENAME_DOCUMENTATION = {
23285
23401
  "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
23402
  "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
23403
  "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."
23404
+ "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
23405
  ],
23290
23406
  examples: [
23291
23407
  { 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 +23970,7 @@ var RULES = {
23854
23970
  };
23855
23971
  var meta = {
23856
23972
  name: "@sarj/eslint-plugin",
23857
- version: "16.2.5"
23973
+ version: "16.2.6"
23858
23974
  };
23859
23975
  var APPLICATION_ONLY_RULES = [];
23860
23976
  var LIBRARY_IMPORT_POLICY = ["error", {