108 lines
2.9 KiB
JavaScript
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,
|
|
};
|
|
},
|
|
};
|