diff --git a/docs/rules/immutable-data.md b/docs/rules/immutable-data.md index 82a2651b2..1f7891de9 100644 --- a/docs/rules/immutable-data.md +++ b/docs/rules/immutable-data.md @@ -70,6 +70,36 @@ type Options = { }; ignoreIdentifierPattern?: string[] | string; ignoreAccessorPattern?: 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; + inherit?: boolean; + disable: boolean; + }>; }; ``` @@ -172,3 +202,28 @@ The following wildcards can be used when specifying a pattern: `**` - Match any depth (including zero). Can only be used as a full accessor.\ `*` - When used as a full accessor, match the next accessor (there must be one). When used as part of an accessor, match any characters. + +### `overrides` + +Allows for applying overrides to the options based on the root object's type. + +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/rules/immutable-data.ts b/src/rules/immutable-data.ts index 58147e25e..fffcab48d 100644 --- a/src/rules/immutable-data.ts +++ b/src/rules/immutable-data.ts @@ -10,11 +10,15 @@ import { type IgnoreAccessorPatternOption, type IgnoreClassesOption, type IgnoreIdentifierPatternOption, + type OverridableOptions, + type RawOverridableOptions, + getCoreOptions, ignoreAccessorPatternOptionSchema, ignoreClassesOptionSchema, ignoreIdentifierPatternOptionSchema, shouldIgnoreClasses, shouldIgnorePattern, + upgradeRawOverridableOptions, } from "#eslint-plugin-functional/options"; import { isExpected, @@ -26,6 +30,7 @@ import { createRule, getTypeOfNode, } from "#eslint-plugin-functional/utils/rule"; +import { overridableOptionsSchema } from "#eslint-plugin-functional/utils/schemas"; import { findRootIdentifier, isDefinedByMutableVariable, @@ -53,62 +58,61 @@ export const name = "immutable-data"; */ export const fullName = `${ruleNameScope}/${name}`; +type CoreOptions = IgnoreAccessorPatternOption & + IgnoreClassesOption & + IgnoreIdentifierPatternOption & { + ignoreImmediateMutation: boolean; + ignoreNonConstDeclarations: + | boolean + | { + treatParametersAsConst: boolean; + }; + }; + /** * The options this rule can take. */ -type Options = [ - IgnoreAccessorPatternOption & - IgnoreClassesOption & - IgnoreIdentifierPatternOption & { - ignoreImmediateMutation: boolean; - ignoreNonConstDeclarations: - | boolean - | { - treatParametersAsConst: boolean; - }; - }, -]; +type RawOptions = [RawOverridableOptions]; +type Options = OverridableOptions; -/** - * The schema for the rule options. - */ -const schema: JSONSchema4[] = [ +const coreOptionsPropertiesSchema = deepmerge( + ignoreIdentifierPatternOptionSchema, + ignoreAccessorPatternOptionSchema, + ignoreClassesOptionSchema, { - type: "object", - properties: deepmerge( - ignoreIdentifierPatternOptionSchema, - ignoreAccessorPatternOptionSchema, - ignoreClassesOptionSchema, - { - ignoreImmediateMutation: { + ignoreImmediateMutation: { + type: "boolean", + }, + ignoreNonConstDeclarations: { + oneOf: [ + { type: "boolean", }, - ignoreNonConstDeclarations: { - oneOf: [ - { + { + type: "object", + properties: { + treatParametersAsConst: { type: "boolean", }, - { - type: "object", - properties: { - treatParametersAsConst: { - type: "boolean", - }, - }, - additionalProperties: false, - }, - ], + }, + additionalProperties: false, }, - } satisfies JSONSchema4ObjectSchema["properties"], - ), - additionalProperties: false, + ], + }, }, +) as NonNullable; + +/** + * The schema for the rule options. + */ +const schema: JSONSchema4[] = [ + overridableOptionsSchema(coreOptionsPropertiesSchema), ]; /** * The default options for the rule. */ -const defaultOptions: Options = [ +const defaultOptions: RawOptions = [ { ignoreClasses: false, ignoreImmediateMutation: true, @@ -128,18 +132,19 @@ const errorMessages = { /** * The meta data for this rule. */ -const meta: NamedCreateRuleCustomMeta = { - type: "suggestion", - docs: { - category: "No Mutations", - description: "Enforce treating data as immutable.", - recommended: "recommended", - recommendedSeverity: "error", - requiresTypeChecking: true, - }, - messages: errorMessages, - schema, -}; +const meta: NamedCreateRuleCustomMeta = + { + type: "suggestion", + docs: { + category: "No Mutations", + description: "Enforce treating data as immutable.", + recommended: "recommended", + recommendedSeverity: "error", + requiresTypeChecking: true, + }, + messages: errorMessages, + schema, + }; /** * Array methods that mutate an array. @@ -220,16 +225,30 @@ const stringConstructorNewObjectReturningMethods = ["split"]; */ function checkAssignmentExpression( node: TSESTree.AssignmentExpression, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + const options = upgradeRawOverridableOptions(rawOptions[0]); + const rootNode = findRootIdentifier(node.left) ?? node.left; + const optionsToUse = getCoreOptions( + rootNode, + context, + options, + ); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreNonConstDeclarations, ignoreClasses, - } = optionsObject; + } = optionsToUse; if ( !isMemberExpression(node.left) || @@ -285,16 +304,30 @@ function checkAssignmentExpression( */ function checkUnaryExpression( node: TSESTree.UnaryExpression, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + const options = upgradeRawOverridableOptions(rawOptions[0]); + const rootNode = findRootIdentifier(node.argument) ?? node.argument; + const optionsToUse = getCoreOptions( + rootNode, + context, + options, + ); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreNonConstDeclarations, ignoreClasses, - } = optionsObject; + } = optionsToUse; if ( !isMemberExpression(node.argument) || @@ -349,16 +382,30 @@ function checkUnaryExpression( */ function checkUpdateExpression( node: TSESTree.UpdateExpression, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + const options = upgradeRawOverridableOptions(rawOptions[0]); + const rootNode = findRootIdentifier(node.argument) ?? node.argument; + const optionsToUse = getCoreOptions( + rootNode, + context, + options, + ); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreNonConstDeclarations, ignoreClasses, - } = optionsObject; + } = optionsToUse; if ( !isMemberExpression(node.argument) || @@ -416,7 +463,7 @@ function checkUpdateExpression( */ function isInChainCallAndFollowsNew( node: TSESTree.Expression, - context: Readonly>, + context: Readonly>, ): boolean { if (isMemberExpression(node)) { return isInChainCallAndFollowsNew(node.object, context); @@ -488,16 +535,30 @@ function isInChainCallAndFollowsNew( */ function checkCallExpression( node: TSESTree.CallExpression, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + const options = upgradeRawOverridableOptions(rawOptions[0]); + const rootNode = findRootIdentifier(node.callee) ?? node.callee; + const optionsToUse = getCoreOptions( + rootNode, + context, + options, + ); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + const { ignoreIdentifierPattern, ignoreAccessorPattern, ignoreNonConstDeclarations, ignoreClasses, - } = optionsObject; + } = optionsToUse; // Not potential object mutation? if ( @@ -517,7 +578,7 @@ function checkCallExpression( }; } - const { ignoreImmediateMutation } = optionsObject; + const { ignoreImmediateMutation } = optionsToUse; // Array mutation? if ( @@ -608,7 +669,7 @@ function checkCallExpression( } // Create the rule. -export const rule = createRule( +export const rule = createRule( name, meta, defaultOptions,