diff --git a/docs/rules/prefer-immutable-types.md b/docs/rules/prefer-immutable-types.md index 18be71d56..4344a9c2d 100644 --- a/docs/rules/prefer-immutable-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -244,6 +244,37 @@ type Options = { ReadonlyDeep?: Array>; Immutable?: Array>; }; + + 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; + inherit?: boolean; + disable: boolean; + }>; }; ``` @@ -475,3 +506,29 @@ 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. + +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/src/options/overrides.ts b/src/options/overrides.ts index 7e8d4f475..7a5616d06 100644 --- a/src/options/overrides.ts +++ b/src/options/overrides.ts @@ -1,7 +1,12 @@ +import assert from "node:assert/strict"; + import { type TSESTree } from "@typescript-eslint/utils"; import { type RuleContext } from "@typescript-eslint/utils/ts-eslint"; import { deepmerge } from "deepmerge-ts"; -import typeMatchesSpecifier from "ts-declaration-location"; +import typeMatchesSpecifier, { + type TypeDeclarationSpecifier, +} from "ts-declaration-location"; +import { type Program, type Type, type TypeNode } from "typescript"; import { getTypeDataOfNode } from "#eslint-plugin-functional/utils/rule"; import { @@ -108,13 +113,30 @@ export function getCoreOptions< } const [type, typeNode] = getTypeDataOfNode(node, context); + return getCoreOptionsForType(type, typeNode, context, options); +} + +export function getCoreOptionsForType< + CoreOptions extends object, + Options extends Readonly>, +>( + type: Type, + typeNode: TypeNode | null, + context: Readonly>, + options: Readonly, +): CoreOptions | null { + const program = context.sourceCode.parserServices?.program ?? undefined; + if (program === undefined) { + return options; + } + const found = options.overrides?.find((override) => (Array.isArray(override.specifiers) ? override.specifiers : [override.specifiers] ).some( (specifier) => - typeMatchesSpecifier(program, specifier, type) && + typeMatchesSpecifierDeep(program, specifier, type) && (specifier.include === undefined || specifier.include.length === 0 || typeMatchesPattern( @@ -139,3 +161,25 @@ export function getCoreOptions< return options; } + +function typeMatchesSpecifierDeep( + program: Program, + specifier: TypeDeclarationSpecifier, + type: Type, +) { + const stack = [type]; + // eslint-disable-next-line functional/no-loop-statements -- best to do this iteratively. + while (stack.length > 0) { + const t = stack.pop() ?? assert.fail(); + + if (typeMatchesSpecifier(program, specifier, t)) { + return true; + } + + if (t.aliasTypeArguments !== undefined) { + stack.push(...t.aliasTypeArguments); + } + } + + return false; +} diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts index c5199e5a2..cec620fcf 100644 --- a/src/rules/prefer-immutable-types.ts +++ b/src/rules/prefer-immutable-types.ts @@ -9,13 +9,19 @@ import { } from "@typescript-eslint/utils/ts-eslint"; import { deepmerge } from "deepmerge-ts"; import { Immutability } from "is-immutable-type"; +import { type Type, type TypeNode } from "typescript"; import { type IgnoreClassesOption, + type OverridableOptions, + type RawOverridableOptions, + getCoreOptions, + getCoreOptionsForType, ignoreClassesOptionSchema, shouldIgnoreClasses, shouldIgnoreInFunction, shouldIgnorePattern, + upgradeRawOverridableOptions, } from "#eslint-plugin-functional/options"; import { ruleNameScope } from "#eslint-plugin-functional/utils/misc"; import { type ESFunctionType } from "#eslint-plugin-functional/utils/node-types"; @@ -24,6 +30,7 @@ import { type RuleResult, createRule, getReturnTypesOfFunction, + getTypeDataOfNode, getTypeImmutabilityOfNode, getTypeImmutabilityOfType, isImplementationOfOverload, @@ -42,6 +49,8 @@ import { isTSTypePredicate, } from "#eslint-plugin-functional/utils/type-guards"; +import { overridableOptionsSchema } from "../utils/schemas"; + /** * The name of this rule. */ @@ -55,7 +64,8 @@ export const fullName = `${ruleNameScope}/${name}`; type RawEnforcement = | Exclude | "None" - | false; + | false + | undefined; type Option = IgnoreClassesOption & { enforcement: RawEnforcement; @@ -64,6 +74,20 @@ type Option = IgnoreClassesOption & { ignoreTypePattern?: string[] | string; }; +type CoreOptions = Option & { + parameters?: Partial