@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 +53 -7
- package/dist/index.js +53 -7
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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) => {
|