From 0130f0fe8cd8b2fd972551129c033de9bf3eae08 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 15 Apr 2024 02:02:38 +1200 Subject: [PATCH] feat(prefer-immutable-types): allow overriding options based on where the type is declared --- docs/rules/functional-parameters.md | 5 +- docs/rules/prefer-immutable-types.md | 19 ++ src/options/index.ts | 1 + src/options/overrides.ts | 68 ++++++ src/rules/functional-parameters.ts | 63 +----- src/rules/prefer-immutable-types.ts | 210 +++++++++++------- .../ts/parameters/invalid.ts | 31 +++ .../ts/parameters/valid.ts | 36 +++ 8 files changed, 293 insertions(+), 140 deletions(-) create mode 100644 src/options/overrides.ts diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md index 002449b7c..c237774e6 100644 --- a/docs/rules/functional-parameters.md +++ b/docs/rules/functional-parameters.md @@ -187,11 +187,12 @@ Getters should always take zero parameters, and setter one. If for some reason y ### `overrides` +_Using this option requires type infomation._ + Allows for applying overrides to the options based on where the function's type is defined. This can be used to override the settings for types coming from 3rd party libraries. -Note: Using this option requires type infomation. -Note 2: Only the first matching override will be used. +Note: Only the first matching override will be used. #### `overrides[n].specifiers` diff --git a/docs/rules/prefer-immutable-types.md b/docs/rules/prefer-immutable-types.md index 202946f70..68ca986a8 100644 --- a/docs/rules/prefer-immutable-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -475,3 +475,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 a0cf52dc5..492024d6e 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"; @@ -71,21 +69,7 @@ type CoreOptions = { type Options = [ IgnoreIdentifierPatternOption & IgnorePrefixSelectorOption & - CoreOptions & { - overrides?: Array< - { - specifiers: TypeDeclarationSpecifier | TypeDeclarationSpecifier[]; - } & ( - | { - options: CoreOptions; - disable?: false; - } - | { - disable: true; - } - ) - >; - }, + OverridableOptions, ]; const coreOptionsPropertiesSchema: JSONSchema4ObjectSchema["properties"] = { @@ -207,39 +191,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. */ @@ -324,7 +275,11 @@ function checkFunction( }; } - const optionsToUse = getCoreOptions(node, context, options); + const optionsToUse = getCoreOptions( + node, + context, + options, + ); return { context, @@ -367,7 +322,7 @@ function checkIdentifier( const optionsToUse = functionNode === null ? optionsObject - : getCoreOptions(functionNode, context, options); + : getCoreOptions(functionNode, context, options); if (optionsToUse === null) { return { context, 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