diff options
Diffstat (limited to 'eslint/rules/prefer-object-params.mjs')
| -rw-r--r-- | eslint/rules/prefer-object-params.mjs | 107 |
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, + }; + }, +}; |
