diff --git a/docs/rules/prefer-immutable-types.md b/docs/rules/prefer-immutable-types.md index 202946f70..09b6f4d0b 100644 --- a/docs/rules/prefer-immutable-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -244,6 +244,24 @@ type Options = { ReadonlyDeep?: Array>; Immutable?: Array>; }; + + overrides?: Array<{ + match: + | { + from: "file"; + path?: string; + } + | { + from: "lib"; + } + | { + from: "package"; + package?: string; + } + | TypeDeclarationSpecifier[]; + options: Omit; + disable: boolean; + }>; }; ``` @@ -475,3 +493,22 @@ It allows for the ability to ignore violations based on the identifier (name) of This option takes a `RegExp` string or an array of `RegExp` strings. It allows for the ability to ignore violations based on the type (as written, with whitespace removed) of the node in question. + +### `overrides` + +Allows for applying overrides to the options based on where the type is defined. +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. + +#### `overrides[n].options` + +The options to use when a specifiers matches. + +#### `overrides[n].disable` + +If true, when a specifier matches, this rule will not be applied to the matching node. diff --git a/src/options/index.ts b/src/options/index.ts index a2ca8f7c3..e5b888321 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -1 +1,2 @@ export * from "./ignore"; +export * from "./overrides"; diff --git a/src/options/overrides.ts b/src/options/overrides.ts new file mode 100644 index 000000000..735ca2256 --- /dev/null +++ b/src/options/overrides.ts @@ -0,0 +1,68 @@ +import { type TSESTree } from "@typescript-eslint/utils"; +import { type RuleContext } from "@typescript-eslint/utils/ts-eslint"; +import typeMatchesSpecifier, { + type TypeDeclarationSpecifier, +} from "ts-declaration-location"; + +import { getTypeOfNode } from "../utils/rule"; + +/** + * Options that can be overridden. + */ +export type OverridableOptions = CoreOptions & { + overrides?: Array< + { + specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[]; + } & ( + | { + options: CoreOptions; + disable?: false; + } + | { + disable: true; + } + ) + >; +}; + +/** + * Get the core options to use, taking into account overrides. + * + * @throws when there is a configuration error. + */ +export function getCoreOptions< + CoreOptions extends object, + Options extends readonly [Readonly>], +>( + node: TSESTree.Node, + context: Readonly>, + options: Readonly, +): CoreOptions | null { + const [optionsObject] = options; + + const program = context.sourceCode.parserServices?.program ?? undefined; + if (program === undefined) { + return optionsObject; + } + + const type = getTypeOfNode(node, context); + const found = optionsObject.overrides?.find((override) => + (Array.isArray(override.specifiers) + ? override.specifiers + : [override.specifiers] + ).some((specifier) => typeMatchesSpecifier(program, specifier, type)), + ); + + if (found !== undefined) { + if (found.disable === true) { + return null; + } + if (found.options === undefined) { + // eslint-disable-next-line functional/no-throw-statements + throw new Error("Configuration error: No options found for override."); + } + return found.options; + } + + return optionsObject; +} diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts index 4a3f505ce..6b4fba80b 100644 --- a/src/rules/functional-parameters.ts +++ b/src/rules/functional-parameters.ts @@ -5,23 +5,21 @@ import { } from "@typescript-eslint/utils/json-schema"; import { type RuleContext } from "@typescript-eslint/utils/ts-eslint"; import { deepmerge } from "deepmerge-ts"; -import typeMatchesSpecifier, { - type TypeDeclarationSpecifier, -} from "ts-declaration-location"; import { + getCoreOptions, ignoreIdentifierPatternOptionSchema, ignorePrefixSelectorOptionSchema, shouldIgnorePattern, type IgnoreIdentifierPatternOption, type IgnorePrefixSelectorOption, + type OverridableOptions, } from "#eslint-plugin-functional/options"; import { typeSpecifiersSchema } from "#eslint-plugin-functional/utils/common-schemas"; import { ruleNameScope } from "#eslint-plugin-functional/utils/misc"; import { type ESFunction } from "#eslint-plugin-functional/utils/node-types"; import { createRuleUsingFunction, - getTypeOfNode, type NamedCreateRuleCustomMeta, type RuleResult, } from "#eslint-plugin-functional/utils/rule"; @@ -69,23 +67,7 @@ type CoreOptions = IgnoreIdentifierPatternOption & /** * The options this rule can take. */ -type Options = [ - CoreOptions & { - overrides?: Array< - { - specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[]; - } & ( - | { - options: CoreOptions; - disable?: false; - } - | { - disable: true; - } - ) - >; - }, -]; +type Options = [OverridableOptions]; const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = { allowRestParameter: { @@ -206,39 +188,6 @@ const meta: NamedCreateRuleCustomMeta = { schema, }; -/** - * Get the core options to use, taking into account overrides. - */ -function getCoreOptions( - node: TSESTree.Node, - context: Readonly>, - options: Readonly, -): CoreOptions | null { - const [optionsObject] = options; - - const program = context.sourceCode.parserServices?.program ?? undefined; - if (program === undefined) { - return optionsObject; - } - - const type = getTypeOfNode(node, context); - const found = optionsObject.overrides?.find((override) => - (Array.isArray(override.specifiers) - ? override.specifiers - : [override.specifiers] - ).some((specifier) => typeMatchesSpecifier(program, specifier, type)), - ); - - if (found !== undefined) { - if (found.disable === true) { - return null; - } - return found.options; - } - - return optionsObject; -} - /** * Get the rest parameter violations. */ @@ -313,7 +262,11 @@ function checkFunction( context: Readonly>, options: Readonly, ): RuleResult { - const optionsToUse = getCoreOptions(node, context, options); + const optionsToUse = getCoreOptions( + node, + context, + options, + ); if (optionsToUse === null) { return { @@ -359,7 +312,7 @@ function checkIdentifier( const optionsToUse = functionNode === null ? options[0] - : getCoreOptions(functionNode, context, options); + : getCoreOptions(functionNode, context, options); if (optionsToUse === null) { return { diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts index ab31d5b62..181a17254 100644 --- a/src/rules/prefer-immutable-types.ts +++ b/src/rules/prefer-immutable-types.ts @@ -11,12 +11,15 @@ import { deepmerge } from "deepmerge-ts"; import { Immutability } from "is-immutable-type"; import { + getCoreOptions, ignoreClassesOptionSchema, shouldIgnoreClasses, shouldIgnoreInFunction, shouldIgnorePattern, type IgnoreClassesOption, + type OverridableOptions, } from "#eslint-plugin-functional/options"; +import { typeSpecifiersSchema } from "#eslint-plugin-functional/utils/common-schemas"; import { ruleNameScope } from "#eslint-plugin-functional/utils/misc"; import { type ESFunctionType } from "#eslint-plugin-functional/utils/node-types"; import { @@ -55,7 +58,8 @@ export const fullName = `${ruleNameScope}/${name}`; type RawEnforcement = | Exclude | "None" - | false; + | false + | undefined; type Option = IgnoreClassesOption & { enforcement: RawEnforcement; @@ -64,6 +68,20 @@ type Option = IgnoreClassesOption & { ignoreTypePattern?: string[] | string; }; +type CoreOptions = Option & { + parameters?: Partial