Repository: eslint-functional/eslint-plugin-functional (ts)

Issue:
`functional/functional-parameters` Limit only to specific types
## Suggestion

When using `functional/functional-parameters`, we have [`ignorePrefixSelector`](https://github.com/eslint-functional/eslint-plugin-functional/blob/main/docs/rules/functional-parameters.md#ignoreprefixselector) but that can't tap into TypeScript type information.

There are a few places that I would love to whitelist in a way that requires type information. It would be great if we had that ability.

Here's one motivating example:

In https://github.com/gcanti/fp-ts, there are many cases where Lazy thunks are used to achieve functional purity.

One case is when lifting `Promise`s (eager, not referentially transparent) into fp-ts `TaskEither`s (lazy, referentially transparent):

```ts
export declare const tryCatch: <E, A>(f: Lazy<Promise<A>>, onRejected: (reason: unknown) => E) => TaskEither<E, A>
```

When calling that, you are frequently taking some existing code that returns a `Promise` and doing this to it:

```ts
TE.tryCatch(() => existingCodeThatReturnsPromise);
```

That lazy thunk (`() => ...`) unfortunately triggers `functional/functional-parameters`, even though in this case we're writing code that in spirit is extremely functional.


Luckily, fp-ts defines `Lazy` like so:

```ts
export interface Lazy<A> {
  (): A
}
```

It could have defined it like `export type Lazy<A> = () => A;`, but for our purposes here we're lucky they made it a named interface.

If we could tap into the type information, we'd know that the contextual type of `() => existingCodeThatReturnsPromise` is actually `Lazy<...>` due to it being passed to `TE.tryCatch` (first param type being: `f: Lazy<Promise<A>>`). That would allow us to whitelist cases like this.


Note that fp-ts also defines a type `IO<A>` that is structurally the same as `Lazy<A>` but has different semantics: `IO<A>` is assumed to be impure. We would not want to whitelist IOs.

Pull request:
feat(functional-parameters): allow overriding options based on where the function type is declared
Fix #575


Patch:
diff --git a/README.md b/README.md
index 0fe4f3c..144791b 100644
--- a/README.md
+++ b/README.md
@@ -111,9 +111,9 @@ The [below section](#rules) gives details on which rules are enabled by each rul
 
 ### Currying
 
-| Name                                                         | Description                    | 💼                           | ⚠️  | 🚫  | 🔧  | 💡  | 💭  | ❌  |
-| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :-- | :-- | :-- | :-- | :-- |
-| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | ☑️ ✅ 🔒 ![badge-currying][] |     |     |     |     |     |     |
+| Name                                                         | Description                    | 💼                           | ⚠️  | 🚫                            | 🔧  | 💡  | 💭  | ❌  |
+| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
+| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | ☑️ ✅ 🔒 ![badge-currying][] |     | ![badge-disableTypeChecked][] |     |     | 💭  |     |
 
 ### No Exceptions
 
diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md
index 007c779..f993f00 100644
--- a/docs/rules/functional-parameters.md
+++ b/docs/rules/functional-parameters.md
@@ -3,7 +3,9 @@
 
 # Enforce functional parameters (`functional/functional-parameters`)
 
-💼 This rule is enabled in the following configs: `currying`, ☑️ `lite`, ✅ `recommended`, 🔒 `strict`.
+💼🚫 This rule is enabled in the following configs: `currying`, ☑️ `lite`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the `disableTypeChecked` config.
+
+💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting).
 
 <!-- end auto-generated rule header -->
 <!-- markdownlint-restore -->
@@ -11,6 +13,8 @@
 
 Disallow use of rest parameters, the `arguments` keyword and enforces that functions take at least 1 parameter.
 
+Note: type information is only required when using the [overrides](#overrides) option.
+
 ## Rule Details
 
 In functions, `arguments` is a special variable that is implicitly available.
@@ -74,6 +78,36 @@ type Options = {
       };
   ignoreIdentifierPattern?: string[] | string;
   ignorePrefixSelector?: string[] | string;
+  overrides?: Array<{
+    match: Array<
+      | {
+          from: "file";
+          path?: string;
+          name?: string | string[];
+          pattern?: RegExp | RegExp[];
+          ignoreName?: string | string[];
+          ignorePattern?: RegExp | RegExp[];
+        }
+      | {
+          from: "lib";
+          name?: string | string[];
+          pattern?: RegExp | RegExp[];
+          ignoreName?: string | string[];
+          ignorePattern?: RegExp | RegExp[];
+        }
+      | {
+          from: "package";
+          package?: string;
+          name?: string | string[];
+          pattern?: RegExp | RegExp[];
+          ignoreName?: string | string[];
+          ignorePattern?: RegExp | RegExp[];
+        }
+    >;
+    options: Omit<Options, "overrides">;
+    inherit?: boolean;
+    disable: boolean;
+  }>;
 };
 ```
 
@@ -208,3 +242,31 @@ const sum = [1, 2, 3].reduce((carry, current) => current, 0);
 
 This option takes a RegExp string or an array of RegExp strings.
 It allows for the ability to ignore violations based on a function's name.
+
+### `overrides`
+
+_Using this option requires type infomation._
+
+Allows for applying overrides to the options based on the function's type.
+This can be used to override the settings for types coming from 3rd party libraries.
+
+Note: Only the first matching override will be used.
+
+#### `overrides[n].specifiers`
+
+A specifier, or an array of specifiers to match the function type against.
+
+In the case of reference types, both the type and its generics will be recursively checked.
+If any of them match, the specifier will be considered a match.
+
+#### `overrides[n].options`
+
+The options to use when a specifiers matches.
+
+#### `overrides[n].inherit`
+
+Inherit the root options? Default is `true`.
+
+#### `overrides[n].disable`
+
+If true, when a specifier matches, this rule will not be applied to the matching node.
diff --git a/knip.jsonc b/knip.jsonc
index 214a6dd..f97d18e 100644
--- a/knip.jsonc
+++ b/knip.jsonc
@@ -2,7 +2,7 @@
   "$schema": "node_modules/knip/schema-jsonc.json",
   "entry": ["src/index.ts!", "tests/**/*.test.ts"],
   "project": ["src/**/*.ts!", "tests/**/*.{js,ts}"],
-  "ignore": ["lib/**", "tests/fixture/file.ts", "src/utils/schemas.ts"],
+  "ignore": ["lib/**", "tests/fixture/file.ts"],
   "ignoreDependencies": [
     // Unknown reason for issue.
     "@vitest/coverage-v8",
diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts
index de1803f..4d65a75 100644
--- a/src/rules/functional-parameters.ts
+++ b/src/rules/functional-parameters.ts
@@ -9,9 +9,13 @@ import { deepmerge } from "deepmerge-ts";
 import {
   type IgnoreIdentifierPatternOption,
   type IgnorePrefixSelectorOption,
+  type OverridableOptions,
+  type RawOverridableOptions,
+  getCoreOptions,
   ignoreIdentifierPatternOptionSchema,
   ignorePrefixSelectorOptionSchema,
   shouldIgnorePattern,
+  upgradeRawOverridableOptions,
 } from "#/options";
 import { ruleNameScope } from "#/utils/misc";
 import type { ESFunction } from "#/utils/node-types";
@@ -21,7 +25,9 @@ import {
   type RuleResult,
   createRuleUsingFunction,
 } from "#/utils/rule";
+import { overridableOptionsSchema } from "#/utils/schemas";
 import {
+  getEnclosingFunction,
   isArgument,
   isGetter,
   isIIFE,
@@ -46,83 +52,82 @@ export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameSco
  */
 type ParameterCountOptions = "atLeastOne" | "exactlyOne";
 
+type CoreOptions = IgnoreIdentifierPatternOption &
+  IgnorePrefixSelectorOption & {
+    allowRestParameter: boolean;
+    allowArgumentsKeyword: boolean;
+    enforceParameterCount:
+      | ParameterCountOptions
+      | false
+      | {
+          count: ParameterCountOptions;
+          ignoreLambdaExpression: boolean;
+          ignoreIIFE: boolean;
+          ignoreGettersAndSetters: boolean;
+        };
+  };
+
 /**
  * The options this rule can take.
  */
-type Options = [
-  IgnoreIdentifierPatternOption &
-    IgnorePrefixSelectorOption & {
-      allowRestParameter: boolean;
-      allowArgumentsKeyword: boolean;
-      enforceParameterCount:
-        | ParameterCountOptions
-        | false
-        | {
-            count: ParameterCountOptions;
-            ignoreLambdaExpression: boolean;
-            ignoreIIFE: boolean;
-            ignoreGettersAndSetters: boolean;
-          };
-    },
-];
+type RawOptions = [RawOverridableOptions<CoreOptions>];
+type Options = OverridableOptions<CoreOptions>;
 
-/**
- * The schema for the rule options.
- */
-const schema: JSONSchema4[] = [
+const coreOptionsPropertiesSchema = deepmerge(
+  ignoreIdentifierPatternOptionSchema,
+  ignorePrefixSelectorOptionSchema,
   {
-    type: "object",
-    properties: deepmerge(
-      ignoreIdentifierPatternOptionSchema,
-      ignorePrefixSelectorOptionSchema,
-      {
-        allowRestParameter: {
+    allowRestParameter: {
+      type: "boolean",
+    },
+    allowArgumentsKeyword: {
+      type: "boolean",
+    },
+    enforceParameterCount: {
+      oneOf: [
+        {
           type: "boolean",
+          enum: [false],
         },
-        allowArgumentsKeyword: {
-          type: "boolean",
+        {
+          type: "string",
+          enum: ["atLeastOne", "exactlyOne"],
         },
-        enforceParameterCount: {
-          oneOf: [
-            {
-              type: "boolean",
-              enum: [false],
-            },
-            {
+        {
+          type: "object",
+          properties: {
+            count: {
               type: "string",
               enum: ["atLeastOne", "exactlyOne"],
             },
-            {
-              type: "object",
-              properties: {
-                count: {
-                  type: "string",
-                  enum: ["atLeastOne", "exactlyOne"],
-                },
-                ignoreGettersAndSetters: {
-                  type: "boolean",
-                },
-                ignoreLambdaExpression: {
-                  type: "boolean",
-                },
-                ignoreIIFE: {
-                  type: "boolean",
-                },
-              },
-              additionalProperties: false,
+            ignoreGettersAndSetters: {
+              type: "boolean",
+            },
+            ignoreLambdaExpression: {
+              type: "boolean",
+            },
+            ignoreIIFE: {
+              type: "boolean",
             },
-          ],
+          },
+          additionalProperties: false,
         },
-      } satisfies JSONSchema4ObjectSchema["properties"],
-    ),
-    additionalProperties: false,
+      ],
+    },
   },
+) as NonNullable<JSONSchema4ObjectSchema["properties"]>;
+
+/**
+ * The schema for the rule options.
+ */
+const schema: JSONSchema4[] = [
+  overridableOptionsSchema(coreOptionsPropertiesSchema),
 ];
 
 /**
  * The default options for the rule.
  */
-const defaultOptions: Options = [
+const defaultOptions: RawOptions = [
   {
     allowRestParameter: false,
     allowArgumentsKeyword: false,
@@ -157,7 +162,7 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
     description: "Enforce functional parameters.",
     recommended: "recommended",
     recommendedSeverity: "error",
-    requiresTypeChecking: false,
+    requiresTypeChecking: true,
   },
   messages: errorMessages,
   schema,
@@ -167,9 +172,9 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
  * Get the rest parameter violations.
  */
 function getRestParamViolations(
-  [{ allowRestParameter }]: Readonly<Options>,
+  { allowRestParameter }: Readonly<CoreOptions>,
   node: ESFunction,
-): RuleResult<keyof typeof errorMessages, Options>["descriptors"] {
+): RuleResult<keyof typeof errorMessages, RawOptions>["descriptors"] {
   return !allowRestParameter &&
     node.params.length > 0 &&
     isRestElement(node.params.at(-1))
@@ -186,9 +191,9 @@ function getRestParamViolations(
  * Get the parameter count violations.
  */
 function getParamCountViolations(
-  [{ enforceParameterCount }]: Readonly<Options>,
+  { enforceParameterCount }: Readonly<CoreOptions>,
   node: ESFunction,
-): RuleResult<keyof typeof errorMessages, Options>["descriptors"] {
+): RuleResult<keyof typeof errorMessages, RawOptions>["descriptors"] {
   if (
     enforceParameterCount === false ||
     (node.params.length === 0 &&
@@ -234,11 +239,24 @@ function getParamCountViolations(
  */
 function checkFunction(
   node: ESFunction,
-  context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
-  options: Readonly<Options>,
-): RuleResult<keyof typeof errorMessages, Options> {
-  const [optionsObject] = options;
-  const { ignoreIdentifierPattern } = optionsObject;
+  context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
+  rawOptions: Readonly<RawOptions>,
+): RuleResult<keyof typeof errorMessages, RawOptions> {
+  const options = upgradeRawOverridableOptions(rawOptions[0]);
+  const optionsToUse = getCoreOptions<CoreOptions, Options>(
+    node,
+    context,
+    options,
+  );
+
+  if (optionsToUse === null) {
+    return {
+      context,
+      descriptors: [],
+    };
+  }
+
+  const { ignoreIdentifierPattern } = optionsToUse;
 
   if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
     return {
@@ -250,8 +268,8 @@ function checkFunction(
   return {
     context,
     descriptors: [
-      ...getRestParamViolations(options, node),
-      ...getParamCountViolations(options, node),
+      ...getRestParamViolations(optionsToUse, node),
+      ...getParamCountViolations(optionsToUse, node),
     ],
   };
 }
@@ -261,11 +279,31 @@ function checkFunction(
  */
 function checkIdentifier(
   node: TSESTree.Identifier,
-  context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
-  options: Readonly<Options>,
-): RuleResult<keyof typeof errorMessages, Options> {
-  const [optionsObject] = options;
-  const { ignoreIdentifierPattern } = optionsObject;
+  context: Readonly<RuleContext<keyof typeof errorMessages, RawOptions>>,
+  rawOptions: Readonly<RawOptions>,
+): RuleResult<keyof typeof errorMessages, RawOptions> {
+  if (node.name !== "arguments") {
+    return {
+      context,
+      descriptors: [],
+    };
+  }
+
+  const functionNode = getEnclosingFunction(node);
+  const options = upgradeRawOverridableOptions(rawOptions[0]);
+  const optionsToUse =
+    functionNode === null
+      ? options
+      : getCoreOptions<CoreOptions, Options>(functionNode, context, options);
+
+  if (optionsToUse === null) {
+    return {
+      context,
+      descriptors: [],
+    };
+  }
+
+  const { ignoreIdentifierPattern } = optionsToUse;
 
   if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) {
     return {
@@ -274,15 +312,12 @@ function checkIdentifier(
     };
   }
 
-  const { allowArgumentsKeyword } = optionsObject;
+  const { allowArgumentsKeyword } = optionsToUse;
 
   return {
     context,
     descriptors:
-      !allowArgumentsKeyword &&
-      node.name === "arguments" &&
-      !isPropertyName(node) &&
-      !isPropertyAccess(node)
+      !allowArgumentsKeyword && !isPropertyName(node) && !isPropertyAccess(node)
         ? [
             {
               node,
@@ -294,8 +329,8 @@ function checkIdentifier(
 }
 
 // Create the rule.
-export const rule: Rule<keyof typeof errorMessages, Options> =
-  createRuleUsingFunction<keyof typeof errorMessages, Options>(
+export const rule: Rule<keyof typeof errorMessages, RawOptions> =
+  createRuleUsingFunction<keyof typeof errorMessages, RawOptions>(
     name,
     meta,
     defaultOptions,
diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts
index dc29305..0b2dc76 100644
--- a/src/utils/schemas.ts
+++ b/src/utils/schemas.ts
@@ -2,6 +2,7 @@ import type {
   JSONSchema4,
   JSONSchema4ObjectSchema,
 } from "@typescript-eslint/utils/json-schema";
+import { deepmerge } from "deepmerge-ts";
 
 const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] =
   {
@@ -63,9 +64,6 @@ const typeSpecifierSchema: JSONSchema4 = {
   ],
 };
 
-export const typeSpecifiersSchema: JSONSchema4 =
-  schemaInstanceOrInstanceArray(typeSpecifierSchema);
-
 export function schemaInstanceOrInstanceArray(
   items: JSONSchema4,
 ): NonNullable<JSONSchema4ObjectSchema["properties"]>[string] {
@@ -79,3 +77,37 @@ export function schemaInstanceOrInstanceArray(
     ],
   };
 }
+
+export function overridableOptionsSchema(
+  coreOptionsPropertiesSchema: NonNullable<
+    JSONSchema4ObjectSchema["properties"]
+  >,
+): JSONSchema4 {
+  return {
+    type: "object",
+    properties: deepmerge(coreOptionsPropertiesSchema, {
+      overrides: {
+        type: "array",
+        items: {
+          type: "object",
+          properties: {
+            specifiers: schemaInstanceOrInstanceArray(typeSpecifierSchema),
+            options: {
+              type: "object",
+              properties: coreOptionsPropertiesSchema,
+              additionalProperties: false,
+            },
+            inherit: {
+              type: "boolean",
+            },
+            disable: {
+              type: "boolean",
+            },
+          },
+          additionalProperties: false,
+        },
+      },
+    } satisfies JSONSchema4ObjectSchema["properties"]),
+    additionalProperties: false,
+  };
+}


Test patch:
diff --git a/tests/rules/functional-parameters.test.ts b/tests/rules/functional-parameters.test.ts
index 533d877..ddffad9 100644
--- a/tests/rules/functional-parameters.test.ts
+++ b/tests/rules/functional-parameters.test.ts
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
 
 import { name, rule } from "#/rules/functional-parameters";
 
-import { esLatestConfig } from "../utils/configs";
+import { esLatestConfig, typescriptConfig } from "../utils/configs";
 
 describe(name, () => {
   describe("javascript - es latest", () => {
@@ -435,4 +435,110 @@ describe(name, () => {
       });
     });
   });
+
+  describe("typescript", () => {
+    const { valid, invalid } = createRuleTester({
+      name,
+      rule,
+      configs: typescriptConfig,
+    });
+
+    describe("overrides", () => {
+      it('override value works - "allowRestParameter"', () => {
+        const code = dedent`
+          function foo(...bar: string[]) {
+            console.log(bar);
+          }
+        `;
+
+        valid({
+          code,
+          options: {
+            allowRestParameter: false,
+            overrides: [
+              {
+                specifiers: {
+                  from: "file",
+                },
+                options: {
+                  allowRestParameter: true,
+                },
+              },
+            ],
+          },
+        });
+      });
+
+      it('override value works - "allowArgumentsKeyword"', () => {
+        const code = dedent`
+          function foo(bar: string[]) {
+            console.log(arguments);
+          }
+        `;
+
+        valid({
+          code,
+          options: {
+            allowArgumentsKeyword: false,
+            overrides: [
+              {
+                specifiers: {
+                  from: "file",
+                },
+                options: {
+                  allowArgumentsKeyword: true,
+                },
+              },
+            ],
+          },
+        });
+      });
+
+      it('disbale override works - "allowRestParameter"', () => {
+        const code = dedent`
+          function foo(...bar: string[]) {
+            console.log(bar);
+          }
+        `;
+
+        valid({
+          code,
+          options: {
+            allowRestParameter: false,
+            overrides: [
+              {
+                specifiers: {
+                  from: "file",
+                },
+                disable: true,
+              },
+            ],
+          },
+        });
+      });
+
+      it('disbale override works - "allowArgumentsKeyword"', () => {
+        const code = dedent`
+          function foo(bar: string[]) {
+            console.log(arguments);
+          }
+        `;
+
+        valid({
+          code,
+          options: {
+            allowArgumentsKeyword: false,
+            overrides: [
+              {
+                specifiers: {
+                  from: "file",
+                },
+                disable: true,
+              },
+            ],
+          },
+        });
+      });
+    });
+  });
 });