@sarj/eslint-plugin 16.19.0 → 16.19.1

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.0",
55
+ version: "16.19.1",
56
56
  packageManager: "npm@12.2.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -14354,9 +14354,10 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
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
- "Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases 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.",
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
14358
  "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
14359
- "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."
14359
+ "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
+ "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."
14360
14361
  ],
14361
14362
  examples: [
14362
14363
  {
@@ -14416,6 +14417,66 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14416
14417
  focusPath: "src/service.test.ts",
14417
14418
  expectedCount: 0,
14418
14419
  public: true
14420
+ },
14421
+ {
14422
+ id: "message-only-oracle",
14423
+ scenarioId: "static-message",
14424
+ title: "A diagnostic string does not make a literal comparison observe behavior",
14425
+ outcome: "match",
14426
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; expect(true, 'service ready').toBe(true);" }],
14427
+ focusPath: "src/service.test.ts",
14428
+ expectedCount: 1,
14429
+ public: true
14430
+ },
14431
+ {
14432
+ id: "message-produced-oracle",
14433
+ scenarioId: "static-message",
14434
+ title: "A diagnostic message can describe an observed result",
14435
+ outcome: "no-match",
14436
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; expect(serviceReady(), 'service ready').toBe(true);" }],
14437
+ focusPath: "src/service.test.ts",
14438
+ expectedCount: 0,
14439
+ public: true
14440
+ },
14441
+ {
14442
+ id: "uncaptured-matcher",
14443
+ scenarioId: "matcher-contract",
14444
+ title: "An uncaptured literal assertion remains redundant",
14445
+ outcome: "match",
14446
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect(true).toBeTruthy();" }],
14447
+ focusPath: "src/assertion.test.ts",
14448
+ expectedCount: 1,
14449
+ public: true
14450
+ },
14451
+ {
14452
+ id: "captured-matcher",
14453
+ scenarioId: "matcher-contract",
14454
+ title: "A throwing matcher observes the inner assertion contract",
14455
+ outcome: "no-match",
14456
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect(() => expect(true).toBeTruthy()).not.toThrow();" }],
14457
+ focusPath: "src/assertion.test.ts",
14458
+ expectedCount: 0,
14459
+ public: true
14460
+ },
14461
+ {
14462
+ id: "unrelated-custom-matcher",
14463
+ scenarioId: "custom-matcher",
14464
+ title: "Adding a different matcher preserves the built-in proof",
14465
+ outcome: "match",
14466
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect.extend({toBeReady: custom}); expect(true).toBe(true);" }],
14467
+ focusPath: "src/assertion.test.ts",
14468
+ expectedCount: 1,
14469
+ public: true
14470
+ },
14471
+ {
14472
+ id: "replaced-custom-matcher",
14473
+ scenarioId: "custom-matcher",
14474
+ title: "A replaced matcher can reject a literal comparison",
14475
+ outcome: "no-match",
14476
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect.extend({toBe: () => ({pass: false, message: () => 'custom contract'})}); expect(true).toBe(true);" }],
14477
+ focusPath: "src/assertion.test.ts",
14478
+ expectedCount: 0,
14479
+ public: true
14419
14480
  }
14420
14481
  ]
14421
14482
  };
@@ -14430,6 +14491,7 @@ var ZERO_ARG_MATCHERS = /* @__PURE__ */ new Set([
14430
14491
  ]);
14431
14492
  var OPERAND_PREVIEW_CHARS = 40;
14432
14493
  var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
14494
+ var THROW_MATCHERS = /* @__PURE__ */ new Set(["toThrow", "toThrowError", "toThrowErrorMatchingSnapshot", "toThrowErrorMatchingInlineSnapshot"]);
14433
14495
  function resolveLiteral(node, sourceCode, seen = /* @__PURE__ */ new Set()) {
14434
14496
  node = unwrapExpression(node);
14435
14497
  if (isLiteral(node)) return node;
@@ -14505,12 +14567,67 @@ function expectOperand(callee, sourceCode) {
14505
14567
  const negated = receiver.type === import_utils80.AST_NODE_TYPES.MemberExpression && import_utils80.ASTUtils.getPropertyName(receiver) === "not";
14506
14568
  if (negated && receiver.type === import_utils80.AST_NODE_TYPES.MemberExpression) receiver = unwrapExpression(receiver.object);
14507
14569
  const expectCallee = receiver.type === import_utils80.AST_NODE_TYPES.CallExpression ? unwrapExpression(receiver.callee) : null;
14508
- if (receiver.type !== import_utils80.AST_NODE_TYPES.CallExpression || expectCallee?.type !== import_utils80.AST_NODE_TYPES.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1) {
14570
+ if (receiver.type !== import_utils80.AST_NODE_TYPES.CallExpression || expectCallee?.type !== import_utils80.AST_NODE_TYPES.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1 && !hasStaticMessage(receiver, expectCallee, sourceCode)) {
14509
14571
  return null;
14510
14572
  }
14511
14573
  const operand = receiver.arguments[0];
14512
14574
  return operand === void 0 ? null : { operand, negated };
14513
14575
  }
14576
+ function hasStaticMessage(call, callee, source) {
14577
+ if (call.arguments.length !== 2) return false;
14578
+ const definition = import_utils80.ASTUtils.findVariable(source.getScope(callee), callee.name)?.defs[0];
14579
+ if (definition?.type !== "ImportBinding" || definition.parent.type !== import_utils80.AST_NODE_TYPES.ImportDeclaration || !["vitest", "@playwright/test"].includes(definition.parent.source.value)) return false;
14580
+ const message = call.arguments[1];
14581
+ const literal2 = message === void 0 ? null : resolveLiteral(message, source);
14582
+ return literal2?.type === import_utils80.AST_NODE_TYPES.Literal && typeof literal2.value === "string" || literal2?.type === import_utils80.AST_NODE_TYPES.TemplateLiteral;
14583
+ }
14584
+ function isExceptionContract(node, source) {
14585
+ let callback = node.parent;
14586
+ while (callback !== void 0 && callback.type !== import_utils80.AST_NODE_TYPES.Program && callback.type !== import_utils80.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils80.AST_NODE_TYPES.FunctionExpression && callback.type !== import_utils80.AST_NODE_TYPES.FunctionDeclaration) callback = callback.parent;
14587
+ if (callback === void 0 || callback.type === import_utils80.AST_NODE_TYPES.Program || callback.type === import_utils80.AST_NODE_TYPES.FunctionDeclaration || callback.async || callback.generator) return false;
14588
+ const call = directArgumentCall(callback);
14589
+ if (call === null || call.optional) return false;
14590
+ let member = outerExpression(call).parent;
14591
+ if (member?.type === import_utils80.AST_NODE_TYPES.MemberExpression && import_utils80.ASTUtils.getPropertyName(member) === "not") member = outerExpression(member).parent;
14592
+ if (member?.type !== import_utils80.AST_NODE_TYPES.MemberExpression || member.optional || !THROW_MATCHERS.has(import_utils80.ASTUtils.getPropertyName(member) ?? "")) return false;
14593
+ const invocation = outerExpression(member).parent;
14594
+ return invocation?.type === import_utils80.AST_NODE_TYPES.CallExpression && !invocation.optional && unwrapExpression(expectOperand(member, source)?.operand ?? member) === callback;
14595
+ }
14596
+ function customizedMatchers(source) {
14597
+ const customized = /* @__PURE__ */ new Set();
14598
+ const visit = (node) => {
14599
+ if (node.type === import_utils80.AST_NODE_TYPES.CallExpression) collectCustomization(node, source, customized);
14600
+ for (const key of source.visitorKeys[node.type] ?? []) {
14601
+ const child = node[key];
14602
+ for (const value of Array.isArray(child) ? child : [child]) {
14603
+ if (typeof value === "object" && value !== null && "type" in value) visit(value);
14604
+ }
14605
+ }
14606
+ };
14607
+ visit(source.ast);
14608
+ return customized;
14609
+ }
14610
+ function collectCustomization(node, source, customized) {
14611
+ const member = unwrapExpression(node.callee);
14612
+ if (member.type !== import_utils80.AST_NODE_TYPES.MemberExpression) return;
14613
+ const receiver = unwrapExpression(member.object);
14614
+ if (receiver.type !== import_utils80.AST_NODE_TYPES.Identifier || runtimeTestFrameworkName(source, receiver, "assertion") !== "expect") return;
14615
+ const method = import_utils80.ASTUtils.getPropertyName(member);
14616
+ const argument = node.arguments[0] === void 0 ? null : unwrapExpression(node.arguments[0]);
14617
+ if (method === "addEqualityTesters" && !(argument?.type === import_utils80.AST_NODE_TYPES.ArrayExpression && argument.elements.length === 0)) {
14618
+ customized.add("toEqual");
14619
+ customized.add("toStrictEqual");
14620
+ }
14621
+ if (method !== "extend") return;
14622
+ if (argument?.type !== import_utils80.AST_NODE_TYPES.ObjectExpression) {
14623
+ customized.add("*");
14624
+ return;
14625
+ }
14626
+ for (const property of argument.properties) {
14627
+ const name = property.type === import_utils80.AST_NODE_TYPES.Property ? import_utils80.ASTUtils.getPropertyName(property) : null;
14628
+ customized.add(name ?? "*");
14629
+ }
14630
+ }
14514
14631
  var no_tautological_expect_default = createRule({
14515
14632
  name: "no-tautological-expect",
14516
14633
  documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
@@ -14530,6 +14647,7 @@ var no_tautological_expect_default = createRule({
14530
14647
  if (!isTestFile(context.filename)) {
14531
14648
  return {};
14532
14649
  }
14650
+ const customized = customizedMatchers(context.sourceCode);
14533
14651
  const preview2 = (node) => {
14534
14652
  const text = context.sourceCode.getText(node).replaceAll(/\s+/gu, " ");
14535
14653
  return text.length > OPERAND_PREVIEW_CHARS ? `${text.slice(0, OPERAND_PREVIEW_CHARS)}\u2026` : text;
@@ -14539,12 +14657,12 @@ var no_tautological_expect_default = createRule({
14539
14657
  const callee = unwrapExpression(node.callee);
14540
14658
  if (callee.type !== import_utils80.AST_NODE_TYPES.MemberExpression) return;
14541
14659
  const matcher = import_utils80.ASTUtils.getPropertyName(callee);
14542
- if (matcher === null) return;
14660
+ if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
14543
14661
  const assertion = expectOperand(callee, context.sourceCode);
14544
14662
  if (assertion === null) return;
14545
14663
  const writtenOperand = assertion.operand;
14546
14664
  const operand = resolveLiteral(writtenOperand, context.sourceCode);
14547
- if (operand === null) {
14665
+ if (operand === null || isExceptionContract(node, context.sourceCode)) {
14548
14666
  return;
14549
14667
  }
14550
14668
  const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
@@ -26014,7 +26132,7 @@ function classMethodCounts(body2, methods) {
26014
26132
 
26015
26133
  // src/rules/source-coupled-test.ts
26016
26134
  var import_utils136 = require("@typescript-eslint/utils");
26017
- var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc?|toml|py|[cm]?[jt]s)$/iu;
26135
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|[cm]?[jt]s)$|(?<!\.(?:tf|tftest))\.json$/iu;
26018
26136
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
26019
26137
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
26020
26138
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
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.0",
4
+ version: "16.19.1",
5
5
  packageManager: "npm@12.2.0",
6
6
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
7
7
  type: "module",
@@ -14335,9 +14335,10 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
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
- "Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases 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.",
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
14339
  "Scalar const bindings and aliases resolve through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded.",
14340
- "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."
14340
+ "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
+ "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."
14341
14342
  ],
14342
14343
  examples: [
14343
14344
  {
@@ -14397,6 +14398,66 @@ var NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION = {
14397
14398
  focusPath: "src/service.test.ts",
14398
14399
  expectedCount: 0,
14399
14400
  public: true
14401
+ },
14402
+ {
14403
+ id: "message-only-oracle",
14404
+ scenarioId: "static-message",
14405
+ title: "A diagnostic string does not make a literal comparison observe behavior",
14406
+ outcome: "match",
14407
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; expect(true, 'service ready').toBe(true);" }],
14408
+ focusPath: "src/service.test.ts",
14409
+ expectedCount: 1,
14410
+ public: true
14411
+ },
14412
+ {
14413
+ id: "message-produced-oracle",
14414
+ scenarioId: "static-message",
14415
+ title: "A diagnostic message can describe an observed result",
14416
+ outcome: "no-match",
14417
+ files: [{ path: "src/service.test.ts", source: "import {expect} from 'vitest'; expect(serviceReady(), 'service ready').toBe(true);" }],
14418
+ focusPath: "src/service.test.ts",
14419
+ expectedCount: 0,
14420
+ public: true
14421
+ },
14422
+ {
14423
+ id: "uncaptured-matcher",
14424
+ scenarioId: "matcher-contract",
14425
+ title: "An uncaptured literal assertion remains redundant",
14426
+ outcome: "match",
14427
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect(true).toBeTruthy();" }],
14428
+ focusPath: "src/assertion.test.ts",
14429
+ expectedCount: 1,
14430
+ public: true
14431
+ },
14432
+ {
14433
+ id: "captured-matcher",
14434
+ scenarioId: "matcher-contract",
14435
+ title: "A throwing matcher observes the inner assertion contract",
14436
+ outcome: "no-match",
14437
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect(() => expect(true).toBeTruthy()).not.toThrow();" }],
14438
+ focusPath: "src/assertion.test.ts",
14439
+ expectedCount: 0,
14440
+ public: true
14441
+ },
14442
+ {
14443
+ id: "unrelated-custom-matcher",
14444
+ scenarioId: "custom-matcher",
14445
+ title: "Adding a different matcher preserves the built-in proof",
14446
+ outcome: "match",
14447
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect.extend({toBeReady: custom}); expect(true).toBe(true);" }],
14448
+ focusPath: "src/assertion.test.ts",
14449
+ expectedCount: 1,
14450
+ public: true
14451
+ },
14452
+ {
14453
+ id: "replaced-custom-matcher",
14454
+ scenarioId: "custom-matcher",
14455
+ title: "A replaced matcher can reject a literal comparison",
14456
+ outcome: "no-match",
14457
+ files: [{ path: "src/assertion.test.ts", source: "import {expect} from 'vitest'; expect.extend({toBe: () => ({pass: false, message: () => 'custom contract'})}); expect(true).toBe(true);" }],
14458
+ focusPath: "src/assertion.test.ts",
14459
+ expectedCount: 0,
14460
+ public: true
14400
14461
  }
14401
14462
  ]
14402
14463
  };
@@ -14411,6 +14472,7 @@ var ZERO_ARG_MATCHERS = /* @__PURE__ */ new Set([
14411
14472
  ]);
14412
14473
  var OPERAND_PREVIEW_CHARS = 40;
14413
14474
  var NUMERIC_SIGNS = /* @__PURE__ */ new Set(["-", "+"]);
14475
+ var THROW_MATCHERS = /* @__PURE__ */ new Set(["toThrow", "toThrowError", "toThrowErrorMatchingSnapshot", "toThrowErrorMatchingInlineSnapshot"]);
14414
14476
  function resolveLiteral(node, sourceCode, seen = /* @__PURE__ */ new Set()) {
14415
14477
  node = unwrapExpression(node);
14416
14478
  if (isLiteral(node)) return node;
@@ -14486,12 +14548,67 @@ function expectOperand(callee, sourceCode) {
14486
14548
  const negated = receiver.type === AST_NODE_TYPES65.MemberExpression && ASTUtils40.getPropertyName(receiver) === "not";
14487
14549
  if (negated && receiver.type === AST_NODE_TYPES65.MemberExpression) receiver = unwrapExpression(receiver.object);
14488
14550
  const expectCallee = receiver.type === AST_NODE_TYPES65.CallExpression ? unwrapExpression(receiver.callee) : null;
14489
- if (receiver.type !== AST_NODE_TYPES65.CallExpression || expectCallee?.type !== AST_NODE_TYPES65.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1) {
14551
+ if (receiver.type !== AST_NODE_TYPES65.CallExpression || expectCallee?.type !== AST_NODE_TYPES65.Identifier || runtimeTestFrameworkName(sourceCode, expectCallee, "assertion") !== "expect" || receiver.arguments.length !== 1 && !hasStaticMessage(receiver, expectCallee, sourceCode)) {
14490
14552
  return null;
14491
14553
  }
14492
14554
  const operand = receiver.arguments[0];
14493
14555
  return operand === void 0 ? null : { operand, negated };
14494
14556
  }
14557
+ function hasStaticMessage(call, callee, source) {
14558
+ if (call.arguments.length !== 2) return false;
14559
+ const definition = ASTUtils40.findVariable(source.getScope(callee), callee.name)?.defs[0];
14560
+ if (definition?.type !== "ImportBinding" || definition.parent.type !== AST_NODE_TYPES65.ImportDeclaration || !["vitest", "@playwright/test"].includes(definition.parent.source.value)) return false;
14561
+ const message = call.arguments[1];
14562
+ const literal2 = message === void 0 ? null : resolveLiteral(message, source);
14563
+ return literal2?.type === AST_NODE_TYPES65.Literal && typeof literal2.value === "string" || literal2?.type === AST_NODE_TYPES65.TemplateLiteral;
14564
+ }
14565
+ function isExceptionContract(node, source) {
14566
+ let callback = node.parent;
14567
+ while (callback !== void 0 && callback.type !== AST_NODE_TYPES65.Program && callback.type !== AST_NODE_TYPES65.ArrowFunctionExpression && callback.type !== AST_NODE_TYPES65.FunctionExpression && callback.type !== AST_NODE_TYPES65.FunctionDeclaration) callback = callback.parent;
14568
+ if (callback === void 0 || callback.type === AST_NODE_TYPES65.Program || callback.type === AST_NODE_TYPES65.FunctionDeclaration || callback.async || callback.generator) return false;
14569
+ const call = directArgumentCall(callback);
14570
+ if (call === null || call.optional) return false;
14571
+ let member = outerExpression(call).parent;
14572
+ if (member?.type === AST_NODE_TYPES65.MemberExpression && ASTUtils40.getPropertyName(member) === "not") member = outerExpression(member).parent;
14573
+ if (member?.type !== AST_NODE_TYPES65.MemberExpression || member.optional || !THROW_MATCHERS.has(ASTUtils40.getPropertyName(member) ?? "")) return false;
14574
+ const invocation = outerExpression(member).parent;
14575
+ return invocation?.type === AST_NODE_TYPES65.CallExpression && !invocation.optional && unwrapExpression(expectOperand(member, source)?.operand ?? member) === callback;
14576
+ }
14577
+ function customizedMatchers(source) {
14578
+ const customized = /* @__PURE__ */ new Set();
14579
+ const visit = (node) => {
14580
+ if (node.type === AST_NODE_TYPES65.CallExpression) collectCustomization(node, source, customized);
14581
+ for (const key of source.visitorKeys[node.type] ?? []) {
14582
+ const child = node[key];
14583
+ for (const value of Array.isArray(child) ? child : [child]) {
14584
+ if (typeof value === "object" && value !== null && "type" in value) visit(value);
14585
+ }
14586
+ }
14587
+ };
14588
+ visit(source.ast);
14589
+ return customized;
14590
+ }
14591
+ function collectCustomization(node, source, customized) {
14592
+ const member = unwrapExpression(node.callee);
14593
+ if (member.type !== AST_NODE_TYPES65.MemberExpression) return;
14594
+ const receiver = unwrapExpression(member.object);
14595
+ if (receiver.type !== AST_NODE_TYPES65.Identifier || runtimeTestFrameworkName(source, receiver, "assertion") !== "expect") return;
14596
+ const method = ASTUtils40.getPropertyName(member);
14597
+ const argument = node.arguments[0] === void 0 ? null : unwrapExpression(node.arguments[0]);
14598
+ if (method === "addEqualityTesters" && !(argument?.type === AST_NODE_TYPES65.ArrayExpression && argument.elements.length === 0)) {
14599
+ customized.add("toEqual");
14600
+ customized.add("toStrictEqual");
14601
+ }
14602
+ if (method !== "extend") return;
14603
+ if (argument?.type !== AST_NODE_TYPES65.ObjectExpression) {
14604
+ customized.add("*");
14605
+ return;
14606
+ }
14607
+ for (const property of argument.properties) {
14608
+ const name = property.type === AST_NODE_TYPES65.Property ? ASTUtils40.getPropertyName(property) : null;
14609
+ customized.add(name ?? "*");
14610
+ }
14611
+ }
14495
14612
  var no_tautological_expect_default = createRule({
14496
14613
  name: "no-tautological-expect",
14497
14614
  documentation: NO_TAUTOLOGICAL_EXPECT_DOCUMENTATION,
@@ -14511,6 +14628,7 @@ var no_tautological_expect_default = createRule({
14511
14628
  if (!isTestFile(context.filename)) {
14512
14629
  return {};
14513
14630
  }
14631
+ const customized = customizedMatchers(context.sourceCode);
14514
14632
  const preview2 = (node) => {
14515
14633
  const text = context.sourceCode.getText(node).replaceAll(/\s+/gu, " ");
14516
14634
  return text.length > OPERAND_PREVIEW_CHARS ? `${text.slice(0, OPERAND_PREVIEW_CHARS)}\u2026` : text;
@@ -14520,12 +14638,12 @@ var no_tautological_expect_default = createRule({
14520
14638
  const callee = unwrapExpression(node.callee);
14521
14639
  if (callee.type !== AST_NODE_TYPES65.MemberExpression) return;
14522
14640
  const matcher = ASTUtils40.getPropertyName(callee);
14523
- if (matcher === null) return;
14641
+ if (matcher === null || !ZERO_ARG_MATCHERS.has(matcher) && !EQUALITY_MATCHERS.has(matcher) || customized.has("*") || customized.has(matcher)) return;
14524
14642
  const assertion = expectOperand(callee, context.sourceCode);
14525
14643
  if (assertion === null) return;
14526
14644
  const writtenOperand = assertion.operand;
14527
14645
  const operand = resolveLiteral(writtenOperand, context.sourceCode);
14528
- if (operand === null) {
14646
+ if (operand === null || isExceptionContract(node, context.sourceCode)) {
14529
14647
  return;
14530
14648
  }
14531
14649
  const writtenMatcher = `${assertion.negated ? "not." : ""}${matcher}`;
@@ -26035,7 +26153,7 @@ function classMethodCounts(body2, methods) {
26035
26153
 
26036
26154
  // src/rules/source-coupled-test.ts
26037
26155
  import { AST_NODE_TYPES as AST_NODE_TYPES116, ASTUtils as ASTUtils75 } from "@typescript-eslint/utils";
26038
- var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc?|toml|py|[cm]?[jt]s)$/iu;
26156
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|jsonc|toml|py|[cm]?[jt]s)$|(?<!\.(?:tf|tftest))\.json$/iu;
26039
26157
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
26040
26158
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
26041
26159
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.19.0",
3
+ "version": "16.19.1",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",