@sarj/eslint-plugin 16.19.1 → 16.19.2

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.19.1",
55
+ version: "16.19.2",
56
56
  packageManager: "npm@12.2.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -14349,13 +14349,14 @@ var no_duplicate_test_case_default = createRule({
14349
14349
  // src/rules/no-tautological-expect.ts
14350
14350
  var import_utils80 = require("@typescript-eslint/utils");
14351
14351
  var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14352
- summary: "Disallow supported assertions known to pass from literals or immutable scalar setup.",
14353
- rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
14352
+ summary: "Disallow supported assertions fixed by literal setup or immutable self identity.",
14353
+ rationale: "An assertion fixed by literal setup or one immutable binding's identity cannot distinguish a wrong result from the intended behavior.",
14354
14354
  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.",
14355
14355
  category: "testing",
14356
14356
  limitations: [
14357
14357
  "Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases, static string messages on imported Vitest/Playwright expect calls, and a single `.not` on zero-argument matchers. Local or type-only bindings, regular expressions, async modifiers and unsupported coercions are excluded; failing constant assertions are not tautologies.",
14358
- "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
14358
+ "The literal proof resolves scalar const bindings and aliases through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded from that proof.",
14359
+ "Positive toBe assertions on the same initialized, unreassigned local identifier const binding also have fixed identity, including runtime results. Different bindings, imported live values, property/getter accesses and deep-equality reflexivity remain excluded from this identity proof.",
14359
14360
  "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.",
14360
14361
  "Direct framework expect.extend registrations exclude replaced built-in matchers; opaque registrations exclude all supported matchers, and nonempty custom equality testers exclude deep equality. Direct synchronous expect callbacks observed by a throwing matcher retain their assertion contracts. Cross-file or indirect matcher customization is not inferred."
14361
14362
  ],
@@ -14477,6 +14478,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14477
14478
  focusPath: "src/assertion.test.ts",
14478
14479
  expectedCount: 0,
14479
14480
  public: true
14481
+ },
14482
+ {
14483
+ id: "self-identity-oracle",
14484
+ scenarioId: "immutable-identity",
14485
+ title: "A returned value's identity with itself cannot reject a wrong result",
14486
+ outcome: "match",
14487
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result).toBe(result);" }],
14488
+ focusPath: "src/service.test.ts",
14489
+ expectedCount: 1,
14490
+ public: true
14491
+ },
14492
+ {
14493
+ id: "independent-result-oracle",
14494
+ scenarioId: "immutable-identity",
14495
+ title: "An independent expected state can reject a wrong result",
14496
+ outcome: "no-match",
14497
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result.status).toBe('ready');" }],
14498
+ focusPath: "src/service.test.ts",
14499
+ expectedCount: 0,
14500
+ public: true
14480
14501
  }
14481
14502
  ]
14482
14503
  };
@@ -14628,6 +14649,15 @@ function collectCustomization(node, source, customized) {
14628
14649
  customized.add(name ?? "*");
14629
14650
  }
14630
14651
  }
14652
+ function hasConstSelfIdentity(actual, expected, source) {
14653
+ actual = unwrapExpression(actual);
14654
+ expected = unwrapExpression(expected);
14655
+ if (actual.type !== import_utils80.AST_NODE_TYPES.Identifier || expected.type !== import_utils80.AST_NODE_TYPES.Identifier) return false;
14656
+ const variable = import_utils80.ASTUtils.findVariable(source.getScope(actual), actual.name);
14657
+ if (variable?.defs.length !== 1 || import_utils80.ASTUtils.findVariable(source.getScope(expected), expected.name) !== variable) return false;
14658
+ const definition = variable.defs[0];
14659
+ return definition?.node.type === import_utils80.AST_NODE_TYPES.VariableDeclarator && definition.node.id.type === import_utils80.AST_NODE_TYPES.Identifier && definition.node.parent.kind === "const" && definition.node.init !== null && definition.node.init.range[1] <= Math.min(actual.range[0], expected.range[0]) && !variable.references.some((reference) => reference.isWrite() && !reference.init);
14660
+ }
14631
14661
  var no_tautological_expect_default = createRule({
14632
14662
  name: "no-tautological-expect",
14633
14663
  documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
@@ -14638,7 +14668,7 @@ var no_tautological_expect_default = createRule({
14638
14668
  },
14639
14669
  schema: [],
14640
14670
  messages: {
14641
- tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by literal setup and does not observe behavior. Assert on a produced value or remove only the redundant assertion, preserving other coverage.",
14671
+ tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by {{reason}} and does not observe behavior. Assert against an independent expectation or remove only the redundant assertion, preserving the action and other coverage.",
14642
14672
  tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
14643
14673
  }
14644
14674
  },
@@ -14659,10 +14689,19 @@ var no_tautological_expect_default = createRule({
14659
14689
  const matcher = import_utils80.ASTUtils.getPropertyName(callee);
14660
14690
  if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
14661
14691
  const assertion = expectOperand(callee, context.sourceCode);
14662
- if (assertion === null) return;
14692
+ if (assertion === null || isExceptionContract(node, context.sourceCode)) return;
14663
14693
  const writtenOperand = assertion.operand;
14694
+ const writtenExpected = node.arguments[0];
14695
+ if (!assertion.negated && matcher === "toBe" && node.arguments.length === 1 && writtenExpected !== void 0 && hasConstSelfIdentity(writtenOperand, writtenExpected, context.sourceCode)) {
14696
+ context.report({
14697
+ node,
14698
+ messageId: "tautologicalComparison",
14699
+ data: { operand: preview2(writtenOperand), matcher, reason: "the same immutable binding" }
14700
+ });
14701
+ return;
14702
+ }
14664
14703
  const operand = resolveLiteral(writtenOperand, context.sourceCode);
14665
- if (operand === null || isExceptionContract(node, context.sourceCode)) {
14704
+ if (operand === null) {
14666
14705
  return;
14667
14706
  }
14668
14707
  const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
@@ -14674,7 +14713,6 @@ var no_tautological_expect_default = createRule({
14674
14713
  });
14675
14714
  return;
14676
14715
  }
14677
- const writtenExpected = node.arguments[0];
14678
14716
  const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
14679
14717
  if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
14680
14718
  return;
@@ -14688,7 +14726,7 @@ var no_tautological_expect_default = createRule({
14688
14726
  context.report({
14689
14727
  node,
14690
14728
  messageId: "tautologicalComparison",
14691
- data: { operand: preview2(writtenOperand), matcher }
14729
+ data: { operand: preview2(writtenOperand), matcher, reason: "literal setup" }
14692
14730
  });
14693
14731
  }
14694
14732
  };
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.19.1",
4
+ version: "16.19.2",
5
5
  packageManager: "npm@12.2.0",
6
6
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
7
7
  type: "module",
@@ -14330,13 +14330,14 @@ var no_duplicate_test_case_default = createRule({
14330
14330
  // src/rules/no-tautological-expect.ts
14331
14331
  import { AST_NODE_TYPES as AST_NODE_TYPES65, ASTUtils as ASTUtils40 } from "@typescript-eslint/utils";
14332
14332
  var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14333
- summary: "Disallow supported assertions known to pass from literals or immutable scalar setup.",
14334
- rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
14333
+ summary: "Disallow supported assertions fixed by literal setup or immutable self identity.",
14334
+ rationale: "An assertion fixed by literal setup or one immutable binding's identity cannot distinguish a wrong result from the intended behavior.",
14335
14335
  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.",
14336
14336
  category: "testing",
14337
14337
  limitations: [
14338
14338
  "Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases, static string messages on imported Vitest/Playwright expect calls, and a single `.not` on zero-argument matchers. Local or type-only bindings, regular expressions, async modifiers and unsupported coercions are excluded; failing constant assertions are not tautologies.",
14339
- "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
14339
+ "The literal proof resolves scalar const bindings and aliases through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded from that proof.",
14340
+ "Positive toBe assertions on the same initialized, unreassigned local identifier const binding also have fixed identity, including runtime results. Different bindings, imported live values, property/getter accesses and deep-equality reflexivity remain excluded from this identity proof.",
14340
14341
  "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.",
14341
14342
  "Direct framework expect.extend registrations exclude replaced built-in matchers; opaque registrations exclude all supported matchers, and nonempty custom equality testers exclude deep equality. Direct synchronous expect callbacks observed by a throwing matcher retain their assertion contracts. Cross-file or indirect matcher customization is not inferred."
14342
14343
  ],
@@ -14458,6 +14459,26 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14458
14459
  focusPath: "src/assertion.test.ts",
14459
14460
  expectedCount: 0,
14460
14461
  public: true
14462
+ },
14463
+ {
14464
+ id: "self-identity-oracle",
14465
+ scenarioId: "immutable-identity",
14466
+ title: "A returned value's identity with itself cannot reject a wrong result",
14467
+ outcome: "match",
14468
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result).toBe(result);" }],
14469
+ focusPath: "src/service.test.ts",
14470
+ expectedCount: 1,
14471
+ public: true
14472
+ },
14473
+ {
14474
+ id: "independent-result-oracle",
14475
+ scenarioId: "immutable-identity",
14476
+ title: "An independent expected state can reject a wrong result",
14477
+ outcome: "no-match",
14478
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; const result = serviceStatus(); expect(result.status).toBe('ready');" }],
14479
+ focusPath: "src/service.test.ts",
14480
+ expectedCount: 0,
14481
+ public: true
14461
14482
  }
14462
14483
  ]
14463
14484
  };
@@ -14609,6 +14630,15 @@ function collectCustomization(node, source, customized) {
14609
14630
  customized.add(name ?? "*");
14610
14631
  }
14611
14632
  }
14633
+ function hasConstSelfIdentity(actual, expected, source) {
14634
+ actual = unwrapExpression(actual);
14635
+ expected = unwrapExpression(expected);
14636
+ if (actual.type !== AST_NODE_TYPES65.Identifier || expected.type !== AST_NODE_TYPES65.Identifier) return false;
14637
+ const variable = ASTUtils40.findVariable(source.getScope(actual), actual.name);
14638
+ if (variable?.defs.length !== 1 || ASTUtils40.findVariable(source.getScope(expected), expected.name) !== variable) return false;
14639
+ const definition = variable.defs[0];
14640
+ return definition?.node.type === AST_NODE_TYPES65.VariableDeclarator && definition.node.id.type === AST_NODE_TYPES65.Identifier && definition.node.parent.kind === "const" && definition.node.init !== null && definition.node.init.range[1] <= Math.min(actual.range[0], expected.range[0]) && !variable.references.some((reference) => reference.isWrite() && !reference.init);
14641
+ }
14612
14642
  var no_tautological_expect_default = createRule({
14613
14643
  name: "no-tautological-expect",
14614
14644
  documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
@@ -14619,7 +14649,7 @@ var no_tautological_expect_default = createRule({
14619
14649
  },
14620
14650
  schema: [],
14621
14651
  messages: {
14622
- tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by literal setup and does not observe behavior. Assert on a produced value or remove only the redundant assertion, preserving other coverage.",
14652
+ tautologicalComparison: "`expect({{operand}}).{{matcher}}({{operand}})` is fixed by {{reason}} and does not observe behavior. Assert against an independent expectation or remove only the redundant assertion, preserving the action and other coverage.",
14623
14653
  tautologicalMatcher: "`expect({{operand}}).{{matcher}}()` is statically known to pass. Assert on a produced value or remove only the redundant assertion, preserving other coverage."
14624
14654
  }
14625
14655
  },
@@ -14640,10 +14670,19 @@ var no_tautological_expect_default = createRule({
14640
14670
  const matcher = ASTUtils40.getPropertyName(callee);
14641
14671
  if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
14642
14672
  const assertion = expectOperand(callee, context.sourceCode);
14643
- if (assertion === null) return;
14673
+ if (assertion === null || isExceptionContract(node, context.sourceCode)) return;
14644
14674
  const writtenOperand = assertion.operand;
14675
+ const writtenExpected = node.arguments[0];
14676
+ if (!assertion.negated && matcher === "toBe" && node.arguments.length === 1 && writtenExpected !== void 0 && hasConstSelfIdentity(writtenOperand, writtenExpected, context.sourceCode)) {
14677
+ context.report({
14678
+ node,
14679
+ messageId: "tautologicalComparison",
14680
+ data: { operand: preview2(writtenOperand), matcher, reason: "the same immutable binding" }
14681
+ });
14682
+ return;
14683
+ }
14645
14684
  const operand = resolveLiteral(writtenOperand, context.sourceCode);
14646
- if (operand === null || isExceptionContract(node, context.sourceCode)) {
14685
+ if (operand === null) {
14647
14686
  return;
14648
14687
  }
14649
14688
  const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
@@ -14655,7 +14694,6 @@ var no_tautological_expect_default = createRule({
14655
14694
  });
14656
14695
  return;
14657
14696
  }
14658
- const writtenExpected = node.arguments[0];
14659
14697
  const expected = writtenExpected === void 0 ? null : resolveLiteral(writtenExpected, context.sourceCode);
14660
14698
  if (assertion.negated || !EQUALITY_MATCHERS.has(matcher) || node.arguments.length !== 1 || expected === null) {
14661
14699
  return;
@@ -14669,7 +14707,7 @@ var no_tautological_expect_default = createRule({
14669
14707
  context.report({
14670
14708
  node,
14671
14709
  messageId: "tautologicalComparison",
14672
- data: { operand: preview2(writtenOperand), matcher }
14710
+ data: { operand: preview2(writtenOperand), matcher, reason: "literal setup" }
14673
14711
  });
14674
14712
  }
14675
14713
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.19.1",
3
+ "version": "16.19.2",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",