summaryrefslogtreecommitdiff
path: root/eslint/rules/__tests__/prefer-object-params.test.mjs
blob: 5d92fcf21a0f70780d930d59ec4c02c10ff719a0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { afterAll, describe, it } from "bun:test";
import { RuleTester } from "eslint";
import tseslint from "typescript-eslint";
import rule from "../prefer-object-params.mjs";

RuleTester.describe = describe;
RuleTester.it = it;
RuleTester.afterAll = afterAll;

const ruleTester = new RuleTester({
	languageOptions: {
		parser: tseslint.parser,
		parserOptions: {
			ecmaVersion: "latest",
			sourceType: "module",
			ecmaFeatures: { jsx: true },
		},
	},
});

const error = { messageId: "preferObjectParams" };

ruleTester.run("prefer-object-params", rule, {
	valid: [
		{ code: "function f() {}" },
		{ code: "function f(a) {}" },
		{ code: "const f = (a) => a;" },
		{ code: "const f = function (a) { return a; };" },
		{ code: "function f({ a, b }) { return a + b; }" },
		{ code: "function f({ a, b }: { a: number; b: number }) { return a + b; }" },

		// Direct callbacks: positional args are dictated by the caller.
		{ code: "[1, 2, 3].reduce((acc, cur) => acc + cur, 0);" },
		{ code: "arr.map((value, index) => value + index);" },
		{ code: "new Promise((resolve, reject) => {});" },
		{ code: "maybeFn?.((a, b) => a + b);" },

		// Inline option-object callbacks: caller dictates the signature
		// (e.g. zustand's `persist`, tanstack-query's `onSuccess`).
		{ code: "someAPI({ on: (a, b) => a + b });" },
		{
			code: "persist((set, get) => ({}), { migrate: (state, version) => state });",
		},
		{ code: "new Foo({ handler: (a, b) => a + b });" },
		{ code: "maybeFn?.({ handler: (a, b) => a + b });" },

		// Deeply-nested inline option callbacks still qualify: the entire
		// option tree is supplied at the call site, so the caller dictates
		// every signature inside it. Real example: better-auth's
		// `betterAuth({ rateLimit: { customStorage: { set: (k, v) => ... }}})`.
		{ code: "someAPI({ outer: { handler: (a, b) => a + b } });" },

		// Type predicates require a positional subject.
		{
			code: "function isFoo(x: unknown, ctx: Ctx): x is Foo { return true; }",
		},
		{
			code: "const isFoo = (x: unknown, ctx: Ctx): x is Foo => true;",
		},
	],
	invalid: [
		{
			code: "function f(a, b) { return a + b; }",
			errors: [error],
		},
		{
			code: "const f = (a, b) => a + b;",
			errors: [error],
		},
		{
			code: "const f = function (a, b) { return a + b; };",
			errors: [error],
		},
		{
			code: "function f(a, b, c) { return a + b + c; }",
			errors: [error],
		},
		{
			code: "function formatTime(seconds: number, fps: number) { return seconds * fps; }",
			errors: [error],
		},
		// A callback assigned to a name and passed indirectly is still a
		// signature the author controls; the carve-out is intentionally narrow
		// to direct callbacks.
		{
			code: "const cb = (a, b) => a + b; arr.reduce(cb, 0);",
			errors: [error],
		},
		// Pre-declared option object: once the object is named, its callback
		// signatures are the author's to evolve.
		{
			code: "const opts = { handler: (a, b) => a + b }; someAPI(opts);",
			errors: [error],
		},
		// The error must point at the function's declaration line, so a
		// `// eslint-disable-next-line` placed above the declaration (the
		// conventional spot) suppresses the violation even when the
		// signature spans multiple lines. Reporting at a parameter row
		// instead leaves the directive inert and `reportUnusedDisableDirectives`
		// then flags every suppressed call site.
		{
			code: [
				"function f(",
				"\ta: number,",
				"\tb: number,",
				") { return a + b; }",
			].join("\n"),
			errors: [{ messageId: "preferObjectParams", line: 1 }],
		},
	],
});