summaryrefslogtreecommitdiff
path: root/eslint/rules/prefer-object-params.mjs
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-29 00:37:56 +0200
committerMaze Winther <mazewinther@gmail.com>2026-05-02 03:27:23 +0200
commita9b9cf6bf50f08723896659634f2e6afc17bcd42 (patch)
tree9175b49ff21f4326872ac9e541d2765cc7994ebc /eslint/rules/prefer-object-params.mjs
parentd6622dc6b3523a5658cd512b44f15f7a66fd540c (diff)
downloadopencut-a9b9cf6bf50f08723896659634f2e6afc17bcd42.tar.gz
chore: switch from biome to eslint + prettier; fix ton of lint issues
Diffstat (limited to 'eslint/rules/prefer-object-params.mjs')
-rw-r--r--eslint/rules/prefer-object-params.mjs107
1 files changed, 107 insertions, 0 deletions
diff --git a/eslint/rules/prefer-object-params.mjs b/eslint/rules/prefer-object-params.mjs
new file mode 100644
index 00000000..a5a8caae
--- /dev/null
+++ b/eslint/rules/prefer-object-params.mjs
@@ -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,
+ };
+ },
+};