OpenCut/eslint/rules/prefer-object-params.mjs

108 lines
2.9 KiB
JavaScript

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,
};
},
};