From 4e6c3a9d5c9124b1e9bc08b6315d0ad6246fc025 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 15 Apr 2024 20:06:41 +1200 Subject: [PATCH] fix(prefer-tacit): don't check member functions by default (#808) fix #805 --- docs/rules/prefer-tacit.md | 56 ++++++++++++++++++++++++++ src/rules/prefer-tacit.ts | 42 ++++++++++++++++--- tests/rules/prefer-tacit/ts/invalid.ts | 47 +++++++++++++++++++++ tests/rules/prefer-tacit/ts/valid.ts | 14 +++++++ 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/docs/rules/prefer-tacit.md b/docs/rules/prefer-tacit.md index 9f00dff79..de8e16df9 100644 --- a/docs/rules/prefer-tacit.md +++ b/docs/rules/prefer-tacit.md @@ -41,4 +41,60 @@ function f(x) { } const foo = [1, 2, 3].map(f); + +const bar = { f }; +const baz = [1, 2, 3].map((x) => bar.f(x)); // Allowed unless using `checkMemberExpressions` +``` + +## Options + +This rule accepts an options object of the following type: + +```ts +type Options = { + checkMemberExpressions: boolean; +}; +``` + +### Default Options + +```ts +type Options = { + checkMemberExpressions: false; +}; +``` + +### `checkMemberExpressions` + +If `true`, calls of member expressions are checked as well. +If `false`, only calls of identifiers are checked. + +#### ❌ Incorrect + + + +```ts +/* eslint functional/prefer-tacit: ["error", { "checkMemberExpressions": true }] */ + +const bar = { + f(x) { + return x + 1; + } +} + +const foo = [1, 2, 3].map((x) => bar.f(x)); +``` + +#### ✅ Correct + +```ts +/* eslint functional/prefer-tacit: ["error", { "checkMemberExpressions": true }] */ + +const bar = { + f(x) { + return x + 1; + } +} + +const foo = [1, 2, 3].map(bar.f.bind(bar)); ``` diff --git a/src/rules/prefer-tacit.ts b/src/rules/prefer-tacit.ts index e137dbc7a..2b9ed5fe8 100644 --- a/src/rules/prefer-tacit.ts +++ b/src/rules/prefer-tacit.ts @@ -25,6 +25,7 @@ import { isBlockStatement, isCallExpression, isIdentifier, + isMemberExpression, isReturnStatement, } from "#eslint-plugin-functional/utils/type-guards"; @@ -41,17 +42,35 @@ export const fullName = `${ruleNameScope}/${name}`; /** * The options this rule can take. */ -type Options = []; +type Options = [ + { + checkMemberExpressions: boolean; + }, +]; /** * The schema for the rule options. */ -const schema: JSONSchema4[] = []; +const schema: JSONSchema4[] = [ + { + type: "object", + properties: { + checkMemberExpressions: { + type: "boolean", + }, + }, + additionalProperties: false, + }, +]; /** * The default options for the rule. */ -const defaultOptions: Options = []; +const defaultOptions: Options = [ + { + checkMemberExpressions: false, + }, +]; /** * The possible error messages. @@ -135,8 +154,12 @@ function fixFunctionCallToReference( return [ fixer.replaceText( - node as TSESTree.Node, - context.sourceCode.getText(caller.callee as TSESTree.Node), + node, + isMemberExpression(caller.callee) + ? `${context.sourceCode.getText( + caller.callee, + )}.bind(${context.sourceCode.getText(caller.callee.object)})` + : context.sourceCode.getText(caller.callee), ), ]; } @@ -196,6 +219,15 @@ function getCallDescriptors( options: Options, caller: TSESTree.CallExpression, ): Array> { + const [{ checkMemberExpressions }] = options; + + if ( + !isIdentifier(caller.callee) && + !(checkMemberExpressions && isMemberExpression(caller.callee)) + ) { + return []; + } + if ( node.params.length === caller.arguments.length && node.params.every((param, index) => { diff --git a/tests/rules/prefer-tacit/ts/invalid.ts b/tests/rules/prefer-tacit/ts/invalid.ts index be779509a..21e775a23 100644 --- a/tests/rules/prefer-tacit/ts/invalid.ts +++ b/tests/rules/prefer-tacit/ts/invalid.ts @@ -293,6 +293,53 @@ const tests: Array< }, ], }, + // Member Call Expression + { + code: dedent` + [''].filter(str => /a/.test(str)); + `, + optionsSet: [[{ checkMemberExpressions: true }]], + errors: [ + { + messageId: "generic", + type: AST_NODE_TYPES.ArrowFunctionExpression, + line: 1, + column: 13, + suggestions: [ + { + messageId: "generic", + output: dedent` + [''].filter(/a/.test.bind(/a/)); + `, + }, + ], + }, + ], + }, + { + code: dedent` + declare const a: { b(arg: string): string; }; + function foo(x) { return a.b(x); } + `, + optionsSet: [[{ checkMemberExpressions: true }]], + errors: [ + { + messageId: "generic", + type: AST_NODE_TYPES.FunctionDeclaration, + line: 2, + column: 1, + suggestions: [ + { + messageId: "generic", + output: dedent` + declare const a: { b(arg: string): string; }; + const foo = a.b.bind(a); + `, + }, + ], + }, + ], + }, ]; export default tests; diff --git a/tests/rules/prefer-tacit/ts/valid.ts b/tests/rules/prefer-tacit/ts/valid.ts index 0497394ae..39630206c 100644 --- a/tests/rules/prefer-tacit/ts/valid.ts +++ b/tests/rules/prefer-tacit/ts/valid.ts @@ -86,6 +86,20 @@ const tests: Array>> = [ dependencyConstraints: { typescript: "4.7.0" }, optionsSet: [[]], }, + // Member Call Expression + { + code: dedent` + [''].filter(str => /a/.test(str)) + `, + optionsSet: [[{ checkMemberExpressions: false }]], + }, + { + code: dedent` + declare const a: { b(arg: string): string; }; + function foo(x) { return a.b(x); } + `, + optionsSet: [[{ checkMemberExpressions: false }]], + }, ]; export default tests;