mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: switch from biome to eslint + prettier; fix ton of lint issues
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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.itOnly = it.only;
|
||||
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 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Prefer a single destructured object parameter over multiple positional parameters",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
preferObjectParams:
|
||||
"Use a single destructured object parameter instead of multiple positional parameters. Example: function someFunction({ name, age }: { name: string, age: number }) {}",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
function isCallArgument(node) {
|
||||
return (
|
||||
node &&
|
||||
(node.type === "CallExpression" ||
|
||||
node.type === "NewExpression" ||
|
||||
node.type === "OptionalCallExpression")
|
||||
);
|
||||
}
|
||||
|
||||
function isDirectCallback(node) {
|
||||
const parent = node.parent;
|
||||
return (
|
||||
isCallArgument(parent) && parent.arguments.includes(node)
|
||||
);
|
||||
}
|
||||
|
||||
// Some APIs receive callbacks via a property somewhere inside an inline
|
||||
// option object (e.g. `persist(state, { migrate: (s, v) => ... })` or
|
||||
// `betterAuth({ rateLimit: { customStorage: { set: (k, v) => ... }}})`).
|
||||
// The caller still dictates the signature, so the same carve-out as
|
||||
// `isDirectCallback` applies. Pre-declared option objects don't qualify —
|
||||
// once you name the object, the signature is yours to evolve.
|
||||
function isInlineOptionCallback(node) {
|
||||
let current = node;
|
||||
let property = current.parent;
|
||||
if (
|
||||
!property ||
|
||||
property.type !== "Property" ||
|
||||
property.value !== current
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let objectExpression = property.parent;
|
||||
while (objectExpression?.type === "ObjectExpression") {
|
||||
const parent = objectExpression.parent;
|
||||
if (isCallArgument(parent) && parent.arguments.includes(objectExpression)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
parent?.type !== "Property" ||
|
||||
parent.value !== objectExpression
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
property = parent;
|
||||
objectExpression = property.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type predicates (`x is Foo`) require a positional subject by language
|
||||
// rule, so the readability/evolution argument doesn't apply. AGENTS.md
|
||||
// carves these out explicitly.
|
||||
function isTypePredicate(node) {
|
||||
return node.returnType?.typeAnnotation?.type === "TSTypePredicate";
|
||||
}
|
||||
|
||||
function reportIfNeeded(node) {
|
||||
if (
|
||||
node.params.length <= 1 ||
|
||||
isDirectCallback(node) ||
|
||||
isInlineOptionCallback(node) ||
|
||||
isTypePredicate(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report from the function's first line through params[1] so a
|
||||
// conventional `// eslint-disable-next-line` placed above the
|
||||
// declaration suppresses the violation. The directive matches
|
||||
// against `loc.start.line`, which must land on the function
|
||||
// declaration line — not on a later parameter row.
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: node.loc.start,
|
||||
end: node.params[1].loc.end,
|
||||
},
|
||||
messageId: "preferObjectParams",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: reportIfNeeded,
|
||||
FunctionDeclaration: reportIfNeeded,
|
||||
FunctionExpression: reportIfNeeded,
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user