@sarj/eslint-plugin 16.18.0 → 16.18.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.18.0",
55
+ version: "16.18.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",
@@ -3986,6 +3986,45 @@ function runtimeImportBinding(binding) {
3986
3986
  if (specifier.type === import_utils25.AST_NODE_TYPES.ImportSpecifier && specifier.importKind === "type") return null;
3987
3987
  return { module: definition.parent.source.value, specifier };
3988
3988
  }
3989
+ function vitestFixtureTestName(source, identifier) {
3990
+ return fixtureTestName(identifier, source, /* @__PURE__ */ new Set());
3991
+ }
3992
+ function fixtureTestName(node, source, seen) {
3993
+ node = unwrapExpression(node);
3994
+ if (node.type === import_utils25.AST_NODE_TYPES.CallExpression) {
3995
+ const callee = unwrapExpression(node.callee);
3996
+ return !node.optional && callee.type === import_utils25.AST_NODE_TYPES.MemberExpression && !callee.optional && import_utils25.ASTUtils.getPropertyName(callee) === "extend" && node.arguments.length > 0 && !node.arguments.some((argument) => argument.type === import_utils25.AST_NODE_TYPES.SpreadElement) ? fixtureTestName(callee.object, source, seen) : null;
3997
+ }
3998
+ if (node.type !== import_utils25.AST_NODE_TYPES.Identifier) return null;
3999
+ const binding = import_utils25.ASTUtils.findVariable(source.getScope(node), node.name);
4000
+ if (binding === null || seen.has(binding) || !unchangedFixtureApi(binding)) return null;
4001
+ seen.add(binding);
4002
+ const imported = runtimeImportBinding(binding);
4003
+ if (imported?.module === "vitest" && imported.specifier.type === import_utils25.AST_NODE_TYPES.ImportSpecifier) {
4004
+ const name = imported.specifier.imported;
4005
+ const value = name.type === import_utils25.AST_NODE_TYPES.Identifier ? name.name : name.value;
4006
+ return value === "test" || value === "it" ? value : null;
4007
+ }
4008
+ const definition = binding.defs[0];
4009
+ if (binding.defs.length !== 1 || definition?.node.type !== import_utils25.AST_NODE_TYPES.VariableDeclarator || definition.node.id.type !== import_utils25.AST_NODE_TYPES.Identifier || definition.node.parent.kind !== "const" || definition.node.init === null || definition.node.init.range[1] > node.range[0]) return null;
4010
+ const initializer = unwrapExpression(definition.node.init);
4011
+ return initializer.type === import_utils25.AST_NODE_TYPES.CallExpression ? fixtureTestName(initializer, source, seen) : null;
4012
+ }
4013
+ var FIXTURE_API_STABILITY = /* @__PURE__ */ new WeakMap();
4014
+ function unchangedFixtureApi(binding) {
4015
+ const cached = FIXTURE_API_STABILITY.get(binding);
4016
+ if (cached !== void 0) return cached;
4017
+ const unchanged = binding.references.every((reference) => {
4018
+ if (reference.isWrite()) return reference.init === true;
4019
+ if (reference.isTypeReference && !reference.isValueReference) return true;
4020
+ let use = outerExpression(reference.identifier);
4021
+ while (use.parent?.type === import_utils25.AST_NODE_TYPES.MemberExpression && use.parent.object === use) use = outerExpression(use.parent);
4022
+ const parent = use.parent;
4023
+ return parent?.type === import_utils25.AST_NODE_TYPES.CallExpression && parent.callee === use || parent?.type === import_utils25.AST_NODE_TYPES.ExportSpecifier || parent?.type === import_utils25.AST_NODE_TYPES.ExportDefaultDeclaration || parent?.type === import_utils25.AST_NODE_TYPES.TSTypeQuery || parent?.type === import_utils25.AST_NODE_TYPES.UnaryExpression && parent.operator === "typeof";
4024
+ });
4025
+ FIXTURE_API_STABILITY.set(binding, unchanged);
4026
+ return unchanged;
4027
+ }
3989
4028
 
3990
4029
  // src/rules/duplicate-test-body.ts
3991
4030
  var TEST_CALLERS = /* @__PURE__ */ new Set(["it", "test"]);
@@ -13308,10 +13347,12 @@ var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
13308
13347
  remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
13309
13348
  category: "testing",
13310
13349
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
13311
- limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
13350
+ limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked, including unchanged local const Vitest test.extend fixture APIs and their beforeEach/afterEach hooks. Escaped or mutated fixture APIs, nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
13312
13351
  examples: [
13313
13352
  { id: "fake-timer", title: "Advance controlled time and restore real timers", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
13314
- { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
13353
+ { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true },
13354
+ { id: "fixture-fixed-sleep", scenarioId: "fixture-runner", title: "Fixture setup does not make a fixed sleep deterministic", outcome: "match", files: [{ path: "src/retry.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true },
13355
+ { id: "fixture-observable-signal", scenarioId: "fixture-runner", title: "Await the signal while reusing fixture setup", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await waitForReady(port); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true }
13315
13356
  ]
13316
13357
  };
13317
13358
  var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
@@ -13380,15 +13421,17 @@ function isTestBody(fn, source) {
13380
13421
  const name = testCallerName(call.callee, source);
13381
13422
  return name !== null && TEST_CALLERS2.has(name);
13382
13423
  }
13383
- var TEST_MODIFIERS3 = /* @__PURE__ */ new Set(["only", "skip", "each", "concurrent", "sequential", "fails", "skipIf", "runIf"]);
13424
+ var TEST_MODIFIERS3 = /* @__PURE__ */ new Set(["only", "skip", "each", "for", "concurrent", "sequential", "fails", "skipIf", "runIf"]);
13384
13425
  function testCallerName(callee, source) {
13385
13426
  callee = unwrapExpression(callee);
13386
13427
  if (callee.type === import_utils73.AST_NODE_TYPES.Identifier) {
13387
- const name = runtimeTestFrameworkName(source, callee, "test");
13428
+ const name = runtimeTestFrameworkName(source, callee, "test") ?? vitestFixtureTestName(source, callee);
13388
13429
  return name !== null && TEST_CALLERS2.has(name) ? name : null;
13389
13430
  }
13390
13431
  if (callee.type === import_utils73.AST_NODE_TYPES.MemberExpression) {
13391
13432
  const member = import_utils73.ASTUtils.getPropertyName(callee);
13433
+ const receiver = unwrapExpression(callee.object);
13434
+ if ((member === "beforeEach" || member === "afterEach") && receiver.type === import_utils73.AST_NODE_TYPES.Identifier && vitestFixtureTestName(source, receiver) !== null) return member;
13392
13435
  return member !== null && TEST_MODIFIERS3.has(member) ? testCallerName(callee.object, source) : null;
13393
13436
  }
13394
13437
  if (callee.type === import_utils73.AST_NODE_TYPES.CallExpression) return testCallerName(callee.callee, source);
@@ -13856,14 +13899,16 @@ var NO_DUPLICATE_TEST_CASE_DOCUMENTATION = {
13856
13899
  category: "testing",
13857
13900
  limitations: [
13858
13901
  "Only literal rows in inline arrays or const tables used exclusively by each or Vitest for calls are compared. Calls, spreads, computed keys, getters, runtime values, tagged tables and mutable or escaped tables are excluded.",
13859
- "Object property order and all row fields, including names and IDs, remain part of case identity; equivalent quoted and unquoted property names share one identity. Runtime imported test aliases are resolved; type-only imports, custom runners and namespace imports are excluded.",
13902
+ "Object property order and all row fields, including names and IDs, remain part of case identity; equivalent quoted and unquoted property names share one identity. Runtime imports and unchanged local const Vitest test.extend fixture APIs are resolved; type-only imports, escaped or mutated fixture APIs, custom runners and namespace imports are excluded.",
13860
13903
  "The rule reports duplicate values, not runtime equivalence. There is no autofix because repeated invocations may exercise shared state or concurrency."
13861
13904
  ],
13862
13905
  examples: [
13863
13906
  { id: "duplicate-row", title: "A copied row misses the intended boundary", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {it} from 'vitest'; it.each([[0, '0ms'], [0, '0ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13864
13907
  { id: "distinct-boundary", title: "Distinct inputs have independent rows", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {it} from 'vitest'; it.each([[0, '0ms'], [1, '1ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true },
13865
13908
  { id: "duplicate-for-row", scenarioId: "for-table", title: "Vitest for tables retain the same case identity", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 0, expected: '0ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13866
- { id: "distinct-for-boundary", scenarioId: "for-table", title: "Vitest for tables preserve distinct boundaries", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 1, expected: '1ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true }
13909
+ { id: "distinct-for-boundary", scenarioId: "for-table", title: "Vitest for tables preserve distinct boundaries", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 1, expected: '1ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true },
13910
+ { id: "fixture-duplicate-row", scenarioId: "fixture-runner", title: "Fixture reuse preserves the literal case check", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 1])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe('1'); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13911
+ { id: "fixture-distinct-row", scenarioId: "fixture-runner", title: "Fixture reuse preserves distinct independent cases", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 2])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe(String(value)); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true }
13867
13912
  ]
13868
13913
  };
13869
13914
  function frameworkTable(node, sourceCode) {
@@ -13878,6 +13923,7 @@ function frameworkTable(node, sourceCode) {
13878
13923
  callee = unwrapExpression(callee.object);
13879
13924
  }
13880
13925
  if (callee.type !== import_utils76.AST_NODE_TYPES.Identifier) return false;
13926
+ if (vitestFixtureTestName(sourceCode, callee) !== null) return true;
13881
13927
  const variable = import_utils76.ASTUtils.findVariable(sourceCode.getScope(callee), callee.name);
13882
13928
  if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name) && !variable?.references.some((reference) => reference.isWrite());
13883
13929
  return variable.defs.length === 1 && variable.defs.every((definition) => {
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.18.0",
4
+ version: "16.18.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",
@@ -3947,6 +3947,45 @@ function runtimeImportBinding(binding) {
3947
3947
  if (specifier.type === AST_NODE_TYPES23.ImportSpecifier && specifier.importKind === "type") return null;
3948
3948
  return { module: definition.parent.source.value, specifier };
3949
3949
  }
3950
+ function vitestFixtureTestName(source, identifier) {
3951
+ return fixtureTestName(identifier, source, /* @__PURE__ */ new Set());
3952
+ }
3953
+ function fixtureTestName(node, source, seen) {
3954
+ node = unwrapExpression(node);
3955
+ if (node.type === AST_NODE_TYPES23.CallExpression) {
3956
+ const callee = unwrapExpression(node.callee);
3957
+ return !node.optional && callee.type === AST_NODE_TYPES23.MemberExpression && !callee.optional && ASTUtils10.getPropertyName(callee) === "extend" && node.arguments.length > 0 && !node.arguments.some((argument) => argument.type === AST_NODE_TYPES23.SpreadElement) ? fixtureTestName(callee.object, source, seen) : null;
3958
+ }
3959
+ if (node.type !== AST_NODE_TYPES23.Identifier) return null;
3960
+ const binding = ASTUtils10.findVariable(source.getScope(node), node.name);
3961
+ if (binding === null || seen.has(binding) || !unchangedFixtureApi(binding)) return null;
3962
+ seen.add(binding);
3963
+ const imported = runtimeImportBinding(binding);
3964
+ if (imported?.module === "vitest" && imported.specifier.type === AST_NODE_TYPES23.ImportSpecifier) {
3965
+ const name = imported.specifier.imported;
3966
+ const value = name.type === AST_NODE_TYPES23.Identifier ? name.name : name.value;
3967
+ return value === "test" || value === "it" ? value : null;
3968
+ }
3969
+ const definition = binding.defs[0];
3970
+ if (binding.defs.length !== 1 || definition?.node.type !== AST_NODE_TYPES23.VariableDeclarator || definition.node.id.type !== AST_NODE_TYPES23.Identifier || definition.node.parent.kind !== "const" || definition.node.init === null || definition.node.init.range[1] > node.range[0]) return null;
3971
+ const initializer = unwrapExpression(definition.node.init);
3972
+ return initializer.type === AST_NODE_TYPES23.CallExpression ? fixtureTestName(initializer, source, seen) : null;
3973
+ }
3974
+ var FIXTURE_API_STABILITY = /* @__PURE__ */ new WeakMap();
3975
+ function unchangedFixtureApi(binding) {
3976
+ const cached = FIXTURE_API_STABILITY.get(binding);
3977
+ if (cached !== void 0) return cached;
3978
+ const unchanged = binding.references.every((reference) => {
3979
+ if (reference.isWrite()) return reference.init === true;
3980
+ if (reference.isTypeReference && !reference.isValueReference) return true;
3981
+ let use = outerExpression(reference.identifier);
3982
+ while (use.parent?.type === AST_NODE_TYPES23.MemberExpression && use.parent.object === use) use = outerExpression(use.parent);
3983
+ const parent = use.parent;
3984
+ return parent?.type === AST_NODE_TYPES23.CallExpression && parent.callee === use || parent?.type === AST_NODE_TYPES23.ExportSpecifier || parent?.type === AST_NODE_TYPES23.ExportDefaultDeclaration || parent?.type === AST_NODE_TYPES23.TSTypeQuery || parent?.type === AST_NODE_TYPES23.UnaryExpression && parent.operator === "typeof";
3985
+ });
3986
+ FIXTURE_API_STABILITY.set(binding, unchanged);
3987
+ return unchanged;
3988
+ }
3950
3989
 
3951
3990
  // src/rules/duplicate-test-body.ts
3952
3991
  var TEST_CALLERS = /* @__PURE__ */ new Set(["it", "test"]);
@@ -13289,10 +13328,12 @@ var NO_SLEEP_IN_TEST_BODY_DOCUMENTATION = {
13289
13328
  remediation: "Await the observable signal, or advance fake timers when supported and restore real timers in finally or a teardown hook.",
13290
13329
  category: "testing",
13291
13330
  filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
13292
- limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
13331
+ limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked, including unchanged local const Vitest test.extend fixture APIs and their beforeEach/afterEach hooks. Escaped or mutated fixture APIs, nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks are excluded."],
13293
13332
  examples: [
13294
13333
  { id: "fake-timer", title: "Advance controlled time and restore real timers", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
13295
- { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
13334
+ { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true },
13335
+ { id: "fixture-fixed-sleep", scenarioId: "fixture-runner", title: "Fixture setup does not make a fixed sleep deterministic", outcome: "match", files: [{ path: "src/retry.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true },
13336
+ { id: "fixture-observable-signal", scenarioId: "fixture-runner", title: "Await the signal while reusing fixture setup", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await waitForReady(port); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true }
13296
13337
  ]
13297
13338
  };
13298
13339
  var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
@@ -13361,15 +13402,17 @@ function isTestBody(fn, source) {
13361
13402
  const name = testCallerName(call.callee, source);
13362
13403
  return name !== null && TEST_CALLERS2.has(name);
13363
13404
  }
13364
- var TEST_MODIFIERS3 = /* @__PURE__ */ new Set(["only", "skip", "each", "concurrent", "sequential", "fails", "skipIf", "runIf"]);
13405
+ var TEST_MODIFIERS3 = /* @__PURE__ */ new Set(["only", "skip", "each", "for", "concurrent", "sequential", "fails", "skipIf", "runIf"]);
13365
13406
  function testCallerName(callee, source) {
13366
13407
  callee = unwrapExpression(callee);
13367
13408
  if (callee.type === AST_NODE_TYPES60.Identifier) {
13368
- const name = runtimeTestFrameworkName(source, callee, "test");
13409
+ const name = runtimeTestFrameworkName(source, callee, "test") ?? vitestFixtureTestName(source, callee);
13369
13410
  return name !== null && TEST_CALLERS2.has(name) ? name : null;
13370
13411
  }
13371
13412
  if (callee.type === AST_NODE_TYPES60.MemberExpression) {
13372
13413
  const member = ASTUtils36.getPropertyName(callee);
13414
+ const receiver = unwrapExpression(callee.object);
13415
+ if ((member === "beforeEach" || member === "afterEach") && receiver.type === AST_NODE_TYPES60.Identifier && vitestFixtureTestName(source, receiver) !== null) return member;
13373
13416
  return member !== null && TEST_MODIFIERS3.has(member) ? testCallerName(callee.object, source) : null;
13374
13417
  }
13375
13418
  if (callee.type === AST_NODE_TYPES60.CallExpression) return testCallerName(callee.callee, source);
@@ -13837,14 +13880,16 @@ var NO_DUPLICATE_TEST_CASE_DOCUMENTATION = {
13837
13880
  category: "testing",
13838
13881
  limitations: [
13839
13882
  "Only literal rows in inline arrays or const tables used exclusively by each or Vitest for calls are compared. Calls, spreads, computed keys, getters, runtime values, tagged tables and mutable or escaped tables are excluded.",
13840
- "Object property order and all row fields, including names and IDs, remain part of case identity; equivalent quoted and unquoted property names share one identity. Runtime imported test aliases are resolved; type-only imports, custom runners and namespace imports are excluded.",
13883
+ "Object property order and all row fields, including names and IDs, remain part of case identity; equivalent quoted and unquoted property names share one identity. Runtime imports and unchanged local const Vitest test.extend fixture APIs are resolved; type-only imports, escaped or mutated fixture APIs, custom runners and namespace imports are excluded.",
13841
13884
  "The rule reports duplicate values, not runtime equivalence. There is no autofix because repeated invocations may exercise shared state or concurrency."
13842
13885
  ],
13843
13886
  examples: [
13844
13887
  { id: "duplicate-row", title: "A copied row misses the intended boundary", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {it} from 'vitest'; it.each([[0, '0ms'], [0, '0ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13845
13888
  { id: "distinct-boundary", title: "Distinct inputs have independent rows", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {it} from 'vitest'; it.each([[0, '0ms'], [1, '1ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true },
13846
13889
  { id: "duplicate-for-row", scenarioId: "for-table", title: "Vitest for tables retain the same case identity", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 0, expected: '0ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13847
- { id: "distinct-for-boundary", scenarioId: "for-table", title: "Vitest for tables preserve distinct boundaries", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 1, expected: '1ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true }
13890
+ { id: "distinct-for-boundary", scenarioId: "for-table", title: "Vitest for tables preserve distinct boundaries", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 1, expected: '1ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true },
13891
+ { id: "fixture-duplicate-row", scenarioId: "fixture-runner", title: "Fixture reuse preserves the literal case check", outcome: "match", files: [{ path: "src/format.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 1])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe('1'); });" }], focusPath: "src/format.test.ts", expectedCount: 1, public: true },
13892
+ { id: "fixture-distinct-row", scenarioId: "fixture-runner", title: "Fixture reuse preserves distinct independent cases", outcome: "no-match", files: [{ path: "src/format.test.ts", source: "import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 2])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe(String(value)); });" }], focusPath: "src/format.test.ts", expectedCount: 0, public: true }
13848
13893
  ]
13849
13894
  };
13850
13895
  function frameworkTable(node, sourceCode) {
@@ -13859,6 +13904,7 @@ function frameworkTable(node, sourceCode) {
13859
13904
  callee = unwrapExpression(callee.object);
13860
13905
  }
13861
13906
  if (callee.type !== AST_NODE_TYPES62.Identifier) return false;
13907
+ if (vitestFixtureTestName(sourceCode, callee) !== null) return true;
13862
13908
  const variable = ASTUtils38.findVariable(sourceCode.getScope(callee), callee.name);
13863
13909
  if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name) && !variable?.references.some((reference) => reference.isWrite());
13864
13910
  return variable.defs.length === 1 && variable.defs.every((definition) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "16.18.0",
3
+ "version": "16.18.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",