@sarj/eslint-plugin 16.5.1 → 16.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
52
52
  // package.json
53
53
  var package_default = {
54
54
  name: "@sarj/eslint-plugin",
55
- version: "16.5.1",
55
+ version: "16.5.3",
56
56
  packageManager: "npm@12.1.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -716,6 +716,9 @@ var REQUIRE_PORT_FOR_SERVICE_DOCUMENTATION = {
716
716
  remediation: "Declare and implement an interface covering the service's public methods.",
717
717
  category: "architecture",
718
718
  aliases: ["require-interface-for-injected-service"],
719
+ limitations: [
720
+ "Retained injection requires an untouched parameter property or a single direct, unconditional constructor assignment. Conditional, repeated, replaced, updated, or deleted stores are unresolved rather than proof of retained injection."
721
+ ],
719
722
  examples: [
720
723
  { id: "declared-service-port", title: "Implement the service port", outcome: "no-match", files: [{ path: "src/service.ts", source: "interface Handler { handle(): void }\nexport class RequestHandler implements Handler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 0, public: true },
721
724
  { id: "concrete-injected-service", title: "Do not expose only the concrete service", outcome: "match", files: [{ path: "src/service.ts", source: "export class RequestHandler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 1, public: true }
@@ -842,6 +845,8 @@ var readConstructor = (ctor, declared, typeParameters) => {
842
845
  const storedFieldsFrom = /* @__PURE__ */ new Map();
843
846
  const storedMemberFieldsFrom = /* @__PURE__ */ new Map();
844
847
  const constructedFieldNames = /* @__PURE__ */ new Set();
848
+ const writtenFieldNames = /* @__PURE__ */ new Set();
849
+ const unstableFieldNames = /* @__PURE__ */ new Set();
845
850
  const localBindings2 = /* @__PURE__ */ new Set();
846
851
  let constructedFields = 0;
847
852
  if (body2 !== null && body2 !== void 0) scanStoredFields(body2);
@@ -850,14 +855,18 @@ var readConstructor = (ctor, declared, typeParameters) => {
850
855
  function recordStoredField(current) {
851
856
  if (current.type === import_utils4.AST_NODE_TYPES.ArrowFunctionExpression || current.type === import_utils4.AST_NODE_TYPES.FunctionExpression || current.type === import_utils4.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils4.AST_NODE_TYPES.ClassExpression || current.type === import_utils4.AST_NODE_TYPES.ClassDeclaration) return;
852
857
  if (current.type === import_utils4.AST_NODE_TYPES.VariableDeclarator && current.id.type === import_utils4.AST_NODE_TYPES.Identifier) localBindings2.add(current.id.name);
853
- const expression = current.type === import_utils4.AST_NODE_TYPES.ExpressionStatement ? current.expression : null;
854
- const storedField = expression?.type === import_utils4.AST_NODE_TYPES.AssignmentExpression && expression.left.type === import_utils4.AST_NODE_TYPES.MemberExpression && expression.left.object.type === import_utils4.AST_NODE_TYPES.ThisExpression ? staticMemberName(expression.left) : null;
858
+ const expression = current.type === import_utils4.AST_NODE_TYPES.AssignmentExpression ? current : null;
859
+ const storedField = writtenInstanceField(current);
860
+ if (storedField !== null) {
861
+ if (writtenFieldNames.has(storedField)) unstableFieldNames.add(storedField);
862
+ writtenFieldNames.add(storedField);
863
+ if (expression?.operator !== "=" || expression.parent.type !== import_utils4.AST_NODE_TYPES.ExpressionStatement || expression.parent.parent !== body3) unstableFieldNames.add(storedField);
864
+ }
855
865
  if (expression?.type !== import_utils4.AST_NODE_TYPES.AssignmentExpression || !STORAGE_ASSIGNMENT_OPERATORS.has(expression.operator) || expression.left.type !== import_utils4.AST_NODE_TYPES.MemberExpression || expression.left.object.type !== import_utils4.AST_NODE_TYPES.ThisExpression || storedField === null) {
856
866
  enqueueChildren(current);
857
867
  return;
858
868
  }
859
- let source = expression.right;
860
- while (source.type === import_utils4.AST_NODE_TYPES.TSNonNullExpression || source.type === import_utils4.AST_NODE_TYPES.TSAsExpression || source.type === import_utils4.AST_NODE_TYPES.TSSatisfiesExpression || source.type === import_utils4.AST_NODE_TYPES.TSTypeAssertion) source = source.expression;
869
+ const source = unwrapExpression(expression.right);
861
870
  if (source.type === import_utils4.AST_NODE_TYPES.NewExpression) {
862
871
  constructedFields += 1;
863
872
  constructedFieldNames.add(storedField);
@@ -878,6 +887,7 @@ var readConstructor = (ctor, declared, typeParameters) => {
878
887
  storedMemberFieldsFrom.set(source.object.name, members);
879
888
  }
880
889
  }
890
+ enqueueChildren(current);
881
891
  }
882
892
  function enqueueChildren(current) {
883
893
  forEachOwnAstChild(current, (child) => {
@@ -895,16 +905,15 @@ var readConstructor = (ctor, declared, typeParameters) => {
895
905
  for (const reference of parameterCollaborators(parameter, declared, storedMemberFieldsFrom)) {
896
906
  if (localBindings2.has(reference.name)) continue;
897
907
  let fields = reference.fields;
898
- if (parameter.type === import_utils4.AST_NODE_TYPES.TSParameterProperty) fields = [reference.name];
899
- else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
900
- const injectedFields = fields.filter((field) => !constructedFieldNames.has(field));
908
+ if (parameter.type === import_utils4.AST_NODE_TYPES.TSParameterProperty) {
909
+ if (writtenFieldNames.has(reference.name)) continue;
910
+ fields = [reference.name];
911
+ } else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
912
+ const injectedFields = fields.filter((field) => !constructedFieldNames.has(field) && !unstableFieldNames.has(field));
901
913
  if (injectedFields.length === 0) continue;
902
914
  if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
903
915
  if (CONFIGISH_NAME_RE.test(reference.name)) continue;
904
- if (typeParameters.has(reference.typeName)) continue;
905
- if (BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName)) continue;
906
- if (VALUE_TYPE_RE.test(reference.typeName)) continue;
907
- if (declared().functionAliases.has(reference.typeName)) continue;
916
+ if (typeParameters.has(reference.typeName) || BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName) || VALUE_TYPE_RE.test(reference.typeName) || declared().functionAliases.has(reference.typeName)) continue;
908
917
  collaborators.push({ ...reference, fields: injectedFields });
909
918
  }
910
919
  }
@@ -1015,6 +1024,29 @@ var instanceField = (candidate2) => {
1015
1024
  while (node.type === import_utils4.AST_NODE_TYPES.ChainExpression || node.type === import_utils4.AST_NODE_TYPES.TSAsExpression || node.type === import_utils4.AST_NODE_TYPES.TSNonNullExpression || node.type === import_utils4.AST_NODE_TYPES.TSSatisfiesExpression || node.type === import_utils4.AST_NODE_TYPES.TSTypeAssertion) node = node.expression;
1016
1025
  return node.type === import_utils4.AST_NODE_TYPES.MemberExpression && node.object.type === import_utils4.AST_NODE_TYPES.ThisExpression ? staticMemberName(node) : null;
1017
1026
  };
1027
+ function writtenInstanceField(node) {
1028
+ if (node.type === import_utils4.AST_NODE_TYPES.AssignmentExpression || node.type === import_utils4.AST_NODE_TYPES.ForOfStatement || node.type === import_utils4.AST_NODE_TYPES.ForInStatement) return instanceField(node.left);
1029
+ if (node.type === import_utils4.AST_NODE_TYPES.UpdateExpression) return instanceField(node.argument);
1030
+ if (node.type === import_utils4.AST_NODE_TYPES.UnaryExpression && node.operator === "delete") return instanceField(node.argument);
1031
+ if (node.type === import_utils4.AST_NODE_TYPES.MemberExpression && isDestructuredWriteTarget(node)) return instanceField(node);
1032
+ return null;
1033
+ }
1034
+ function isDestructuredWriteTarget(node) {
1035
+ const access = outerExpression(node);
1036
+ let target = access;
1037
+ for (; ; ) {
1038
+ const parent = target.parent;
1039
+ if (parent.type === import_utils4.AST_NODE_TYPES.Property && parent.value === target && parent.parent.type === import_utils4.AST_NODE_TYPES.ObjectPattern) {
1040
+ target = parent.parent;
1041
+ continue;
1042
+ }
1043
+ if (parent.type === import_utils4.AST_NODE_TYPES.ArrayPattern || parent.type === import_utils4.AST_NODE_TYPES.ObjectPattern || parent.type === import_utils4.AST_NODE_TYPES.RestElement && parent.argument === target || parent.type === import_utils4.AST_NODE_TYPES.AssignmentPattern && parent.left === target) {
1044
+ target = parent;
1045
+ continue;
1046
+ }
1047
+ return target !== access && (parent.type === import_utils4.AST_NODE_TYPES.AssignmentExpression || parent.type === import_utils4.AST_NODE_TYPES.ForOfStatement || parent.type === import_utils4.AST_NODE_TYPES.ForInStatement) && parent.left === target;
1048
+ }
1049
+ }
1018
1050
  var behaviorallyInvokedFields = (body2) => {
1019
1051
  const invoked = /* @__PURE__ */ new Set();
1020
1052
  const visit = (current) => {
@@ -1331,13 +1363,23 @@ function createServiceOperationResolver(program) {
1331
1363
  }
1332
1364
  function serviceOperationBodies(node) {
1333
1365
  const bodies = /* @__PURE__ */ new Map();
1366
+ const written = /* @__PURE__ */ new Set();
1367
+ const pending = [node.body];
1368
+ while (pending.length > 0) {
1369
+ const current = pending.pop();
1370
+ if (current === void 0) break;
1371
+ const name = writtenInstanceField(current);
1372
+ if (name !== null) written.add(name);
1373
+ forEachOwnAstChild(current, (child) => {
1374
+ pending.push(child);
1375
+ });
1376
+ }
1334
1377
  for (const member of node.body.body) {
1335
1378
  if (member.type !== import_utils4.AST_NODE_TYPES.MethodDefinition && member.type !== import_utils4.AST_NODE_TYPES.PropertyDefinition) continue;
1336
- if (member.static || member.decorators.length > 0) continue;
1337
- if (member.type === import_utils4.AST_NODE_TYPES.MethodDefinition && member.kind !== "method") continue;
1379
+ if (member.static || member.decorators.length > 0 || member.type === import_utils4.AST_NODE_TYPES.MethodDefinition && member.kind !== "method") continue;
1338
1380
  if (member.value?.type !== import_utils4.AST_NODE_TYPES.FunctionExpression && member.value?.type !== import_utils4.AST_NODE_TYPES.ArrowFunctionExpression) continue;
1339
1381
  const name = declaredMemberName(member);
1340
- if (name !== null && member.value.body !== null) bodies.set(name, member.value.body);
1382
+ if (name !== null && !written.has(name) && member.value.body !== null) bodies.set(name, member.value.body);
1341
1383
  }
1342
1384
  return bodies;
1343
1385
  }
@@ -1450,6 +1492,7 @@ var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1450
1492
  "The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.",
1451
1493
  "Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.",
1452
1494
  "Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.",
1495
+ "Constructor retention requires an untouched parameter property or one direct, unconditional assignment. Written or replaced operation members do not prove their declared bodies execute; external aliases, mutators, and callbacks are not resolved.",
1453
1496
  "Generated files and JavaScript without implements syntax are excluded."
1454
1497
  ],
1455
1498
  examples: [
@@ -12751,11 +12794,12 @@ var import_utils73 = require("@typescript-eslint/utils");
12751
12794
  var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
12752
12795
  summary: "Disallow supported assertions known to pass from literals or immutable scalar setup.",
12753
12796
  rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
12754
- remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
12797
+ remediation: "Assert on a value produced by the behavior under test against an independently specified expectation, or remove only the redundant assertion while preserving coverage.",
12755
12798
  category: "testing",
12756
12799
  limitations: [
12757
12800
  "Only direct supported `expect` matcher calls in recognized test files are inspected. Local expect bindings, regular expressions, and unsupported coercions are excluded; failing constant assertions are not tautologies.",
12758
- "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded."
12801
+ "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
12802
+ "Expected-side runtime dependence, including a complete inline snapshot computed from its actual value, remains manual review; independent static interpolation and intentional partial masking are valid."
12759
12803
  ],
12760
12804
  examples: [
12761
12805
  {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@sarj/eslint-plugin",
4
- version: "16.5.1",
4
+ version: "16.5.3",
5
5
  packageManager: "npm@12.1.0",
6
6
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
7
7
  type: "module",
@@ -665,6 +665,9 @@ var REQUIRE_PORT_FOR_SERVICE_DOCUMENTATION = {
665
665
  remediation: "Declare and implement an interface covering the service's public methods.",
666
666
  category: "architecture",
667
667
  aliases: ["require-interface-for-injected-service"],
668
+ limitations: [
669
+ "Retained injection requires an untouched parameter property or a single direct, unconditional constructor assignment. Conditional, repeated, replaced, updated, or deleted stores are unresolved rather than proof of retained injection."
670
+ ],
668
671
  examples: [
669
672
  { id: "declared-service-port", title: "Implement the service port", outcome: "no-match", files: [{ path: "src/service.ts", source: "interface Handler { handle(): void }\nexport class RequestHandler implements Handler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 0, public: true },
670
673
  { id: "concrete-injected-service", title: "Do not expose only the concrete service", outcome: "match", files: [{ path: "src/service.ts", source: "export class RequestHandler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 1, public: true }
@@ -791,6 +794,8 @@ var readConstructor = (ctor, declared, typeParameters) => {
791
794
  const storedFieldsFrom = /* @__PURE__ */ new Map();
792
795
  const storedMemberFieldsFrom = /* @__PURE__ */ new Map();
793
796
  const constructedFieldNames = /* @__PURE__ */ new Set();
797
+ const writtenFieldNames = /* @__PURE__ */ new Set();
798
+ const unstableFieldNames = /* @__PURE__ */ new Set();
794
799
  const localBindings2 = /* @__PURE__ */ new Set();
795
800
  let constructedFields = 0;
796
801
  if (body2 !== null && body2 !== void 0) scanStoredFields(body2);
@@ -799,14 +804,18 @@ var readConstructor = (ctor, declared, typeParameters) => {
799
804
  function recordStoredField(current) {
800
805
  if (current.type === AST_NODE_TYPES3.ArrowFunctionExpression || current.type === AST_NODE_TYPES3.FunctionExpression || current.type === AST_NODE_TYPES3.FunctionDeclaration || current.type === AST_NODE_TYPES3.ClassExpression || current.type === AST_NODE_TYPES3.ClassDeclaration) return;
801
806
  if (current.type === AST_NODE_TYPES3.VariableDeclarator && current.id.type === AST_NODE_TYPES3.Identifier) localBindings2.add(current.id.name);
802
- const expression = current.type === AST_NODE_TYPES3.ExpressionStatement ? current.expression : null;
803
- const storedField = expression?.type === AST_NODE_TYPES3.AssignmentExpression && expression.left.type === AST_NODE_TYPES3.MemberExpression && expression.left.object.type === AST_NODE_TYPES3.ThisExpression ? staticMemberName(expression.left) : null;
807
+ const expression = current.type === AST_NODE_TYPES3.AssignmentExpression ? current : null;
808
+ const storedField = writtenInstanceField(current);
809
+ if (storedField !== null) {
810
+ if (writtenFieldNames.has(storedField)) unstableFieldNames.add(storedField);
811
+ writtenFieldNames.add(storedField);
812
+ if (expression?.operator !== "=" || expression.parent.type !== AST_NODE_TYPES3.ExpressionStatement || expression.parent.parent !== body3) unstableFieldNames.add(storedField);
813
+ }
804
814
  if (expression?.type !== AST_NODE_TYPES3.AssignmentExpression || !STORAGE_ASSIGNMENT_OPERATORS.has(expression.operator) || expression.left.type !== AST_NODE_TYPES3.MemberExpression || expression.left.object.type !== AST_NODE_TYPES3.ThisExpression || storedField === null) {
805
815
  enqueueChildren(current);
806
816
  return;
807
817
  }
808
- let source = expression.right;
809
- while (source.type === AST_NODE_TYPES3.TSNonNullExpression || source.type === AST_NODE_TYPES3.TSAsExpression || source.type === AST_NODE_TYPES3.TSSatisfiesExpression || source.type === AST_NODE_TYPES3.TSTypeAssertion) source = source.expression;
818
+ const source = unwrapExpression(expression.right);
810
819
  if (source.type === AST_NODE_TYPES3.NewExpression) {
811
820
  constructedFields += 1;
812
821
  constructedFieldNames.add(storedField);
@@ -827,6 +836,7 @@ var readConstructor = (ctor, declared, typeParameters) => {
827
836
  storedMemberFieldsFrom.set(source.object.name, members);
828
837
  }
829
838
  }
839
+ enqueueChildren(current);
830
840
  }
831
841
  function enqueueChildren(current) {
832
842
  forEachOwnAstChild(current, (child) => {
@@ -844,16 +854,15 @@ var readConstructor = (ctor, declared, typeParameters) => {
844
854
  for (const reference of parameterCollaborators(parameter, declared, storedMemberFieldsFrom)) {
845
855
  if (localBindings2.has(reference.name)) continue;
846
856
  let fields = reference.fields;
847
- if (parameter.type === AST_NODE_TYPES3.TSParameterProperty) fields = [reference.name];
848
- else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
849
- const injectedFields = fields.filter((field) => !constructedFieldNames.has(field));
857
+ if (parameter.type === AST_NODE_TYPES3.TSParameterProperty) {
858
+ if (writtenFieldNames.has(reference.name)) continue;
859
+ fields = [reference.name];
860
+ } else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
861
+ const injectedFields = fields.filter((field) => !constructedFieldNames.has(field) && !unstableFieldNames.has(field));
850
862
  if (injectedFields.length === 0) continue;
851
863
  if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
852
864
  if (CONFIGISH_NAME_RE.test(reference.name)) continue;
853
- if (typeParameters.has(reference.typeName)) continue;
854
- if (BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName)) continue;
855
- if (VALUE_TYPE_RE.test(reference.typeName)) continue;
856
- if (declared().functionAliases.has(reference.typeName)) continue;
865
+ if (typeParameters.has(reference.typeName) || BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName) || VALUE_TYPE_RE.test(reference.typeName) || declared().functionAliases.has(reference.typeName)) continue;
857
866
  collaborators.push({ ...reference, fields: injectedFields });
858
867
  }
859
868
  }
@@ -964,6 +973,29 @@ var instanceField = (candidate2) => {
964
973
  while (node.type === AST_NODE_TYPES3.ChainExpression || node.type === AST_NODE_TYPES3.TSAsExpression || node.type === AST_NODE_TYPES3.TSNonNullExpression || node.type === AST_NODE_TYPES3.TSSatisfiesExpression || node.type === AST_NODE_TYPES3.TSTypeAssertion) node = node.expression;
965
974
  return node.type === AST_NODE_TYPES3.MemberExpression && node.object.type === AST_NODE_TYPES3.ThisExpression ? staticMemberName(node) : null;
966
975
  };
976
+ function writtenInstanceField(node) {
977
+ if (node.type === AST_NODE_TYPES3.AssignmentExpression || node.type === AST_NODE_TYPES3.ForOfStatement || node.type === AST_NODE_TYPES3.ForInStatement) return instanceField(node.left);
978
+ if (node.type === AST_NODE_TYPES3.UpdateExpression) return instanceField(node.argument);
979
+ if (node.type === AST_NODE_TYPES3.UnaryExpression && node.operator === "delete") return instanceField(node.argument);
980
+ if (node.type === AST_NODE_TYPES3.MemberExpression && isDestructuredWriteTarget(node)) return instanceField(node);
981
+ return null;
982
+ }
983
+ function isDestructuredWriteTarget(node) {
984
+ const access = outerExpression(node);
985
+ let target = access;
986
+ for (; ; ) {
987
+ const parent = target.parent;
988
+ if (parent.type === AST_NODE_TYPES3.Property && parent.value === target && parent.parent.type === AST_NODE_TYPES3.ObjectPattern) {
989
+ target = parent.parent;
990
+ continue;
991
+ }
992
+ if (parent.type === AST_NODE_TYPES3.ArrayPattern || parent.type === AST_NODE_TYPES3.ObjectPattern || parent.type === AST_NODE_TYPES3.RestElement && parent.argument === target || parent.type === AST_NODE_TYPES3.AssignmentPattern && parent.left === target) {
993
+ target = parent;
994
+ continue;
995
+ }
996
+ return target !== access && (parent.type === AST_NODE_TYPES3.AssignmentExpression || parent.type === AST_NODE_TYPES3.ForOfStatement || parent.type === AST_NODE_TYPES3.ForInStatement) && parent.left === target;
997
+ }
998
+ }
967
999
  var behaviorallyInvokedFields = (body2) => {
968
1000
  const invoked = /* @__PURE__ */ new Set();
969
1001
  const visit = (current) => {
@@ -1280,13 +1312,23 @@ function createServiceOperationResolver(program) {
1280
1312
  }
1281
1313
  function serviceOperationBodies(node) {
1282
1314
  const bodies = /* @__PURE__ */ new Map();
1315
+ const written = /* @__PURE__ */ new Set();
1316
+ const pending = [node.body];
1317
+ while (pending.length > 0) {
1318
+ const current = pending.pop();
1319
+ if (current === void 0) break;
1320
+ const name = writtenInstanceField(current);
1321
+ if (name !== null) written.add(name);
1322
+ forEachOwnAstChild(current, (child) => {
1323
+ pending.push(child);
1324
+ });
1325
+ }
1283
1326
  for (const member of node.body.body) {
1284
1327
  if (member.type !== AST_NODE_TYPES3.MethodDefinition && member.type !== AST_NODE_TYPES3.PropertyDefinition) continue;
1285
- if (member.static || member.decorators.length > 0) continue;
1286
- if (member.type === AST_NODE_TYPES3.MethodDefinition && member.kind !== "method") continue;
1328
+ if (member.static || member.decorators.length > 0 || member.type === AST_NODE_TYPES3.MethodDefinition && member.kind !== "method") continue;
1287
1329
  if (member.value?.type !== AST_NODE_TYPES3.FunctionExpression && member.value?.type !== AST_NODE_TYPES3.ArrowFunctionExpression) continue;
1288
1330
  const name = declaredMemberName(member);
1289
- if (name !== null && member.value.body !== null) bodies.set(name, member.value.body);
1331
+ if (name !== null && !written.has(name) && member.value.body !== null) bodies.set(name, member.value.body);
1290
1332
  }
1291
1333
  return bodies;
1292
1334
  }
@@ -1399,6 +1441,7 @@ var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
1399
1441
  "The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.",
1400
1442
  "Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.",
1401
1443
  "Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.",
1444
+ "Constructor retention requires an untouched parameter property or one direct, unconditional assignment. Written or replaced operation members do not prove their declared bodies execute; external aliases, mutators, and callbacks are not resolved.",
1402
1445
  "Generated files and JavaScript without implements syntax are excluded."
1403
1446
  ],
1404
1447
  examples: [
@@ -12735,11 +12778,12 @@ import { AST_NODE_TYPES as AST_NODE_TYPES60, ASTUtils as ASTUtils35 } from "@typ
12735
12778
  var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
12736
12779
  summary: "Disallow supported assertions known to pass from literals or immutable scalar setup.",
12737
12780
  rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
12738
- remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
12781
+ remediation: "Assert on a value produced by the behavior under test against an independently specified expectation, or remove only the redundant assertion while preserving coverage.",
12739
12782
  category: "testing",
12740
12783
  limitations: [
12741
12784
  "Only direct supported `expect` matcher calls in recognized test files are inspected. Local expect bindings, regular expressions, and unsupported coercions are excluded; failing constant assertions are not tautologies.",
12742
- "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded."
12785
+ "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
12786
+ "Expected-side runtime dependence, including a complete inline snapshot computed from its actual value, remains manual review; independent static interpolation and intentional partial masking are valid."
12743
12787
  ],
12744
12788
  examples: [
12745
12789
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.5.1",
3
+ "version": "16.5.3",
4
4
  "packageManager": "npm@12.1.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",