@sarj/eslint-plugin 16.16.0 → 16.16.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 +49 -28
- package/dist/index.js +49 -28
- 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.16.
|
|
55
|
+
version: "16.16.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",
|
|
@@ -13647,26 +13647,30 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
13647
13647
|
var import_utils75 = require("@typescript-eslint/utils");
|
|
13648
13648
|
var TEST_MODULES = /* @__PURE__ */ new Set(["vitest", "@jest/globals", "bun:test"]);
|
|
13649
13649
|
var TEST_NAMES2 = /* @__PURE__ */ new Set(["it", "test"]);
|
|
13650
|
-
var MODIFIERS = /* @__PURE__ */ new Set(["only", "skip", "concurrent", "fails", "failing", "todo"]);
|
|
13650
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["only", "skip", "concurrent", "sequential", "fails", "failing", "todo"]);
|
|
13651
13651
|
var NO_DUPLICATE_TEST_CASE_DOCUMENTATION = {
|
|
13652
13652
|
defaultLevel: "error",
|
|
13653
|
-
summary: "Report repeated literal rows in framework
|
|
13653
|
+
summary: "Report repeated literal rows in framework each tables and Vitest for tables.",
|
|
13654
13654
|
rationale: "Identical rows repeat the same inputs and expected values without exposing a distinct case. Accidental duplicates consume test time and hide a missing boundary vector.",
|
|
13655
13655
|
remediation: "Remove the accidental duplicate or replace it with the intended distinct boundary. Preserve intentional repeated or stateful scenarios with a local explanation; do not delete their coverage automatically.",
|
|
13656
13656
|
category: "testing",
|
|
13657
13657
|
limitations: [
|
|
13658
|
-
"Only literal rows in inline arrays or const tables used exclusively by each calls are compared. Calls, spreads, computed keys, getters, runtime values, tagged tables and mutable or escaped tables are excluded.",
|
|
13659
|
-
"Object property order and all row fields, including names and IDs, remain part of case identity.
|
|
13658
|
+
"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.",
|
|
13659
|
+
"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.",
|
|
13660
13660
|
"The rule reports duplicate values, not runtime equivalence. There is no autofix because repeated invocations may exercise shared state or concurrency."
|
|
13661
13661
|
],
|
|
13662
13662
|
examples: [
|
|
13663
13663
|
{ 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 },
|
|
13664
|
-
{ 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 }
|
|
13664
|
+
{ 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 },
|
|
13665
|
+
{ 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 },
|
|
13666
|
+
{ 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 }
|
|
13665
13667
|
]
|
|
13666
13668
|
};
|
|
13667
|
-
function
|
|
13669
|
+
function frameworkTable(node, sourceCode) {
|
|
13668
13670
|
let callee = unwrapExpression(node.callee);
|
|
13669
|
-
if (callee.type !== import_utils75.AST_NODE_TYPES.MemberExpression
|
|
13671
|
+
if (callee.type !== import_utils75.AST_NODE_TYPES.MemberExpression) return false;
|
|
13672
|
+
const method = import_utils75.ASTUtils.getPropertyName(callee);
|
|
13673
|
+
if (method !== "each" && method !== "for") return false;
|
|
13670
13674
|
callee = unwrapExpression(callee.object);
|
|
13671
13675
|
while (callee.type === import_utils75.AST_NODE_TYPES.MemberExpression) {
|
|
13672
13676
|
const name = import_utils75.ASTUtils.getPropertyName(callee);
|
|
@@ -13675,12 +13679,12 @@ function frameworkEach(node, sourceCode) {
|
|
|
13675
13679
|
}
|
|
13676
13680
|
if (callee.type !== import_utils75.AST_NODE_TYPES.Identifier) return false;
|
|
13677
13681
|
const variable = import_utils75.ASTUtils.findVariable(sourceCode.getScope(callee), callee.name);
|
|
13678
|
-
if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name);
|
|
13682
|
+
if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name) && !variable?.references.some((reference) => reference.isWrite());
|
|
13679
13683
|
return variable.defs.length === 1 && variable.defs.every((definition) => {
|
|
13680
|
-
if (definition.node.type !== import_utils75.AST_NODE_TYPES.ImportSpecifier) return false;
|
|
13684
|
+
if (definition.node.type !== import_utils75.AST_NODE_TYPES.ImportSpecifier || definition.node.importKind === "type") return false;
|
|
13681
13685
|
const imported = definition.node.imported;
|
|
13682
13686
|
const declaration = definition.node.parent;
|
|
13683
|
-
return declaration.type === import_utils75.AST_NODE_TYPES.ImportDeclaration && TEST_NAMES2.has(imported.type === import_utils75.AST_NODE_TYPES.Identifier ? imported.name : imported.value) && TEST_MODULES.has(declaration.source.value);
|
|
13687
|
+
return declaration.type === import_utils75.AST_NODE_TYPES.ImportDeclaration && declaration.importKind !== "type" && TEST_NAMES2.has(imported.type === import_utils75.AST_NODE_TYPES.Identifier ? imported.name : imported.value) && (method === "for" ? declaration.source.value === "vitest" : TEST_MODULES.has(declaration.source.value));
|
|
13684
13688
|
});
|
|
13685
13689
|
}
|
|
13686
13690
|
function table(node, sourceCode) {
|
|
@@ -13694,7 +13698,7 @@ function table(node, sourceCode) {
|
|
|
13694
13698
|
if (reference.init) return true;
|
|
13695
13699
|
if (reference.identifier.type !== import_utils75.AST_NODE_TYPES.Identifier) return false;
|
|
13696
13700
|
const use = outerExpression(reference.identifier);
|
|
13697
|
-
return use.parent.type === import_utils75.AST_NODE_TYPES.CallExpression && use.parent.arguments[0] === use &&
|
|
13701
|
+
return use.parent.type === import_utils75.AST_NODE_TYPES.CallExpression && use.parent.arguments[0] === use && frameworkTable(use.parent, sourceCode);
|
|
13698
13702
|
})) return null;
|
|
13699
13703
|
const initializer = unwrapExpression(definition.node.init);
|
|
13700
13704
|
return initializer.type === import_utils75.AST_NODE_TYPES.ArrayExpression ? initializer : null;
|
|
@@ -13724,7 +13728,7 @@ function objectKey(node) {
|
|
|
13724
13728
|
const fields = [];
|
|
13725
13729
|
for (const property of node.properties) {
|
|
13726
13730
|
if (property.type !== import_utils75.AST_NODE_TYPES.Property || property.computed || property.kind !== "init" || property.method) return null;
|
|
13727
|
-
const key =
|
|
13731
|
+
const key = import_utils75.ASTUtils.getPropertyName(property);
|
|
13728
13732
|
const value = literalKey(property.value);
|
|
13729
13733
|
if (key === null || value === null) return null;
|
|
13730
13734
|
fields.push(JSON.stringify([key, value]));
|
|
@@ -13747,7 +13751,7 @@ var no_duplicate_test_case_default = createRule({
|
|
|
13747
13751
|
return {
|
|
13748
13752
|
CallExpression(node) {
|
|
13749
13753
|
const callee = unwrapExpression(node.callee);
|
|
13750
|
-
if (callee.type !== import_utils75.AST_NODE_TYPES.CallExpression || !
|
|
13754
|
+
if (callee.type !== import_utils75.AST_NODE_TYPES.CallExpression || !frameworkTable(callee, context.sourceCode)) return;
|
|
13751
13755
|
const input = callee.arguments[0];
|
|
13752
13756
|
if (callee.arguments.length !== 1 || input === void 0) return;
|
|
13753
13757
|
const cases = table(input, context.sourceCode);
|
|
@@ -21559,10 +21563,12 @@ var PREFER_WHOLE_OBJECT_ASSERTION_DOCUMENTATION = {
|
|
|
21559
21563
|
category: "testing",
|
|
21560
21564
|
aliases: ["strict-test-assertions"],
|
|
21561
21565
|
autofix: "none",
|
|
21562
|
-
limitations: ["Undefined-property assertions are excluded because whole-object matching can require previously absent properties. No automatic rewrite: grouping can change observable getter or proxy reads."],
|
|
21566
|
+
limitations: ["Undefined-property assertions are excluded because whole-object matching can require previously absent properties. Indexed runs with an immediately adjacent literal exact-length assertion are excluded; non-adjacent length checks and intentional prefix contracts remain manual review. No automatic rewrite: grouping can change observable getter or proxy reads."],
|
|
21563
21567
|
examples: [
|
|
21564
21568
|
{ id: "whole-object", title: "Assert the object once", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true },
|
|
21565
|
-
{ id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true }
|
|
21569
|
+
{ id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true },
|
|
21570
|
+
{ id: "indexed-prefix", scenarioId: "array-length", title: "Review the collection contract before combining indexed assertions", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true },
|
|
21571
|
+
{ id: "length-and-prefix", scenarioId: "array-length", title: "A separate exact length can accompany an intentional prefix contract", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(users).toHaveLength(3);\nexpect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true }
|
|
21566
21572
|
]
|
|
21567
21573
|
};
|
|
21568
21574
|
function literalText(node, getText) {
|
|
@@ -21623,7 +21629,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21623
21629
|
},
|
|
21624
21630
|
messages: {
|
|
21625
21631
|
combineAssertions: "Consider combining these {{count}} assertions on `{{receiver}}` with `toMatchObject` when structural matching preserves property presence and getter or proxy behavior.",
|
|
21626
|
-
assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]`
|
|
21632
|
+
assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]` separately. If the contract requires the complete array, consider `expect({{receiver}}).{{matcher}}([ \u2026 ])`, which also checks for extra elements. Preserve intentional prefix checks and separate length assertions."
|
|
21627
21633
|
},
|
|
21628
21634
|
schema: []
|
|
21629
21635
|
},
|
|
@@ -21689,7 +21695,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21689
21695
|
data: { count: String(run.length), receiver: receiverText }
|
|
21690
21696
|
});
|
|
21691
21697
|
}
|
|
21692
|
-
function reportIndexRun(run) {
|
|
21698
|
+
function reportIndexRun(run, before, after) {
|
|
21693
21699
|
const first = run[0];
|
|
21694
21700
|
if (first === void 0 || !ARRAY_MATCHERS.has(first.matcher)) {
|
|
21695
21701
|
return;
|
|
@@ -21704,6 +21710,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21704
21710
|
if (indices.size !== run.length || Math.max(...indices) !== run.length - 1) {
|
|
21705
21711
|
return;
|
|
21706
21712
|
}
|
|
21713
|
+
if (isExactLengthAssertion(before, first.receiver) || isExactLengthAssertion(after, first.receiver)) return;
|
|
21707
21714
|
context.report({
|
|
21708
21715
|
node: first.statement,
|
|
21709
21716
|
messageId: "assertArrayOnce",
|
|
@@ -21717,18 +21724,19 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21717
21724
|
}
|
|
21718
21725
|
function checkBody(body2) {
|
|
21719
21726
|
let run = [];
|
|
21727
|
+
let start = 0;
|
|
21720
21728
|
const flush = () => {
|
|
21721
21729
|
const first = run[0];
|
|
21722
21730
|
if (first !== void 0 && run.length >= MIN_RUN_LENGTH) {
|
|
21723
21731
|
if (first.key.kind === "property") {
|
|
21724
21732
|
reportPropertyRun(run);
|
|
21725
21733
|
} else {
|
|
21726
|
-
reportIndexRun(run);
|
|
21734
|
+
reportIndexRun(run, body2[start - 1], body2[start + run.length]);
|
|
21727
21735
|
}
|
|
21728
21736
|
}
|
|
21729
21737
|
run = [];
|
|
21730
21738
|
};
|
|
21731
|
-
for (const statement of body2) {
|
|
21739
|
+
for (const [index, statement] of body2.entries()) {
|
|
21732
21740
|
const assertion = parseAssertion(statement);
|
|
21733
21741
|
const previous = run.at(-1);
|
|
21734
21742
|
if (assertion !== null && previous !== void 0 && previous.key.kind === assertion.key.kind && sourceCode.getText(previous.receiver) === sourceCode.getText(assertion.receiver)) {
|
|
@@ -21738,6 +21746,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21738
21746
|
flush();
|
|
21739
21747
|
if (assertion !== null) {
|
|
21740
21748
|
run = [assertion];
|
|
21749
|
+
start = index;
|
|
21741
21750
|
}
|
|
21742
21751
|
}
|
|
21743
21752
|
flush();
|
|
@@ -21783,14 +21792,26 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21783
21792
|
return assertionExpectation(statement, call, receiver, key, matcher);
|
|
21784
21793
|
}
|
|
21785
21794
|
function isTestExpect(callee) {
|
|
21786
|
-
|
|
21787
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
|
|
21791
|
-
|
|
21792
|
-
|
|
21793
|
-
|
|
21795
|
+
return runtimeTestFrameworkName(sourceCode, callee, "assertion") === "expect";
|
|
21796
|
+
}
|
|
21797
|
+
function isExactLengthAssertion(statement, receiver) {
|
|
21798
|
+
if (statement?.type !== import_utils118.AST_NODE_TYPES.ExpressionStatement || statement.expression.type !== import_utils118.AST_NODE_TYPES.CallExpression) return false;
|
|
21799
|
+
const call = statement.expression;
|
|
21800
|
+
const callee = unwrapExpression(call.callee);
|
|
21801
|
+
if (callee.type !== import_utils118.AST_NODE_TYPES.MemberExpression || call.arguments.length !== 1) return false;
|
|
21802
|
+
const matcher = import_utils118.ASTUtils.getPropertyName(callee);
|
|
21803
|
+
const assertion = unwrapExpression(callee.object);
|
|
21804
|
+
if (assertion.type !== import_utils118.AST_NODE_TYPES.CallExpression || assertion.arguments.length !== 1) return false;
|
|
21805
|
+
const expect = unwrapExpression(assertion.callee);
|
|
21806
|
+
if (expect.type !== import_utils118.AST_NODE_TYPES.Identifier || !isTestExpect(expect)) return false;
|
|
21807
|
+
const expected = call.arguments[0] === void 0 ? void 0 : unwrapExpression(call.arguments[0]);
|
|
21808
|
+
if (expected?.type !== import_utils118.AST_NODE_TYPES.Literal || typeof expected.value !== "number" || !Number.isInteger(expected.value) || expected.value < 0) return false;
|
|
21809
|
+
let actual = assertion.arguments[0] === void 0 ? void 0 : unwrapExpression(assertion.arguments[0]);
|
|
21810
|
+
if (matcher !== "toHaveLength") {
|
|
21811
|
+
if (matcher === null || !MERGEABLE_MATCHERS.has(matcher) || actual?.type !== import_utils118.AST_NODE_TYPES.MemberExpression || actual.optional || import_utils118.ASTUtils.getPropertyName(actual) !== "length") return false;
|
|
21812
|
+
actual = unwrapExpression(actual.object);
|
|
21813
|
+
}
|
|
21814
|
+
return actual !== void 0 && sourceCode.getText(actual) === sourceCode.getText(receiver);
|
|
21794
21815
|
}
|
|
21795
21816
|
function assertionExpectation(statement, call, receiver, key, matcher) {
|
|
21796
21817
|
const synthetic = SYNTHETIC_LITERAL_MATCHERS.get(matcher);
|
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.16.
|
|
4
|
+
version: "16.16.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",
|
|
@@ -13628,26 +13628,30 @@ var no_string_concat_in_loop_default = createRule({
|
|
|
13628
13628
|
import { AST_NODE_TYPES as AST_NODE_TYPES61, ASTUtils as ASTUtils37 } from "@typescript-eslint/utils";
|
|
13629
13629
|
var TEST_MODULES = /* @__PURE__ */ new Set(["vitest", "@jest/globals", "bun:test"]);
|
|
13630
13630
|
var TEST_NAMES2 = /* @__PURE__ */ new Set(["it", "test"]);
|
|
13631
|
-
var MODIFIERS = /* @__PURE__ */ new Set(["only", "skip", "concurrent", "fails", "failing", "todo"]);
|
|
13631
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["only", "skip", "concurrent", "sequential", "fails", "failing", "todo"]);
|
|
13632
13632
|
var NO_DUPLICATE_TEST_CASE_DOCUMENTATION = {
|
|
13633
13633
|
defaultLevel: "error",
|
|
13634
|
-
summary: "Report repeated literal rows in framework
|
|
13634
|
+
summary: "Report repeated literal rows in framework each tables and Vitest for tables.",
|
|
13635
13635
|
rationale: "Identical rows repeat the same inputs and expected values without exposing a distinct case. Accidental duplicates consume test time and hide a missing boundary vector.",
|
|
13636
13636
|
remediation: "Remove the accidental duplicate or replace it with the intended distinct boundary. Preserve intentional repeated or stateful scenarios with a local explanation; do not delete their coverage automatically.",
|
|
13637
13637
|
category: "testing",
|
|
13638
13638
|
limitations: [
|
|
13639
|
-
"Only literal rows in inline arrays or const tables used exclusively by each calls are compared. Calls, spreads, computed keys, getters, runtime values, tagged tables and mutable or escaped tables are excluded.",
|
|
13640
|
-
"Object property order and all row fields, including names and IDs, remain part of case identity.
|
|
13639
|
+
"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.",
|
|
13640
|
+
"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.",
|
|
13641
13641
|
"The rule reports duplicate values, not runtime equivalence. There is no autofix because repeated invocations may exercise shared state or concurrency."
|
|
13642
13642
|
],
|
|
13643
13643
|
examples: [
|
|
13644
13644
|
{ 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 },
|
|
13645
|
-
{ 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 }
|
|
13645
|
+
{ 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 },
|
|
13646
|
+
{ 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 },
|
|
13647
|
+
{ 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 }
|
|
13646
13648
|
]
|
|
13647
13649
|
};
|
|
13648
|
-
function
|
|
13650
|
+
function frameworkTable(node, sourceCode) {
|
|
13649
13651
|
let callee = unwrapExpression(node.callee);
|
|
13650
|
-
if (callee.type !== AST_NODE_TYPES61.MemberExpression
|
|
13652
|
+
if (callee.type !== AST_NODE_TYPES61.MemberExpression) return false;
|
|
13653
|
+
const method = ASTUtils37.getPropertyName(callee);
|
|
13654
|
+
if (method !== "each" && method !== "for") return false;
|
|
13651
13655
|
callee = unwrapExpression(callee.object);
|
|
13652
13656
|
while (callee.type === AST_NODE_TYPES61.MemberExpression) {
|
|
13653
13657
|
const name = ASTUtils37.getPropertyName(callee);
|
|
@@ -13656,12 +13660,12 @@ function frameworkEach(node, sourceCode) {
|
|
|
13656
13660
|
}
|
|
13657
13661
|
if (callee.type !== AST_NODE_TYPES61.Identifier) return false;
|
|
13658
13662
|
const variable = ASTUtils37.findVariable(sourceCode.getScope(callee), callee.name);
|
|
13659
|
-
if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name);
|
|
13663
|
+
if (variable === null || variable.defs.length === 0) return TEST_NAMES2.has(callee.name) && !variable?.references.some((reference) => reference.isWrite());
|
|
13660
13664
|
return variable.defs.length === 1 && variable.defs.every((definition) => {
|
|
13661
|
-
if (definition.node.type !== AST_NODE_TYPES61.ImportSpecifier) return false;
|
|
13665
|
+
if (definition.node.type !== AST_NODE_TYPES61.ImportSpecifier || definition.node.importKind === "type") return false;
|
|
13662
13666
|
const imported = definition.node.imported;
|
|
13663
13667
|
const declaration = definition.node.parent;
|
|
13664
|
-
return declaration.type === AST_NODE_TYPES61.ImportDeclaration && TEST_NAMES2.has(imported.type === AST_NODE_TYPES61.Identifier ? imported.name : imported.value) && TEST_MODULES.has(declaration.source.value);
|
|
13668
|
+
return declaration.type === AST_NODE_TYPES61.ImportDeclaration && declaration.importKind !== "type" && TEST_NAMES2.has(imported.type === AST_NODE_TYPES61.Identifier ? imported.name : imported.value) && (method === "for" ? declaration.source.value === "vitest" : TEST_MODULES.has(declaration.source.value));
|
|
13665
13669
|
});
|
|
13666
13670
|
}
|
|
13667
13671
|
function table(node, sourceCode) {
|
|
@@ -13675,7 +13679,7 @@ function table(node, sourceCode) {
|
|
|
13675
13679
|
if (reference.init) return true;
|
|
13676
13680
|
if (reference.identifier.type !== AST_NODE_TYPES61.Identifier) return false;
|
|
13677
13681
|
const use = outerExpression(reference.identifier);
|
|
13678
|
-
return use.parent.type === AST_NODE_TYPES61.CallExpression && use.parent.arguments[0] === use &&
|
|
13682
|
+
return use.parent.type === AST_NODE_TYPES61.CallExpression && use.parent.arguments[0] === use && frameworkTable(use.parent, sourceCode);
|
|
13679
13683
|
})) return null;
|
|
13680
13684
|
const initializer = unwrapExpression(definition.node.init);
|
|
13681
13685
|
return initializer.type === AST_NODE_TYPES61.ArrayExpression ? initializer : null;
|
|
@@ -13705,7 +13709,7 @@ function objectKey(node) {
|
|
|
13705
13709
|
const fields = [];
|
|
13706
13710
|
for (const property of node.properties) {
|
|
13707
13711
|
if (property.type !== AST_NODE_TYPES61.Property || property.computed || property.kind !== "init" || property.method) return null;
|
|
13708
|
-
const key =
|
|
13712
|
+
const key = ASTUtils37.getPropertyName(property);
|
|
13709
13713
|
const value = literalKey(property.value);
|
|
13710
13714
|
if (key === null || value === null) return null;
|
|
13711
13715
|
fields.push(JSON.stringify([key, value]));
|
|
@@ -13728,7 +13732,7 @@ var no_duplicate_test_case_default = createRule({
|
|
|
13728
13732
|
return {
|
|
13729
13733
|
CallExpression(node) {
|
|
13730
13734
|
const callee = unwrapExpression(node.callee);
|
|
13731
|
-
if (callee.type !== AST_NODE_TYPES61.CallExpression || !
|
|
13735
|
+
if (callee.type !== AST_NODE_TYPES61.CallExpression || !frameworkTable(callee, context.sourceCode)) return;
|
|
13732
13736
|
const input = callee.arguments[0];
|
|
13733
13737
|
if (callee.arguments.length !== 1 || input === void 0) return;
|
|
13734
13738
|
const cases = table(input, context.sourceCode);
|
|
@@ -21567,10 +21571,12 @@ var PREFER_WHOLE_OBJECT_ASSERTION_DOCUMENTATION = {
|
|
|
21567
21571
|
category: "testing",
|
|
21568
21572
|
aliases: ["strict-test-assertions"],
|
|
21569
21573
|
autofix: "none",
|
|
21570
|
-
limitations: ["Undefined-property assertions are excluded because whole-object matching can require previously absent properties. No automatic rewrite: grouping can change observable getter or proxy reads."],
|
|
21574
|
+
limitations: ["Undefined-property assertions are excluded because whole-object matching can require previously absent properties. Indexed runs with an immediately adjacent literal exact-length assertion are excluded; non-adjacent length checks and intentional prefix contracts remain manual review. No automatic rewrite: grouping can change observable getter or proxy reads."],
|
|
21571
21575
|
examples: [
|
|
21572
21576
|
{ id: "whole-object", title: "Assert the object once", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true },
|
|
21573
|
-
{ id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true }
|
|
21577
|
+
{ id: "member-run", title: "Consider grouping related data properties", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true },
|
|
21578
|
+
{ id: "indexed-prefix", scenarioId: "array-length", title: "Review the collection contract before combining indexed assertions", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true },
|
|
21579
|
+
{ id: "length-and-prefix", scenarioId: "array-length", title: "A separate exact length can accompany an intentional prefix contract", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(users).toHaveLength(3);\nexpect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true }
|
|
21574
21580
|
]
|
|
21575
21581
|
};
|
|
21576
21582
|
function literalText(node, getText) {
|
|
@@ -21631,7 +21637,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21631
21637
|
},
|
|
21632
21638
|
messages: {
|
|
21633
21639
|
combineAssertions: "Consider combining these {{count}} assertions on `{{receiver}}` with `toMatchObject` when structural matching preserves property presence and getter or proxy behavior.",
|
|
21634
|
-
assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]`
|
|
21640
|
+
assertArrayOnce: "These {{count}} assertions check `{{receiver}}[0]`\u2026`{{receiver}}[{{last}}]` separately. If the contract requires the complete array, consider `expect({{receiver}}).{{matcher}}([ \u2026 ])`, which also checks for extra elements. Preserve intentional prefix checks and separate length assertions."
|
|
21635
21641
|
},
|
|
21636
21642
|
schema: []
|
|
21637
21643
|
},
|
|
@@ -21697,7 +21703,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21697
21703
|
data: { count: String(run.length), receiver: receiverText }
|
|
21698
21704
|
});
|
|
21699
21705
|
}
|
|
21700
|
-
function reportIndexRun(run) {
|
|
21706
|
+
function reportIndexRun(run, before, after) {
|
|
21701
21707
|
const first = run[0];
|
|
21702
21708
|
if (first === void 0 || !ARRAY_MATCHERS.has(first.matcher)) {
|
|
21703
21709
|
return;
|
|
@@ -21712,6 +21718,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21712
21718
|
if (indices.size !== run.length || Math.max(...indices) !== run.length - 1) {
|
|
21713
21719
|
return;
|
|
21714
21720
|
}
|
|
21721
|
+
if (isExactLengthAssertion(before, first.receiver) || isExactLengthAssertion(after, first.receiver)) return;
|
|
21715
21722
|
context.report({
|
|
21716
21723
|
node: first.statement,
|
|
21717
21724
|
messageId: "assertArrayOnce",
|
|
@@ -21725,18 +21732,19 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21725
21732
|
}
|
|
21726
21733
|
function checkBody(body2) {
|
|
21727
21734
|
let run = [];
|
|
21735
|
+
let start = 0;
|
|
21728
21736
|
const flush = () => {
|
|
21729
21737
|
const first = run[0];
|
|
21730
21738
|
if (first !== void 0 && run.length >= MIN_RUN_LENGTH) {
|
|
21731
21739
|
if (first.key.kind === "property") {
|
|
21732
21740
|
reportPropertyRun(run);
|
|
21733
21741
|
} else {
|
|
21734
|
-
reportIndexRun(run);
|
|
21742
|
+
reportIndexRun(run, body2[start - 1], body2[start + run.length]);
|
|
21735
21743
|
}
|
|
21736
21744
|
}
|
|
21737
21745
|
run = [];
|
|
21738
21746
|
};
|
|
21739
|
-
for (const statement of body2) {
|
|
21747
|
+
for (const [index, statement] of body2.entries()) {
|
|
21740
21748
|
const assertion = parseAssertion(statement);
|
|
21741
21749
|
const previous = run.at(-1);
|
|
21742
21750
|
if (assertion !== null && previous !== void 0 && previous.key.kind === assertion.key.kind && sourceCode.getText(previous.receiver) === sourceCode.getText(assertion.receiver)) {
|
|
@@ -21746,6 +21754,7 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21746
21754
|
flush();
|
|
21747
21755
|
if (assertion !== null) {
|
|
21748
21756
|
run = [assertion];
|
|
21757
|
+
start = index;
|
|
21749
21758
|
}
|
|
21750
21759
|
}
|
|
21751
21760
|
flush();
|
|
@@ -21791,14 +21800,26 @@ var prefer_whole_object_assertion_default = createRule({
|
|
|
21791
21800
|
return assertionExpectation(statement, call, receiver, key, matcher);
|
|
21792
21801
|
}
|
|
21793
21802
|
function isTestExpect(callee) {
|
|
21794
|
-
|
|
21795
|
-
|
|
21796
|
-
|
|
21797
|
-
|
|
21798
|
-
|
|
21799
|
-
|
|
21800
|
-
|
|
21801
|
-
|
|
21803
|
+
return runtimeTestFrameworkName(sourceCode, callee, "assertion") === "expect";
|
|
21804
|
+
}
|
|
21805
|
+
function isExactLengthAssertion(statement, receiver) {
|
|
21806
|
+
if (statement?.type !== AST_NODE_TYPES101.ExpressionStatement || statement.expression.type !== AST_NODE_TYPES101.CallExpression) return false;
|
|
21807
|
+
const call = statement.expression;
|
|
21808
|
+
const callee = unwrapExpression(call.callee);
|
|
21809
|
+
if (callee.type !== AST_NODE_TYPES101.MemberExpression || call.arguments.length !== 1) return false;
|
|
21810
|
+
const matcher = ASTUtils63.getPropertyName(callee);
|
|
21811
|
+
const assertion = unwrapExpression(callee.object);
|
|
21812
|
+
if (assertion.type !== AST_NODE_TYPES101.CallExpression || assertion.arguments.length !== 1) return false;
|
|
21813
|
+
const expect = unwrapExpression(assertion.callee);
|
|
21814
|
+
if (expect.type !== AST_NODE_TYPES101.Identifier || !isTestExpect(expect)) return false;
|
|
21815
|
+
const expected = call.arguments[0] === void 0 ? void 0 : unwrapExpression(call.arguments[0]);
|
|
21816
|
+
if (expected?.type !== AST_NODE_TYPES101.Literal || typeof expected.value !== "number" || !Number.isInteger(expected.value) || expected.value < 0) return false;
|
|
21817
|
+
let actual = assertion.arguments[0] === void 0 ? void 0 : unwrapExpression(assertion.arguments[0]);
|
|
21818
|
+
if (matcher !== "toHaveLength") {
|
|
21819
|
+
if (matcher === null || !MERGEABLE_MATCHERS.has(matcher) || actual?.type !== AST_NODE_TYPES101.MemberExpression || actual.optional || ASTUtils63.getPropertyName(actual) !== "length") return false;
|
|
21820
|
+
actual = unwrapExpression(actual.object);
|
|
21821
|
+
}
|
|
21822
|
+
return actual !== void 0 && sourceCode.getText(actual) === sourceCode.getText(receiver);
|
|
21802
21823
|
}
|
|
21803
21824
|
function assertionExpectation(statement, call, receiver, key, matcher) {
|
|
21804
21825
|
const synthetic = SYNTHETIC_LITERAL_MATCHERS.get(matcher);
|